@moda-ai/cli 1.22.2 → 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-pwbvtes8.js → cli-mnqa4rp6.js} +1 -750
- package/dist/cli-stmjdgvy.js +2842 -0
- package/dist/{cli-g8qs2tj9.js → cli-y55bgg14.js} +223 -3020
- package/dist/{cli-qszcpda3.js → cli-ysegdrm5.js} +5 -3
- package/dist/cli.js +19 -15
- package/dist/harness-github-actions-7t3cncnt.js +95 -0
- package/dist/{index-6h0nc1r5.js → index-4ser01zr.js} +17 -13
- package/dist/{provision-j5gq8cvx.js → provision-hm1ppwj6.js} +5 -3
- package/package.json +1 -1
- package/skills/integration/index.json +2 -2
|
@@ -6,32 +6,24 @@ import {
|
|
|
6
6
|
MAX_RECORDED_SKIPS,
|
|
7
7
|
MAX_SCANNED_FILE_BYTES,
|
|
8
8
|
SKIPPED_DIRS,
|
|
9
|
+
__require,
|
|
9
10
|
createCommandContext,
|
|
10
11
|
embeddedTenantIdFromApiKey,
|
|
11
|
-
ensureSecretFilePermissions,
|
|
12
12
|
getActiveProfile,
|
|
13
|
-
getModaPaths,
|
|
14
|
-
isAuthSessionValid,
|
|
15
13
|
isEvalLikePath,
|
|
16
|
-
isLoopbackBaseUrl,
|
|
17
14
|
isSecretishPath,
|
|
18
15
|
isSkippedDir,
|
|
19
16
|
isSkippedPathPrefix,
|
|
20
17
|
isTextFile,
|
|
21
|
-
loadAuthSession,
|
|
22
|
-
loadProfileConfig,
|
|
23
18
|
packSnapshot,
|
|
24
19
|
redactDeep,
|
|
25
20
|
resolveApiKey,
|
|
26
21
|
resolveIngestUrl,
|
|
27
22
|
resolveModaBaseUrl,
|
|
28
|
-
resolveSecretReference,
|
|
29
23
|
resolveTenantId,
|
|
30
|
-
secretFilePathFromRef,
|
|
31
24
|
shouldInventoryFile,
|
|
32
|
-
stringOption
|
|
33
|
-
|
|
34
|
-
} from "./cli-pwbvtes8.js";
|
|
25
|
+
stringOption
|
|
26
|
+
} from "./cli-mnqa4rp6.js";
|
|
35
27
|
|
|
36
28
|
// src/api-client.ts
|
|
37
29
|
var REQUEST_TIMEOUT_MS = 30000;
|
|
@@ -131,1947 +123,9 @@ function resetApiRequestCount() {
|
|
|
131
123
|
apiRequestCount = 0;
|
|
132
124
|
}
|
|
133
125
|
|
|
134
|
-
// src/prompts.ts
|
|
135
|
-
import { execFileSync } from "node:child_process";
|
|
136
|
-
import { createHash } from "node:crypto";
|
|
137
|
-
import {
|
|
138
|
-
existsSync as existsSync2,
|
|
139
|
-
lstatSync,
|
|
140
|
-
mkdirSync,
|
|
141
|
-
readFileSync,
|
|
142
|
-
readdirSync,
|
|
143
|
-
writeFileSync as writeFileSync2
|
|
144
|
-
} from "node:fs";
|
|
145
|
-
import { dirname, isAbsolute as isAbsolute3, join, relative, resolve as resolve3 } from "node:path";
|
|
146
|
-
|
|
147
|
-
// src/prompts-ab.ts
|
|
148
|
-
import { existsSync } from "node:fs";
|
|
149
|
-
import { isAbsolute, resolve } from "node:path";
|
|
150
|
-
var DEFAULT_SEEDS_PER_CASE = 3;
|
|
151
|
-
var DEFAULT_POLL_INTERVAL_MS = 15000;
|
|
152
|
-
var DEFAULT_WAIT_TIMEOUT_MS = 2 * 60 * 60 * 1000;
|
|
153
|
-
var TERMINAL_RUN_STATUSES = new Set(["completed", "skipped", "error"]);
|
|
154
|
-
async function runPromptAb(flags, profileOptions, context) {
|
|
155
|
-
validateConfig();
|
|
156
|
-
const tenantId = flags["tenant-id"] || resolveTenantId(profileOptions);
|
|
157
|
-
if (!tenantId) {
|
|
158
|
-
throw new Error("Missing tenant id. Run `moda init` or pass --tenant-id=<id>.");
|
|
159
|
-
}
|
|
160
|
-
const baselineSource = flags.baseline || flags["baseline-file"] || flags["baseline-key"];
|
|
161
|
-
const candidateSource = flags.candidate || flags["candidate-file"] || flags["candidate-key"];
|
|
162
|
-
if (!baselineSource || !candidateSource) {
|
|
163
|
-
throw new Error("Usage: moda prompts ab --baseline=<file|key> --candidate=<file|key> [--auto-generate|--set-id=ID|--conversations=id1,id2]");
|
|
164
|
-
}
|
|
165
|
-
if (flags.sync === "true") {
|
|
166
|
-
await runPromptSync({ ...flags, watch: "false" }, profileOptions);
|
|
167
|
-
}
|
|
168
|
-
const promptArms = {
|
|
169
|
-
prod: resolvePromptArmSpec(baselineSource, "baseline"),
|
|
170
|
-
proposed: resolvePromptArmSpec(candidateSource, "candidate")
|
|
171
|
-
};
|
|
172
|
-
const replaySetId = await ensureReplaySet(flags, tenantId, profileOptions);
|
|
173
|
-
const seedsPerCase = parsePositiveInt(flags.seeds ?? flags["seeds-per-case"], DEFAULT_SEEDS_PER_CASE, 10);
|
|
174
|
-
const assistantModel = (flags.model ?? flags["assistant-model"] ?? "").trim();
|
|
175
|
-
const enqueue = await callControlAPI(`/tenants/${encodeURIComponent(tenantId)}/replay-sets/${encodeURIComponent(replaySetId)}/run`, {
|
|
176
|
-
method: "POST",
|
|
177
|
-
body: JSON.stringify({
|
|
178
|
-
promptArms,
|
|
179
|
-
seedsPerCase,
|
|
180
|
-
assistantModel: assistantModel || undefined,
|
|
181
|
-
promotePrimary: flags["no-promote"] !== "true"
|
|
182
|
-
})
|
|
183
|
-
}, profileOptions, { timeoutMs: 120000, retries: 1 });
|
|
184
|
-
const noWait = flags.wait === "false" || flags["no-wait"] === "true";
|
|
185
|
-
if (noWait) {
|
|
186
|
-
context.output.writeData({
|
|
187
|
-
replaySetId,
|
|
188
|
-
runId: enqueue.runId,
|
|
189
|
-
status: enqueue.status,
|
|
190
|
-
message: enqueue.message ?? "Replay comparison queued"
|
|
191
|
-
});
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
const timeoutMs = parsePositiveInt(flags.timeout, DEFAULT_WAIT_TIMEOUT_MS, 24 * 60 * 60 * 1000);
|
|
195
|
-
const pollIntervalMs = parsePositiveInt(flags["poll-interval"], DEFAULT_POLL_INTERVAL_MS, 120000);
|
|
196
|
-
const deadline = Date.now() + timeoutMs;
|
|
197
|
-
let latest = null;
|
|
198
|
-
while (Date.now() < deadline) {
|
|
199
|
-
latest = await fetchLatestRun(tenantId, replaySetId, enqueue.runId, profileOptions);
|
|
200
|
-
const status = latest.run?.status ?? "";
|
|
201
|
-
const runId = latest.run?.runId ?? "";
|
|
202
|
-
if (TERMINAL_RUN_STATUSES.has(status) && runId === enqueue.runId) {
|
|
203
|
-
break;
|
|
204
|
-
}
|
|
205
|
-
if (context.outputMode === "human") {
|
|
206
|
-
process.stderr.write(`Waiting for replay run (${status || "pending"})...
|
|
207
|
-
`);
|
|
208
|
-
}
|
|
209
|
-
await sleep(pollIntervalMs);
|
|
210
|
-
}
|
|
211
|
-
if (!latest?.run || latest.run.runId !== enqueue.runId || !TERMINAL_RUN_STATUSES.has(latest.run.status)) {
|
|
212
|
-
throw new Error(`Timed out after ${timeoutMs}ms waiting for replay run ${enqueue.runId} to finish`);
|
|
213
|
-
}
|
|
214
|
-
const result = {
|
|
215
|
-
replaySetId,
|
|
216
|
-
runId: latest.run.runId,
|
|
217
|
-
status: latest.run.status,
|
|
218
|
-
verdict: formatVerdict(latest),
|
|
219
|
-
run: { ...latest.run },
|
|
220
|
-
cases: latest.cases.map((row) => ({ ...row }))
|
|
221
|
-
};
|
|
222
|
-
if (context.outputMode === "human") {
|
|
223
|
-
printHumanVerdict(result);
|
|
224
|
-
}
|
|
225
|
-
context.output.writeData(result);
|
|
226
|
-
}
|
|
227
|
-
function resolvePromptArmSpec(source, label, opts = {}) {
|
|
228
|
-
const trimmed = source.trim();
|
|
229
|
-
if (!trimmed) {
|
|
230
|
-
throw new Error(`Empty prompt source for ${label}`);
|
|
231
|
-
}
|
|
232
|
-
const fromDiscovery = () => {
|
|
233
|
-
const discovered = discoverPrompts();
|
|
234
|
-
const match = discovered.find((prompt) => prompt.key === trimmed) ?? discovered.find((prompt) => prompt.sourcePath === trimmed);
|
|
235
|
-
return match ? { key: match.key, content: match.content } : null;
|
|
236
|
-
};
|
|
237
|
-
const absPath = isAbsolute(trimmed) ? trimmed : resolve(process.cwd(), trimmed);
|
|
238
|
-
let key;
|
|
239
|
-
let content;
|
|
240
|
-
const preferred = opts.preferKey ? fromDiscovery() : null;
|
|
241
|
-
if (preferred) {
|
|
242
|
-
key = preferred.key;
|
|
243
|
-
content = preferred.content;
|
|
244
|
-
} else if (opts.preferKey) {
|
|
245
|
-
throw new Error(`Could not resolve prompt key '${trimmed}' — no discovered prompt matched this key`);
|
|
246
|
-
} else if (existsSync(absPath)) {
|
|
247
|
-
const parsed = parsePromptFileForAb(absPath);
|
|
248
|
-
key = parsed.key;
|
|
249
|
-
content = parsed.content;
|
|
250
|
-
} else {
|
|
251
|
-
const match = fromDiscovery();
|
|
252
|
-
if (!match) {
|
|
253
|
-
throw new Error(`Could not resolve prompt '${trimmed}' — pass a .prompt.md path or a discovered prompt key`);
|
|
254
|
-
}
|
|
255
|
-
key = match.key;
|
|
256
|
-
content = match.content;
|
|
257
|
-
}
|
|
258
|
-
const locked = readPromptLock()?.prompts[key];
|
|
259
|
-
return {
|
|
260
|
-
content,
|
|
261
|
-
prompt_version_id: locked?.versionId ?? "",
|
|
262
|
-
prompt_id: locked?.promptId ?? "",
|
|
263
|
-
prompt_label: label
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
async function ensureReplaySet(flags, tenantId, profileOptions) {
|
|
267
|
-
const existingSetId = (flags["set-id"] || flags["replay-set-id"] || "").trim();
|
|
268
|
-
if (existingSetId) {
|
|
269
|
-
return existingSetId;
|
|
270
|
-
}
|
|
271
|
-
const conversations = parseCsv(flags.conversations ?? flags["conversation-ids"]);
|
|
272
|
-
if (conversations.length) {
|
|
273
|
-
return createSetFromConversations(conversations, flags, tenantId, profileOptions);
|
|
274
|
-
}
|
|
275
|
-
if (flags["auto-generate"] === "false" && !existingSetId) {
|
|
276
|
-
throw new Error("Provide --set-id=, --conversations=, or allow --auto-generate (default)");
|
|
277
|
-
}
|
|
278
|
-
const caseCount = parsePositiveInt(flags.cases ?? flags["case-count"], 5, 25);
|
|
279
|
-
const lookbackDays = parsePositiveInt(flags["lookback-days"], 30, 365);
|
|
280
|
-
const name = (flags.name ?? flags["set-name"] ?? `Prompt A/B ${new Date().toISOString().slice(0, 10)}`).trim();
|
|
281
|
-
const generated = await callControlAPI(`/tenants/${encodeURIComponent(tenantId)}/replay-sets/auto-generate`, {
|
|
282
|
-
method: "POST",
|
|
283
|
-
body: JSON.stringify({ name, caseCount, lookbackDays })
|
|
284
|
-
}, profileOptions, { timeoutMs: 300000, retries: 0 });
|
|
285
|
-
if (!generated?.id) {
|
|
286
|
-
throw new Error("Auto-generate did not return a replay set id");
|
|
287
|
-
}
|
|
288
|
-
return generated.id;
|
|
289
|
-
}
|
|
290
|
-
async function createSetFromConversations(conversationIds, flags, tenantId, profileOptions) {
|
|
291
|
-
const name = (flags.name ?? flags["set-name"] ?? `Prompt A/B ${conversationIds.length} conv`).trim();
|
|
292
|
-
const created = await callControlAPI(`/tenants/${encodeURIComponent(tenantId)}/replay-sets`, {
|
|
293
|
-
method: "POST",
|
|
294
|
-
body: JSON.stringify({ name, description: "Created by moda prompts ab" })
|
|
295
|
-
}, profileOptions);
|
|
296
|
-
let position = 0;
|
|
297
|
-
for (const conversationId of conversationIds) {
|
|
298
|
-
const scenario = await loadScenarioFromConversation(conversationId);
|
|
299
|
-
await callControlAPI(`/tenants/${encodeURIComponent(tenantId)}/replay-sets/${encodeURIComponent(created.id)}/cases`, {
|
|
300
|
-
method: "POST",
|
|
301
|
-
body: JSON.stringify({
|
|
302
|
-
title: conversationId,
|
|
303
|
-
scenario,
|
|
304
|
-
sourceConversationId: conversationId,
|
|
305
|
-
successCriteria: [
|
|
306
|
-
"The agent understands the user request",
|
|
307
|
-
"The agent uses tools appropriately when needed",
|
|
308
|
-
"The agent provides a helpful final answer"
|
|
309
|
-
],
|
|
310
|
-
position: position++
|
|
311
|
-
})
|
|
312
|
-
}, profileOptions);
|
|
313
|
-
}
|
|
314
|
-
return created.id;
|
|
315
|
-
}
|
|
316
|
-
async function loadScenarioFromConversation(conversationId) {
|
|
317
|
-
try {
|
|
318
|
-
const data = await callDataAPI(`/conversations/${encodeURIComponent(conversationId)}/context?msg_index=0&window=0`);
|
|
319
|
-
const firstUser = (data.messages ?? []).find((message) => message.role === "user" && String(message.content ?? "").trim());
|
|
320
|
-
const opening = String(firstUser?.content ?? "").trim();
|
|
321
|
-
if (opening) {
|
|
322
|
-
return `The user says: "${opening}"
|
|
323
|
-
|
|
324
|
-
Replay the conversation faithfully and help them accomplish their goal.`;
|
|
325
|
-
}
|
|
326
|
-
} catch {}
|
|
327
|
-
return `Replay conversation ${conversationId}: help the user accomplish their original goal using the available tools.`;
|
|
328
|
-
}
|
|
329
|
-
async function fetchLatestRun(tenantId, replaySetId, runId, profileOptions) {
|
|
330
|
-
return callControlAPI(`/tenants/${encodeURIComponent(tenantId)}/replay-sets/${encodeURIComponent(replaySetId)}/runs/latest?runId=${encodeURIComponent(runId)}`, { method: "GET" }, profileOptions, { timeoutMs: 60000, retries: 2 });
|
|
331
|
-
}
|
|
332
|
-
function candidateOutcome(payload) {
|
|
333
|
-
const run = payload.run;
|
|
334
|
-
if (!run || run.status !== "completed")
|
|
335
|
-
return "no-run";
|
|
336
|
-
const delta = run.proposedPassCount - run.prodPassCount;
|
|
337
|
-
if (delta > 0)
|
|
338
|
-
return "candidate";
|
|
339
|
-
if (delta < 0)
|
|
340
|
-
return "baseline";
|
|
341
|
-
return "tie";
|
|
342
|
-
}
|
|
343
|
-
function formatVerdict(payload) {
|
|
344
|
-
const run = payload.run;
|
|
345
|
-
if (!run)
|
|
346
|
-
return "no run";
|
|
347
|
-
if (run.status === "skipped")
|
|
348
|
-
return `skipped: ${run.notes || "no reason"}`;
|
|
349
|
-
if (run.status === "error")
|
|
350
|
-
return `error: ${run.notes || "unknown error"}`;
|
|
351
|
-
if (run.status !== "completed")
|
|
352
|
-
return `status=${run.status}`;
|
|
353
|
-
const delta = run.proposedPassCount - run.prodPassCount;
|
|
354
|
-
const winner = delta > 0 ? "candidate wins" : delta < 0 ? "baseline wins" : "tie on case passes";
|
|
355
|
-
const rates = `baseline ${run.prodPassCount}/${run.caseCount} (${pct(run.prodPassRate)}), candidate ${run.proposedPassCount}/${run.caseCount} (${pct(run.proposedPassRate)})`;
|
|
356
|
-
const notes = run.notes?.trim();
|
|
357
|
-
return `${winner}; ${rates}${notes ? `; ${notes}` : ""}`;
|
|
358
|
-
}
|
|
359
|
-
function printHumanVerdict(result) {
|
|
360
|
-
process.stderr.write(`
|
|
361
|
-
Prompt A/B replay verdict
|
|
362
|
-
`);
|
|
363
|
-
process.stderr.write(`-------------------------
|
|
364
|
-
`);
|
|
365
|
-
process.stderr.write(`Replay set: ${result.replaySetId}
|
|
366
|
-
`);
|
|
367
|
-
process.stderr.write(`Run id: ${result.runId}
|
|
368
|
-
`);
|
|
369
|
-
process.stderr.write(`Status: ${result.status}
|
|
370
|
-
`);
|
|
371
|
-
process.stderr.write(`Verdict: ${result.verdict}
|
|
372
|
-
`);
|
|
373
|
-
if (result.cases.length) {
|
|
374
|
-
process.stderr.write(`
|
|
375
|
-
Per-case:
|
|
376
|
-
`);
|
|
377
|
-
for (const row of result.cases) {
|
|
378
|
-
const caseId = String(row.caseId ?? "");
|
|
379
|
-
const prod = formatPass(row.prodPass);
|
|
380
|
-
const proposed = formatPass(row.proposedPass);
|
|
381
|
-
process.stderr.write(` ${caseId}: baseline=${prod} candidate=${proposed}
|
|
382
|
-
`);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
process.stderr.write(`
|
|
386
|
-
`);
|
|
387
|
-
}
|
|
388
|
-
function formatPass(value) {
|
|
389
|
-
if (value === null || value === undefined)
|
|
390
|
-
return "abstained";
|
|
391
|
-
return value ? "pass" : "fail";
|
|
392
|
-
}
|
|
393
|
-
function pct(rate) {
|
|
394
|
-
return `${Math.round(Math.max(0, Math.min(1, rate)) * 100)}%`;
|
|
395
|
-
}
|
|
396
|
-
async function enqueueAndPollComparison(args, profileOptions) {
|
|
397
|
-
const enqueue = await callControlAPI(`/tenants/${encodeURIComponent(args.tenantId)}/replay-sets/${encodeURIComponent(args.replaySetId)}/run`, {
|
|
398
|
-
method: "POST",
|
|
399
|
-
body: JSON.stringify({
|
|
400
|
-
promptArms: args.promptArms,
|
|
401
|
-
seedsPerCase: args.seedsPerCase ?? DEFAULT_SEEDS_PER_CASE,
|
|
402
|
-
assistantModel: args.assistantModel || undefined,
|
|
403
|
-
promotePrimary: args.promotePrimary ?? false
|
|
404
|
-
})
|
|
405
|
-
}, profileOptions, { timeoutMs: 120000, retries: 1 });
|
|
406
|
-
const timeoutMs = args.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS;
|
|
407
|
-
const pollIntervalMs = args.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
408
|
-
const deadline = Date.now() + timeoutMs;
|
|
409
|
-
let latest = null;
|
|
410
|
-
while (Date.now() < deadline) {
|
|
411
|
-
latest = await fetchLatestRun(args.tenantId, args.replaySetId, enqueue.runId, profileOptions);
|
|
412
|
-
const status = latest.run?.status ?? "";
|
|
413
|
-
const runId = latest.run?.runId ?? "";
|
|
414
|
-
if (TERMINAL_RUN_STATUSES.has(status) && runId === enqueue.runId) {
|
|
415
|
-
return latest;
|
|
416
|
-
}
|
|
417
|
-
args.onWaiting?.(status || "pending");
|
|
418
|
-
await sleep(pollIntervalMs);
|
|
419
|
-
}
|
|
420
|
-
if (!latest?.run || latest.run.runId !== enqueue.runId || !TERMINAL_RUN_STATUSES.has(latest.run.status)) {
|
|
421
|
-
throw new Error(`Timed out after ${timeoutMs}ms waiting for replay run ${enqueue.runId} to finish`);
|
|
422
|
-
}
|
|
423
|
-
return latest;
|
|
424
|
-
}
|
|
425
|
-
function parsePositiveInt(raw, fallback, max) {
|
|
426
|
-
if (!raw)
|
|
427
|
-
return fallback;
|
|
428
|
-
const parsed = Number.parseInt(raw, 10);
|
|
429
|
-
if (!Number.isFinite(parsed) || parsed < 1)
|
|
430
|
-
return fallback;
|
|
431
|
-
return Math.min(parsed, max);
|
|
432
|
-
}
|
|
433
|
-
function parseCsv(raw) {
|
|
434
|
-
if (!raw?.trim())
|
|
435
|
-
return [];
|
|
436
|
-
return raw.split(",").map((part) => part.trim()).filter(Boolean);
|
|
437
|
-
}
|
|
438
|
-
function sleep(ms) {
|
|
439
|
-
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// src/prompts-propose.ts
|
|
443
|
-
import { writeFileSync } from "node:fs";
|
|
444
|
-
import { isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
|
|
445
|
-
var PROPOSE_TIMEOUT_MS = 300000;
|
|
446
|
-
async function runPromptPropose(positionals, flags, profileOptions, context) {
|
|
447
|
-
validateConfig();
|
|
448
|
-
const promptKey = flags["prompt-key"] || positionals[1];
|
|
449
|
-
const fromRunId = flags["from-run"] || flags["from-run-id"] || flags["run-id"];
|
|
450
|
-
const replaySetId = flags["set-id"] || flags["replay-set-id"];
|
|
451
|
-
if (!promptKey || !fromRunId || !replaySetId) {
|
|
452
|
-
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]");
|
|
453
|
-
}
|
|
454
|
-
const maxDossiers = parsePositiveInt2(flags["max-dossiers"], 8, 16);
|
|
455
|
-
const model = (flags.model ?? flags["revise-model"] ?? "").trim();
|
|
456
|
-
const result = await callControlAPI(`/prompts/${encodeURIComponent(promptKey)}/propose`, {
|
|
457
|
-
method: "POST",
|
|
458
|
-
body: JSON.stringify({ fromRunId, replaySetId, maxDossiers, model: model || undefined })
|
|
459
|
-
}, profileOptions, { timeoutMs: PROPOSE_TIMEOUT_MS, retries: 0 });
|
|
460
|
-
let outPath;
|
|
461
|
-
if (result.status === "proposed" && result.content && flags.out) {
|
|
462
|
-
outPath = isAbsolute2(flags.out) ? flags.out : resolve2(process.cwd(), flags.out);
|
|
463
|
-
writeFileSync(outPath, result.content, "utf8");
|
|
464
|
-
}
|
|
465
|
-
const wantGate = flags.gate === "true";
|
|
466
|
-
const promoteOnWin = flags["promote-on-win"] === "true";
|
|
467
|
-
let gate;
|
|
468
|
-
if (wantGate) {
|
|
469
|
-
if (result.status !== "proposed" || !result.content) {
|
|
470
|
-
gate = {
|
|
471
|
-
ran: false,
|
|
472
|
-
scope: "full-set",
|
|
473
|
-
scopeNote: `no candidate to gate (${result.status})`,
|
|
474
|
-
verdict: "skipped",
|
|
475
|
-
outcome: "no-run",
|
|
476
|
-
runId: null,
|
|
477
|
-
promoted: false
|
|
478
|
-
};
|
|
479
|
-
} else {
|
|
480
|
-
gate = await runGate({
|
|
481
|
-
result,
|
|
482
|
-
promptKey,
|
|
483
|
-
replaySetId,
|
|
484
|
-
flags,
|
|
485
|
-
promoteOnWin,
|
|
486
|
-
profileOptions,
|
|
487
|
-
context
|
|
488
|
-
});
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
if (context.outputMode === "human") {
|
|
492
|
-
printHuman(result, outPath, gate);
|
|
493
|
-
}
|
|
494
|
-
context.output.writeData({
|
|
495
|
-
...result,
|
|
496
|
-
...outPath ? { outPath } : {},
|
|
497
|
-
...gate ? { gate } : {}
|
|
498
|
-
});
|
|
499
|
-
}
|
|
500
|
-
async function runGate(args) {
|
|
501
|
-
const { result, promptKey, replaySetId, flags, promoteOnWin, profileOptions, context } = args;
|
|
502
|
-
const tenantId = flags["tenant-id"] || resolveTenantId(profileOptions);
|
|
503
|
-
if (!tenantId) {
|
|
504
|
-
throw new Error("Missing tenant id for --gate. Run `moda init` or pass --tenant-id=<id>.");
|
|
505
|
-
}
|
|
506
|
-
const prodArm = resolvePromptArmSpec(promptKey, "baseline", { preferKey: true });
|
|
507
|
-
const proposedArm = {
|
|
508
|
-
content: result.content ?? "",
|
|
509
|
-
prompt_version_id: result.versionId ?? "",
|
|
510
|
-
prompt_id: "",
|
|
511
|
-
prompt_label: "candidate"
|
|
512
|
-
};
|
|
513
|
-
const seedsPerCase = parsePositiveInt2(flags.seeds ?? flags["seeds-per-case"], 3, 10);
|
|
514
|
-
const assistantModel = (flags["assistant-model"] ?? "").trim();
|
|
515
|
-
const timeoutMs = parsePositiveInt2(flags["gate-timeout"] ?? flags.timeout, 2 * 60 * 60 * 1000, 24 * 60 * 60 * 1000);
|
|
516
|
-
const pollIntervalMs = parsePositiveInt2(flags["poll-interval"], 15000, 120000);
|
|
517
|
-
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";
|
|
518
|
-
if (context.outputMode === "human") {
|
|
519
|
-
process.stderr.write(`
|
|
520
|
-
Gating candidate for '${promptKey}' — ${scopeNote}...
|
|
521
|
-
`);
|
|
522
|
-
}
|
|
523
|
-
const payload = await enqueueAndPollComparison({
|
|
524
|
-
tenantId,
|
|
525
|
-
replaySetId,
|
|
526
|
-
promptArms: { prod: prodArm, proposed: proposedArm },
|
|
527
|
-
seedsPerCase,
|
|
528
|
-
assistantModel: assistantModel || undefined,
|
|
529
|
-
promotePrimary: false,
|
|
530
|
-
timeoutMs,
|
|
531
|
-
pollIntervalMs,
|
|
532
|
-
onWaiting: context.outputMode === "human" ? (status) => process.stderr.write(`Waiting for gate run (${status})...
|
|
533
|
-
`) : undefined
|
|
534
|
-
}, profileOptions);
|
|
535
|
-
const outcome = candidateOutcome(payload);
|
|
536
|
-
const gate = {
|
|
537
|
-
ran: true,
|
|
538
|
-
scope: "full-set",
|
|
539
|
-
scopeNote,
|
|
540
|
-
verdict: formatVerdict(payload),
|
|
541
|
-
outcome,
|
|
542
|
-
runId: payload.run?.runId ?? null,
|
|
543
|
-
promoted: false
|
|
544
|
-
};
|
|
545
|
-
if (promoteOnWin) {
|
|
546
|
-
if (outcome === "candidate" && result.versionId) {
|
|
547
|
-
await callControlAPI(`/prompts/${encodeURIComponent(promptKey)}/labels`, { method: "POST", body: JSON.stringify({ label: "prod", versionId: result.versionId }) }, profileOptions);
|
|
548
|
-
gate.promoted = true;
|
|
549
|
-
} else {
|
|
550
|
-
gate.promoteSkippedReason = outcome === "candidate" ? "no candidate versionId to promote" : `candidate did not strictly win (${outcome})`;
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
return gate;
|
|
554
|
-
}
|
|
555
|
-
function printHuman(result, outPath, gate) {
|
|
556
|
-
const w = (s) => process.stderr.write(s + `
|
|
557
|
-
`);
|
|
558
|
-
if (result.status !== "proposed") {
|
|
559
|
-
w(`No candidate produced (${result.status}): ${result.note}`);
|
|
560
|
-
return;
|
|
561
|
-
}
|
|
562
|
-
w(`Proposed candidate for '${result.promptKey}'`);
|
|
563
|
-
w(` new version: ${result.versionId} (unlabeled — not promoted)`);
|
|
564
|
-
w(` base version: ${result.baseVersionId ?? "(none)"}`);
|
|
565
|
-
w(` from run: built from ${result.dossierCount} failure dossier(s)`);
|
|
566
|
-
if (result.changelog)
|
|
567
|
-
w(` changelog: ${result.changelog}`);
|
|
568
|
-
if (result.risks.length)
|
|
569
|
-
w(` risks: ${result.risks.map((r) => `- ${r}`).join(`
|
|
570
|
-
`)}`);
|
|
571
|
-
if (outPath)
|
|
572
|
-
w(` written to: ${outPath}`);
|
|
573
|
-
if (result.lowN) {
|
|
574
|
-
w("");
|
|
575
|
-
w(` ⚠ low sample: ${result.lowN}`);
|
|
576
|
-
}
|
|
577
|
-
w("");
|
|
578
|
-
if (gate?.ran) {
|
|
579
|
-
w("Gate (automatic A/B of candidate vs baseline):");
|
|
580
|
-
w(` scope: ${gate.scopeNote}`);
|
|
581
|
-
w(` verdict: ${gate.verdict}`);
|
|
582
|
-
if (gate.runId)
|
|
583
|
-
w(` run id: ${gate.runId}`);
|
|
584
|
-
if (gate.promoted) {
|
|
585
|
-
w(` promoted: candidate won — promoted version ${result.versionId} to label 'prod'`);
|
|
586
|
-
} else if (gate.promoteSkippedReason) {
|
|
587
|
-
w(` promote: skipped (${gate.promoteSkippedReason})`);
|
|
588
|
-
}
|
|
589
|
-
w("");
|
|
590
|
-
if (!gate.promoted) {
|
|
591
|
-
w(` moda prompts promote ${result.promptKey} --label=prod --version=${result.versionId}`);
|
|
592
|
-
}
|
|
593
|
-
return;
|
|
594
|
-
}
|
|
595
|
-
if (gate && !gate.ran) {
|
|
596
|
-
w(`Gate not run: ${gate.scopeNote}`);
|
|
597
|
-
w("");
|
|
598
|
-
}
|
|
599
|
-
w("Next — gate on holdout cases the revision never saw, then promote if it wins:");
|
|
600
|
-
if (result.holdoutCaseIds.length) {
|
|
601
|
-
w(` holdout cases (${result.holdoutCaseIds.length}): ${result.holdoutCaseIds.join(", ")}`);
|
|
602
|
-
} else {
|
|
603
|
-
w(" (too few failures to reserve a holdout — gate on a fresh --auto-generate set)");
|
|
604
|
-
}
|
|
605
|
-
if (outPath) {
|
|
606
|
-
w(` moda prompts ab --baseline-key=${result.promptKey} --candidate=${outPath} --auto-generate`);
|
|
607
|
-
} else {
|
|
608
|
-
w(` # re-run with --out=<path.prompt.md> to materialize the candidate, then:`);
|
|
609
|
-
w(` moda prompts ab --baseline-key=${result.promptKey} --candidate=<path.prompt.md> --auto-generate`);
|
|
610
|
-
}
|
|
611
|
-
w(` # or: moda prompts propose ... --gate --promote-on-win (auto-A/B, promote on a strict win)`);
|
|
612
|
-
w(` moda prompts promote ${result.promptKey} --label=prod --version=${result.versionId}`);
|
|
613
|
-
}
|
|
614
|
-
function parsePositiveInt2(raw, fallback, max) {
|
|
615
|
-
const n = Number.parseInt(raw ?? "", 10);
|
|
616
|
-
if (!Number.isFinite(n) || n <= 0)
|
|
617
|
-
return fallback;
|
|
618
|
-
return Math.min(n, max);
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
// src/prompts.ts
|
|
622
|
-
var MODA_DIR = ".moda";
|
|
623
|
-
var MANIFEST_PATH = join(MODA_DIR, "prompts.yml");
|
|
624
|
-
var LOCK_PATH = join(MODA_DIR, "prompts.lock.json");
|
|
625
|
-
var DEFAULT_PROMPT_PATHS = [
|
|
626
|
-
"prompts/**/*.prompt.md",
|
|
627
|
-
"prompts/**/*.prompt.json",
|
|
628
|
-
"prompts/**/*.prompt.yaml",
|
|
629
|
-
"prompts/**/*.prompt.yml"
|
|
630
|
-
];
|
|
631
|
-
async function runPromptsCommand(input, legacyFlags, legacyProfileOptions) {
|
|
632
|
-
const context = Array.isArray(input) ? createCommandContext({
|
|
633
|
-
command: "prompts",
|
|
634
|
-
positional: input[0],
|
|
635
|
-
positionals: input,
|
|
636
|
-
flags: legacyFlags ?? {}
|
|
637
|
-
}, {
|
|
638
|
-
commandFlags: legacyFlags ?? {},
|
|
639
|
-
outputMode: legacyFlags?.agent === "true" ? "agent" : legacyFlags?.json === "true" ? "json" : "human",
|
|
640
|
-
env: legacyProfileOptions?.env
|
|
641
|
-
}) : input;
|
|
642
|
-
const positionals = context.positionals;
|
|
643
|
-
const flags = context.flags;
|
|
644
|
-
const profileOptions = legacyProfileOptions ?? { profile: context.profile, env: context.env };
|
|
645
|
-
const subcommand = positionals[0] || "status";
|
|
646
|
-
switch (subcommand) {
|
|
647
|
-
case "init":
|
|
648
|
-
await initPrompts({ silent: true });
|
|
649
|
-
context.output.writeData({
|
|
650
|
-
manifest: MANIFEST_PATH,
|
|
651
|
-
lockfile: LOCK_PATH,
|
|
652
|
-
promptPaths: readManifest().promptPaths
|
|
653
|
-
});
|
|
654
|
-
break;
|
|
655
|
-
case "status":
|
|
656
|
-
context.output.writeData(await getPromptStatus());
|
|
657
|
-
break;
|
|
658
|
-
case "diff":
|
|
659
|
-
context.output.writeData(await getPromptStatus());
|
|
660
|
-
break;
|
|
661
|
-
case "sync":
|
|
662
|
-
await syncPrompts(flags, profileOptions, context);
|
|
663
|
-
break;
|
|
664
|
-
case "promote":
|
|
665
|
-
await promotePrompt(positionals, flags, profileOptions, context);
|
|
666
|
-
break;
|
|
667
|
-
case "ab":
|
|
668
|
-
await runPromptAb(flags, profileOptions, context);
|
|
669
|
-
break;
|
|
670
|
-
case "propose":
|
|
671
|
-
await runPromptPropose(positionals, flags, profileOptions, context);
|
|
672
|
-
break;
|
|
673
|
-
default:
|
|
674
|
-
throw new Error(`Unknown prompts command '${subcommand}'. Use init, status, diff, sync, promote, ab, or propose.`);
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
async function initPrompts(options = {}) {
|
|
678
|
-
mkdirSync(MODA_DIR, { recursive: true });
|
|
679
|
-
if (!existsSync2(MANIFEST_PATH)) {
|
|
680
|
-
writeFileSync2(MANIFEST_PATH, [
|
|
681
|
-
"version: 1",
|
|
682
|
-
"prompt_paths:",
|
|
683
|
-
...DEFAULT_PROMPT_PATHS.map((path) => ` - "${path}"`),
|
|
684
|
-
""
|
|
685
|
-
].join(`
|
|
686
|
-
`));
|
|
687
|
-
}
|
|
688
|
-
if (!options.silent) {
|
|
689
|
-
printJson({
|
|
690
|
-
manifest: MANIFEST_PATH,
|
|
691
|
-
lockfile: LOCK_PATH,
|
|
692
|
-
promptPaths: readManifest().promptPaths
|
|
693
|
-
});
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
async function syncPrompts(flags, profileOptions, context) {
|
|
697
|
-
if (flags.watch === "true") {
|
|
698
|
-
await watchPromptSync(flags, profileOptions);
|
|
699
|
-
return;
|
|
700
|
-
}
|
|
701
|
-
await syncPromptsOnce(flags, profileOptions, context);
|
|
702
|
-
}
|
|
703
|
-
async function runPromptSync(flags, profileOptions) {
|
|
704
|
-
await initPrompts({ silent: true });
|
|
705
|
-
const prompts = discoverPrompts();
|
|
706
|
-
const sourceCommit = getGitCommit();
|
|
707
|
-
const manifestHash = hashFile(MANIFEST_PATH);
|
|
708
|
-
const dryRun = flags["dry-run"] === "true";
|
|
709
|
-
const response = await callControlAPI("/prompts/sync", {
|
|
710
|
-
method: "POST",
|
|
711
|
-
body: JSON.stringify({
|
|
712
|
-
prompts: prompts.map((prompt) => ({
|
|
713
|
-
...prompt,
|
|
714
|
-
sourceCommit,
|
|
715
|
-
manifestHash
|
|
716
|
-
})),
|
|
717
|
-
sourceCommit,
|
|
718
|
-
manifestHash,
|
|
719
|
-
dryRun
|
|
720
|
-
})
|
|
721
|
-
}, profileOptions);
|
|
722
|
-
if (!dryRun) {
|
|
723
|
-
writeLock(prompts, response.synced ?? []);
|
|
724
|
-
}
|
|
725
|
-
return { synced: response.synced ?? [], dryRun };
|
|
726
|
-
}
|
|
727
|
-
async function syncPromptsOnce(flags, profileOptions, context) {
|
|
728
|
-
const result = await runPromptSync(flags, profileOptions);
|
|
729
|
-
const payload = {
|
|
730
|
-
synced: result.synced,
|
|
731
|
-
lockfile: result.dryRun ? undefined : LOCK_PATH
|
|
732
|
-
};
|
|
733
|
-
if (context)
|
|
734
|
-
context.output.writeData(payload);
|
|
735
|
-
else
|
|
736
|
-
printJson(payload);
|
|
737
|
-
}
|
|
738
|
-
async function watchPromptSync(flags, profileOptions) {
|
|
739
|
-
console.error("Watching prompt files. Press Ctrl+C to stop.");
|
|
740
|
-
let lastSignature = "";
|
|
741
|
-
const intervalMs = Math.max(1000, Number(flags.interval ?? 2500));
|
|
742
|
-
while (true) {
|
|
743
|
-
const status = await getPromptStatus();
|
|
744
|
-
const signature = stableJson(status.prompts);
|
|
745
|
-
if (signature !== lastSignature) {
|
|
746
|
-
lastSignature = signature;
|
|
747
|
-
if (status.changed > 0 || status.new > 0 || status.deleted > 0) {
|
|
748
|
-
await syncPromptsOnce({ ...flags, watch: "false" }, profileOptions);
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
await new Promise((resolve4) => setTimeout(resolve4, intervalMs));
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
async function promotePrompt(positionals, flags, profileOptions, context) {
|
|
755
|
-
const key = positionals[1];
|
|
756
|
-
const label = flags.label;
|
|
757
|
-
const versionId = flags.version || flags["version-id"];
|
|
758
|
-
if (!key)
|
|
759
|
-
throw new Error("Usage: moda prompts promote <prompt_key> --label=prod --version=<version_id>");
|
|
760
|
-
if (!label)
|
|
761
|
-
throw new Error("Missing required flag: --label=dev|staging|prod");
|
|
762
|
-
if (!versionId)
|
|
763
|
-
throw new Error("Missing required flag: --version=<version_id>");
|
|
764
|
-
const response = await callControlAPI(`/prompts/${encodeURIComponent(key)}/labels`, {
|
|
765
|
-
method: "POST",
|
|
766
|
-
body: JSON.stringify({ label, versionId })
|
|
767
|
-
}, profileOptions);
|
|
768
|
-
context.output.writeData(response);
|
|
769
|
-
}
|
|
770
|
-
async function getPromptStatus() {
|
|
771
|
-
await initPrompts({ silent: true });
|
|
772
|
-
const prompts = discoverPrompts();
|
|
773
|
-
const lock = readLock();
|
|
774
|
-
const rows = prompts.map((prompt) => {
|
|
775
|
-
const locked = lock?.prompts[prompt.key];
|
|
776
|
-
const state = !locked ? "new" : locked.contentHash === prompt.contentHash ? "unchanged" : "changed";
|
|
777
|
-
return {
|
|
778
|
-
key: prompt.key,
|
|
779
|
-
sourcePath: prompt.sourcePath,
|
|
780
|
-
contentHash: prompt.contentHash,
|
|
781
|
-
lockedHash: locked?.contentHash ?? null,
|
|
782
|
-
versionId: locked?.versionId ?? null,
|
|
783
|
-
state
|
|
784
|
-
};
|
|
785
|
-
});
|
|
786
|
-
const discoveredKeys = new Set(prompts.map((prompt) => prompt.key));
|
|
787
|
-
for (const [key, locked] of Object.entries(lock?.prompts ?? {})) {
|
|
788
|
-
if (!discoveredKeys.has(key)) {
|
|
789
|
-
rows.push({
|
|
790
|
-
key,
|
|
791
|
-
sourcePath: locked.sourcePath,
|
|
792
|
-
contentHash: null,
|
|
793
|
-
lockedHash: locked.contentHash,
|
|
794
|
-
versionId: locked.versionId ?? null,
|
|
795
|
-
state: "deleted"
|
|
796
|
-
});
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
return {
|
|
800
|
-
manifest: MANIFEST_PATH,
|
|
801
|
-
lockfile: LOCK_PATH,
|
|
802
|
-
total: rows.length,
|
|
803
|
-
changed: rows.filter((row) => row.state === "changed").length,
|
|
804
|
-
new: rows.filter((row) => row.state === "new").length,
|
|
805
|
-
deleted: rows.filter((row) => row.state === "deleted").length,
|
|
806
|
-
prompts: rows
|
|
807
|
-
};
|
|
808
|
-
}
|
|
809
|
-
function discoverPrompts() {
|
|
810
|
-
const manifest = readManifest();
|
|
811
|
-
const rules = manifest.promptPaths.map((pattern) => {
|
|
812
|
-
const suffixMatch = pattern.match(/\.prompt\.(md|json|ya?ml)$/i);
|
|
813
|
-
if (!suffixMatch)
|
|
814
|
-
return null;
|
|
815
|
-
const suffix = `.prompt.${suffixMatch[1]}`;
|
|
816
|
-
const globIndex = pattern.search(/[*?[]/);
|
|
817
|
-
const baseRaw = globIndex === -1 ? dirname(pattern) : pattern.slice(0, globIndex);
|
|
818
|
-
const base = baseRaw.replace(/[\\/]+$/, "") || ".";
|
|
819
|
-
const baseAbs = isAbsolute3(base) ? base : resolve3(process.cwd(), base);
|
|
820
|
-
return { baseAbs, suffix };
|
|
821
|
-
}).filter((rule) => Boolean(rule));
|
|
822
|
-
const effectiveRules = rules.length ? rules : [
|
|
823
|
-
{ baseAbs: process.cwd(), suffix: ".prompt.md" },
|
|
824
|
-
{ baseAbs: process.cwd(), suffix: ".prompt.json" }
|
|
825
|
-
];
|
|
826
|
-
const discovered = new Set;
|
|
827
|
-
for (const { baseAbs, suffix } of effectiveRules) {
|
|
828
|
-
if (!existsSync2(baseAbs))
|
|
829
|
-
continue;
|
|
830
|
-
for (const file of walk(baseAbs)) {
|
|
831
|
-
if (file.endsWith(suffix))
|
|
832
|
-
discovered.add(file);
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
return [...discovered].sort().map(parsePromptFile);
|
|
836
|
-
}
|
|
837
|
-
function parsePromptFile(absPath) {
|
|
838
|
-
const sourcePath = relative(process.cwd(), absPath);
|
|
839
|
-
const raw = readFileSync(absPath, "utf8");
|
|
840
|
-
if (absPath.endsWith(".json")) {
|
|
841
|
-
const parsed = JSON.parse(raw);
|
|
842
|
-
const key2 = parsed.key || keyFromPath(sourcePath);
|
|
843
|
-
const content = parsed.content || JSON.stringify(parsed.messages ?? [], null, 2);
|
|
844
|
-
return normalizePrompt({ ...parsed, key: key2, name: parsed.name || key2, sourcePath, content });
|
|
845
|
-
}
|
|
846
|
-
const { frontmatter, body } = parseFrontmatter(raw);
|
|
847
|
-
const key = frontmatter.key || keyFromPath(sourcePath);
|
|
848
|
-
return normalizePrompt({
|
|
849
|
-
key,
|
|
850
|
-
name: frontmatter.name || key,
|
|
851
|
-
description: frontmatter.description,
|
|
852
|
-
category: frontmatter.category,
|
|
853
|
-
systemPrompt: frontmatter.system_prompt || frontmatter.systemPrompt,
|
|
854
|
-
modelConfig: frontmatter.model ? { model: frontmatter.model } : undefined,
|
|
855
|
-
sourcePath,
|
|
856
|
-
content: body.trim()
|
|
857
|
-
});
|
|
858
|
-
}
|
|
859
|
-
function normalizePrompt(prompt) {
|
|
860
|
-
const normalized = stableJson({
|
|
861
|
-
key: prompt.key,
|
|
862
|
-
content: prompt.content,
|
|
863
|
-
systemPrompt: prompt.systemPrompt ?? null,
|
|
864
|
-
messages: prompt.messages ?? [],
|
|
865
|
-
variables: prompt.variables ?? [],
|
|
866
|
-
variablesSchema: prompt.variablesSchema ?? {},
|
|
867
|
-
modelConfig: prompt.modelConfig ?? {},
|
|
868
|
-
toolRefs: prompt.toolRefs ?? [],
|
|
869
|
-
responseSchema: prompt.responseSchema ?? null
|
|
870
|
-
});
|
|
871
|
-
return {
|
|
872
|
-
key: prompt.key,
|
|
873
|
-
name: prompt.name || prompt.key,
|
|
874
|
-
description: prompt.description,
|
|
875
|
-
sourcePath: prompt.sourcePath,
|
|
876
|
-
content: prompt.content,
|
|
877
|
-
systemPrompt: prompt.systemPrompt,
|
|
878
|
-
messages: prompt.messages,
|
|
879
|
-
variables: prompt.variables,
|
|
880
|
-
variablesSchema: prompt.variablesSchema,
|
|
881
|
-
modelConfig: prompt.modelConfig,
|
|
882
|
-
toolRefs: prompt.toolRefs,
|
|
883
|
-
responseSchema: prompt.responseSchema,
|
|
884
|
-
contentHash: sha256(normalized)
|
|
885
|
-
};
|
|
886
|
-
}
|
|
887
|
-
function readManifest() {
|
|
888
|
-
if (!existsSync2(MANIFEST_PATH)) {
|
|
889
|
-
return { promptPaths: DEFAULT_PROMPT_PATHS };
|
|
890
|
-
}
|
|
891
|
-
const text = readFileSync(MANIFEST_PATH, "utf8");
|
|
892
|
-
const promptPaths = [...text.matchAll(/^\s*-\s*["']?([^"'\n]+)["']?\s*$/gm)].map((match) => match[1]).filter((path) => path.includes(".prompt."));
|
|
893
|
-
return { promptPaths: promptPaths.length ? promptPaths : DEFAULT_PROMPT_PATHS };
|
|
894
|
-
}
|
|
895
|
-
function readPromptLock() {
|
|
896
|
-
return readLock();
|
|
897
|
-
}
|
|
898
|
-
function parsePromptFileForAb(absPath) {
|
|
899
|
-
const parsed = parsePromptFile(absPath);
|
|
900
|
-
return { key: parsed.key, content: parsed.content };
|
|
901
|
-
}
|
|
902
|
-
function readLock() {
|
|
903
|
-
if (!existsSync2(LOCK_PATH))
|
|
904
|
-
return null;
|
|
905
|
-
return JSON.parse(readFileSync(LOCK_PATH, "utf8"));
|
|
906
|
-
}
|
|
907
|
-
function writeLock(prompts, synced) {
|
|
908
|
-
const syncedByKey = new Map(synced.map((item) => [item.key, item]));
|
|
909
|
-
const lock = {
|
|
910
|
-
version: 1,
|
|
911
|
-
generatedAt: new Date().toISOString(),
|
|
912
|
-
prompts: Object.fromEntries(prompts.map((prompt) => {
|
|
913
|
-
const result = syncedByKey.get(prompt.key);
|
|
914
|
-
return [prompt.key, {
|
|
915
|
-
key: prompt.key,
|
|
916
|
-
sourcePath: prompt.sourcePath,
|
|
917
|
-
contentHash: prompt.contentHash,
|
|
918
|
-
promptId: result?.promptId,
|
|
919
|
-
versionId: result?.versionId
|
|
920
|
-
}];
|
|
921
|
-
}))
|
|
922
|
-
};
|
|
923
|
-
mkdirSync(dirname(LOCK_PATH), { recursive: true });
|
|
924
|
-
writeFileSync2(LOCK_PATH, `${JSON.stringify(lock, null, 2)}
|
|
925
|
-
`);
|
|
926
|
-
}
|
|
927
|
-
function walk(dir) {
|
|
928
|
-
let entries;
|
|
929
|
-
try {
|
|
930
|
-
entries = readdirSync(dir);
|
|
931
|
-
} catch {
|
|
932
|
-
return [];
|
|
933
|
-
}
|
|
934
|
-
const files = [];
|
|
935
|
-
for (const entry of entries) {
|
|
936
|
-
if (entry === "node_modules" || entry === ".git" || entry === "dist" || entry === "build")
|
|
937
|
-
continue;
|
|
938
|
-
const fullPath = join(dir, entry);
|
|
939
|
-
let stat;
|
|
940
|
-
try {
|
|
941
|
-
stat = lstatSync(fullPath);
|
|
942
|
-
} catch {
|
|
943
|
-
continue;
|
|
944
|
-
}
|
|
945
|
-
if (stat.isSymbolicLink())
|
|
946
|
-
continue;
|
|
947
|
-
if (stat.isDirectory())
|
|
948
|
-
files.push(...walk(fullPath));
|
|
949
|
-
else if (stat.isFile())
|
|
950
|
-
files.push(fullPath);
|
|
951
|
-
}
|
|
952
|
-
return files;
|
|
953
|
-
}
|
|
954
|
-
function parseFrontmatter(raw) {
|
|
955
|
-
if (!raw.startsWith("---"))
|
|
956
|
-
return { frontmatter: {}, body: raw };
|
|
957
|
-
const end = raw.indexOf(`
|
|
958
|
-
---`, 3);
|
|
959
|
-
if (end === -1)
|
|
960
|
-
return { frontmatter: {}, body: raw };
|
|
961
|
-
const frontmatterText = raw.slice(3, end).trim();
|
|
962
|
-
const frontmatter = Object.fromEntries(frontmatterText.split(`
|
|
963
|
-
`).map((line) => line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/)).filter((match) => Boolean(match)).map((match) => [match[1], match[2].replace(/^["']|["']$/g, "")]));
|
|
964
|
-
return { frontmatter, body: raw.slice(end + 4) };
|
|
965
|
-
}
|
|
966
|
-
function keyFromPath(path) {
|
|
967
|
-
return path.replace(/^prompts\//, "").replace(/\.prompt\.(md|json|ya?ml)$/i, "").replace(/[\\/]/g, ".");
|
|
968
|
-
}
|
|
969
|
-
function hashFile(path) {
|
|
970
|
-
return existsSync2(path) ? sha256(readFileSync(path, "utf8")) : "";
|
|
971
|
-
}
|
|
972
|
-
function getGitCommit() {
|
|
973
|
-
try {
|
|
974
|
-
return execFileSync("git", ["rev-parse", "HEAD"], {
|
|
975
|
-
encoding: "utf8",
|
|
976
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
977
|
-
}).trim();
|
|
978
|
-
} catch {
|
|
979
|
-
return;
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
function stableJson(value) {
|
|
983
|
-
return JSON.stringify(sortJson(value));
|
|
984
|
-
}
|
|
985
|
-
function sortJson(value) {
|
|
986
|
-
if (Array.isArray(value))
|
|
987
|
-
return value.map(sortJson);
|
|
988
|
-
if (value && typeof value === "object") {
|
|
989
|
-
return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => [key, sortJson(item)]));
|
|
990
|
-
}
|
|
991
|
-
return value;
|
|
992
|
-
}
|
|
993
|
-
function sha256(input) {
|
|
994
|
-
return createHash("sha256").update(input).digest("hex");
|
|
995
|
-
}
|
|
996
|
-
function printJson(value) {
|
|
997
|
-
console.log(JSON.stringify(value, null, 2));
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
// src/init/rules.ts
|
|
1001
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1002
|
-
import { dirname as dirname2, join as join2 } from "node:path";
|
|
1003
|
-
import { fileURLToPath } from "node:url";
|
|
1004
|
-
import { confirm } from "@inquirer/prompts";
|
|
1005
|
-
var PACKAGE_ROOT = join2(dirname2(fileURLToPath(import.meta.url)), "..", "..");
|
|
1006
|
-
var SKILLS_SOURCE_DIR = join2(PACKAGE_ROOT, "skills");
|
|
1007
|
-
var SKILLS = [
|
|
1008
|
-
{
|
|
1009
|
-
id: "moda-cli",
|
|
1010
|
-
label: "moda-cli — let coding agents query Moda analytics via the CLI"
|
|
1011
|
-
}
|
|
1012
|
-
];
|
|
1013
|
-
var ADAPTERS = {
|
|
1014
|
-
"Claude Code": {
|
|
1015
|
-
name: "Claude Code",
|
|
1016
|
-
targetFor: (skillId) => ({
|
|
1017
|
-
dir: join2(".claude", "skills", skillId),
|
|
1018
|
-
filename: "SKILL.md"
|
|
1019
|
-
}),
|
|
1020
|
-
transform: (content) => content
|
|
1021
|
-
},
|
|
1022
|
-
Cursor: {
|
|
1023
|
-
name: "Cursor",
|
|
1024
|
-
targetFor: (skillId) => ({
|
|
1025
|
-
dir: join2(".cursor", "rules"),
|
|
1026
|
-
filename: `${skillId}.mdc`
|
|
1027
|
-
}),
|
|
1028
|
-
transform: claudeSkillToCursorRule
|
|
1029
|
-
}
|
|
1030
|
-
};
|
|
1031
|
-
var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
|
|
1032
|
-
var DESCRIPTION_RE = /^description:\s*(.+?)\s*$/m;
|
|
1033
|
-
function claudeSkillToCursorRule(skillContent) {
|
|
1034
|
-
const match = FRONTMATTER_RE.exec(skillContent);
|
|
1035
|
-
if (!match) {
|
|
1036
|
-
return skillContent;
|
|
1037
|
-
}
|
|
1038
|
-
const [, frontmatter, body] = match;
|
|
1039
|
-
const descMatch = DESCRIPTION_RE.exec(frontmatter);
|
|
1040
|
-
const description = descMatch?.[1].trim() ?? "";
|
|
1041
|
-
const cursorFrontmatter = `---
|
|
1042
|
-
` + `description: ${JSON.stringify(description)}
|
|
1043
|
-
` + `alwaysApply: false
|
|
1044
|
-
` + `---
|
|
1045
|
-
`;
|
|
1046
|
-
return `${cursorFrontmatter}
|
|
1047
|
-
${body.replace(/^\s+/, "")}`;
|
|
1048
|
-
}
|
|
1049
|
-
function loadSkillSource(skillId) {
|
|
1050
|
-
const path = join2(SKILLS_SOURCE_DIR, skillId, "SKILL.md");
|
|
1051
|
-
if (!existsSync3(path)) {
|
|
1052
|
-
throw new Error(`Skill source missing: ${path}`);
|
|
1053
|
-
}
|
|
1054
|
-
return readFileSync2(path, "utf8");
|
|
1055
|
-
}
|
|
1056
|
-
function installSkillForAgent(skill, adapter, content) {
|
|
1057
|
-
const { dir, filename } = adapter.targetFor(skill.id);
|
|
1058
|
-
const dirPath = join2(process.cwd(), dir);
|
|
1059
|
-
const filePath = join2(dirPath, filename);
|
|
1060
|
-
const relativePath = join2(dir, filename);
|
|
1061
|
-
try {
|
|
1062
|
-
mkdirSync2(dirPath, { recursive: true });
|
|
1063
|
-
if (existsSync3(filePath)) {
|
|
1064
|
-
return { agent: adapter.name, relativePath, status: "exists" };
|
|
1065
|
-
}
|
|
1066
|
-
writeFileSync3(filePath, adapter.transform(content));
|
|
1067
|
-
return { agent: adapter.name, relativePath, status: "wrote" };
|
|
1068
|
-
} catch (err) {
|
|
1069
|
-
return {
|
|
1070
|
-
agent: adapter.name,
|
|
1071
|
-
relativePath,
|
|
1072
|
-
status: "failed",
|
|
1073
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1074
|
-
};
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
async function installRules(detectedAgents, options = {}) {
|
|
1078
|
-
const adapters = detectedAgents.map((a) => ADAPTERS[a]).filter((a) => a !== undefined);
|
|
1079
|
-
if (adapters.length === 0) {
|
|
1080
|
-
if (!options.silent)
|
|
1081
|
-
console.log(" No supported coding agents detected");
|
|
1082
|
-
return;
|
|
1083
|
-
}
|
|
1084
|
-
const agentList = adapters.map((a) => a.name).join(", ");
|
|
1085
|
-
const skillList = SKILLS.map((s) => `
|
|
1086
|
-
• ${s.label}`).join("");
|
|
1087
|
-
const shouldInstall = options.yes ? true : await confirm({
|
|
1088
|
-
message: `Install Moda skills for ${agentList}?${skillList}
|
|
1089
|
-
Continue?`,
|
|
1090
|
-
default: true
|
|
1091
|
-
});
|
|
1092
|
-
if (!shouldInstall) {
|
|
1093
|
-
if (!options.silent)
|
|
1094
|
-
console.log(" Skipped");
|
|
1095
|
-
return;
|
|
1096
|
-
}
|
|
1097
|
-
let wrote = 0;
|
|
1098
|
-
let skipped = 0;
|
|
1099
|
-
let failed = 0;
|
|
1100
|
-
for (const skill of SKILLS) {
|
|
1101
|
-
let content;
|
|
1102
|
-
try {
|
|
1103
|
-
content = loadSkillSource(skill.id);
|
|
1104
|
-
} catch (err) {
|
|
1105
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1106
|
-
console.error(` Warning: ${message}`);
|
|
1107
|
-
failed += adapters.length;
|
|
1108
|
-
continue;
|
|
1109
|
-
}
|
|
1110
|
-
for (const adapter of adapters) {
|
|
1111
|
-
const outcome = installSkillForAgent(skill, adapter, content);
|
|
1112
|
-
switch (outcome.status) {
|
|
1113
|
-
case "wrote":
|
|
1114
|
-
if (!options.silent)
|
|
1115
|
-
console.log(` ${outcome.agent}: wrote ${outcome.relativePath}`);
|
|
1116
|
-
wrote++;
|
|
1117
|
-
break;
|
|
1118
|
-
case "exists":
|
|
1119
|
-
if (!options.silent) {
|
|
1120
|
-
console.log(` ${outcome.agent}: ${outcome.relativePath} already exists — skipping`);
|
|
1121
|
-
}
|
|
1122
|
-
skipped++;
|
|
1123
|
-
break;
|
|
1124
|
-
case "failed":
|
|
1125
|
-
if (!options.silent) {
|
|
1126
|
-
console.error(` ${outcome.agent}: failed to write ${outcome.relativePath} — ${outcome.error}`);
|
|
1127
|
-
}
|
|
1128
|
-
failed++;
|
|
1129
|
-
break;
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
const summary = [
|
|
1134
|
-
wrote && `${wrote} written`,
|
|
1135
|
-
skipped && `${skipped} already present`,
|
|
1136
|
-
failed && `${failed} failed`
|
|
1137
|
-
].filter(Boolean).join(", ");
|
|
1138
|
-
if (summary) {
|
|
1139
|
-
if (!options.silent)
|
|
1140
|
-
console.log(` Done — ${summary}`);
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
// src/init/mcp.ts
|
|
1145
|
-
import { existsSync as existsSync4 } from "node:fs";
|
|
1146
|
-
import { join as join3 } from "node:path";
|
|
1147
|
-
import { execSync } from "node:child_process";
|
|
1148
|
-
function isOnPath(cmd) {
|
|
1149
|
-
try {
|
|
1150
|
-
execSync(`which ${cmd}`, { stdio: "pipe" });
|
|
1151
|
-
return true;
|
|
1152
|
-
} catch {
|
|
1153
|
-
return false;
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
|
-
function detectAgents() {
|
|
1157
|
-
const cwd = process.cwd();
|
|
1158
|
-
const agents = [];
|
|
1159
|
-
if (existsSync4(join3(cwd, ".claude")) || isOnPath("claude")) {
|
|
1160
|
-
agents.push("Claude Code");
|
|
1161
|
-
}
|
|
1162
|
-
if (existsSync4(join3(cwd, ".cursor"))) {
|
|
1163
|
-
agents.push("Cursor");
|
|
1164
|
-
}
|
|
1165
|
-
return agents;
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
// src/skills.ts
|
|
1169
|
-
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
1170
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
1171
|
-
import {
|
|
1172
|
-
existsSync as existsSync6,
|
|
1173
|
-
lstatSync as lstatSync2,
|
|
1174
|
-
mkdirSync as mkdirSync4,
|
|
1175
|
-
readdirSync as readdirSync3,
|
|
1176
|
-
readFileSync as readFileSync4,
|
|
1177
|
-
writeFileSync as writeFileSync5
|
|
1178
|
-
} from "node:fs";
|
|
1179
|
-
import { basename, dirname as dirname4, join as join5, relative as relative3 } from "node:path";
|
|
1180
|
-
|
|
1181
|
-
// src/skills-manifest.ts
|
|
1182
|
-
import {
|
|
1183
|
-
existsSync as existsSync5,
|
|
1184
|
-
mkdirSync as mkdirSync3,
|
|
1185
|
-
readdirSync as readdirSync2,
|
|
1186
|
-
readFileSync as readFileSync3,
|
|
1187
|
-
rmdirSync,
|
|
1188
|
-
unlinkSync,
|
|
1189
|
-
writeFileSync as writeFileSync4
|
|
1190
|
-
} from "node:fs";
|
|
1191
|
-
import { dirname as dirname3, isAbsolute as isAbsolute4, join as join4, normalize, relative as relative2, resolve as resolve4 } from "node:path";
|
|
1192
|
-
var SKILLS_MANIFEST_PATH = join4(".moda", "skills.yml");
|
|
1193
|
-
var SKILL_SOURCE_HARNESS = "moda";
|
|
1194
|
-
var SKILL_SOURCE_INTEGRATION_BUNDLED = "integration-bundled";
|
|
1195
|
-
function readSkillsManifest(cwd) {
|
|
1196
|
-
const manifestPath = join4(cwd, SKILLS_MANIFEST_PATH);
|
|
1197
|
-
if (!existsSync5(manifestPath))
|
|
1198
|
-
return null;
|
|
1199
|
-
let text = "";
|
|
1200
|
-
try {
|
|
1201
|
-
text = readFileSync3(manifestPath, "utf8");
|
|
1202
|
-
} catch {
|
|
1203
|
-
return null;
|
|
1204
|
-
}
|
|
1205
|
-
return {
|
|
1206
|
-
tenantId: readYamlScalar(text, "tenant_id"),
|
|
1207
|
-
generationRunId: readYamlScalar(text, "generation_run_id"),
|
|
1208
|
-
generationVersion: Number(readYamlScalar(text, "generation_version") || 0),
|
|
1209
|
-
releaseKey: readYamlScalar(text, "release_key"),
|
|
1210
|
-
statusFilter: readYamlScalar(text, "status_filter"),
|
|
1211
|
-
releasedAt: readYamlScalar(text, "released_at"),
|
|
1212
|
-
managedPaths: readManagedPaths(text),
|
|
1213
|
-
records: readSkillRecords(text)
|
|
1214
|
-
};
|
|
1215
|
-
}
|
|
1216
|
-
function manifestHeaderFrom(manifest) {
|
|
1217
|
-
return {
|
|
1218
|
-
tenantId: manifest?.tenantId ?? "",
|
|
1219
|
-
releaseKey: manifest?.releaseKey ?? "",
|
|
1220
|
-
statusFilter: manifest?.statusFilter ?? "",
|
|
1221
|
-
generationRunId: manifest?.generationRunId ?? "",
|
|
1222
|
-
generationVersion: manifest?.generationVersion ?? 0,
|
|
1223
|
-
releasedAt: manifest?.releasedAt ?? ""
|
|
1224
|
-
};
|
|
1225
|
-
}
|
|
1226
|
-
function writeSkillsManifest(cwd, header, records) {
|
|
1227
|
-
const manifestPath = join4(cwd, SKILLS_MANIFEST_PATH);
|
|
1228
|
-
const managedPaths = [...new Set(records.flatMap((record) => record.paths))].sort();
|
|
1229
|
-
const lines = [
|
|
1230
|
-
"version: 1",
|
|
1231
|
-
"source: moda",
|
|
1232
|
-
`tenant_id: ${yamlQuote(header.tenantId)}`,
|
|
1233
|
-
`release_key: ${yamlQuote(header.releaseKey)}`,
|
|
1234
|
-
`status_filter: ${yamlQuote(header.statusFilter)}`,
|
|
1235
|
-
`generation_run_id: ${yamlQuote(header.generationRunId)}`,
|
|
1236
|
-
`generation_version: ${header.generationVersion}`,
|
|
1237
|
-
`released_at: ${yamlQuote(header.releasedAt)}`,
|
|
1238
|
-
`installed_at: ${yamlQuote(new Date().toISOString())}`,
|
|
1239
|
-
"managed_paths:",
|
|
1240
|
-
...managedPaths.map((path) => ` - ${yamlQuote(path)}`),
|
|
1241
|
-
"skills:",
|
|
1242
|
-
...records.flatMap((record) => [
|
|
1243
|
-
` - skill_id: ${yamlQuote(record.skillId)}`,
|
|
1244
|
-
` content_hash: ${yamlQuote(record.contentHash)}`,
|
|
1245
|
-
` source: ${yamlQuote(record.source)}`,
|
|
1246
|
-
" paths:",
|
|
1247
|
-
...record.paths.map((path) => ` - ${yamlQuote(path)}`)
|
|
1248
|
-
]),
|
|
1249
|
-
""
|
|
1250
|
-
];
|
|
1251
|
-
mkdirSync3(dirname3(manifestPath), { recursive: true });
|
|
1252
|
-
writeFileSync4(manifestPath, lines.join(`
|
|
1253
|
-
`));
|
|
1254
|
-
}
|
|
1255
|
-
function upsertSkillManifestRecord(cwd, record) {
|
|
1256
|
-
const manifest = readSkillsManifest(cwd);
|
|
1257
|
-
const records = (manifest?.records ?? []).filter((existing) => !(existing.skillId === record.skillId && existing.source === record.source));
|
|
1258
|
-
records.push(record);
|
|
1259
|
-
writeSkillsManifest(cwd, manifestHeaderFrom(manifest), records);
|
|
1260
|
-
}
|
|
1261
|
-
function pruneManagedSkillPaths(cwd, managedPaths, log) {
|
|
1262
|
-
let pruned = 0;
|
|
1263
|
-
for (const rawPath of [...new Set(managedPaths)]) {
|
|
1264
|
-
const safePath = safeManagedRelativePath(rawPath);
|
|
1265
|
-
if (!safePath)
|
|
1266
|
-
continue;
|
|
1267
|
-
const absolutePath = absoluteManagedPath(cwd, safePath);
|
|
1268
|
-
if (!absolutePath || !existsSync5(absolutePath))
|
|
1269
|
-
continue;
|
|
1270
|
-
try {
|
|
1271
|
-
unlinkSync(absolutePath);
|
|
1272
|
-
pruned++;
|
|
1273
|
-
log(` pruned ${safePath}`);
|
|
1274
|
-
pruneEmptyClaudeSkillDir(cwd, safePath);
|
|
1275
|
-
} catch {}
|
|
1276
|
-
}
|
|
1277
|
-
return pruned;
|
|
1278
|
-
}
|
|
1279
|
-
function pruneEmptyClaudeSkillDir(cwd, relativePath) {
|
|
1280
|
-
if (!relativePath.startsWith(".claude/skills/") || !relativePath.endsWith("/SKILL.md"))
|
|
1281
|
-
return;
|
|
1282
|
-
const dir = dirname3(join4(cwd, relativePath));
|
|
1283
|
-
try {
|
|
1284
|
-
if (existsSync5(dir) && readdirSync2(dir).length === 0) {
|
|
1285
|
-
rmdirSync(dir);
|
|
1286
|
-
}
|
|
1287
|
-
} catch {}
|
|
1288
|
-
}
|
|
1289
|
-
function absoluteManagedPath(cwd, relativePath) {
|
|
1290
|
-
const abs = resolve4(cwd, relativePath);
|
|
1291
|
-
const root = resolve4(cwd);
|
|
1292
|
-
const rel = relative2(root, abs);
|
|
1293
|
-
if (rel.startsWith("..") || isAbsolute4(rel))
|
|
1294
|
-
return null;
|
|
1295
|
-
return abs;
|
|
1296
|
-
}
|
|
1297
|
-
function safeManagedRelativePath(rawPath) {
|
|
1298
|
-
if (!rawPath || rawPath.includes("\x00") || isAbsolute4(rawPath))
|
|
1299
|
-
return null;
|
|
1300
|
-
const normalized = normalize(rawPath).replace(/\\/g, "/");
|
|
1301
|
-
if (normalized.startsWith("../") || normalized === ".." || normalized.includes("/../"))
|
|
1302
|
-
return null;
|
|
1303
|
-
const isClaudeSkill = normalized.startsWith(".claude/skills/") && normalized.endsWith("/SKILL.md");
|
|
1304
|
-
const isCursorRule = normalized.startsWith(".cursor/rules/") && normalized.endsWith(".mdc");
|
|
1305
|
-
return isClaudeSkill || isCursorRule ? normalized : null;
|
|
1306
|
-
}
|
|
1307
|
-
function readYamlScalar(text, key) {
|
|
1308
|
-
const match = text.match(new RegExp(`^${key}:\\s*(.*?)\\s*$`, "m"));
|
|
1309
|
-
return match ? parseYamlScalar(match[1]) : "";
|
|
1310
|
-
}
|
|
1311
|
-
function readSkillRecords(text) {
|
|
1312
|
-
const records = [];
|
|
1313
|
-
let inSkills = false;
|
|
1314
|
-
let current = null;
|
|
1315
|
-
let inPaths = false;
|
|
1316
|
-
for (const line of text.split(/\r?\n/)) {
|
|
1317
|
-
if (line.trim() === "skills:") {
|
|
1318
|
-
inSkills = true;
|
|
1319
|
-
continue;
|
|
1320
|
-
}
|
|
1321
|
-
if (!inSkills)
|
|
1322
|
-
continue;
|
|
1323
|
-
if (line.length > 0 && !line.startsWith(" "))
|
|
1324
|
-
break;
|
|
1325
|
-
const skillIdMatch = line.match(/^ {2}- skill_id:\s*(.*?)\s*$/);
|
|
1326
|
-
if (skillIdMatch) {
|
|
1327
|
-
if (current)
|
|
1328
|
-
records.push(current);
|
|
1329
|
-
current = {
|
|
1330
|
-
skillId: parseYamlScalar(skillIdMatch[1]),
|
|
1331
|
-
contentHash: "",
|
|
1332
|
-
source: SKILL_SOURCE_HARNESS,
|
|
1333
|
-
paths: []
|
|
1334
|
-
};
|
|
1335
|
-
inPaths = false;
|
|
1336
|
-
continue;
|
|
1337
|
-
}
|
|
1338
|
-
if (!current)
|
|
1339
|
-
continue;
|
|
1340
|
-
const hashMatch = line.match(/^ {4}content_hash:\s*(.*?)\s*$/);
|
|
1341
|
-
if (hashMatch) {
|
|
1342
|
-
current.contentHash = parseYamlScalar(hashMatch[1]);
|
|
1343
|
-
continue;
|
|
1344
|
-
}
|
|
1345
|
-
const sourceMatch = line.match(/^ {4}source:\s*(.*?)\s*$/);
|
|
1346
|
-
if (sourceMatch) {
|
|
1347
|
-
current.source = parseYamlScalar(sourceMatch[1]) || SKILL_SOURCE_HARNESS;
|
|
1348
|
-
continue;
|
|
1349
|
-
}
|
|
1350
|
-
if (/^ {4}paths:\s*$/.test(line)) {
|
|
1351
|
-
inPaths = true;
|
|
1352
|
-
continue;
|
|
1353
|
-
}
|
|
1354
|
-
const pathMatch = inPaths ? line.match(/^ {6}-\s*(.*?)\s*$/) : null;
|
|
1355
|
-
if (pathMatch) {
|
|
1356
|
-
const value = parseYamlScalar(pathMatch[1]);
|
|
1357
|
-
if (value)
|
|
1358
|
-
current.paths.push(value);
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
|
-
if (current)
|
|
1362
|
-
records.push(current);
|
|
1363
|
-
return records;
|
|
1364
|
-
}
|
|
1365
|
-
function readManagedPaths(text) {
|
|
1366
|
-
const paths = [];
|
|
1367
|
-
let inManagedPaths = false;
|
|
1368
|
-
for (const line of text.split(/\r?\n/)) {
|
|
1369
|
-
if (line.trim() === "managed_paths:") {
|
|
1370
|
-
inManagedPaths = true;
|
|
1371
|
-
continue;
|
|
1372
|
-
}
|
|
1373
|
-
if (inManagedPaths && line.length > 0 && !line.startsWith(" "))
|
|
1374
|
-
break;
|
|
1375
|
-
if (!inManagedPaths)
|
|
1376
|
-
continue;
|
|
1377
|
-
const match = line.match(/^\s*-\s*(.*?)\s*$/);
|
|
1378
|
-
if (match)
|
|
1379
|
-
paths.push(parseYamlScalar(match[1]));
|
|
1380
|
-
}
|
|
1381
|
-
return paths.filter(Boolean);
|
|
1382
|
-
}
|
|
1383
|
-
function parseYamlScalar(raw) {
|
|
1384
|
-
const value = raw.trim();
|
|
1385
|
-
if (!value)
|
|
1386
|
-
return "";
|
|
1387
|
-
if (value.startsWith('"')) {
|
|
1388
|
-
try {
|
|
1389
|
-
return String(JSON.parse(value));
|
|
1390
|
-
} catch {
|
|
1391
|
-
return value.replace(/^"|"$/g, "");
|
|
1392
|
-
}
|
|
1393
|
-
}
|
|
1394
|
-
return value.replace(/^'|'$/g, "");
|
|
1395
|
-
}
|
|
1396
|
-
function yamlQuote(value) {
|
|
1397
|
-
return JSON.stringify(value);
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
|
-
// src/skills.ts
|
|
1401
|
-
var SKILLS_REQUEST_TIMEOUT_MS = 15000;
|
|
1402
|
-
var VALID_STATUSES = new Set(["approved", "proposed", "all"]);
|
|
1403
|
-
var SKILL_PROPOSALS_MANIFEST_PATH = join5(".moda", "skill-proposals.yml");
|
|
1404
|
-
async function fetchSkills(status) {
|
|
1405
|
-
const apiKey = resolveApiKey();
|
|
1406
|
-
if (!apiKey) {
|
|
1407
|
-
throw new CliInputError("No Moda API key found. Run `moda init` first (or set MODA_API_KEY).");
|
|
1408
|
-
}
|
|
1409
|
-
const ingestUrl = resolveIngestUrl().replace(/\/$/, "");
|
|
1410
|
-
const url = `${ingestUrl}/v1/skills?status=${encodeURIComponent(status)}`;
|
|
1411
|
-
const controller = new AbortController;
|
|
1412
|
-
const timeout = setTimeout(() => controller.abort(), SKILLS_REQUEST_TIMEOUT_MS);
|
|
1413
|
-
let response;
|
|
1414
|
-
try {
|
|
1415
|
-
response = await fetch(url, {
|
|
1416
|
-
headers: { Authorization: `Bearer ${apiKey}` },
|
|
1417
|
-
signal: controller.signal
|
|
1418
|
-
});
|
|
1419
|
-
} catch (err) {
|
|
1420
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1421
|
-
throw new Error(`Network error: failed to reach ${ingestUrl} (${message})`);
|
|
1422
|
-
} finally {
|
|
1423
|
-
clearTimeout(timeout);
|
|
1424
|
-
}
|
|
1425
|
-
if (!response.ok) {
|
|
1426
|
-
const body = await response.text().catch(() => "");
|
|
1427
|
-
throw new Error(`Failed to fetch skills (HTTP ${response.status}): ${body.slice(0, 200)}`);
|
|
1428
|
-
}
|
|
1429
|
-
const data = await response.json();
|
|
1430
|
-
if (!data.success) {
|
|
1431
|
-
throw new Error(data.message || "Failed to fetch skills");
|
|
1432
|
-
}
|
|
1433
|
-
const skills = Array.isArray(data.skills) ? data.skills : [];
|
|
1434
|
-
const release = data.release && typeof data.release === "object" ? data.release : null;
|
|
1435
|
-
return {
|
|
1436
|
-
skills,
|
|
1437
|
-
tenantId: data.tenant_id || resolveTenantId() || "",
|
|
1438
|
-
release,
|
|
1439
|
-
releaseKey: releaseKeyFor(release, skills, status),
|
|
1440
|
-
status
|
|
1441
|
-
};
|
|
1442
|
-
}
|
|
1443
|
-
async function skillRunsRequest(url) {
|
|
1444
|
-
const apiKey = resolveApiKey();
|
|
1445
|
-
if (!apiKey) {
|
|
1446
|
-
throw new CliInputError("No Moda API key found. Run `moda init` first (or set MODA_API_KEY).");
|
|
1447
|
-
}
|
|
1448
|
-
const controller = new AbortController;
|
|
1449
|
-
const timeout = setTimeout(() => controller.abort(), SKILLS_REQUEST_TIMEOUT_MS);
|
|
1450
|
-
let response;
|
|
1451
|
-
try {
|
|
1452
|
-
response = await fetch(url, {
|
|
1453
|
-
headers: { Authorization: `Bearer ${apiKey}` },
|
|
1454
|
-
signal: controller.signal
|
|
1455
|
-
});
|
|
1456
|
-
} catch (err) {
|
|
1457
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1458
|
-
throw new Error(`Network error: failed to reach ${url} (${message})`);
|
|
1459
|
-
} finally {
|
|
1460
|
-
clearTimeout(timeout);
|
|
1461
|
-
}
|
|
1462
|
-
const text = await response.text().catch(() => "");
|
|
1463
|
-
if (!response.ok && response.status !== 404) {
|
|
1464
|
-
throw new Error(`Failed to fetch run status (HTTP ${response.status}): ${text.slice(0, 200)}`);
|
|
1465
|
-
}
|
|
1466
|
-
let body = {};
|
|
1467
|
-
try {
|
|
1468
|
-
body = text ? JSON.parse(text) : {};
|
|
1469
|
-
} catch {
|
|
1470
|
-
body = {};
|
|
1471
|
-
}
|
|
1472
|
-
return { status: response.status, body };
|
|
1473
|
-
}
|
|
1474
|
-
async function fetchSkillRunStatus(runId) {
|
|
1475
|
-
const ingestUrl = resolveIngestUrl().replace(/\/$/, "");
|
|
1476
|
-
let resolvedRunId = runId;
|
|
1477
|
-
if (!resolvedRunId) {
|
|
1478
|
-
const list = await skillRunsRequest(`${ingestUrl}/v1/skills/runs?limit=1`);
|
|
1479
|
-
const listBody = list.body;
|
|
1480
|
-
const latest = Array.isArray(listBody.runs) ? listBody.runs[0] : undefined;
|
|
1481
|
-
if (!latest)
|
|
1482
|
-
return { run: null, events: [] };
|
|
1483
|
-
resolvedRunId = latest.generation_run_id;
|
|
1484
|
-
if (!resolvedRunId)
|
|
1485
|
-
return { run: latest, events: [] };
|
|
1486
|
-
}
|
|
1487
|
-
const detail = await skillRunsRequest(`${ingestUrl}/v1/skills/runs/${encodeURIComponent(resolvedRunId)}`);
|
|
1488
|
-
if (detail.status === 404) {
|
|
1489
|
-
if (runId) {
|
|
1490
|
-
throw new CliInputError(`No skill generation run found with id '${runId}'`);
|
|
1491
|
-
}
|
|
1492
|
-
return { run: null, events: [] };
|
|
1493
|
-
}
|
|
1494
|
-
const detailBody = detail.body;
|
|
1495
|
-
return {
|
|
1496
|
-
run: detailBody.run ?? null,
|
|
1497
|
-
events: Array.isArray(detailBody.events) ? detailBody.events : []
|
|
1498
|
-
};
|
|
1499
|
-
}
|
|
1500
|
-
var STATUS_HUMAN_EVENT_LIMIT = 10;
|
|
1501
|
-
function formatRunStatusHuman(run, events) {
|
|
1502
|
-
const counters = run.counters ?? {};
|
|
1503
|
-
const lines = [
|
|
1504
|
-
run.generation_run_id && `run: ${run.generation_run_id}`,
|
|
1505
|
-
`status: ${run.status || "unknown"}`,
|
|
1506
|
-
run.current_stage && `stage: ${run.current_stage}`,
|
|
1507
|
-
run.outcome && `outcome: ${run.outcome}`,
|
|
1508
|
-
`processed: ${Number(counters.sessions_processed ?? 0)}`,
|
|
1509
|
-
`failed: ${Number(counters.sessions_failed ?? 0)}`,
|
|
1510
|
-
Number(counters.distilled_skills_generated ?? 0) > 0 && `distilled: ${Number(counters.distilled_skills_generated ?? 0)}`,
|
|
1511
|
-
Number(counters.skills_indexed ?? 0) > 0 && `indexed: ${Number(counters.skills_indexed ?? 0)}`,
|
|
1512
|
-
(run.generated_skill_ids?.length ?? 0) > 0 && `skills: ${(run.generated_skill_ids ?? []).join(", ")}`,
|
|
1513
|
-
run.error && `error: ${run.error}`,
|
|
1514
|
-
run.started_at && `started: ${run.started_at}`,
|
|
1515
|
-
run.updated_at && `updated: ${run.updated_at}`
|
|
1516
|
-
].filter(Boolean);
|
|
1517
|
-
if (events.length > 0) {
|
|
1518
|
-
lines.push("recent events (newest first):");
|
|
1519
|
-
for (const event of events.slice(0, STATUS_HUMAN_EVENT_LIMIT)) {
|
|
1520
|
-
lines.push(` ${[event.created_at, event.level, event.stage, event.message].filter(Boolean).join(" ")}`);
|
|
1521
|
-
}
|
|
1522
|
-
}
|
|
1523
|
-
return lines.join(`
|
|
1524
|
-
`);
|
|
1525
|
-
}
|
|
1526
|
-
function writeIfChanged(filePath, content) {
|
|
1527
|
-
try {
|
|
1528
|
-
mkdirSync4(dirname4(filePath), { recursive: true });
|
|
1529
|
-
if (existsSync6(filePath)) {
|
|
1530
|
-
const current = readFileSync4(filePath, "utf8");
|
|
1531
|
-
if (current === content)
|
|
1532
|
-
return "unchanged";
|
|
1533
|
-
writeFileSync5(filePath, content);
|
|
1534
|
-
return "updated";
|
|
1535
|
-
}
|
|
1536
|
-
writeFileSync5(filePath, content);
|
|
1537
|
-
return "wrote";
|
|
1538
|
-
} catch {
|
|
1539
|
-
return "failed";
|
|
1540
|
-
}
|
|
1541
|
-
}
|
|
1542
|
-
async function pullSkills(options = {}) {
|
|
1543
|
-
const status = options.status ?? "approved";
|
|
1544
|
-
const cwd = options.cwd ?? process.cwd();
|
|
1545
|
-
const log = (msg) => {
|
|
1546
|
-
if (!options.silent)
|
|
1547
|
-
console.log(msg);
|
|
1548
|
-
};
|
|
1549
|
-
const fetched = await fetchSkills(status);
|
|
1550
|
-
const { skills } = fetched;
|
|
1551
|
-
const summary = {
|
|
1552
|
-
total: skills.length,
|
|
1553
|
-
pruned: 0,
|
|
1554
|
-
wrote: 0,
|
|
1555
|
-
updated: 0,
|
|
1556
|
-
unchanged: 0,
|
|
1557
|
-
failed: 0
|
|
1558
|
-
};
|
|
1559
|
-
if (skills.length === 0) {
|
|
1560
|
-
log(" No skills to pull yet.");
|
|
1561
|
-
return summary;
|
|
1562
|
-
}
|
|
1563
|
-
const previousManifest = readSkillsManifest(cwd);
|
|
1564
|
-
const preservedIntegrationRecords = (previousManifest?.records ?? []).filter((record) => record.source === SKILL_SOURCE_INTEGRATION_BUNDLED);
|
|
1565
|
-
if (previousManifest && previousManifest.managedPaths.length > 0 && previousManifest.releaseKey !== fetched.releaseKey) {
|
|
1566
|
-
const integrationPaths = new Set(preservedIntegrationRecords.flatMap((record) => record.paths));
|
|
1567
|
-
const harnessPaths = previousManifest.managedPaths.filter((path) => !integrationPaths.has(path));
|
|
1568
|
-
if (harnessPaths.length > 0) {
|
|
1569
|
-
summary.pruned = pruneManagedSkillPaths(cwd, harnessPaths, log);
|
|
1570
|
-
}
|
|
1571
|
-
}
|
|
1572
|
-
const agents = detectAgents();
|
|
1573
|
-
const writeCursor = agents.includes("Cursor") || existsSync6(join5(cwd, ".cursor"));
|
|
1574
|
-
const records = [];
|
|
1575
|
-
for (const skill of skills) {
|
|
1576
|
-
if (!skill.skill_id || !skill.skill_md) {
|
|
1577
|
-
summary.failed++;
|
|
1578
|
-
continue;
|
|
1579
|
-
}
|
|
1580
|
-
const safeId = basename(skill.skill_id);
|
|
1581
|
-
if (!safeId || !/^[A-Za-z0-9_-]+$/.test(safeId)) {
|
|
1582
|
-
summary.failed++;
|
|
1583
|
-
log(` skipped invalid skill_id: ${JSON.stringify(skill.skill_id).slice(0, 80)}`);
|
|
1584
|
-
continue;
|
|
1585
|
-
}
|
|
1586
|
-
const claudePath = join5(cwd, ".claude", "skills", safeId, "SKILL.md");
|
|
1587
|
-
const claudeStatus = writeIfChanged(claudePath, skill.skill_md);
|
|
1588
|
-
const claudeRelativePath = `.claude/skills/${safeId}/SKILL.md`;
|
|
1589
|
-
recordOutcome(summary, claudeStatus, log, claudeRelativePath);
|
|
1590
|
-
const paths = [];
|
|
1591
|
-
if (claudeStatus !== "failed")
|
|
1592
|
-
paths.push(claudeRelativePath);
|
|
1593
|
-
if (writeCursor) {
|
|
1594
|
-
const cursorPath = join5(cwd, ".cursor", "rules", `${safeId}.mdc`);
|
|
1595
|
-
const cursorStatus = writeIfChanged(cursorPath, claudeSkillToCursorRule(skill.skill_md));
|
|
1596
|
-
const cursorRelativePath = `.cursor/rules/${safeId}.mdc`;
|
|
1597
|
-
recordOutcome(summary, cursorStatus, log, cursorRelativePath);
|
|
1598
|
-
if (cursorStatus !== "failed")
|
|
1599
|
-
paths.push(cursorRelativePath);
|
|
1600
|
-
}
|
|
1601
|
-
if (paths.length > 0) {
|
|
1602
|
-
records.push({
|
|
1603
|
-
skillId: safeId,
|
|
1604
|
-
contentHash: sha2562(skill.skill_md),
|
|
1605
|
-
source: SKILL_SOURCE_HARNESS,
|
|
1606
|
-
paths
|
|
1607
|
-
});
|
|
1608
|
-
}
|
|
1609
|
-
}
|
|
1610
|
-
writeSkillsManifest(cwd, manifestHeaderFor(fetched), [...records, ...preservedIntegrationRecords]);
|
|
1611
|
-
return summary;
|
|
1612
|
-
}
|
|
1613
|
-
function recordOutcome(summary, status, log, relativePath) {
|
|
1614
|
-
switch (status) {
|
|
1615
|
-
case "wrote":
|
|
1616
|
-
summary.wrote++;
|
|
1617
|
-
log(` wrote ${relativePath}`);
|
|
1618
|
-
break;
|
|
1619
|
-
case "updated":
|
|
1620
|
-
summary.updated++;
|
|
1621
|
-
log(` updated ${relativePath}`);
|
|
1622
|
-
break;
|
|
1623
|
-
case "unchanged":
|
|
1624
|
-
summary.unchanged++;
|
|
1625
|
-
break;
|
|
1626
|
-
case "failed":
|
|
1627
|
-
summary.failed++;
|
|
1628
|
-
log(` failed ${relativePath}`);
|
|
1629
|
-
break;
|
|
1630
|
-
}
|
|
1631
|
-
}
|
|
1632
|
-
var SKILLS_DIR = join5(".claude", "skills");
|
|
1633
|
-
var SKILL_FILE = "SKILL.md";
|
|
1634
|
-
var MAX_SKILL_WALK_DEPTH = 4;
|
|
1635
|
-
async function runSkillSync(flags = {}, profileOptions = {}, options = {}) {
|
|
1636
|
-
const cwd = options.cwd ?? process.cwd();
|
|
1637
|
-
const dryRun = flags["dry-run"] === "true";
|
|
1638
|
-
const skills = discoverLocalSkills(cwd);
|
|
1639
|
-
if (skills.length === 0) {
|
|
1640
|
-
return { synced: [], dryRun };
|
|
1641
|
-
}
|
|
1642
|
-
const sourceCommit = getGitCommit2(cwd);
|
|
1643
|
-
const response = await callControlAPI("/skills/sync", {
|
|
1644
|
-
method: "POST",
|
|
1645
|
-
body: JSON.stringify({
|
|
1646
|
-
skills: skills.map((skill) => ({
|
|
1647
|
-
key: skill.key,
|
|
1648
|
-
name: skill.name,
|
|
1649
|
-
description: skill.description,
|
|
1650
|
-
skillMd: skill.skillMd,
|
|
1651
|
-
sourcePath: skill.sourcePath,
|
|
1652
|
-
contentHash: skill.contentHash,
|
|
1653
|
-
sourceCommit
|
|
1654
|
-
})),
|
|
1655
|
-
sourceCommit,
|
|
1656
|
-
dryRun
|
|
1657
|
-
})
|
|
1658
|
-
}, profileOptions);
|
|
1659
|
-
return { synced: response.synced ?? [], dryRun };
|
|
1660
|
-
}
|
|
1661
|
-
function discoverLocalSkills(cwd) {
|
|
1662
|
-
const skillsRoot = join5(cwd, SKILLS_DIR);
|
|
1663
|
-
if (!existsSync6(skillsRoot))
|
|
1664
|
-
return [];
|
|
1665
|
-
const managed = new Set(managedSkillPaths(cwd));
|
|
1666
|
-
const skills = [];
|
|
1667
|
-
for (const absPath of walkSkillFiles(skillsRoot, 0)) {
|
|
1668
|
-
const sourcePath = relative3(cwd, absPath).replace(/\\/g, "/");
|
|
1669
|
-
if (managed.has(sourcePath))
|
|
1670
|
-
continue;
|
|
1671
|
-
const key = basename(dirname4(absPath));
|
|
1672
|
-
if (key === "moda-cli" || !/^[A-Za-z0-9_-]+$/.test(key))
|
|
1673
|
-
continue;
|
|
1674
|
-
let content;
|
|
1675
|
-
try {
|
|
1676
|
-
content = readFileSync4(absPath, "utf8");
|
|
1677
|
-
} catch {
|
|
1678
|
-
continue;
|
|
1679
|
-
}
|
|
1680
|
-
if (!content.trim())
|
|
1681
|
-
continue;
|
|
1682
|
-
const { name, description } = parseSkillFrontmatter(content, key);
|
|
1683
|
-
skills.push({
|
|
1684
|
-
key,
|
|
1685
|
-
name,
|
|
1686
|
-
description,
|
|
1687
|
-
skillMd: content,
|
|
1688
|
-
sourcePath,
|
|
1689
|
-
contentHash: sha2562(content)
|
|
1690
|
-
});
|
|
1691
|
-
}
|
|
1692
|
-
return skills.sort((a, b) => a.key.localeCompare(b.key));
|
|
1693
|
-
}
|
|
1694
|
-
function managedSkillPaths(cwd) {
|
|
1695
|
-
const manifest = readSkillsManifest(cwd);
|
|
1696
|
-
if (!manifest)
|
|
1697
|
-
return [];
|
|
1698
|
-
return manifest.managedPaths.filter((path) => path.startsWith(".claude/skills/") && path.endsWith("/SKILL.md"));
|
|
1699
|
-
}
|
|
1700
|
-
function walkSkillFiles(dir, depth) {
|
|
1701
|
-
if (depth > MAX_SKILL_WALK_DEPTH)
|
|
1702
|
-
return [];
|
|
1703
|
-
let entries;
|
|
1704
|
-
try {
|
|
1705
|
-
entries = readdirSync3(dir);
|
|
1706
|
-
} catch {
|
|
1707
|
-
return [];
|
|
1708
|
-
}
|
|
1709
|
-
const files = [];
|
|
1710
|
-
for (const entry of entries) {
|
|
1711
|
-
const fullPath = join5(dir, entry);
|
|
1712
|
-
let stat;
|
|
1713
|
-
try {
|
|
1714
|
-
stat = lstatSync2(fullPath);
|
|
1715
|
-
} catch {
|
|
1716
|
-
continue;
|
|
1717
|
-
}
|
|
1718
|
-
if (stat.isSymbolicLink())
|
|
1719
|
-
continue;
|
|
1720
|
-
if (stat.isDirectory()) {
|
|
1721
|
-
files.push(...walkSkillFiles(fullPath, depth + 1));
|
|
1722
|
-
} else if (stat.isFile() && entry === SKILL_FILE) {
|
|
1723
|
-
files.push(fullPath);
|
|
1724
|
-
}
|
|
1725
|
-
}
|
|
1726
|
-
return files;
|
|
1727
|
-
}
|
|
1728
|
-
function parseSkillFrontmatter(raw, fallbackKey) {
|
|
1729
|
-
let name = fallbackKey;
|
|
1730
|
-
let description;
|
|
1731
|
-
if (raw.startsWith("---")) {
|
|
1732
|
-
const end = raw.indexOf(`
|
|
1733
|
-
---`, 3);
|
|
1734
|
-
if (end !== -1) {
|
|
1735
|
-
const frontmatter = raw.slice(3, end);
|
|
1736
|
-
const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
|
|
1737
|
-
const descMatch = frontmatter.match(/^description:\s*(.+)$/m);
|
|
1738
|
-
if (nameMatch)
|
|
1739
|
-
name = stripQuotes(nameMatch[1].trim()) || fallbackKey;
|
|
1740
|
-
if (descMatch)
|
|
1741
|
-
description = stripQuotes(descMatch[1].trim()) || undefined;
|
|
1742
|
-
}
|
|
1743
|
-
}
|
|
1744
|
-
return { name, description };
|
|
1745
|
-
}
|
|
1746
|
-
function stripQuotes(value) {
|
|
1747
|
-
return value.replace(/^["']|["']$/g, "");
|
|
1748
|
-
}
|
|
1749
|
-
function getGitCommit2(cwd) {
|
|
1750
|
-
try {
|
|
1751
|
-
return execFileSync2("git", ["rev-parse", "HEAD"], {
|
|
1752
|
-
cwd,
|
|
1753
|
-
encoding: "utf8",
|
|
1754
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
1755
|
-
}).trim();
|
|
1756
|
-
} catch {
|
|
1757
|
-
return;
|
|
1758
|
-
}
|
|
1759
|
-
}
|
|
1760
|
-
async function fetchSkillProposals(flags, profileOptions = {}) {
|
|
1761
|
-
const params = new URLSearchParams;
|
|
1762
|
-
if (flags["generation-run-id"])
|
|
1763
|
-
params.set("generationRunId", flags["generation-run-id"]);
|
|
1764
|
-
const status = flags.status ?? "ready_for_pr";
|
|
1765
|
-
if (status !== "all")
|
|
1766
|
-
params.set("status", status);
|
|
1767
|
-
if (flags["tenant-id"])
|
|
1768
|
-
params.set("tenantId", flags["tenant-id"]);
|
|
1769
|
-
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
1770
|
-
const response = await callControlAPI(`/skills/proposals${suffix}`, { method: "GET" }, profileOptions);
|
|
1771
|
-
return Array.isArray(response.proposals) ? response.proposals : [];
|
|
1772
|
-
}
|
|
1773
|
-
async function applySkillProposal(proposalId, flags, profileOptions = {}, options = {}) {
|
|
1774
|
-
const cwd = options.cwd ?? process.cwd();
|
|
1775
|
-
const params = new URLSearchParams;
|
|
1776
|
-
if (flags["tenant-id"])
|
|
1777
|
-
params.set("tenantId", flags["tenant-id"]);
|
|
1778
|
-
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
1779
|
-
const proposal = await callControlAPI(`/skills/proposals/${encodeURIComponent(proposalId)}${suffix}`, { method: "GET" }, profileOptions);
|
|
1780
|
-
if (!proposal.skillMd?.trim()) {
|
|
1781
|
-
throw new CliInputError(`Proposal ${proposalId} does not include SKILL.md content`);
|
|
1782
|
-
}
|
|
1783
|
-
const safeId = safeSkillId(proposal.skillId);
|
|
1784
|
-
const relativePath = `.claude/skills/${safeId}/SKILL.md`;
|
|
1785
|
-
const writeStatus = writeIfChanged(join5(cwd, relativePath), proposal.skillMd);
|
|
1786
|
-
if (writeStatus === "failed") {
|
|
1787
|
-
throw new Error(`Failed to write ${relativePath}`);
|
|
1788
|
-
}
|
|
1789
|
-
writeSkillProposalManifest(cwd, proposal, relativePath);
|
|
1790
|
-
let acknowledged = true;
|
|
1791
|
-
let ackError;
|
|
1792
|
-
try {
|
|
1793
|
-
await callControlAPI(`/skills/proposals/${encodeURIComponent(proposalId)}/mark-applied${suffix}`, {
|
|
1794
|
-
method: "POST",
|
|
1795
|
-
body: JSON.stringify({ appliedBy: "moda-cli" })
|
|
1796
|
-
}, profileOptions);
|
|
1797
|
-
} catch (err) {
|
|
1798
|
-
acknowledged = false;
|
|
1799
|
-
ackError = err instanceof Error ? err.message : String(err);
|
|
1800
|
-
}
|
|
1801
|
-
return {
|
|
1802
|
-
proposalId,
|
|
1803
|
-
skillId: proposal.skillId,
|
|
1804
|
-
path: relativePath,
|
|
1805
|
-
status: writeStatus,
|
|
1806
|
-
acknowledged,
|
|
1807
|
-
ackError
|
|
1808
|
-
};
|
|
1809
|
-
}
|
|
1810
|
-
function safeSkillId(skillId) {
|
|
1811
|
-
const safeId = basename(skillId || "");
|
|
1812
|
-
if (!safeId || !/^[A-Za-z0-9_-]+$/.test(safeId)) {
|
|
1813
|
-
throw new CliInputError(`Invalid skill_id in proposal: ${JSON.stringify(skillId).slice(0, 80)}`);
|
|
1814
|
-
}
|
|
1815
|
-
return safeId;
|
|
1816
|
-
}
|
|
1817
|
-
function writeSkillProposalManifest(cwd, proposal, relativePath) {
|
|
1818
|
-
const manifestPath = join5(cwd, SKILL_PROPOSALS_MANIFEST_PATH);
|
|
1819
|
-
mkdirSync4(dirname4(manifestPath), { recursive: true });
|
|
1820
|
-
const existing = existsSync6(manifestPath) ? readFileSync4(manifestPath, "utf8") : `applied_proposals:
|
|
1821
|
-
`;
|
|
1822
|
-
const entry = [
|
|
1823
|
-
` - proposal_id: "${escapeYamlString(proposal.proposalId)}"`,
|
|
1824
|
-
` skill_id: "${escapeYamlString(proposal.skillId)}"`,
|
|
1825
|
-
` skill_family_id: "${escapeYamlString(proposal.skillFamilyId)}"`,
|
|
1826
|
-
` skill_version: ${Number(proposal.skillVersion || 0)}`,
|
|
1827
|
-
` generation_run_id: "${escapeYamlString(proposal.generationRunId)}"`,
|
|
1828
|
-
` clustering_run_id: "${escapeYamlString(proposal.clusteringRunId)}"`,
|
|
1829
|
-
` topic_id: "${escapeYamlString(proposal.topicId)}"`,
|
|
1830
|
-
` replay_set_id: "${escapeYamlString(proposal.replaySetId)}"`,
|
|
1831
|
-
` delta_pass_rate: ${Number(proposal.deltaPassRate || 0)}`,
|
|
1832
|
-
` path: "${escapeYamlString(relativePath)}"`,
|
|
1833
|
-
` applied_at: "${new Date().toISOString()}"`
|
|
1834
|
-
].join(`
|
|
1835
|
-
`);
|
|
1836
|
-
const prefix = existing.endsWith(`
|
|
1837
|
-
`) ? existing : `${existing}
|
|
1838
|
-
`;
|
|
1839
|
-
writeFileSync5(manifestPath, `${prefix}${entry}
|
|
1840
|
-
`);
|
|
1841
|
-
}
|
|
1842
|
-
function escapeYamlString(value) {
|
|
1843
|
-
return String(value ?? "").replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
1844
|
-
}
|
|
1845
|
-
function formatProposalRate(value) {
|
|
1846
|
-
return `${(Number(value || 0) * 100).toFixed(1)}%`;
|
|
1847
|
-
}
|
|
1848
|
-
async function runSkillsCommand(input) {
|
|
1849
|
-
const context = "output" in input ? input : createCommandContext({
|
|
1850
|
-
command: input.command,
|
|
1851
|
-
positional: input.positional,
|
|
1852
|
-
positionals: input.positionals,
|
|
1853
|
-
flags: input.flags
|
|
1854
|
-
}, {
|
|
1855
|
-
outputMode: input.flags.agent === "true" ? "agent" : input.flags.json === "true" ? "json" : "human"
|
|
1856
|
-
});
|
|
1857
|
-
const sub = context.positional;
|
|
1858
|
-
const positionals = context.positionals ?? (sub ? [sub] : []);
|
|
1859
|
-
const flags = context.flags;
|
|
1860
|
-
if (sub === "gen") {
|
|
1861
|
-
const result = await generateSkills(flags);
|
|
1862
|
-
if (context.outputMode !== "human") {
|
|
1863
|
-
context.output.writeData(result, {
|
|
1864
|
-
status: Number(result.sessions_failed ?? 0) > 0 ? "error" : "success"
|
|
1865
|
-
});
|
|
1866
|
-
} else if (result.accepted) {
|
|
1867
|
-
console.log([
|
|
1868
|
-
"accepted: true",
|
|
1869
|
-
result.generation_run_id && `generation_run_id: ${result.generation_run_id}`,
|
|
1870
|
-
result.message && `message: ${result.message}`,
|
|
1871
|
-
result.generation_run_id && `follow progress: moda skills status ${result.generation_run_id}`
|
|
1872
|
-
].filter(Boolean).join(`
|
|
1873
|
-
`));
|
|
1874
|
-
} else {
|
|
1875
|
-
console.log([
|
|
1876
|
-
`processed: ${result.sessions_processed ?? 0}`,
|
|
1877
|
-
`failed: ${result.sessions_failed ?? 0}`,
|
|
1878
|
-
`pending: ${result.sessions_pending ?? 0}`,
|
|
1879
|
-
`pending_found: ${result.pending_sessions_found ?? 0}`,
|
|
1880
|
-
`distilled: ${Number(result.cluster_result?.distilled_skills_generated ?? 0)}`,
|
|
1881
|
-
`indexed: ${Number(result.cluster_result?.skills_indexed ?? 0)}`,
|
|
1882
|
-
result.distillation_skipped && `distillation skipped: ${result.distillation_skip_reason || "unknown"}`
|
|
1883
|
-
].filter(Boolean).join(`
|
|
1884
|
-
`));
|
|
1885
|
-
}
|
|
1886
|
-
return Number(result.sessions_failed ?? 0) > 0 ? 1 : 0;
|
|
1887
|
-
}
|
|
1888
|
-
if (sub === "status") {
|
|
1889
|
-
const runId = positionals[1] || flags["run-id"] || undefined;
|
|
1890
|
-
const { run, events } = await fetchSkillRunStatus(runId);
|
|
1891
|
-
if (context.outputMode !== "human") {
|
|
1892
|
-
context.output.writeData({ run, events }, { status: run?.status === "failed" ? "error" : "success" });
|
|
1893
|
-
} else if (!run) {
|
|
1894
|
-
console.log("No skill generation runs found yet. Start one with `moda skills gen`.");
|
|
1895
|
-
} else {
|
|
1896
|
-
console.log(formatRunStatusHuman(run, events));
|
|
1897
|
-
}
|
|
1898
|
-
return run?.status === "failed" ? 1 : 0;
|
|
1899
|
-
}
|
|
1900
|
-
if (sub === "proposals" && positionals[1] === "list") {
|
|
1901
|
-
const proposals = await fetchSkillProposals(flags, { profile: context.profile });
|
|
1902
|
-
if (flags.json === "true") {
|
|
1903
|
-
console.log(JSON.stringify({ proposals }));
|
|
1904
|
-
} else if (proposals.length === 0) {
|
|
1905
|
-
console.log("No skill proposals found.");
|
|
1906
|
-
} else {
|
|
1907
|
-
for (const proposal of proposals) {
|
|
1908
|
-
console.log([
|
|
1909
|
-
proposal.proposalId,
|
|
1910
|
-
proposal.proposalStatus,
|
|
1911
|
-
`${proposal.skillId}@v${proposal.skillVersion}`,
|
|
1912
|
-
`delta=${formatProposalRate(proposal.deltaPassRate)}`,
|
|
1913
|
-
`baseline=${formatProposalRate(proposal.baselinePassRate)}`,
|
|
1914
|
-
`candidate=${formatProposalRate(proposal.candidatePassRate)}`,
|
|
1915
|
-
proposal.name
|
|
1916
|
-
].filter(Boolean).join("\t"));
|
|
1917
|
-
}
|
|
1918
|
-
}
|
|
1919
|
-
return 0;
|
|
1920
|
-
}
|
|
1921
|
-
if (sub === "proposal" && positionals[1] === "apply") {
|
|
1922
|
-
const proposalId = positionals[2] || flags["proposal-id"];
|
|
1923
|
-
if (!proposalId) {
|
|
1924
|
-
throw new CliInputError("A proposal id is required", "Usage: moda skills proposal apply <proposal-id>");
|
|
1925
|
-
}
|
|
1926
|
-
const result = await applySkillProposal(proposalId, flags, { profile: context.profile });
|
|
1927
|
-
if (flags.json === "true") {
|
|
1928
|
-
console.log(JSON.stringify(result));
|
|
1929
|
-
} else {
|
|
1930
|
-
console.log([
|
|
1931
|
-
`${result.status} ${result.path}`,
|
|
1932
|
-
result.acknowledged ? "acknowledged: true" : "acknowledged: false"
|
|
1933
|
-
].join(`
|
|
1934
|
-
`));
|
|
1935
|
-
}
|
|
1936
|
-
if (!result.acknowledged) {
|
|
1937
|
-
console.error(`Warning: wrote ${result.path} but failed to acknowledge proposal ${proposalId} on the server` + (result.ackError ? ` (${result.ackError})` : "") + ". The proposal is still listed as ready — re-run `moda skills proposal apply` to acknowledge it.");
|
|
1938
|
-
return 1;
|
|
1939
|
-
}
|
|
1940
|
-
return 0;
|
|
1941
|
-
}
|
|
1942
|
-
if (sub === "sync") {
|
|
1943
|
-
const result = await runSkillSync(flags, { profile: context.profile, env: context.env });
|
|
1944
|
-
context.output.writeData({ synced: result.synced, dryRun: result.dryRun });
|
|
1945
|
-
return 0;
|
|
1946
|
-
}
|
|
1947
|
-
if (sub !== "pull") {
|
|
1948
|
-
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]");
|
|
1949
|
-
}
|
|
1950
|
-
const status = (flags.status ?? "approved").toLowerCase();
|
|
1951
|
-
if (!VALID_STATUSES.has(status)) {
|
|
1952
|
-
throw new CliInputError(`Invalid --status '${status}'`, `Must be one of: ${[...VALID_STATUSES].join(", ")}`);
|
|
1953
|
-
}
|
|
1954
|
-
const summary = await pullSkills({ status, silent: context.outputMode !== "human" });
|
|
1955
|
-
const parts = [
|
|
1956
|
-
summary.pruned && `${summary.pruned} pruned`,
|
|
1957
|
-
summary.wrote && `${summary.wrote} written`,
|
|
1958
|
-
summary.updated && `${summary.updated} updated`,
|
|
1959
|
-
summary.unchanged && `${summary.unchanged} unchanged`,
|
|
1960
|
-
summary.failed && `${summary.failed} failed`
|
|
1961
|
-
].filter(Boolean);
|
|
1962
|
-
if (context.outputMode === "human" && summary.total > 0) {
|
|
1963
|
-
console.log(` Done — ${parts.join(", ") || "no changes"}`);
|
|
1964
|
-
}
|
|
1965
|
-
if (context.outputMode !== "human") {
|
|
1966
|
-
context.output.writeData({ status, summary });
|
|
1967
|
-
}
|
|
1968
|
-
return summary.failed > 0 ? 1 : 0;
|
|
1969
|
-
}
|
|
1970
|
-
async function generateSkills(flags) {
|
|
1971
|
-
const apiKey = resolveApiKey();
|
|
1972
|
-
if (!apiKey) {
|
|
1973
|
-
throw new CliInputError("No Moda API key found. Run `moda init` first (or set MODA_API_KEY).");
|
|
1974
|
-
}
|
|
1975
|
-
const tenantId = flags["tenant-id"] || resolveTenantId();
|
|
1976
|
-
const directHarnessUrl = flags["harness-url"] || process.env.MODA_SKILL_HARNESS_URL;
|
|
1977
|
-
if (directHarnessUrl && !tenantId) {
|
|
1978
|
-
throw new CliInputError("No Moda tenant id found. Run `moda init` first (or set MODA_TENANT_ID / --tenant-id).");
|
|
1979
|
-
}
|
|
1980
|
-
const endpoint = directHarnessUrl ? `${directHarnessUrl.replace(/\/+$/, "")}/skills/generate` : `${resolveIngestUrl().replace(/\/+$/, "")}/v1/skills/generate`;
|
|
1981
|
-
const waitForCompletion = flags.wait === "true" || flags["wait-for-completion"] === "true";
|
|
1982
|
-
const forceReprocess = flags["force-reprocess"] === "true";
|
|
1983
|
-
const source = normalizeSkillSource(flags.source);
|
|
1984
|
-
if (source === "coding_agent") {
|
|
1985
|
-
console.error("Note: --source=coding-agent is deprecated for skills gen (SDK-only). The run will find no coding-agent sessions; use --source=all or --source=sdk.");
|
|
1986
|
-
}
|
|
1987
|
-
const body = compactObject({
|
|
1988
|
-
tenant_id: tenantId || undefined,
|
|
1989
|
-
max_sessions: parsePositiveInt3(flags["max-sessions"], 0),
|
|
1990
|
-
lookback_hours: parsePositiveInt3(flags["lookback-hours"], 0),
|
|
1991
|
-
start_at: flags["start-at"] || undefined,
|
|
1992
|
-
end_at: flags["end-at"] || undefined,
|
|
1993
|
-
clustering_run_id: flags["clustering-run-id"] || undefined,
|
|
1994
|
-
replay_set_id: flags["replay-set-id"] || flags["eval-set-id"] || undefined,
|
|
1995
|
-
run_clustering: flags["no-clustering"] !== "true",
|
|
1996
|
-
source,
|
|
1997
|
-
clustering_timeout_seconds: parsePositiveInt3(flags["clustering-timeout-seconds"], 7200),
|
|
1998
|
-
clustering_poll_seconds: parsePositiveInt3(flags["clustering-poll-seconds"], 15),
|
|
1999
|
-
max_cluster_pct: parseOptionalFloat(flags["max-cluster-pct"]),
|
|
2000
|
-
reprocess_segments: flags["reprocess-segments"] === "true" || forceReprocess,
|
|
2001
|
-
run_replay: flags.replay === "true" && flags["no-replay"] !== "true",
|
|
2002
|
-
max_traces: parsePositiveInt3(flags["max-traces"], 0),
|
|
2003
|
-
improve_candidates: flags["improve-candidates"] === "true",
|
|
2004
|
-
improvement_rounds: parsePositiveInt3(flags["improvement-rounds"], 2),
|
|
2005
|
-
force_reprocess: forceReprocess,
|
|
2006
|
-
wait_for_completion: waitForCompletion
|
|
2007
|
-
});
|
|
2008
|
-
const headers = {
|
|
2009
|
-
Authorization: `Bearer ${apiKey}`,
|
|
2010
|
-
"Content-Type": "application/json"
|
|
2011
|
-
};
|
|
2012
|
-
if (tenantId)
|
|
2013
|
-
headers["X-Moda-Tenant-Id"] = tenantId;
|
|
2014
|
-
const response = await fetch(endpoint, {
|
|
2015
|
-
method: "POST",
|
|
2016
|
-
headers,
|
|
2017
|
-
body: JSON.stringify(body)
|
|
2018
|
-
});
|
|
2019
|
-
if (!response.ok) {
|
|
2020
|
-
const text = await response.text().catch(() => "");
|
|
2021
|
-
throw new Error(`Failed to generate skills (HTTP ${response.status}): ${text.slice(0, 200)}`);
|
|
2022
|
-
}
|
|
2023
|
-
return await response.json();
|
|
2024
|
-
}
|
|
2025
|
-
function parsePositiveInt3(value, fallback) {
|
|
2026
|
-
if (!value)
|
|
2027
|
-
return fallback;
|
|
2028
|
-
const parsed = Number.parseInt(value, 10);
|
|
2029
|
-
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
|
|
2030
|
-
}
|
|
2031
|
-
function parseOptionalFloat(value) {
|
|
2032
|
-
if (!value)
|
|
2033
|
-
return;
|
|
2034
|
-
const parsed = Number.parseFloat(value);
|
|
2035
|
-
return Number.isFinite(parsed) ? parsed : undefined;
|
|
2036
|
-
}
|
|
2037
|
-
function compactObject(input) {
|
|
2038
|
-
return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
|
|
2039
|
-
}
|
|
2040
|
-
function normalizeSkillSource(value) {
|
|
2041
|
-
const normalized = (value || "all").toLowerCase().replace(/-/g, "_");
|
|
2042
|
-
if (["all", "sdk", "coding_agent"].includes(normalized))
|
|
2043
|
-
return normalized;
|
|
2044
|
-
return "all";
|
|
2045
|
-
}
|
|
2046
|
-
function releaseKeyFor(release, skills, status) {
|
|
2047
|
-
if (release?.generation_run_id)
|
|
2048
|
-
return `${status}:run:${release.generation_run_id}`;
|
|
2049
|
-
if (Number(release?.generation_version || 0) > 0) {
|
|
2050
|
-
return `${status}:version:${Number(release?.generation_version)}`;
|
|
2051
|
-
}
|
|
2052
|
-
return `${status}:legacy:${sha2562(JSON.stringify(skills.map((skill) => ({
|
|
2053
|
-
skill_id: skill.skill_id,
|
|
2054
|
-
hash: sha2562(skill.skill_md || "")
|
|
2055
|
-
})).sort((a, b) => a.skill_id.localeCompare(b.skill_id))))}`;
|
|
2056
|
-
}
|
|
2057
|
-
function manifestHeaderFor(fetched) {
|
|
2058
|
-
const release = fetched.release;
|
|
2059
|
-
return {
|
|
2060
|
-
tenantId: fetched.tenantId,
|
|
2061
|
-
releaseKey: fetched.releaseKey,
|
|
2062
|
-
statusFilter: fetched.status,
|
|
2063
|
-
generationRunId: release?.generation_run_id || "",
|
|
2064
|
-
generationVersion: Number(release?.generation_version || 0),
|
|
2065
|
-
releasedAt: release?.released_at || ""
|
|
2066
|
-
};
|
|
2067
|
-
}
|
|
2068
|
-
function sha2562(input) {
|
|
2069
|
-
return createHash2("sha256").update(input).digest("hex");
|
|
2070
|
-
}
|
|
2071
|
-
|
|
2072
126
|
// src/harness-remote.ts
|
|
2073
|
-
import { existsSync as
|
|
2074
|
-
import { join as
|
|
127
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
128
|
+
import { join as join3 } from "node:path";
|
|
2075
129
|
|
|
2076
130
|
// src/markdown-terminal.ts
|
|
2077
131
|
function shouldColorTerminalOutput(opts = {}) {
|
|
@@ -2213,23 +267,23 @@ function collapseBlankRuns(lines) {
|
|
|
2213
267
|
}
|
|
2214
268
|
|
|
2215
269
|
// src/harness-report.ts
|
|
2216
|
-
import { createHash
|
|
270
|
+
import { createHash } from "node:crypto";
|
|
2217
271
|
import {
|
|
2218
|
-
existsSync
|
|
2219
|
-
lstatSync
|
|
2220
|
-
mkdirSync
|
|
2221
|
-
readFileSync
|
|
2222
|
-
writeFileSync
|
|
272
|
+
existsSync,
|
|
273
|
+
lstatSync,
|
|
274
|
+
mkdirSync,
|
|
275
|
+
readFileSync,
|
|
276
|
+
writeFileSync
|
|
2223
277
|
} from "node:fs";
|
|
2224
|
-
import { basename
|
|
2225
|
-
import { confirm
|
|
278
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
279
|
+
import { confirm } from "@inquirer/prompts";
|
|
2226
280
|
var HARNESS_REPORT_SCHEMA_VERSION = "harness_report.v0.1";
|
|
2227
281
|
var HARNESS_REPORT_VALIDATION_SCHEMA_VERSION = "harness_report_validation.v0.1";
|
|
2228
282
|
var HARNESS_REPORT_APPROVAL_SCHEMA_VERSION = "harness_report_approval.v0.1";
|
|
2229
|
-
var HARNESS_REPORT_PATH =
|
|
2230
|
-
var HARNESS_REPORT_MARKDOWN_PATH =
|
|
2231
|
-
var HARNESS_REPORT_VALIDATION_PATH =
|
|
2232
|
-
var HARNESS_REPORT_APPROVAL_PATH =
|
|
283
|
+
var HARNESS_REPORT_PATH = join(".moda", "harness-report.json");
|
|
284
|
+
var HARNESS_REPORT_MARKDOWN_PATH = join(".moda", "HARNESS_REPORT.md");
|
|
285
|
+
var HARNESS_REPORT_VALIDATION_PATH = join(".moda", "harness-report-validation.json");
|
|
286
|
+
var HARNESS_REPORT_APPROVAL_PATH = join(".moda", "harness-report-approval.json");
|
|
2233
287
|
var HARNESS_GRAPH_ARRAY_FIELDS = [
|
|
2234
288
|
"agentFamilies",
|
|
2235
289
|
"runtimeAgents",
|
|
@@ -2682,7 +736,7 @@ async function approveHarnessReport(options) {
|
|
|
2682
736
|
if (!options.interactive || process.stdin.isTTY !== true) {
|
|
2683
737
|
throw new CliInputError("Harness report approval requires an interactive confirmation or --yes.", "Review the report, then run `moda harness approve --yes`.");
|
|
2684
738
|
}
|
|
2685
|
-
const approved = await
|
|
739
|
+
const approved = await confirm({
|
|
2686
740
|
message: [
|
|
2687
741
|
"Approve this harness report for Moda sync?",
|
|
2688
742
|
`${report.summary.runtimeAgentCount} runtime agent(s), ${report.summary.artifactCount} artifact(s), ${report.claims.length} cited claim(s).`
|
|
@@ -2701,9 +755,9 @@ async function approveHarnessReport(options) {
|
|
|
2701
755
|
approved: true,
|
|
2702
756
|
approvedAt: new Date().toISOString(),
|
|
2703
757
|
approvedBy,
|
|
2704
|
-
reportPath: normalizePath(
|
|
758
|
+
reportPath: normalizePath(relative(options.rootDir, options.reportPath)) || HARNESS_REPORT_PATH
|
|
2705
759
|
};
|
|
2706
|
-
writeJson(
|
|
760
|
+
writeJson(join(options.rootDir, HARNESS_REPORT_APPROVAL_PATH), approval);
|
|
2707
761
|
return { report, validation, approval };
|
|
2708
762
|
}
|
|
2709
763
|
function loadHarnessReportForSync(rootDir, reportPath) {
|
|
@@ -2713,13 +767,13 @@ function loadHarnessReportForSync(rootDir, reportPath) {
|
|
|
2713
767
|
throw new CliInputError("Harness report validation failed; cannot sync.", validation.errors.slice(0, 5).join(`
|
|
2714
768
|
`));
|
|
2715
769
|
}
|
|
2716
|
-
const approvalPath =
|
|
2717
|
-
const approval =
|
|
770
|
+
const approvalPath = join(rootDir, HARNESS_REPORT_APPROVAL_PATH);
|
|
771
|
+
const approval = existsSync(approvalPath) ? JSON.parse(readFileSync(approvalPath, "utf8")) : null;
|
|
2718
772
|
const approved = approval?.schemaVersion === HARNESS_REPORT_APPROVAL_SCHEMA_VERSION && approval.reportId === report.reportId && approval.reportHash === hashHarnessReport(report) && approval.approved === true;
|
|
2719
773
|
return { report, validation, approval, approved };
|
|
2720
774
|
}
|
|
2721
775
|
function readHarnessReport(reportPath) {
|
|
2722
|
-
return normalizeHarnessReport(JSON.parse(
|
|
776
|
+
return normalizeHarnessReport(JSON.parse(readFileSync(reportPath, "utf8")));
|
|
2723
777
|
}
|
|
2724
778
|
function normalizeHarnessReport(value) {
|
|
2725
779
|
const report = cloneJson(value);
|
|
@@ -3306,18 +1360,18 @@ function limitReportEvidence(claims, citations) {
|
|
|
3306
1360
|
};
|
|
3307
1361
|
}
|
|
3308
1362
|
function resolveHarnessReportPath(rootDir, value) {
|
|
3309
|
-
return
|
|
1363
|
+
return resolve(rootDir, value && value !== "true" ? value : HARNESS_REPORT_PATH);
|
|
3310
1364
|
}
|
|
3311
1365
|
function writeHarnessReportArtifacts(options) {
|
|
3312
1366
|
const written = [];
|
|
3313
|
-
writeJson(
|
|
1367
|
+
writeJson(join(options.rootDir, HARNESS_REPORT_PATH), options.report);
|
|
3314
1368
|
written.push(HARNESS_REPORT_PATH);
|
|
3315
|
-
writeJson(
|
|
1369
|
+
writeJson(join(options.rootDir, HARNESS_REPORT_VALIDATION_PATH), options.validation);
|
|
3316
1370
|
written.push(HARNESS_REPORT_VALIDATION_PATH);
|
|
3317
1371
|
if (options.markdown) {
|
|
3318
|
-
const markdownPath =
|
|
3319
|
-
|
|
3320
|
-
|
|
1372
|
+
const markdownPath = join(options.rootDir, HARNESS_REPORT_MARKDOWN_PATH);
|
|
1373
|
+
mkdirSync(dirname(markdownPath), { recursive: true });
|
|
1374
|
+
writeFileSync(markdownPath, renderHarnessReportMarkdown(options.report, options.validation));
|
|
3321
1375
|
written.push(HARNESS_REPORT_MARKDOWN_PATH);
|
|
3322
1376
|
}
|
|
3323
1377
|
return written;
|
|
@@ -3391,10 +1445,10 @@ function hashHarnessReport(report) {
|
|
|
3391
1445
|
return hashStableJson(report);
|
|
3392
1446
|
}
|
|
3393
1447
|
function hashStableJson(value) {
|
|
3394
|
-
return
|
|
1448
|
+
return createHash("sha256").update(JSON.stringify(sortJson(value))).digest("hex");
|
|
3395
1449
|
}
|
|
3396
1450
|
function graphForReport(graph) {
|
|
3397
|
-
return
|
|
1451
|
+
return sortJson({
|
|
3398
1452
|
...graph,
|
|
3399
1453
|
scan: {
|
|
3400
1454
|
...graph.scan,
|
|
@@ -3443,14 +1497,14 @@ function upsertCitation(citations, citationIndex, evidence, rootDir) {
|
|
|
3443
1497
|
function citationExcerpt(rootDir, path, line) {
|
|
3444
1498
|
if (isUnsafeCitationPath(path))
|
|
3445
1499
|
return {};
|
|
3446
|
-
const absPath =
|
|
3447
|
-
if (!isInside(rootDir, absPath) || !
|
|
1500
|
+
const absPath = resolve(rootDir, path);
|
|
1501
|
+
if (!isInside(rootDir, absPath) || !existsSync(absPath))
|
|
3448
1502
|
return {};
|
|
3449
1503
|
try {
|
|
3450
|
-
const stat =
|
|
1504
|
+
const stat = lstatSync(absPath);
|
|
3451
1505
|
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 128 * 1024)
|
|
3452
1506
|
return {};
|
|
3453
|
-
const lines =
|
|
1507
|
+
const lines = readFileSync(absPath, "utf8").split(/\r?\n/);
|
|
3454
1508
|
const selected = line ? lines[line - 1] : lines.find((candidate) => candidate.trim().length > 0);
|
|
3455
1509
|
if (!selected)
|
|
3456
1510
|
return {};
|
|
@@ -3588,17 +1642,17 @@ function validateCitation(citation, rootDir, errors, warnings, options) {
|
|
|
3588
1642
|
errors.push(`Citation ${citation.id} points to an unsafe path.`);
|
|
3589
1643
|
return;
|
|
3590
1644
|
}
|
|
3591
|
-
const absPath =
|
|
1645
|
+
const absPath = resolve(rootDir, citation.path);
|
|
3592
1646
|
if (!isInside(rootDir, absPath)) {
|
|
3593
1647
|
errors.push(`Citation ${citation.id} points outside the harness root.`);
|
|
3594
1648
|
return;
|
|
3595
1649
|
}
|
|
3596
|
-
if (!
|
|
1650
|
+
if (!existsSync(absPath)) {
|
|
3597
1651
|
errors.push(`Citation ${citation.id} file does not exist: ${citation.path}.`);
|
|
3598
1652
|
return;
|
|
3599
1653
|
}
|
|
3600
1654
|
try {
|
|
3601
|
-
const stat =
|
|
1655
|
+
const stat = lstatSync(absPath);
|
|
3602
1656
|
if (stat.isSymbolicLink())
|
|
3603
1657
|
errors.push(`Citation ${citation.id} points to a symlink.`);
|
|
3604
1658
|
if (stat.isDirectory()) {
|
|
@@ -3607,7 +1661,7 @@ function validateCitation(citation, rootDir, errors, warnings, options) {
|
|
|
3607
1661
|
}
|
|
3608
1662
|
if (!stat.isFile())
|
|
3609
1663
|
errors.push(`Citation ${citation.id} does not point to a file.`);
|
|
3610
|
-
const lines =
|
|
1664
|
+
const lines = readFileSync(absPath, "utf8").split(/\r?\n/);
|
|
3611
1665
|
if (citation.line !== undefined) {
|
|
3612
1666
|
const lineCount = lines.length;
|
|
3613
1667
|
if (citation.line < 1 || citation.line > lineCount) {
|
|
@@ -3651,24 +1705,24 @@ function isUnsafeCitationPath(path) {
|
|
|
3651
1705
|
return path.startsWith("/") || path.includes("..") || path.split(/[\\/]/).some((part) => part === "..") || isSecretishPath2(path);
|
|
3652
1706
|
}
|
|
3653
1707
|
function isSecretishPath2(path) {
|
|
3654
|
-
const name =
|
|
1708
|
+
const name = basename(path).toLowerCase();
|
|
3655
1709
|
if (name === ".env.example" || name === ".env.sample" || name === ".env.template" || name === ".env.defaults" || /^\.env\..*(example|sample|template)$/.test(name)) {
|
|
3656
1710
|
return false;
|
|
3657
1711
|
}
|
|
3658
1712
|
return name === ".env" || name.startsWith(".env.") || name.includes("secret") || name.includes("private-key") || name.endsWith(".pem");
|
|
3659
1713
|
}
|
|
3660
1714
|
function isInside(rootDir, absPath) {
|
|
3661
|
-
const rel =
|
|
1715
|
+
const rel = relative(resolve(rootDir), resolve(absPath));
|
|
3662
1716
|
return rel === "" || !rel.startsWith("..") && !rel.startsWith("/");
|
|
3663
1717
|
}
|
|
3664
1718
|
function writeJson(path, value) {
|
|
3665
|
-
|
|
3666
|
-
|
|
1719
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
1720
|
+
writeFileSync(path, `${JSON.stringify(value, null, 2)}
|
|
3667
1721
|
`);
|
|
3668
1722
|
}
|
|
3669
1723
|
function stableId(prefix, input) {
|
|
3670
1724
|
const slug = input.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 64);
|
|
3671
|
-
const digest =
|
|
1725
|
+
const digest = createHash("sha256").update(input).digest("hex").slice(0, 12);
|
|
3672
1726
|
if (!slug)
|
|
3673
1727
|
return `${prefix}_${digest}`;
|
|
3674
1728
|
return slug.length >= 64 ? `${prefix}_${slug.slice(0, 51)}_${digest}` : `${prefix}_${slug}`;
|
|
@@ -3676,11 +1730,11 @@ function stableId(prefix, input) {
|
|
|
3676
1730
|
function titleFromId(id) {
|
|
3677
1731
|
return id.replace(/^[a-z]+_/, "").split(/[_\-/]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ") || id;
|
|
3678
1732
|
}
|
|
3679
|
-
function
|
|
1733
|
+
function sortJson(value) {
|
|
3680
1734
|
if (Array.isArray(value))
|
|
3681
|
-
return value.map((item) =>
|
|
1735
|
+
return value.map((item) => sortJson(item));
|
|
3682
1736
|
if (value && typeof value === "object") {
|
|
3683
|
-
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => [key,
|
|
1737
|
+
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => [key, sortJson(item)]));
|
|
3684
1738
|
}
|
|
3685
1739
|
return value;
|
|
3686
1740
|
}
|
|
@@ -3692,19 +1746,19 @@ function normalizePath(path) {
|
|
|
3692
1746
|
}
|
|
3693
1747
|
|
|
3694
1748
|
// src/harness.ts
|
|
3695
|
-
import { createHash as
|
|
3696
|
-
import { execFileSync
|
|
1749
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1750
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
3697
1751
|
import { homedir } from "node:os";
|
|
3698
1752
|
import {
|
|
3699
|
-
existsSync as
|
|
3700
|
-
lstatSync as
|
|
3701
|
-
mkdirSync as
|
|
3702
|
-
readFileSync as
|
|
3703
|
-
readdirSync
|
|
3704
|
-
writeFileSync as
|
|
1753
|
+
existsSync as existsSync3,
|
|
1754
|
+
lstatSync as lstatSync3,
|
|
1755
|
+
mkdirSync as mkdirSync3,
|
|
1756
|
+
readFileSync as readFileSync3,
|
|
1757
|
+
readdirSync,
|
|
1758
|
+
writeFileSync as writeFileSync3
|
|
3705
1759
|
} from "node:fs";
|
|
3706
|
-
import { basename as
|
|
3707
|
-
import { confirm as
|
|
1760
|
+
import { basename as basename4, delimiter, dirname as dirname4, join as join2, relative as relative3, resolve as resolve4, sep } from "node:path";
|
|
1761
|
+
import { confirm as confirm2, select } from "@inquirer/prompts";
|
|
3708
1762
|
|
|
3709
1763
|
// src/analyst-tui.ts
|
|
3710
1764
|
class AnalystTui {
|
|
@@ -3995,8 +2049,8 @@ function stripAnsi2(value) {
|
|
|
3995
2049
|
}
|
|
3996
2050
|
|
|
3997
2051
|
// src/harness-artifact-bodies.ts
|
|
3998
|
-
import { existsSync as
|
|
3999
|
-
import { basename as
|
|
2052
|
+
import { existsSync as existsSync2, lstatSync as lstatSync2, readFileSync as readFileSync2, realpathSync } from "node:fs";
|
|
2053
|
+
import { basename as basename2, dirname as dirname2, isAbsolute, relative as relative2, resolve as resolve2 } from "node:path";
|
|
4000
2054
|
var MAX_SCANNED_FILE_BYTES2 = 512 * 1024;
|
|
4001
2055
|
var SNAPSHOT_BUDGET_SKIP_REASONS = new Set([
|
|
4002
2056
|
"file too large for harness scan",
|
|
@@ -4016,7 +2070,7 @@ function isSecretishPath3(path) {
|
|
|
4016
2070
|
return normalized.includes(".env") || normalized.endsWith(".pem") || normalized.endsWith(".key") || normalized.includes("/secrets/") || normalized.includes("/credentials/");
|
|
4017
2071
|
}
|
|
4018
2072
|
function isOutsideRoot(rel) {
|
|
4019
|
-
return rel === "" || rel === ".." || rel.startsWith("../") ||
|
|
2073
|
+
return rel === "" || rel === ".." || rel.startsWith("../") || isAbsolute(rel);
|
|
4020
2074
|
}
|
|
4021
2075
|
function walkAncestorDirs(fromAbs, rootAbs, maxDepth = 8) {
|
|
4022
2076
|
const dirs = [];
|
|
@@ -4025,7 +2079,7 @@ function walkAncestorDirs(fromAbs, rootAbs, maxDepth = 8) {
|
|
|
4025
2079
|
dirs.push(cursor);
|
|
4026
2080
|
if (normalizePath2(cursor) === normalizePath2(rootAbs))
|
|
4027
2081
|
break;
|
|
4028
|
-
const parent =
|
|
2082
|
+
const parent = dirname2(cursor);
|
|
4029
2083
|
if (parent === cursor)
|
|
4030
2084
|
break;
|
|
4031
2085
|
cursor = parent;
|
|
@@ -4033,16 +2087,16 @@ function walkAncestorDirs(fromAbs, rootAbs, maxDepth = 8) {
|
|
|
4033
2087
|
return dirs;
|
|
4034
2088
|
}
|
|
4035
2089
|
function readBoundedRepoText(absPath, rootDir) {
|
|
4036
|
-
if (!
|
|
2090
|
+
if (!existsSync2(absPath))
|
|
4037
2091
|
return;
|
|
4038
|
-
const rel = normalizePath2(
|
|
2092
|
+
const rel = normalizePath2(relative2(rootDir, absPath));
|
|
4039
2093
|
if (isOutsideRoot(rel))
|
|
4040
2094
|
return;
|
|
4041
2095
|
if (isSecretishPath3(rel))
|
|
4042
2096
|
return;
|
|
4043
2097
|
let stat;
|
|
4044
2098
|
try {
|
|
4045
|
-
stat =
|
|
2099
|
+
stat = lstatSync2(absPath);
|
|
4046
2100
|
} catch {
|
|
4047
2101
|
return;
|
|
4048
2102
|
}
|
|
@@ -4052,20 +2106,20 @@ function readBoundedRepoText(absPath, rootDir) {
|
|
|
4052
2106
|
if (!isTextFile2(rel))
|
|
4053
2107
|
return;
|
|
4054
2108
|
try {
|
|
4055
|
-
if (isOutsideRoot(normalizePath2(
|
|
2109
|
+
if (isOutsideRoot(normalizePath2(relative2(realpathSync(rootDir), realpathSync(absPath))))) {
|
|
4056
2110
|
return;
|
|
4057
2111
|
}
|
|
4058
2112
|
} catch {
|
|
4059
2113
|
return;
|
|
4060
2114
|
}
|
|
4061
2115
|
try {
|
|
4062
|
-
return
|
|
2116
|
+
return readFileSync2(absPath, "utf8");
|
|
4063
2117
|
} catch {
|
|
4064
2118
|
return;
|
|
4065
2119
|
}
|
|
4066
2120
|
}
|
|
4067
2121
|
function readRepoRelativeText(rootDir, relPath) {
|
|
4068
|
-
return readBoundedRepoText(
|
|
2122
|
+
return readBoundedRepoText(resolve2(rootDir, relPath), rootDir);
|
|
4069
2123
|
}
|
|
4070
2124
|
function isDedicatedToolFilePath(path) {
|
|
4071
2125
|
return /(^|\/)tools?\//i.test(path) && /\.(ts|tsx|js|mjs|py)$/i.test(path);
|
|
@@ -4106,14 +2160,14 @@ function annotatedSourceCandidates(sourcePath, annotation) {
|
|
|
4106
2160
|
const explicitSourcePath = normalizePath2(sourcePath) !== normalizePath2(annotation.evidence.path);
|
|
4107
2161
|
if (!explicitSourcePath) {
|
|
4108
2162
|
return [
|
|
4109
|
-
|
|
4110
|
-
|
|
2163
|
+
resolve2(annotation.rootDir, sourcePath),
|
|
2164
|
+
resolve2(dirname2(annotation.sourceAbsPath), sourcePath),
|
|
4111
2165
|
annotation.sourceAbsPath
|
|
4112
2166
|
];
|
|
4113
2167
|
}
|
|
4114
|
-
const rootAbs =
|
|
4115
|
-
const candidates = walkAncestorDirs(
|
|
4116
|
-
candidates.push(
|
|
2168
|
+
const rootAbs = resolve2(annotation.rootDir);
|
|
2169
|
+
const candidates = walkAncestorDirs(dirname2(annotation.sourceAbsPath), rootAbs).map((dir) => resolve2(dir, sourcePath));
|
|
2170
|
+
candidates.push(resolve2(annotation.rootDir, sourcePath));
|
|
4117
2171
|
return candidates;
|
|
4118
2172
|
}
|
|
4119
2173
|
function readAnnotatedSourceText(sourcePath, annotation) {
|
|
@@ -4136,7 +2190,7 @@ function resolveAnnotatedSourcePath(sourcePath, annotation) {
|
|
|
4136
2190
|
seen.add(candidate);
|
|
4137
2191
|
if (readBoundedRepoText(candidate, annotation.rootDir) === undefined)
|
|
4138
2192
|
continue;
|
|
4139
|
-
const rel = normalizePath2(
|
|
2193
|
+
const rel = normalizePath2(relative2(annotation.rootDir, candidate));
|
|
4140
2194
|
if (!isOutsideRoot(rel))
|
|
4141
2195
|
return rel;
|
|
4142
2196
|
}
|
|
@@ -4150,7 +2204,7 @@ function readPromptOrToolBody(type, sourcePath, annotation) {
|
|
|
4150
2204
|
return;
|
|
4151
2205
|
if (type === "prompt" || isDedicatedToolFilePath(sourcePath))
|
|
4152
2206
|
return text;
|
|
4153
|
-
return extractToolBodyFromText(text, first(annotation.values.name) ??
|
|
2207
|
+
return extractToolBodyFromText(text, first(annotation.values.name) ?? basename2(sourcePath)) ?? text;
|
|
4154
2208
|
}
|
|
4155
2209
|
function extractToolBody(file, tool) {
|
|
4156
2210
|
const text = file.text ?? readRepoRelativeText(file.rootDir, file.path);
|
|
@@ -4194,9 +2248,9 @@ function readSourcePathRelativeToEvidence(artifact, rootDir) {
|
|
|
4194
2248
|
const evidencePath = firstEvidencePath(artifact);
|
|
4195
2249
|
if (!evidencePath)
|
|
4196
2250
|
return;
|
|
4197
|
-
const rootAbs =
|
|
4198
|
-
for (const dir of walkAncestorDirs(
|
|
4199
|
-
const text = readBoundedRepoText(
|
|
2251
|
+
const rootAbs = resolve2(rootDir);
|
|
2252
|
+
for (const dir of walkAncestorDirs(dirname2(resolve2(rootDir, evidencePath)), rootAbs)) {
|
|
2253
|
+
const text = readBoundedRepoText(resolve2(dir, artifact.sourcePath), rootDir);
|
|
4200
2254
|
if (text !== undefined)
|
|
4201
2255
|
return text;
|
|
4202
2256
|
}
|
|
@@ -4225,8 +2279,8 @@ function enrichScanCaptureMetadata(graph) {
|
|
|
4225
2279
|
}
|
|
4226
2280
|
|
|
4227
2281
|
// src/harness-candidates.ts
|
|
4228
|
-
import { mkdirSync as
|
|
4229
|
-
import { basename as
|
|
2282
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2283
|
+
import { basename as basename3, dirname as dirname3, extname, resolve as resolve3 } from "node:path";
|
|
4230
2284
|
var EXCERPT_MAX_CHARS = 240;
|
|
4231
2285
|
var MAX_CANDIDATES_PER_FILE_PER_SIGNAL = 20;
|
|
4232
2286
|
var MIN_PROMPT_LITERAL_CHARS = 160;
|
|
@@ -4544,7 +2598,7 @@ function detectAdjacentStringBlocks(file, out) {
|
|
|
4544
2598
|
}
|
|
4545
2599
|
var PROMPT_MODULE_NAME_HINT = /(^|[._-])(system[._-]?)?prompts?([._-]|$)/i;
|
|
4546
2600
|
function detectPromptModule(file, out) {
|
|
4547
|
-
const base =
|
|
2601
|
+
const base = basename3(file.path, extname(file.path));
|
|
4548
2602
|
if (!PROMPT_MODULE_NAME_HINT.test(base))
|
|
4549
2603
|
return;
|
|
4550
2604
|
out.push({
|
|
@@ -4607,12 +2661,12 @@ function detectPromptFile(file, out) {
|
|
|
4607
2661
|
const ext = extname(file.path).toLowerCase();
|
|
4608
2662
|
if (!PROMPT_DOC_EXTENSIONS.has(ext))
|
|
4609
2663
|
return;
|
|
4610
|
-
const base =
|
|
2664
|
+
const base = basename3(file.path).toLowerCase();
|
|
4611
2665
|
if (NON_PROMPT_DOC_BASENAMES.has(base))
|
|
4612
2666
|
return;
|
|
4613
2667
|
if (/(^|\/)(docs?|documentation|examples?|\.github)\//i.test(file.path))
|
|
4614
2668
|
return;
|
|
4615
|
-
const dir =
|
|
2669
|
+
const dir = dirname3(file.path);
|
|
4616
2670
|
const nameHint = PROMPT_NAME_HINT.test(base);
|
|
4617
2671
|
const dirHint = AGENTIC_DIR_HINT.test(dir);
|
|
4618
2672
|
const score = instructionScore(file.text.slice(0, 20000));
|
|
@@ -4677,7 +2731,7 @@ function detectHarnessCandidatesInFiles(files) {
|
|
|
4677
2731
|
function detectHarnessCandidates(rootDir) {
|
|
4678
2732
|
const startedAt = Date.now();
|
|
4679
2733
|
const skipped = [];
|
|
4680
|
-
const root =
|
|
2734
|
+
const root = resolve3(rootDir);
|
|
4681
2735
|
const inventory = inventoryFiles(root, skipped, {
|
|
4682
2736
|
includeFile: (path) => PROMPT_DOC_EXTENSIONS.has(extname(path).toLowerCase())
|
|
4683
2737
|
});
|
|
@@ -4703,9 +2757,9 @@ function detectHarnessCandidates(rootDir) {
|
|
|
4703
2757
|
return redactDeep(report);
|
|
4704
2758
|
}
|
|
4705
2759
|
function writeHarnessCandidatesReport(report, outPath) {
|
|
4706
|
-
const target =
|
|
4707
|
-
|
|
4708
|
-
|
|
2760
|
+
const target = resolve3(outPath);
|
|
2761
|
+
mkdirSync2(dirname3(target), { recursive: true });
|
|
2762
|
+
writeFileSync2(target, `${JSON.stringify(report, null, 2)}
|
|
4709
2763
|
`, "utf8");
|
|
4710
2764
|
return target;
|
|
4711
2765
|
}
|
|
@@ -5039,17 +3093,17 @@ function wrapLine(text, width, indent) {
|
|
|
5039
3093
|
}
|
|
5040
3094
|
|
|
5041
3095
|
// src/harness.ts
|
|
5042
|
-
var
|
|
5043
|
-
var HARNESS_PATH =
|
|
5044
|
-
var HARNESS_SUMMARY_PATH =
|
|
5045
|
-
var SYNC_STATE_PATH =
|
|
3096
|
+
var MODA_DIR = ".moda";
|
|
3097
|
+
var HARNESS_PATH = join2(MODA_DIR, "harness.json");
|
|
3098
|
+
var HARNESS_SUMMARY_PATH = join2(MODA_DIR, "HARNESS.md");
|
|
3099
|
+
var SYNC_STATE_PATH = join2(MODA_DIR, "sync-state.json");
|
|
5046
3100
|
var SCHEMA_VERSION = "harness.v0.1";
|
|
5047
3101
|
var REPORT_JSON_BEGIN = "MODA_HARNESS_REPORT_JSON_BEGIN";
|
|
5048
3102
|
var REPORT_JSON_END = "MODA_HARNESS_REPORT_JSON_END";
|
|
5049
3103
|
var EXTERNAL_ANALYST_ADAPTERS = ["claude", "codex", "cursor"];
|
|
5050
3104
|
async function runHarnessCommand(context) {
|
|
5051
3105
|
const subcommand = context.positionals[0] ?? "scan";
|
|
5052
|
-
const rootDir =
|
|
3106
|
+
const rootDir = resolve4(context.cwd, context.flags.path ?? ".");
|
|
5053
3107
|
const selectedAgentId = selectedRuntimeAgentId(context.flags);
|
|
5054
3108
|
const tenant = resolveHarnessTenant(context.flags, context);
|
|
5055
3109
|
const scanPlan = buildCommandScanPlan(context, rootDir);
|
|
@@ -5100,7 +3154,7 @@ async function runHarnessCommand(context) {
|
|
|
5100
3154
|
throw new CliInputError("Harness report is not approved; refusing to sync.", "Run `moda harness validate-report`, review the report, then run `moda harness approve --yes` before syncing.");
|
|
5101
3155
|
}
|
|
5102
3156
|
const syncGraph2 = graphForReport(report.graph);
|
|
5103
|
-
const sourceCommit2 = report.source.sourceCommit ??
|
|
3157
|
+
const sourceCommit2 = report.source.sourceCommit ?? getGitCommit(rootDir);
|
|
5104
3158
|
const localGraphHash2 = hashStableJson(syncGraph2);
|
|
5105
3159
|
const plan2 = createHarnessSyncPlan({
|
|
5106
3160
|
context,
|
|
@@ -5156,7 +3210,7 @@ async function runHarnessCommand(context) {
|
|
|
5156
3210
|
report: {
|
|
5157
3211
|
reportId: report.reportId,
|
|
5158
3212
|
reportHash,
|
|
5159
|
-
path: normalizePath3(
|
|
3213
|
+
path: normalizePath3(relative3(rootDir, reportPath)) || ".moda/harness-report.json"
|
|
5160
3214
|
},
|
|
5161
3215
|
validation,
|
|
5162
3216
|
approval: {
|
|
@@ -5180,7 +3234,7 @@ async function runHarnessCommand(context) {
|
|
|
5180
3234
|
pathWasExplicit: context.flags.path !== undefined
|
|
5181
3235
|
});
|
|
5182
3236
|
const syncGraph = graphForSync(graph);
|
|
5183
|
-
const sourceCommit =
|
|
3237
|
+
const sourceCommit = getGitCommit(rootDir);
|
|
5184
3238
|
const localGraphHash = hashStableJson2(syncGraph);
|
|
5185
3239
|
const plan = createHarnessSyncPlan({
|
|
5186
3240
|
context,
|
|
@@ -5272,6 +3326,23 @@ async function runHarnessCommand(context) {
|
|
|
5272
3326
|
message: "Starting Moda Analyst harness analysis.",
|
|
5273
3327
|
elapsed_ms: Date.now() - context.startedAt
|
|
5274
3328
|
});
|
|
3329
|
+
if (context.flags["github-actions"] === "true") {
|
|
3330
|
+
const { runGithubActionsAnalyze } = await import("./harness-github-actions-7t3cncnt.js");
|
|
3331
|
+
const result = await runGithubActionsAnalyze(rootDir, {
|
|
3332
|
+
writeReport: (report2) => {
|
|
3333
|
+
const normalized = normalizeHarnessReport(report2);
|
|
3334
|
+
const validation2 = validateHarnessReport(normalized, rootDir);
|
|
3335
|
+
writeHarnessReportArtifacts({
|
|
3336
|
+
rootDir,
|
|
3337
|
+
report: normalized,
|
|
3338
|
+
validation: validation2,
|
|
3339
|
+
markdown: context.outputMode === "human"
|
|
3340
|
+
});
|
|
3341
|
+
}
|
|
3342
|
+
});
|
|
3343
|
+
context.output.writeData({ schema: "harness_github_actions.v0.1", ...result });
|
|
3344
|
+
return { exitCode: result.status === "completed" ? 0 : 1 };
|
|
3345
|
+
}
|
|
5275
3346
|
if (isRemoteHarnessAnalyze(context)) {
|
|
5276
3347
|
return runRemoteHarnessAnalyst({ context, rootDir, scanPlan, tenant });
|
|
5277
3348
|
}
|
|
@@ -5309,7 +3380,7 @@ async function runHarnessCommand(context) {
|
|
|
5309
3380
|
const report = createHarnessReport({
|
|
5310
3381
|
graph,
|
|
5311
3382
|
rootDir,
|
|
5312
|
-
sourceCommit:
|
|
3383
|
+
sourceCommit: getGitCommit(rootDir),
|
|
5313
3384
|
analystAdapter: "local-scan"
|
|
5314
3385
|
});
|
|
5315
3386
|
const validation = validateHarnessReport(report, rootDir);
|
|
@@ -5373,7 +3444,7 @@ async function runHarnessCommand(context) {
|
|
|
5373
3444
|
report: {
|
|
5374
3445
|
reportId: approval.report.reportId,
|
|
5375
3446
|
reportHash: hashHarnessReport(approval.report),
|
|
5376
|
-
path: normalizePath3(
|
|
3447
|
+
path: normalizePath3(relative3(rootDir, reportPath)) || ".moda/harness-report.json"
|
|
5377
3448
|
},
|
|
5378
3449
|
validation: approval.validation,
|
|
5379
3450
|
approval: approval.approval
|
|
@@ -5393,9 +3464,9 @@ async function runHarnessCommand(context) {
|
|
|
5393
3464
|
throw new CliInputError("Missing output path for --out.", "Usage: moda harness candidates --out=<file>");
|
|
5394
3465
|
}
|
|
5395
3466
|
if (outFlag && !context.dryRun) {
|
|
5396
|
-
written.push(writeHarnessCandidatesReport(report,
|
|
3467
|
+
written.push(writeHarnessCandidatesReport(report, resolve4(context.cwd, outFlag)));
|
|
5397
3468
|
}
|
|
5398
|
-
writeHumanProgress(context, `Harness candidates: ${report.stats.candidateCount} candidates across
|
|
3469
|
+
writeHumanProgress(context, `Harness candidates: ${report.stats.candidateCount} candidates across ${report.stats.filesWithCandidates} files (${report.stats.filesScanned} scanned, ${report.stats.filesPerSecond.toLocaleString()} files/sec).`);
|
|
5399
3470
|
context.output.writeData({ scanPlan, ...report, written, dryRun: context.dryRun });
|
|
5400
3471
|
return { exitCode: 0 };
|
|
5401
3472
|
}
|
|
@@ -5441,8 +3512,8 @@ async function runHarnessCommand(context) {
|
|
|
5441
3512
|
return { exitCode: 0 };
|
|
5442
3513
|
}
|
|
5443
3514
|
case "status": {
|
|
5444
|
-
const manifestPath =
|
|
5445
|
-
const graph =
|
|
3515
|
+
const manifestPath = join2(rootDir, HARNESS_PATH);
|
|
3516
|
+
const graph = existsSync3(manifestPath) ? readHarnessGraph(manifestPath) : scanHarness({
|
|
5446
3517
|
rootDir,
|
|
5447
3518
|
tenantId: tenant?.tenantId,
|
|
5448
3519
|
tenantSource: tenant?.source
|
|
@@ -5452,7 +3523,7 @@ async function runHarnessCommand(context) {
|
|
|
5452
3523
|
const remoteRun = readRemoteRunState(rootDir);
|
|
5453
3524
|
context.output.writeData({
|
|
5454
3525
|
manifest: HARNESS_PATH,
|
|
5455
|
-
exists:
|
|
3526
|
+
exists: existsSync3(manifestPath),
|
|
5456
3527
|
summary: summarizeHarness(graph),
|
|
5457
3528
|
graphHash: currentGraphHash,
|
|
5458
3529
|
remoteRun: remoteRun ? {
|
|
@@ -5587,14 +3658,14 @@ async function runExternalHarnessAnalyst(options) {
|
|
|
5587
3658
|
reportPath: HARNESS_REPORT_PATH
|
|
5588
3659
|
}));
|
|
5589
3660
|
}
|
|
5590
|
-
const timeoutMs = numericEnv("MODA_HARNESS_ANALYST_TIMEOUT_MS",
|
|
3661
|
+
const timeoutMs = numericEnv("MODA_HARNESS_ANALYST_TIMEOUT_MS", 1200000);
|
|
5591
3662
|
try {
|
|
5592
3663
|
const result = await runExternalAnalystWithRetries(invocation, {
|
|
5593
3664
|
cwd: options.rootDir,
|
|
5594
3665
|
env: buildExternalAnalystEnv(options.context.env),
|
|
5595
3666
|
inheritedOutput,
|
|
5596
3667
|
timeoutMs,
|
|
5597
|
-
maxBuffer:
|
|
3668
|
+
maxBuffer: 20971520,
|
|
5598
3669
|
progress: inheritedOutput && !options.context.quiet || options.context.outputMode === "agent-stream",
|
|
5599
3670
|
progressToStderr: inheritedOutput && !options.context.quiet && !analystTui,
|
|
5600
3671
|
streamEvents: invocation.streamEvents,
|
|
@@ -5704,8 +3775,8 @@ function buildExternalAnalystEnv(env) {
|
|
|
5704
3775
|
return next;
|
|
5705
3776
|
}
|
|
5706
3777
|
function readExternalAnalystReport(rootDir, result, adapter) {
|
|
5707
|
-
const reportPath =
|
|
5708
|
-
if (
|
|
3778
|
+
const reportPath = resolve4(rootDir, HARNESS_REPORT_PATH);
|
|
3779
|
+
if (existsSync3(reportPath))
|
|
5709
3780
|
return repairExternalAnalystReport(readHarnessReport(reportPath), rootDir);
|
|
5710
3781
|
const output = [
|
|
5711
3782
|
result.stdout,
|
|
@@ -5791,12 +3862,12 @@ function repairCitationLines(report, rootDir) {
|
|
|
5791
3862
|
if (citation.type !== "file" && citation.type !== "generated_doc" || !citation.path || !citation.excerpt)
|
|
5792
3863
|
continue;
|
|
5793
3864
|
const excerpt = citation.excerpt;
|
|
5794
|
-
const candidatePath =
|
|
5795
|
-
if (!
|
|
3865
|
+
const candidatePath = resolve4(rootDir, citation.path);
|
|
3866
|
+
if (!existsSync3(candidatePath))
|
|
5796
3867
|
continue;
|
|
5797
3868
|
let text;
|
|
5798
3869
|
try {
|
|
5799
|
-
text =
|
|
3870
|
+
text = readFileSync3(candidatePath, "utf8");
|
|
5800
3871
|
} catch {
|
|
5801
3872
|
continue;
|
|
5802
3873
|
}
|
|
@@ -6015,7 +4086,7 @@ async function runExternalAnalystWithRetries(invocation, options) {
|
|
|
6015
4086
|
process.stderr.write(`${renderHarnessProgress(message)}
|
|
6016
4087
|
`);
|
|
6017
4088
|
if (options.backoffMs > 0)
|
|
6018
|
-
await
|
|
4089
|
+
await sleep(options.backoffMs);
|
|
6019
4090
|
}
|
|
6020
4091
|
lastResult = await runExternalAnalystProcess(invocation, options);
|
|
6021
4092
|
if (!isRetryableExternalAnalystFailure(lastResult))
|
|
@@ -6036,7 +4107,7 @@ function externalAnalystMaxAttempts(adapter) {
|
|
|
6036
4107
|
const defaultRetries = adapter === "claude" ? 2 : 1;
|
|
6037
4108
|
return 1 + numericEnvAllowZero("MODA_HARNESS_ANALYST_RETRIES", defaultRetries);
|
|
6038
4109
|
}
|
|
6039
|
-
function
|
|
4110
|
+
function sleep(ms) {
|
|
6040
4111
|
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
6041
4112
|
}
|
|
6042
4113
|
function recordExternalAgentTextChunk(text, tui) {
|
|
@@ -6336,13 +4407,13 @@ async function runCodingAgent(options) {
|
|
|
6336
4407
|
stream,
|
|
6337
4408
|
mode: options.mode
|
|
6338
4409
|
});
|
|
6339
|
-
const timeoutMs = options.timeoutMs ?? numericEnv("MODA_HARNESS_ANALYST_TIMEOUT_MS",
|
|
4410
|
+
const timeoutMs = options.timeoutMs ?? numericEnv("MODA_HARNESS_ANALYST_TIMEOUT_MS", 1200000);
|
|
6340
4411
|
const result = await runExternalAnalystWithRetries(invocation, {
|
|
6341
4412
|
cwd: options.rootDir,
|
|
6342
4413
|
env: buildExternalAnalystEnv(options.env ?? process.env),
|
|
6343
4414
|
inheritedOutput: false,
|
|
6344
4415
|
timeoutMs,
|
|
6345
|
-
maxBuffer:
|
|
4416
|
+
maxBuffer: 20971520,
|
|
6346
4417
|
progress: Boolean(options.onProgress) || Boolean(options.progressToStderr),
|
|
6347
4418
|
progressToStderr: options.progressToStderr ?? false,
|
|
6348
4419
|
streamEvents: stream,
|
|
@@ -6483,10 +4554,10 @@ function listAnalystPreflightPaths(rootDir) {
|
|
|
6483
4554
|
}
|
|
6484
4555
|
function readGitTrackedPaths(rootDir) {
|
|
6485
4556
|
try {
|
|
6486
|
-
return
|
|
4557
|
+
return execFileSync("git", ["ls-files", "-co", "--exclude-standard"], {
|
|
6487
4558
|
cwd: rootDir,
|
|
6488
4559
|
encoding: "utf8",
|
|
6489
|
-
maxBuffer:
|
|
4560
|
+
maxBuffer: 4194304
|
|
6490
4561
|
}).split(/\r?\n/).filter(Boolean);
|
|
6491
4562
|
} catch {
|
|
6492
4563
|
return [];
|
|
@@ -6497,10 +4568,10 @@ function readDirectoryPreflightPaths(rootDir) {
|
|
|
6497
4568
|
const stack = ["."];
|
|
6498
4569
|
while (stack.length > 0 && paths.length < 2000) {
|
|
6499
4570
|
const current = stack.pop() ?? ".";
|
|
6500
|
-
const absolute = current === "." ? rootDir :
|
|
4571
|
+
const absolute = current === "." ? rootDir : join2(rootDir, current);
|
|
6501
4572
|
let entries;
|
|
6502
4573
|
try {
|
|
6503
|
-
entries =
|
|
4574
|
+
entries = readdirSync(absolute);
|
|
6504
4575
|
} catch {
|
|
6505
4576
|
continue;
|
|
6506
4577
|
}
|
|
@@ -6508,10 +4579,10 @@ function readDirectoryPreflightPaths(rootDir) {
|
|
|
6508
4579
|
const child = current === "." ? entry : `${current}/${entry}`;
|
|
6509
4580
|
if (shouldSkipPreflightPath(child))
|
|
6510
4581
|
continue;
|
|
6511
|
-
const childAbsolute =
|
|
4582
|
+
const childAbsolute = join2(rootDir, child);
|
|
6512
4583
|
let stat;
|
|
6513
4584
|
try {
|
|
6514
|
-
stat =
|
|
4585
|
+
stat = lstatSync3(childAbsolute);
|
|
6515
4586
|
} catch {
|
|
6516
4587
|
continue;
|
|
6517
4588
|
}
|
|
@@ -6536,7 +4607,7 @@ function shouldSkipPreflightPath(path) {
|
|
|
6536
4607
|
return isSecretishPath(normalized);
|
|
6537
4608
|
}
|
|
6538
4609
|
function isManifestLikePath(path) {
|
|
6539
|
-
const name =
|
|
4610
|
+
const name = basename4(path).toLowerCase();
|
|
6540
4611
|
return /^(package\.json|pyproject\.toml|requirements.*\.txt|uv\.lock|poetry\.lock|pnpm-lock\.yaml|bun\.lockb?|yarn\.lock|cargo\.toml|go\.mod|deno\.json|turbo\.json|langgraph\.json|agents\.json|config\.(ts|js|json|yaml|yml|toml))$/.test(name);
|
|
6541
4612
|
}
|
|
6542
4613
|
function isDocLikePath(path) {
|
|
@@ -6566,15 +4637,15 @@ function resolveAnalystCommand(adapter) {
|
|
|
6566
4637
|
return findOnPath("cursor-agent") || findOnPath("cursor-cli") || findOnPath("cursor") || findOnPath("/Applications/Cursor.app/Contents/Resources/app/bin/cursor");
|
|
6567
4638
|
}
|
|
6568
4639
|
function cursorSubcommandArgs(command, args) {
|
|
6569
|
-
const name =
|
|
4640
|
+
const name = basename4(command);
|
|
6570
4641
|
return name === "cursor" || name === "cursor-cli" ? ["agent", ...args] : args;
|
|
6571
4642
|
}
|
|
6572
4643
|
function findOnPath(command) {
|
|
6573
|
-
if (command.includes("/") &&
|
|
4644
|
+
if (command.includes("/") && existsSync3(command))
|
|
6574
4645
|
return command;
|
|
6575
4646
|
for (const dir of (process.env.PATH ?? "").split(delimiter).filter(Boolean)) {
|
|
6576
|
-
const candidate =
|
|
6577
|
-
if (
|
|
4647
|
+
const candidate = join2(dir, command);
|
|
4648
|
+
if (existsSync3(candidate))
|
|
6578
4649
|
return candidate;
|
|
6579
4650
|
}
|
|
6580
4651
|
return;
|
|
@@ -6587,7 +4658,7 @@ function truncateText(value, maxLength) {
|
|
|
6587
4658
|
return compact.length > maxLength ? `${compact.slice(0, maxLength - 1)}...` : compact;
|
|
6588
4659
|
}
|
|
6589
4660
|
function scanHarness(options = {}) {
|
|
6590
|
-
const rootDir =
|
|
4661
|
+
const rootDir = resolve4(options.rootDir ?? process.cwd());
|
|
6591
4662
|
const skipped = [];
|
|
6592
4663
|
const files = inventoryFiles(rootDir, skipped);
|
|
6593
4664
|
const annotations = files.flatMap((file) => parseAnnotations(file));
|
|
@@ -6595,7 +4666,7 @@ function scanHarness(options = {}) {
|
|
|
6595
4666
|
const readPlan = buildHarnessReadPlan(rootDir, options.pathWasExplicit === true);
|
|
6596
4667
|
const graph = {
|
|
6597
4668
|
schemaVersion: SCHEMA_VERSION,
|
|
6598
|
-
harnessId: stableId2("harness", repo.name ||
|
|
4669
|
+
harnessId: stableId2("harness", repo.name || basename4(rootDir) || "repo"),
|
|
6599
4670
|
tenant: options.tenantId ? {
|
|
6600
4671
|
tenantId: options.tenantId,
|
|
6601
4672
|
source: options.tenantSource ?? "scan_option"
|
|
@@ -6650,11 +4721,11 @@ function collectSnapshotInputs(rootDir, maxFiles, maxTextBytes) {
|
|
|
6650
4721
|
const inputs = [];
|
|
6651
4722
|
let inventoryFileCount = 0;
|
|
6652
4723
|
let inventoryTextBytes = 0;
|
|
6653
|
-
function
|
|
6654
|
-
const path = normalizePath3(
|
|
4724
|
+
function walk(current) {
|
|
4725
|
+
const path = normalizePath3(relative3(rootDir, current)) || ".";
|
|
6655
4726
|
let stat;
|
|
6656
4727
|
try {
|
|
6657
|
-
stat =
|
|
4728
|
+
stat = lstatSync3(current);
|
|
6658
4729
|
} catch {
|
|
6659
4730
|
return;
|
|
6660
4731
|
}
|
|
@@ -6665,8 +4736,8 @@ function collectSnapshotInputs(rootDir, maxFiles, maxTextBytes) {
|
|
|
6665
4736
|
if (stat.isDirectory()) {
|
|
6666
4737
|
if (path !== "." && (isSkippedDir(path) || isSkippedPathPrefix(path)))
|
|
6667
4738
|
return;
|
|
6668
|
-
for (const entry of
|
|
6669
|
-
|
|
4739
|
+
for (const entry of readdirSync(current).sort())
|
|
4740
|
+
walk(join2(current, entry));
|
|
6670
4741
|
return;
|
|
6671
4742
|
}
|
|
6672
4743
|
if (!stat.isFile())
|
|
@@ -6689,7 +4760,7 @@ function collectSnapshotInputs(rootDir, maxFiles, maxTextBytes) {
|
|
|
6689
4760
|
let text = "";
|
|
6690
4761
|
if (isTextFile(path)) {
|
|
6691
4762
|
try {
|
|
6692
|
-
text =
|
|
4763
|
+
text = readFileSync3(current, "utf8");
|
|
6693
4764
|
inventoryTextBytes += Buffer.byteLength(text, "utf8");
|
|
6694
4765
|
} catch {
|
|
6695
4766
|
return;
|
|
@@ -6698,21 +4769,21 @@ function collectSnapshotInputs(rootDir, maxFiles, maxTextBytes) {
|
|
|
6698
4769
|
inputs.push({ path, text, byteSize: stat.size });
|
|
6699
4770
|
inventoryFileCount += 1;
|
|
6700
4771
|
}
|
|
6701
|
-
|
|
4772
|
+
walk(rootDir);
|
|
6702
4773
|
return inputs;
|
|
6703
4774
|
}
|
|
6704
4775
|
function buildSourceSnapshot(rootDir) {
|
|
6705
|
-
const root =
|
|
4776
|
+
const root = resolve4(rootDir);
|
|
6706
4777
|
const maxFiles = numericEnv("MODA_HARNESS_MAX_FILES", DEFAULT_MAX_INVENTORY_FILES);
|
|
6707
4778
|
const maxTextBytes = numericEnv("MODA_HARNESS_MAX_TEXT_BYTES", DEFAULT_MAX_INVENTORY_TEXT_BYTES);
|
|
6708
4779
|
const inputs = collectSnapshotInputs(root, maxFiles, maxTextBytes);
|
|
6709
4780
|
const repo = detectRepo(root, inputs.map((input) => ({
|
|
6710
4781
|
path: input.path,
|
|
6711
|
-
absPath:
|
|
4782
|
+
absPath: join2(root, input.path),
|
|
6712
4783
|
rootDir: root,
|
|
6713
4784
|
text: input.text
|
|
6714
4785
|
})));
|
|
6715
|
-
const gitCommit =
|
|
4786
|
+
const gitCommit = getGitCommit(root);
|
|
6716
4787
|
return packSnapshot(inputs, {
|
|
6717
4788
|
repoName: repo.name,
|
|
6718
4789
|
...gitCommit ? { gitCommit } : {},
|
|
@@ -6722,7 +4793,7 @@ function buildSourceSnapshot(rootDir) {
|
|
|
6722
4793
|
}
|
|
6723
4794
|
function buildCommandScanPlan(context, rootDir) {
|
|
6724
4795
|
const explicitPath = context.flags.path !== undefined;
|
|
6725
|
-
const rel = normalizePath3(
|
|
4796
|
+
const rel = normalizePath3(relative3(context.cwd, rootDir)) || ".";
|
|
6726
4797
|
const scope = explicitPath && rel !== "." ? "narrow" : "broad";
|
|
6727
4798
|
const machineReadable = context.outputMode !== "human";
|
|
6728
4799
|
const approvalRequired = context.positional === "analyze" && scope === "broad" && !context.yes;
|
|
@@ -6759,7 +4830,7 @@ async function approveScanIfNeeded(scanPlan, options = {}) {
|
|
|
6759
4830
|
willSkip: scanPlan.readPlan.willSkip
|
|
6760
4831
|
}));
|
|
6761
4832
|
}
|
|
6762
|
-
const approved = await
|
|
4833
|
+
const approved = await confirm2({
|
|
6763
4834
|
message: options.message ?? "Scan this project to build a Moda harness manifest?",
|
|
6764
4835
|
default: true
|
|
6765
4836
|
});
|
|
@@ -6785,7 +4856,7 @@ async function confirmHarnessDelete(context, idOrKey) {
|
|
|
6785
4856
|
resume_command: ["moda", "harness", "delete", idOrKey, "--yes"]
|
|
6786
4857
|
}, "Re-run with `--yes` to confirm the deletion.");
|
|
6787
4858
|
}
|
|
6788
|
-
const approved = await
|
|
4859
|
+
const approved = await confirm2({ message, default: false });
|
|
6789
4860
|
if (!approved) {
|
|
6790
4861
|
throw new CliInputError("Harness delete was not approved.", "Re-run with `--yes` to skip the prompt, or leave the harness in place.");
|
|
6791
4862
|
}
|
|
@@ -6807,7 +4878,7 @@ function writeHumanBlock(context, message) {
|
|
|
6807
4878
|
function buildHarnessReadPlan(rootDir, pathWasExplicit) {
|
|
6808
4879
|
return {
|
|
6809
4880
|
scope: pathWasExplicit ? "narrow" : "broad",
|
|
6810
|
-
rootPaths: [normalizePath3(
|
|
4881
|
+
rootPaths: [normalizePath3(relative3(rootDir, rootDir)) || "."],
|
|
6811
4882
|
willRead: [
|
|
6812
4883
|
"package manifests",
|
|
6813
4884
|
"source files under the selected path",
|
|
@@ -6925,11 +4996,11 @@ async function syncHarnessWithRemote(context, body) {
|
|
|
6925
4996
|
}, profileOptions);
|
|
6926
4997
|
}
|
|
6927
4998
|
function readHarnessSyncState(rootDir) {
|
|
6928
|
-
const path =
|
|
6929
|
-
if (!
|
|
4999
|
+
const path = join2(rootDir, SYNC_STATE_PATH);
|
|
5000
|
+
if (!existsSync3(path))
|
|
6930
5001
|
return;
|
|
6931
5002
|
try {
|
|
6932
|
-
const parsed = JSON.parse(
|
|
5003
|
+
const parsed = JSON.parse(readFileSync3(path, "utf8"));
|
|
6933
5004
|
if (!isRecord3(parsed) || parsed.schemaVersion !== "sync_state.v0.1") {
|
|
6934
5005
|
return;
|
|
6935
5006
|
}
|
|
@@ -6939,9 +5010,9 @@ function readHarnessSyncState(rootDir) {
|
|
|
6939
5010
|
}
|
|
6940
5011
|
}
|
|
6941
5012
|
function writeHarnessSyncState(rootDir, state) {
|
|
6942
|
-
const path =
|
|
6943
|
-
|
|
6944
|
-
|
|
5013
|
+
const path = join2(rootDir, SYNC_STATE_PATH);
|
|
5014
|
+
mkdirSync3(dirname4(path), { recursive: true });
|
|
5015
|
+
writeFileSync3(path, `${JSON.stringify(state, null, 2)}
|
|
6945
5016
|
`);
|
|
6946
5017
|
return SYNC_STATE_PATH;
|
|
6947
5018
|
}
|
|
@@ -6953,11 +5024,11 @@ function selectedRuntimeAgentId(flags) {
|
|
|
6953
5024
|
return;
|
|
6954
5025
|
}
|
|
6955
5026
|
function loadOrScanHarness(rootDir, tenant, context) {
|
|
6956
|
-
const manifestPath =
|
|
6957
|
-
if (
|
|
5027
|
+
const manifestPath = join2(rootDir, HARNESS_PATH);
|
|
5028
|
+
if (existsSync3(manifestPath))
|
|
6958
5029
|
return readHarnessGraph(manifestPath);
|
|
6959
5030
|
if (context) {
|
|
6960
|
-
writeHarnessNotice(context, `No ${HARNESS_PATH} found; showing a live scan of ${rootDir}.
|
|
5031
|
+
writeHarnessNotice(context, `No ${HARNESS_PATH} found; showing a live scan of ${rootDir}. This is local scan output, NOT synced or account data. Run \`moda harness scan\` to persist it and \`moda harness sync\` to upload it.`);
|
|
6961
5032
|
}
|
|
6962
5033
|
return scanHarness({
|
|
6963
5034
|
rootDir,
|
|
@@ -7010,43 +5081,43 @@ var FRESHER_REPORT_MAX_DIRS = 4000;
|
|
|
7010
5081
|
function findFresherHarnessReports(rootDir) {
|
|
7011
5082
|
let rootMtimeMs = 0;
|
|
7012
5083
|
try {
|
|
7013
|
-
rootMtimeMs =
|
|
5084
|
+
rootMtimeMs = lstatSync3(join2(rootDir, HARNESS_REPORT_PATH)).mtimeMs;
|
|
7014
5085
|
} catch {
|
|
7015
5086
|
rootMtimeMs = 0;
|
|
7016
5087
|
}
|
|
7017
5088
|
const fresher = [];
|
|
7018
5089
|
let visited = 0;
|
|
7019
|
-
const
|
|
5090
|
+
const walk = (dir, depth) => {
|
|
7020
5091
|
if (depth > FRESHER_REPORT_MAX_DEPTH || visited >= FRESHER_REPORT_MAX_DIRS)
|
|
7021
5092
|
return;
|
|
7022
5093
|
visited += 1;
|
|
7023
5094
|
let entries;
|
|
7024
5095
|
try {
|
|
7025
|
-
entries =
|
|
5096
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
7026
5097
|
} catch {
|
|
7027
5098
|
return;
|
|
7028
5099
|
}
|
|
7029
5100
|
for (const entry of entries) {
|
|
7030
5101
|
if (!entry.isDirectory() || entry.name.startsWith(".") || SKIPPED_DIRS.has(entry.name))
|
|
7031
5102
|
continue;
|
|
7032
|
-
const child =
|
|
5103
|
+
const child = join2(dir, entry.name);
|
|
7033
5104
|
try {
|
|
7034
|
-
const candidate =
|
|
5105
|
+
const candidate = lstatSync3(join2(child, HARNESS_REPORT_PATH));
|
|
7035
5106
|
if (candidate.isFile() && candidate.mtimeMs > rootMtimeMs) {
|
|
7036
5107
|
fresher.push({
|
|
7037
|
-
path: normalizePath3(
|
|
5108
|
+
path: normalizePath3(relative3(rootDir, join2(child, HARNESS_REPORT_PATH))),
|
|
7038
5109
|
mtimeMs: candidate.mtimeMs
|
|
7039
5110
|
});
|
|
7040
5111
|
}
|
|
7041
5112
|
} catch {}
|
|
7042
|
-
|
|
5113
|
+
walk(child, depth + 1);
|
|
7043
5114
|
}
|
|
7044
5115
|
};
|
|
7045
|
-
|
|
5116
|
+
walk(rootDir, 1);
|
|
7046
5117
|
return fresher.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
7047
5118
|
}
|
|
7048
5119
|
function warnFresherSubdirHarnessReports(context, rootDir, explicitReport) {
|
|
7049
|
-
const explicitReportPath = explicitReport && explicitReport !== "true" ? normalizePath3(
|
|
5120
|
+
const explicitReportPath = explicitReport && explicitReport !== "true" ? normalizePath3(relative3(rootDir, resolve4(rootDir, explicitReport))) : undefined;
|
|
7050
5121
|
if (explicitReportPath && explicitReportPath !== normalizePath3(HARNESS_REPORT_PATH))
|
|
7051
5122
|
return;
|
|
7052
5123
|
let fresher;
|
|
@@ -7058,18 +5129,18 @@ function warnFresherSubdirHarnessReports(context, rootDir, explicitReport) {
|
|
|
7058
5129
|
if (fresher.length === 0)
|
|
7059
5130
|
return;
|
|
7060
5131
|
const newest = fresher[0];
|
|
7061
|
-
const subdir = normalizePath3(
|
|
7062
|
-
const rootState =
|
|
7063
|
-
writeHarnessNotice(context, `A fresher harness report exists at ${newest.path} (this directory has ${rootState}).
|
|
5132
|
+
const subdir = normalizePath3(dirname4(dirname4(newest.path)));
|
|
5133
|
+
const rootState = existsSync3(join2(rootDir, HARNESS_REPORT_PATH)) ? `older ${HARNESS_REPORT_PATH}` : `no ${HARNESS_REPORT_PATH}`;
|
|
5134
|
+
writeHarnessNotice(context, `A fresher harness report exists at ${newest.path} (this directory has ${rootState}). If you meant that report, re-run with --path=${subdir}.`);
|
|
7064
5135
|
}
|
|
7065
5136
|
function readHarnessGraph(manifestPath) {
|
|
7066
|
-
return JSON.parse(
|
|
5137
|
+
return JSON.parse(readFileSync3(manifestPath, "utf8"));
|
|
7067
5138
|
}
|
|
7068
5139
|
function writeHarnessArtifacts(graph, rootDir, options = {}) {
|
|
7069
|
-
const modaDir =
|
|
7070
|
-
|
|
7071
|
-
const manifestPath =
|
|
7072
|
-
|
|
5140
|
+
const modaDir = join2(rootDir, MODA_DIR);
|
|
5141
|
+
mkdirSync3(modaDir, { recursive: true });
|
|
5142
|
+
const manifestPath = join2(rootDir, HARNESS_PATH);
|
|
5143
|
+
writeFileSync3(manifestPath, `${JSON.stringify(graph, null, 2)}
|
|
7073
5144
|
`);
|
|
7074
5145
|
const written = [HARNESS_PATH];
|
|
7075
5146
|
if (options.summary) {
|
|
@@ -7078,9 +5149,9 @@ function writeHarnessArtifacts(graph, rootDir, options = {}) {
|
|
|
7078
5149
|
return written;
|
|
7079
5150
|
}
|
|
7080
5151
|
function writeHarnessSummary(graph, rootDir) {
|
|
7081
|
-
const summaryPath =
|
|
7082
|
-
|
|
7083
|
-
|
|
5152
|
+
const summaryPath = join2(rootDir, HARNESS_SUMMARY_PATH);
|
|
5153
|
+
mkdirSync3(dirname4(summaryPath), { recursive: true });
|
|
5154
|
+
writeFileSync3(summaryPath, renderHarnessMarkdown(graph));
|
|
7084
5155
|
return HARNESS_SUMMARY_PATH;
|
|
7085
5156
|
}
|
|
7086
5157
|
function renderHarnessMarkdown(graph) {
|
|
@@ -7176,7 +5247,7 @@ function renderFrameworkAndProviderLines(graph) {
|
|
|
7176
5247
|
return lines;
|
|
7177
5248
|
}
|
|
7178
5249
|
function graphForSync(graph) {
|
|
7179
|
-
return
|
|
5250
|
+
return sortJson2({
|
|
7180
5251
|
...graph,
|
|
7181
5252
|
scan: {
|
|
7182
5253
|
...graph.scan,
|
|
@@ -7187,19 +5258,19 @@ function graphForSync(graph) {
|
|
|
7187
5258
|
});
|
|
7188
5259
|
}
|
|
7189
5260
|
function hashStableJson2(value) {
|
|
7190
|
-
return hashText(JSON.stringify(
|
|
5261
|
+
return hashText(JSON.stringify(sortJson2(value)));
|
|
7191
5262
|
}
|
|
7192
|
-
function
|
|
5263
|
+
function sortJson2(value) {
|
|
7193
5264
|
if (Array.isArray(value))
|
|
7194
|
-
return value.map((item) =>
|
|
5265
|
+
return value.map((item) => sortJson2(item));
|
|
7195
5266
|
if (value && typeof value === "object") {
|
|
7196
|
-
return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => [key,
|
|
5267
|
+
return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => [key, sortJson2(item)]));
|
|
7197
5268
|
}
|
|
7198
5269
|
return value;
|
|
7199
5270
|
}
|
|
7200
|
-
function
|
|
5271
|
+
function getGitCommit(rootDir) {
|
|
7201
5272
|
try {
|
|
7202
|
-
return
|
|
5273
|
+
return execFileSync("git", ["rev-parse", "HEAD"], {
|
|
7203
5274
|
cwd: rootDir,
|
|
7204
5275
|
encoding: "utf8",
|
|
7205
5276
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -7221,15 +5292,15 @@ function explainAgent(graph, agent) {
|
|
|
7221
5292
|
}
|
|
7222
5293
|
function inventoryFiles(rootDir, skipped, options = {}) {
|
|
7223
5294
|
const files = [];
|
|
7224
|
-
const rootReal =
|
|
5295
|
+
const rootReal = resolve4(rootDir);
|
|
7225
5296
|
const maxFiles = numericEnv("MODA_HARNESS_MAX_FILES", DEFAULT_MAX_INVENTORY_FILES);
|
|
7226
5297
|
const maxTextBytes = numericEnv("MODA_HARNESS_MAX_TEXT_BYTES", DEFAULT_MAX_INVENTORY_TEXT_BYTES);
|
|
7227
5298
|
let textBytes = 0;
|
|
7228
|
-
function
|
|
7229
|
-
const rel = normalizePath3(
|
|
5299
|
+
function walk(current) {
|
|
5300
|
+
const rel = normalizePath3(relative3(rootReal, current)) || ".";
|
|
7230
5301
|
let stat;
|
|
7231
5302
|
try {
|
|
7232
|
-
stat =
|
|
5303
|
+
stat = lstatSync3(current);
|
|
7233
5304
|
} catch {
|
|
7234
5305
|
recordSkipped(skipped, { path: rel, reason: "stat failed" });
|
|
7235
5306
|
return;
|
|
@@ -7243,8 +5314,8 @@ function inventoryFiles(rootDir, skipped, options = {}) {
|
|
|
7243
5314
|
recordSkipped(skipped, { path: rel, reason: "ignored directory" });
|
|
7244
5315
|
return;
|
|
7245
5316
|
}
|
|
7246
|
-
for (const entry of
|
|
7247
|
-
|
|
5317
|
+
for (const entry of readdirSync(current).sort()) {
|
|
5318
|
+
walk(join2(current, entry));
|
|
7248
5319
|
}
|
|
7249
5320
|
return;
|
|
7250
5321
|
}
|
|
@@ -7275,7 +5346,7 @@ function inventoryFiles(rootDir, skipped, options = {}) {
|
|
|
7275
5346
|
recordSkipped(skipped, { path: rel, reason: "scan text budget exceeded" });
|
|
7276
5347
|
} else {
|
|
7277
5348
|
try {
|
|
7278
|
-
item.text =
|
|
5349
|
+
item.text = readFileSync3(current, "utf8");
|
|
7279
5350
|
textBytes += Buffer.byteLength(item.text, "utf8");
|
|
7280
5351
|
} catch {
|
|
7281
5352
|
recordSkipped(skipped, { path: rel, reason: "text read failed" });
|
|
@@ -7284,7 +5355,7 @@ function inventoryFiles(rootDir, skipped, options = {}) {
|
|
|
7284
5355
|
}
|
|
7285
5356
|
files.push(item);
|
|
7286
5357
|
}
|
|
7287
|
-
|
|
5358
|
+
walk(rootReal);
|
|
7288
5359
|
return files;
|
|
7289
5360
|
}
|
|
7290
5361
|
function shouldSkipDirectory(rel) {
|
|
@@ -7330,7 +5401,7 @@ function numericEnvAllowZero(name, fallback) {
|
|
|
7330
5401
|
function detectRepo(rootDir, files) {
|
|
7331
5402
|
const packageJson = files.find((file) => file.path === "package.json");
|
|
7332
5403
|
const pyproject = files.find((file) => file.path === "pyproject.toml");
|
|
7333
|
-
const name = packageJson?.text ? safePackageName(packageJson.text) ??
|
|
5404
|
+
const name = packageJson?.text ? safePackageName(packageJson.text) ?? basename4(rootDir) : pyproject?.text ? safeTomlName(pyproject.text) ?? basename4(rootDir) : basename4(rootDir);
|
|
7334
5405
|
const packageManagers = [
|
|
7335
5406
|
files.some((file) => file.path === "bun.lock" || file.path === "bun.lockb") ? "bun" : null,
|
|
7336
5407
|
files.some((file) => file.path === "pnpm-lock.yaml") ? "pnpm" : null,
|
|
@@ -7442,7 +5513,7 @@ function runtimeAgentFromAnnotation(annotation) {
|
|
|
7442
5513
|
kind: "runtime_agent",
|
|
7443
5514
|
purpose: first2(values.purpose) ?? first2(values.description),
|
|
7444
5515
|
familyId: first2(values.family),
|
|
7445
|
-
root: normalizePath3(first2(values.root) ??
|
|
5516
|
+
root: normalizePath3(first2(values.root) ?? dirname4(annotation.evidence.path)),
|
|
7446
5517
|
entrypointIds: values.entrypoint ?? [stableId2("entry", annotation.evidence.path)],
|
|
7447
5518
|
channelAdapterIds: values.channel ?? [],
|
|
7448
5519
|
artifactIds: [...values.artifact ?? [], ...values.prompt ?? [], ...values.tool ?? []],
|
|
@@ -7582,7 +5653,7 @@ function maybeAddPromptArtifact(graph, file) {
|
|
|
7582
5653
|
if (!isPrompt)
|
|
7583
5654
|
return;
|
|
7584
5655
|
const id = stableId2("prompt", file.path);
|
|
7585
|
-
const text = file.text ??
|
|
5656
|
+
const text = file.text ?? readFileSync3(file.absPath, "utf8");
|
|
7586
5657
|
upsertArtifact(graph, {
|
|
7587
5658
|
id,
|
|
7588
5659
|
type: "prompt",
|
|
@@ -7604,7 +5675,7 @@ function maybeAddSkillArtifact(graph, file) {
|
|
|
7604
5675
|
upsertArtifact(graph, {
|
|
7605
5676
|
id: stableId2("skill", file.path),
|
|
7606
5677
|
type: "skill",
|
|
7607
|
-
name: titleFromId2(
|
|
5678
|
+
name: titleFromId2(basename4(file.path).replace(/\.(md|mdc)$/i, "")),
|
|
7608
5679
|
scope: "global",
|
|
7609
5680
|
sourcePath: file.path,
|
|
7610
5681
|
contentHash: hashText(file.text ?? ""),
|
|
@@ -7647,7 +5718,7 @@ function inferRuntimeAgent(file, graph) {
|
|
|
7647
5718
|
return null;
|
|
7648
5719
|
if (!/\.(ts|tsx|js|mjs|py)$/i.test(file.path))
|
|
7649
5720
|
return null;
|
|
7650
|
-
if (
|
|
5721
|
+
if (basename4(file.path) === "__init__.py")
|
|
7651
5722
|
return null;
|
|
7652
5723
|
const loweredPath = file.path.toLowerCase();
|
|
7653
5724
|
const loweredText = file.text.toLowerCase();
|
|
@@ -7678,7 +5749,7 @@ function inferRuntimeAgent(file, graph) {
|
|
|
7678
5749
|
name: titleFromId2(file.path.replace(/\.(ts|tsx|js|mjs|py)$/i, "")),
|
|
7679
5750
|
kind: "runtime_agent",
|
|
7680
5751
|
purpose: inferAgentPurpose(file),
|
|
7681
|
-
root: normalizePath3(
|
|
5752
|
+
root: normalizePath3(dirname4(file.path)),
|
|
7682
5753
|
entrypointIds: [entryId],
|
|
7683
5754
|
channelAdapterIds: channelIds,
|
|
7684
5755
|
artifactIds,
|
|
@@ -7754,7 +5825,7 @@ function inferArtifactsForFile(file, graph) {
|
|
|
7754
5825
|
return false;
|
|
7755
5826
|
if (artifact.sourcePath === file.path)
|
|
7756
5827
|
return true;
|
|
7757
|
-
const base =
|
|
5828
|
+
const base = basename4(artifact.sourcePath);
|
|
7758
5829
|
if (text.includes(base) || text.includes(artifact.sourcePath))
|
|
7759
5830
|
return true;
|
|
7760
5831
|
if (artifact.type === "tool") {
|
|
@@ -8401,7 +6472,7 @@ function upsertDeployment(graph, next) {
|
|
|
8401
6472
|
}
|
|
8402
6473
|
function contentHashForAnnotatedSource(sourcePath, annotation) {
|
|
8403
6474
|
const bodyAnnotation = annotation;
|
|
8404
|
-
const rootCandidate =
|
|
6475
|
+
const rootCandidate = resolve4(annotation.rootDir, sourcePath);
|
|
8405
6476
|
const candidates = [
|
|
8406
6477
|
rootCandidate,
|
|
8407
6478
|
...annotatedSourceCandidates(sourcePath, bodyAnnotation).filter((c) => c !== rootCandidate)
|
|
@@ -8418,14 +6489,14 @@ function contentHashForAnnotatedSource(sourcePath, annotation) {
|
|
|
8418
6489
|
return;
|
|
8419
6490
|
}
|
|
8420
6491
|
function readSmallTextFileForHash(absPath, rootDir) {
|
|
8421
|
-
if (!
|
|
6492
|
+
if (!existsSync3(absPath))
|
|
8422
6493
|
return;
|
|
8423
|
-
const rel = normalizePath3(
|
|
6494
|
+
const rel = normalizePath3(relative3(rootDir, absPath));
|
|
8424
6495
|
if (isSecretishPath(rel))
|
|
8425
6496
|
return;
|
|
8426
6497
|
let stat;
|
|
8427
6498
|
try {
|
|
8428
|
-
stat =
|
|
6499
|
+
stat = lstatSync3(absPath);
|
|
8429
6500
|
} catch {
|
|
8430
6501
|
return;
|
|
8431
6502
|
}
|
|
@@ -8435,7 +6506,7 @@ function readSmallTextFileForHash(absPath, rootDir) {
|
|
|
8435
6506
|
if (!isTextFile(rel))
|
|
8436
6507
|
return;
|
|
8437
6508
|
try {
|
|
8438
|
-
return
|
|
6509
|
+
return readFileSync3(absPath, "utf8");
|
|
8439
6510
|
} catch {
|
|
8440
6511
|
return;
|
|
8441
6512
|
}
|
|
@@ -8519,13 +6590,13 @@ function titleFromId2(id) {
|
|
|
8519
6590
|
return id.replace(/^(agent|family|channel|prompt|tool|skill|eval|identity|deploy|entry|env)_/, "").split(/[_/.-]+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ") || id;
|
|
8520
6591
|
}
|
|
8521
6592
|
function promptNameFromPath(path) {
|
|
8522
|
-
return titleFromId2(
|
|
6593
|
+
return titleFromId2(basename4(path).replace(/\.(prompt\.)?(md|json|ya?ml)$/i, ""));
|
|
8523
6594
|
}
|
|
8524
6595
|
function normalizePath3(path) {
|
|
8525
6596
|
return path.split(sep).join("/");
|
|
8526
6597
|
}
|
|
8527
6598
|
function hashText(text) {
|
|
8528
|
-
return
|
|
6599
|
+
return createHash2("sha256").update(text).digest("hex");
|
|
8529
6600
|
}
|
|
8530
6601
|
|
|
8531
6602
|
// src/harness-remote.ts
|
|
@@ -8536,7 +6607,7 @@ var MIN_REMOTE_POLL_DELAY_MS = 250;
|
|
|
8536
6607
|
var REMOTE_POLL_MAX_CONSECUTIVE_FAILURES = 4;
|
|
8537
6608
|
var REMOTE_UPLOAD_TIMEOUT_MS = 120000;
|
|
8538
6609
|
var REMOTE_STATUS_TIMEOUT_MS = 30000;
|
|
8539
|
-
var HARNESS_REMOTE_RUN_STATE_PATH =
|
|
6610
|
+
var HARNESS_REMOTE_RUN_STATE_PATH = join3(".moda", "harness-remote-run.json");
|
|
8540
6611
|
function isRemoteHarnessAnalyze(context) {
|
|
8541
6612
|
const flag = context.flags.remote;
|
|
8542
6613
|
const noRemote = context.flags["no-remote"] ?? context.flags.no_remote;
|
|
@@ -8551,11 +6622,11 @@ function isRemoteHarnessAnalyze(context) {
|
|
|
8551
6622
|
return env !== "" && env !== "0" && env !== "false";
|
|
8552
6623
|
}
|
|
8553
6624
|
function readRemoteRunState(rootDir) {
|
|
8554
|
-
const path =
|
|
8555
|
-
if (!
|
|
6625
|
+
const path = join3(rootDir, HARNESS_REMOTE_RUN_STATE_PATH);
|
|
6626
|
+
if (!existsSync4(path))
|
|
8556
6627
|
return;
|
|
8557
6628
|
try {
|
|
8558
|
-
const parsed = JSON.parse(
|
|
6629
|
+
const parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
8559
6630
|
if (!parsed || typeof parsed.run_id !== "string" || parsed.run_id.length === 0)
|
|
8560
6631
|
return;
|
|
8561
6632
|
return {
|
|
@@ -8569,8 +6640,8 @@ function readRemoteRunState(rootDir) {
|
|
|
8569
6640
|
}
|
|
8570
6641
|
function writeRemoteRunState(rootDir, state) {
|
|
8571
6642
|
try {
|
|
8572
|
-
|
|
8573
|
-
|
|
6643
|
+
mkdirSync4(join3(rootDir, ".moda"), { recursive: true });
|
|
6644
|
+
writeFileSync4(join3(rootDir, HARNESS_REMOTE_RUN_STATE_PATH), `${JSON.stringify(state, null, 2)}
|
|
8574
6645
|
`);
|
|
8575
6646
|
} catch {}
|
|
8576
6647
|
}
|
|
@@ -8666,7 +6737,7 @@ async function pollRemoteAnalyzeUntilDone(options) {
|
|
|
8666
6737
|
message: `Status poll failed (${consecutiveFailures}/${REMOTE_POLL_MAX_CONSECUTIVE_FAILURES}); retrying.`,
|
|
8667
6738
|
elapsed_ms: Date.now() - context.startedAt
|
|
8668
6739
|
});
|
|
8669
|
-
await
|
|
6740
|
+
await sleep(Math.min(delay, Math.max(deadline - Date.now(), MIN_REMOTE_POLL_DELAY_MS)));
|
|
8670
6741
|
if (signal?.aborted)
|
|
8671
6742
|
return last;
|
|
8672
6743
|
delay = Math.min(delay * 2, capMs);
|
|
@@ -8686,7 +6757,7 @@ async function pollRemoteAnalyzeUntilDone(options) {
|
|
|
8686
6757
|
message: `Server-side analysis status: ${last?.status ?? "running"}.`,
|
|
8687
6758
|
elapsed_ms: Date.now() - context.startedAt
|
|
8688
6759
|
});
|
|
8689
|
-
await
|
|
6760
|
+
await sleep(Math.min(delay, remaining));
|
|
8690
6761
|
if (signal?.aborted)
|
|
8691
6762
|
return last;
|
|
8692
6763
|
delay = Math.min(delay * 2, capMs);
|
|
@@ -8906,872 +6977,4 @@ async function remoteAnalyzeFetch(url, apiKey, init, timeoutMs) {
|
|
|
8906
6977
|
return response.json();
|
|
8907
6978
|
}
|
|
8908
6979
|
|
|
8909
|
-
|
|
8910
|
-
import { createHash as createHash5 } from "node:crypto";
|
|
8911
|
-
import { existsSync as existsSync11, readFileSync as readFileSync9, statSync } from "node:fs";
|
|
8912
|
-
import { join as join9 } from "node:path";
|
|
8913
|
-
async function runDoctorCommand(context) {
|
|
8914
|
-
const report = await buildDoctorReport(context, {
|
|
8915
|
-
offline: context.flags.offline === "true"
|
|
8916
|
-
});
|
|
8917
|
-
if (context.outputMode === "human") {
|
|
8918
|
-
if (!context.quiet)
|
|
8919
|
-
console.log(renderDoctorHuman(report));
|
|
8920
|
-
return { exitCode: 0 };
|
|
8921
|
-
}
|
|
8922
|
-
context.output.writeData(report, {
|
|
8923
|
-
status: report.summary.fail > 0 ? "error" : "success",
|
|
8924
|
-
...doctorAgentWriteOptions(report, "doctor")
|
|
8925
|
-
});
|
|
8926
|
-
return { exitCode: 0 };
|
|
8927
|
-
}
|
|
8928
|
-
async function runStatusCommand(context) {
|
|
8929
|
-
const report = await buildDoctorReport(context, {
|
|
8930
|
-
offline: context.flags.offline === "true" || context.flags.online !== "true"
|
|
8931
|
-
});
|
|
8932
|
-
const topBroken = firstBrokenCheck(report.checks);
|
|
8933
|
-
const statusPayload = {
|
|
8934
|
-
schemaVersion: "status.v0.1",
|
|
8935
|
-
generatedAt: report.generatedAt,
|
|
8936
|
-
status: report.summary.status,
|
|
8937
|
-
activeProfile: report.profile.active,
|
|
8938
|
-
auth: {
|
|
8939
|
-
apiKey: resolveApiKey() ? "configured" : "missing"
|
|
8940
|
-
},
|
|
8941
|
-
workspace: {
|
|
8942
|
-
tenantId: resolveTenantId() ?? null,
|
|
8943
|
-
baseUrl: resolveModaBaseUrl(),
|
|
8944
|
-
ingestUrl: resolveIngestUrl()
|
|
8945
|
-
},
|
|
8946
|
-
dataFlow: checkStatus(report.checks, "cloud_data_flow"),
|
|
8947
|
-
harness: checkStatus(report.checks, "harness_manifest_current"),
|
|
8948
|
-
harnessSync: checkStatus(report.checks, "harness_sync_state_present"),
|
|
8949
|
-
harnessReport: checkStatus(report.checks, "harness_report_approved_synced"),
|
|
8950
|
-
shadowSpark: checkStatus(report.checks, "shadow_spark_workspace_health"),
|
|
8951
|
-
prompts: checkStatus(report.checks, "prompts_discovered"),
|
|
8952
|
-
skills: checkStatus(report.checks, "skills_discovered"),
|
|
8953
|
-
topBrokenCheck: topBroken ? {
|
|
8954
|
-
id: topBroken.id,
|
|
8955
|
-
status: topBroken.status,
|
|
8956
|
-
severity: topBroken.severity,
|
|
8957
|
-
nextCommand: topBroken.nextCommand
|
|
8958
|
-
} : null,
|
|
8959
|
-
nextCommands: report.nextCommands.slice(0, 3)
|
|
8960
|
-
};
|
|
8961
|
-
if (context.outputMode === "human") {
|
|
8962
|
-
if (!context.quiet)
|
|
8963
|
-
console.log(renderStatusHuman(statusPayload));
|
|
8964
|
-
return { exitCode: 0 };
|
|
8965
|
-
}
|
|
8966
|
-
context.output.writeData(statusPayload, {
|
|
8967
|
-
status: report.summary.fail > 0 ? "error" : "success",
|
|
8968
|
-
...doctorAgentWriteOptions(report, "status")
|
|
8969
|
-
});
|
|
8970
|
-
return { exitCode: 0 };
|
|
8971
|
-
}
|
|
8972
|
-
async function buildDoctorReport(context, options) {
|
|
8973
|
-
const checks = [];
|
|
8974
|
-
const profileConfig = loadProfileConfig({ env: context.env });
|
|
8975
|
-
const activeProfile = getActiveProfile({ profile: context.profile, env: context.env });
|
|
8976
|
-
const paths = getModaPaths({ env: context.env });
|
|
8977
|
-
const profileOptions = { profile: context.profile, env: context.env };
|
|
8978
|
-
const apiKey = resolveApiKey(profileOptions);
|
|
8979
|
-
const tenantId = resolveTenantId(profileOptions);
|
|
8980
|
-
const baseUrl = resolveModaBaseUrl(profileOptions);
|
|
8981
|
-
const ingestUrl = resolveIngestUrl(profileOptions);
|
|
8982
|
-
checks.push({
|
|
8983
|
-
id: "active_profile_resolves",
|
|
8984
|
-
group: "config",
|
|
8985
|
-
title: "Active profile resolves",
|
|
8986
|
-
status: activeProfile ? "pass" : "fail",
|
|
8987
|
-
severity: activeProfile ? "info" : "critical",
|
|
8988
|
-
evidence: [
|
|
8989
|
-
{ label: "active_profile", value: profileConfig.active_profile },
|
|
8990
|
-
{ label: "environment", value: activeProfile.environment }
|
|
8991
|
-
],
|
|
8992
|
-
remediation: activeProfile ? undefined : "Create or select a Moda profile.",
|
|
8993
|
-
nextCommand: activeProfile ? undefined : "moda profiles list"
|
|
8994
|
-
});
|
|
8995
|
-
checks.push({
|
|
8996
|
-
id: "api_key_resolves",
|
|
8997
|
-
group: "config",
|
|
8998
|
-
title: "API key resolves",
|
|
8999
|
-
status: apiKey ? "pass" : "fail",
|
|
9000
|
-
severity: apiKey ? "info" : "critical",
|
|
9001
|
-
evidence: [{ label: "source", value: apiKey ? "environment/profile/config" : "none" }],
|
|
9002
|
-
remediation: apiKey ? undefined : "Authenticate and create a tenant API key.",
|
|
9003
|
-
nextCommand: apiKey ? undefined : "moda init"
|
|
9004
|
-
});
|
|
9005
|
-
checks.push({
|
|
9006
|
-
id: "tenant_resolves",
|
|
9007
|
-
group: "config",
|
|
9008
|
-
title: "Tenant/workspace resolves",
|
|
9009
|
-
status: tenantId ? "pass" : "warning",
|
|
9010
|
-
severity: tenantId ? "info" : "warning",
|
|
9011
|
-
evidence: [{ label: "tenant_id", value: tenantId ? "configured" : "missing" }],
|
|
9012
|
-
remediation: tenantId ? undefined : "Select a tenant during init or set MODA_TENANT_ID.",
|
|
9013
|
-
nextCommand: tenantId ? undefined : "moda init"
|
|
9014
|
-
});
|
|
9015
|
-
checks.push(authTokenCheck(context, Boolean(apiKey)));
|
|
9016
|
-
checks.push(configPermissionCheck(paths.legacyConfigPath));
|
|
9017
|
-
if (activeProfile.api_key_ref) {
|
|
9018
|
-
const secretPath = secretFilePathFromRef(activeProfile.api_key_ref);
|
|
9019
|
-
if (secretPath)
|
|
9020
|
-
checks.push(secretPermissionCheck(secretPath));
|
|
9021
|
-
checks.push({
|
|
9022
|
-
id: "profile_api_key_ref_resolves",
|
|
9023
|
-
group: "config",
|
|
9024
|
-
title: "Profile API key reference resolves",
|
|
9025
|
-
status: resolveSecretReference(activeProfile.api_key_ref) ? "pass" : "warning",
|
|
9026
|
-
severity: "warning",
|
|
9027
|
-
evidence: [{ label: "api_key_ref", value: activeProfile.api_key_ref }],
|
|
9028
|
-
remediation: "Create or update the profile API key reference.",
|
|
9029
|
-
nextCommand: "moda profiles create"
|
|
9030
|
-
});
|
|
9031
|
-
}
|
|
9032
|
-
checks.push(urlCheck("base_url_valid", "config", "Base URL is valid", baseUrl));
|
|
9033
|
-
checks.push(urlCheck("ingest_url_valid", "config", "Ingest URL is valid", ingestUrl));
|
|
9034
|
-
checks.push(localBaseUrlCheck(activeProfile.environment, baseUrl));
|
|
9035
|
-
const manifestPath = join9(context.cwd, ".moda", "harness.json");
|
|
9036
|
-
const manifestExists = existsSync11(manifestPath);
|
|
9037
|
-
checks.push({
|
|
9038
|
-
id: "harness_manifest_exists",
|
|
9039
|
-
group: "harness",
|
|
9040
|
-
title: "Harness manifest exists",
|
|
9041
|
-
status: manifestExists ? "pass" : "warning",
|
|
9042
|
-
severity: manifestExists ? "info" : "warning",
|
|
9043
|
-
evidence: [{ label: "path", value: ".moda/harness.json" }],
|
|
9044
|
-
remediation: manifestExists ? undefined : "Run `moda harness scan --summary` to scan the project and write a harness manifest.",
|
|
9045
|
-
nextCommand: manifestExists ? undefined : "moda harness scan --summary"
|
|
9046
|
-
});
|
|
9047
|
-
checks.push(harnessCurrentCheck(context.cwd, manifestPath, manifestExists, tenantId));
|
|
9048
|
-
checks.push(harnessSyncStateCheck(context.cwd));
|
|
9049
|
-
checks.push(harnessReportStateCheck(context.cwd));
|
|
9050
|
-
checks.push(discoveryCheck("prompts_discovered", "harness", "Prompts are discovered", context.cwd, [
|
|
9051
|
-
"prompts",
|
|
9052
|
-
".moda/prompts.yml"
|
|
9053
|
-
], "moda prompts init"));
|
|
9054
|
-
checks.push(discoveryCheck("skills_discovered", "harness", "Skills are discovered", context.cwd, [
|
|
9055
|
-
"skills",
|
|
9056
|
-
".claude/skills",
|
|
9057
|
-
".cursor/rules"
|
|
9058
|
-
], "moda skills pull"));
|
|
9059
|
-
checks.push(discoveryCheck("evals_discovered", "harness", "Evals/tests are discovered", context.cwd, [
|
|
9060
|
-
"evals",
|
|
9061
|
-
"tests",
|
|
9062
|
-
"__tests__"
|
|
9063
|
-
]));
|
|
9064
|
-
checks.push(sdkInitializationCheck(context.cwd));
|
|
9065
|
-
checks.push(validationEventPathCheck(ingestUrl));
|
|
9066
|
-
if (options.offline || !apiKey) {
|
|
9067
|
-
checks.push(...offlineCloudChecks(options.offline));
|
|
9068
|
-
} else {
|
|
9069
|
-
checks.push(...await cloudDataFlowChecks(ingestUrl, apiKey));
|
|
9070
|
-
}
|
|
9071
|
-
const nextCommands = nextCommandsForChecks(checks);
|
|
9072
|
-
const summary = summarizeChecks(checks);
|
|
9073
|
-
return {
|
|
9074
|
-
schemaVersion: "doctor.v0.1",
|
|
9075
|
-
generatedAt: new Date().toISOString(),
|
|
9076
|
-
profile: {
|
|
9077
|
-
active: activeProfile.name,
|
|
9078
|
-
requested: context.profile
|
|
9079
|
-
},
|
|
9080
|
-
summary,
|
|
9081
|
-
checks,
|
|
9082
|
-
nextCommands,
|
|
9083
|
-
fixPlan: checks.filter((check) => check.status === "fail" || check.status === "warning").map((check) => ({
|
|
9084
|
-
checkId: check.id,
|
|
9085
|
-
command: check.nextCommand,
|
|
9086
|
-
action: check.remediation ?? "Review this diagnostic."
|
|
9087
|
-
}))
|
|
9088
|
-
};
|
|
9089
|
-
}
|
|
9090
|
-
function authTokenCheck(context, hasApiKey) {
|
|
9091
|
-
const session = loadAuthSession({
|
|
9092
|
-
profile: context.profile,
|
|
9093
|
-
env: context.env,
|
|
9094
|
-
includeExpired: true
|
|
9095
|
-
});
|
|
9096
|
-
const valid = isAuthSessionValid(session);
|
|
9097
|
-
if (!valid && hasApiKey) {
|
|
9098
|
-
return {
|
|
9099
|
-
id: "auth_token_valid",
|
|
9100
|
-
group: "config",
|
|
9101
|
-
title: "Browser auth token is valid",
|
|
9102
|
-
status: "skipped",
|
|
9103
|
-
severity: "info",
|
|
9104
|
-
evidence: [{ label: "reason", value: "api key configured" }]
|
|
9105
|
-
};
|
|
9106
|
-
}
|
|
9107
|
-
return {
|
|
9108
|
-
id: "auth_token_valid",
|
|
9109
|
-
group: "config",
|
|
9110
|
-
title: "Browser auth token is valid",
|
|
9111
|
-
status: valid ? "pass" : "warning",
|
|
9112
|
-
severity: valid ? "info" : "warning",
|
|
9113
|
-
evidence: [
|
|
9114
|
-
{ label: "token", value: session ? "present" : "missing" },
|
|
9115
|
-
...session ? [{ label: "expires_at", value: new Date(session.expires_at * 1000).toISOString() }] : []
|
|
9116
|
-
],
|
|
9117
|
-
remediation: valid ? undefined : "Refresh browser authentication.",
|
|
9118
|
-
nextCommand: valid ? undefined : "moda init"
|
|
9119
|
-
};
|
|
9120
|
-
}
|
|
9121
|
-
function configPermissionCheck(path) {
|
|
9122
|
-
if (!existsSync11(path)) {
|
|
9123
|
-
return {
|
|
9124
|
-
id: "config_file_permissions",
|
|
9125
|
-
group: "config",
|
|
9126
|
-
title: "Config file permissions",
|
|
9127
|
-
status: "skipped",
|
|
9128
|
-
severity: "info",
|
|
9129
|
-
evidence: [{ label: "path", value: path }]
|
|
9130
|
-
};
|
|
9131
|
-
}
|
|
9132
|
-
const permission = ensureSecretFilePermissions(path);
|
|
9133
|
-
return {
|
|
9134
|
-
id: "config_file_permissions",
|
|
9135
|
-
group: "config",
|
|
9136
|
-
title: "Config file permissions",
|
|
9137
|
-
status: permission.secure ? "pass" : "fail",
|
|
9138
|
-
severity: permission.secure ? "info" : "critical",
|
|
9139
|
-
evidence: [
|
|
9140
|
-
{ label: "path", value: path },
|
|
9141
|
-
{ label: "mode", value: permission.mode ?? "unknown" }
|
|
9142
|
-
],
|
|
9143
|
-
remediation: permission.secure ? undefined : "Restrict config file permissions to the current user."
|
|
9144
|
-
};
|
|
9145
|
-
}
|
|
9146
|
-
function secretPermissionCheck(path) {
|
|
9147
|
-
const permission = ensureSecretFilePermissions(path);
|
|
9148
|
-
return {
|
|
9149
|
-
id: "secret_file_permissions",
|
|
9150
|
-
group: "config",
|
|
9151
|
-
title: "Secret file permissions",
|
|
9152
|
-
status: permission.secure ? "pass" : "fail",
|
|
9153
|
-
severity: permission.secure ? "info" : "critical",
|
|
9154
|
-
evidence: [
|
|
9155
|
-
{ label: "path", value: path },
|
|
9156
|
-
{ label: "mode", value: permission.mode ?? "unknown" }
|
|
9157
|
-
],
|
|
9158
|
-
remediation: permission.secure ? undefined : "Restrict secret file permissions to the current user."
|
|
9159
|
-
};
|
|
9160
|
-
}
|
|
9161
|
-
function urlCheck(id, group, title, value) {
|
|
9162
|
-
let valid = false;
|
|
9163
|
-
try {
|
|
9164
|
-
const parsed = new URL(value);
|
|
9165
|
-
valid = parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
9166
|
-
} catch {
|
|
9167
|
-
valid = false;
|
|
9168
|
-
}
|
|
9169
|
-
return {
|
|
9170
|
-
id,
|
|
9171
|
-
group,
|
|
9172
|
-
title,
|
|
9173
|
-
status: valid ? "pass" : "fail",
|
|
9174
|
-
severity: valid ? "info" : "critical",
|
|
9175
|
-
evidence: [{ label: "url", value }],
|
|
9176
|
-
remediation: valid ? undefined : "Configure a valid http(s) URL.",
|
|
9177
|
-
nextCommand: valid ? undefined : "moda profiles create"
|
|
9178
|
-
};
|
|
9179
|
-
}
|
|
9180
|
-
function localBaseUrlCheck(environment, baseUrl) {
|
|
9181
|
-
if (environment !== "local") {
|
|
9182
|
-
return {
|
|
9183
|
-
id: "local_base_url_frontend",
|
|
9184
|
-
group: "config",
|
|
9185
|
-
title: "Local base URL targets the frontend",
|
|
9186
|
-
status: "skipped",
|
|
9187
|
-
severity: "info",
|
|
9188
|
-
evidence: [{ label: "environment", value: environment }]
|
|
9189
|
-
};
|
|
9190
|
-
}
|
|
9191
|
-
let port = null;
|
|
9192
|
-
let loopback = false;
|
|
9193
|
-
try {
|
|
9194
|
-
const parsed = new URL(baseUrl);
|
|
9195
|
-
loopback = isLoopbackBaseUrl(baseUrl);
|
|
9196
|
-
port = parsed.port || (parsed.protocol === "https:" ? "443" : "80");
|
|
9197
|
-
} catch {
|
|
9198
|
-
return {
|
|
9199
|
-
id: "local_base_url_frontend",
|
|
9200
|
-
group: "config",
|
|
9201
|
-
title: "Local base URL targets the frontend",
|
|
9202
|
-
status: "fail",
|
|
9203
|
-
severity: "critical",
|
|
9204
|
-
evidence: [{ label: "base_url", value: baseUrl }],
|
|
9205
|
-
remediation: "Set the local profile base URL to the frontend origin (http://localhost:3000).",
|
|
9206
|
-
nextCommand: "moda profiles create local --environment local --base-url http://localhost:3000"
|
|
9207
|
-
};
|
|
9208
|
-
}
|
|
9209
|
-
const looksLikeBackend = loopback && (port === "3001" || port === "3002");
|
|
9210
|
-
if (looksLikeBackend) {
|
|
9211
|
-
return {
|
|
9212
|
-
id: "local_base_url_frontend",
|
|
9213
|
-
group: "config",
|
|
9214
|
-
title: "Local base URL targets the frontend",
|
|
9215
|
-
status: "warning",
|
|
9216
|
-
severity: "warning",
|
|
9217
|
-
evidence: [
|
|
9218
|
-
{ label: "base_url", value: baseUrl },
|
|
9219
|
-
{ label: "port", value: port ?? "unknown" }
|
|
9220
|
-
],
|
|
9221
|
-
remediation: "Local CLI auth and control APIs are served by the frontend. " + "Use http://localhost:3000 (or your worktree frontend port), not the backend :3001.",
|
|
9222
|
-
nextCommand: "moda profiles create local --environment local --base-url http://localhost:3000 --use"
|
|
9223
|
-
};
|
|
9224
|
-
}
|
|
9225
|
-
if (!loopback) {
|
|
9226
|
-
return {
|
|
9227
|
-
id: "local_base_url_frontend",
|
|
9228
|
-
group: "config",
|
|
9229
|
-
title: "Local base URL targets the frontend",
|
|
9230
|
-
status: "warning",
|
|
9231
|
-
severity: "warning",
|
|
9232
|
-
evidence: [{ label: "base_url", value: baseUrl }],
|
|
9233
|
-
remediation: "Local profile should use a loopback frontend URL (http://localhost:3000). " + "Activate with `moda profiles use local` or set MODA_PROFILE=local.",
|
|
9234
|
-
nextCommand: "moda profiles use local"
|
|
9235
|
-
};
|
|
9236
|
-
}
|
|
9237
|
-
return {
|
|
9238
|
-
id: "local_base_url_frontend",
|
|
9239
|
-
group: "config",
|
|
9240
|
-
title: "Local base URL targets the frontend",
|
|
9241
|
-
status: "pass",
|
|
9242
|
-
severity: "info",
|
|
9243
|
-
evidence: [{ label: "base_url", value: baseUrl }]
|
|
9244
|
-
};
|
|
9245
|
-
}
|
|
9246
|
-
function harnessCurrentCheck(cwd, manifestPath, manifestExists, tenantId) {
|
|
9247
|
-
if (!manifestExists) {
|
|
9248
|
-
return {
|
|
9249
|
-
id: "harness_manifest_current",
|
|
9250
|
-
group: "harness",
|
|
9251
|
-
title: "Harness manifest matches scanner output",
|
|
9252
|
-
status: "skipped",
|
|
9253
|
-
severity: "info",
|
|
9254
|
-
evidence: [{ label: "reason", value: "manifest missing" }],
|
|
9255
|
-
nextCommand: "moda harness scan --summary"
|
|
9256
|
-
};
|
|
9257
|
-
}
|
|
9258
|
-
try {
|
|
9259
|
-
const current = scanHarness({
|
|
9260
|
-
rootDir: cwd,
|
|
9261
|
-
scannerVersion: "doctor",
|
|
9262
|
-
tenantId,
|
|
9263
|
-
tenantSource: tenantId ? "env_or_config" : undefined
|
|
9264
|
-
});
|
|
9265
|
-
const saved = JSON.parse(readFileSync9(manifestPath, "utf8"));
|
|
9266
|
-
const savedHash = stableGraphHash(saved);
|
|
9267
|
-
const currentHash = stableGraphHash(current);
|
|
9268
|
-
const matches = savedHash === currentHash;
|
|
9269
|
-
return {
|
|
9270
|
-
id: "harness_manifest_current",
|
|
9271
|
-
group: "harness",
|
|
9272
|
-
title: "Harness manifest matches scanner output",
|
|
9273
|
-
status: matches ? "pass" : "warning",
|
|
9274
|
-
severity: matches ? "info" : "warning",
|
|
9275
|
-
evidence: [
|
|
9276
|
-
{ label: "saved_hash", value: savedHash.slice(0, 12) },
|
|
9277
|
-
{ label: "current_hash", value: currentHash.slice(0, 12) },
|
|
9278
|
-
...matches ? [] : [{ label: "diff", value: "Manifest hash differs from fresh scanner output." }]
|
|
9279
|
-
],
|
|
9280
|
-
remediation: matches ? undefined : "Refresh the local harness manifest.",
|
|
9281
|
-
nextCommand: matches ? undefined : "moda harness scan --summary"
|
|
9282
|
-
};
|
|
9283
|
-
} catch (err) {
|
|
9284
|
-
return {
|
|
9285
|
-
id: "harness_manifest_current",
|
|
9286
|
-
group: "harness",
|
|
9287
|
-
title: "Harness manifest matches scanner output",
|
|
9288
|
-
status: "fail",
|
|
9289
|
-
severity: "critical",
|
|
9290
|
-
evidence: [{ label: "error", value: err instanceof Error ? err.message : String(err) }],
|
|
9291
|
-
remediation: "Regenerate the harness manifest.",
|
|
9292
|
-
nextCommand: "moda harness scan --summary"
|
|
9293
|
-
};
|
|
9294
|
-
}
|
|
9295
|
-
}
|
|
9296
|
-
function harnessSyncStateCheck(cwd) {
|
|
9297
|
-
const state = readHarnessSyncState(cwd);
|
|
9298
|
-
if (!state) {
|
|
9299
|
-
return {
|
|
9300
|
-
id: "harness_sync_state_present",
|
|
9301
|
-
group: "harness",
|
|
9302
|
-
title: "Harness sync state exists",
|
|
9303
|
-
status: "warning",
|
|
9304
|
-
severity: "warning",
|
|
9305
|
-
evidence: [{ label: "path", value: ".moda/sync-state.json" }],
|
|
9306
|
-
remediation: "Sync the harness graph so Moda can correlate local state with cloud state.",
|
|
9307
|
-
nextCommand: "moda harness sync --dry-run"
|
|
9308
|
-
};
|
|
9309
|
-
}
|
|
9310
|
-
return {
|
|
9311
|
-
id: "harness_sync_state_present",
|
|
9312
|
-
group: "harness",
|
|
9313
|
-
title: "Harness sync state exists",
|
|
9314
|
-
status: "pass",
|
|
9315
|
-
severity: "info",
|
|
9316
|
-
evidence: [
|
|
9317
|
-
{ label: "updated_at", value: state.updatedAt },
|
|
9318
|
-
{ label: "profile", value: state.profile },
|
|
9319
|
-
{ label: "harness_id", value: state.harnessId },
|
|
9320
|
-
{ label: "remote_version_id", value: state.remote.versionId || "none" }
|
|
9321
|
-
]
|
|
9322
|
-
};
|
|
9323
|
-
}
|
|
9324
|
-
function harnessReportStateCheck(cwd) {
|
|
9325
|
-
const reportPath = join9(cwd, HARNESS_REPORT_PATH);
|
|
9326
|
-
if (!existsSync11(reportPath)) {
|
|
9327
|
-
return {
|
|
9328
|
-
id: "harness_report_approved_synced",
|
|
9329
|
-
group: "harness",
|
|
9330
|
-
title: "Harness analyst report is approved and synced",
|
|
9331
|
-
status: "warning",
|
|
9332
|
-
severity: "warning",
|
|
9333
|
-
evidence: [{ label: "path", value: HARNESS_REPORT_PATH }],
|
|
9334
|
-
remediation: "Run a Moda Analyst pass so the harness graph has cited evidence before cloud sync.",
|
|
9335
|
-
nextCommand: "moda harness analyze --summary"
|
|
9336
|
-
};
|
|
9337
|
-
}
|
|
9338
|
-
try {
|
|
9339
|
-
const report = readHarnessReport(reportPath);
|
|
9340
|
-
const validation = validateHarnessReport(report, cwd);
|
|
9341
|
-
const reportHash = hashHarnessReport(report);
|
|
9342
|
-
const evidence = [
|
|
9343
|
-
{ label: "path", value: HARNESS_REPORT_PATH },
|
|
9344
|
-
{ label: "report_hash", value: reportHash.slice(0, 12) },
|
|
9345
|
-
{ label: "validation", value: validation.status }
|
|
9346
|
-
];
|
|
9347
|
-
if (validation.status !== "pass") {
|
|
9348
|
-
return {
|
|
9349
|
-
id: "harness_report_approved_synced",
|
|
9350
|
-
group: "harness",
|
|
9351
|
-
title: "Harness analyst report is approved and synced",
|
|
9352
|
-
status: "fail",
|
|
9353
|
-
severity: "critical",
|
|
9354
|
-
evidence: [
|
|
9355
|
-
...evidence,
|
|
9356
|
-
{ label: "errors", value: validation.errors.slice(0, 3).join(" | ") || "validation failed" }
|
|
9357
|
-
],
|
|
9358
|
-
remediation: "Regenerate or fix the harness report before approving or syncing.",
|
|
9359
|
-
nextCommand: "moda harness analyze --summary"
|
|
9360
|
-
};
|
|
9361
|
-
}
|
|
9362
|
-
const approval = readHarnessReportApproval(cwd);
|
|
9363
|
-
const approved = approval?.schemaVersion === HARNESS_REPORT_APPROVAL_SCHEMA_VERSION && approval.reportId === report.reportId && approval.reportHash === reportHash && approval.approved === true;
|
|
9364
|
-
evidence.push({ label: "approval", value: approved ? "approved" : approval ? "stale_or_invalid" : "missing" });
|
|
9365
|
-
if (approval && !approved) {
|
|
9366
|
-
return {
|
|
9367
|
-
id: "harness_report_approved_synced",
|
|
9368
|
-
group: "harness",
|
|
9369
|
-
title: "Harness analyst report is approved and synced",
|
|
9370
|
-
status: "fail",
|
|
9371
|
-
severity: "critical",
|
|
9372
|
-
evidence,
|
|
9373
|
-
remediation: "Approval no longer matches the current report. Review and approve the report again.",
|
|
9374
|
-
nextCommand: "moda harness approve --yes"
|
|
9375
|
-
};
|
|
9376
|
-
}
|
|
9377
|
-
if (!approved) {
|
|
9378
|
-
return {
|
|
9379
|
-
id: "harness_report_approved_synced",
|
|
9380
|
-
group: "harness",
|
|
9381
|
-
title: "Harness analyst report is approved and synced",
|
|
9382
|
-
status: "warning",
|
|
9383
|
-
severity: "warning",
|
|
9384
|
-
evidence,
|
|
9385
|
-
remediation: "Review and approve the validated harness report before syncing.",
|
|
9386
|
-
nextCommand: "moda harness approve --yes"
|
|
9387
|
-
};
|
|
9388
|
-
}
|
|
9389
|
-
const state = readHarnessSyncState(cwd);
|
|
9390
|
-
const synced = state?.source === "approved_report" && state.report?.reportHash === reportHash;
|
|
9391
|
-
evidence.push({ label: "sync", value: synced ? "synced" : "not_synced_to_this_report" }, ...state?.report?.reportHash ? [{ label: "sync_report_hash", value: state.report.reportHash.slice(0, 12) }] : []);
|
|
9392
|
-
return {
|
|
9393
|
-
id: "harness_report_approved_synced",
|
|
9394
|
-
group: "harness",
|
|
9395
|
-
title: "Harness analyst report is approved and synced",
|
|
9396
|
-
status: synced ? "pass" : "warning",
|
|
9397
|
-
severity: synced ? "info" : "warning",
|
|
9398
|
-
evidence,
|
|
9399
|
-
remediation: synced ? undefined : "Sync the approved harness report so Moda cloud has the same harness graph.",
|
|
9400
|
-
nextCommand: synced ? undefined : "moda harness sync --from-report .moda/harness-report.json"
|
|
9401
|
-
};
|
|
9402
|
-
} catch (err) {
|
|
9403
|
-
return {
|
|
9404
|
-
id: "harness_report_approved_synced",
|
|
9405
|
-
group: "harness",
|
|
9406
|
-
title: "Harness analyst report is approved and synced",
|
|
9407
|
-
status: "fail",
|
|
9408
|
-
severity: "critical",
|
|
9409
|
-
evidence: [
|
|
9410
|
-
{ label: "path", value: HARNESS_REPORT_PATH },
|
|
9411
|
-
{ label: "error", value: err instanceof Error ? err.message : String(err) }
|
|
9412
|
-
],
|
|
9413
|
-
remediation: "Regenerate the harness report.",
|
|
9414
|
-
nextCommand: "moda harness analyze --summary"
|
|
9415
|
-
};
|
|
9416
|
-
}
|
|
9417
|
-
}
|
|
9418
|
-
function readHarnessReportApproval(cwd) {
|
|
9419
|
-
const approvalPath = join9(cwd, HARNESS_REPORT_APPROVAL_PATH);
|
|
9420
|
-
if (!existsSync11(approvalPath))
|
|
9421
|
-
return null;
|
|
9422
|
-
try {
|
|
9423
|
-
return JSON.parse(readFileSync9(approvalPath, "utf8"));
|
|
9424
|
-
} catch {
|
|
9425
|
-
return {
|
|
9426
|
-
schemaVersion: HARNESS_REPORT_APPROVAL_SCHEMA_VERSION,
|
|
9427
|
-
reportId: "unreadable",
|
|
9428
|
-
reportHash: "unreadable",
|
|
9429
|
-
approved: true,
|
|
9430
|
-
approvedAt: "",
|
|
9431
|
-
approvedBy: "local-human",
|
|
9432
|
-
reportPath: HARNESS_REPORT_PATH
|
|
9433
|
-
};
|
|
9434
|
-
}
|
|
9435
|
-
}
|
|
9436
|
-
function discoveryCheck(id, group, title, cwd, relativePaths, nextCommand) {
|
|
9437
|
-
const found = relativePaths.filter((path) => existsSync11(join9(cwd, path)));
|
|
9438
|
-
return {
|
|
9439
|
-
id,
|
|
9440
|
-
group,
|
|
9441
|
-
title,
|
|
9442
|
-
status: found.length > 0 ? "pass" : "warning",
|
|
9443
|
-
severity: found.length > 0 ? "info" : "warning",
|
|
9444
|
-
evidence: [{ label: "paths", value: found.length > 0 ? found.join(", ") : "none" }],
|
|
9445
|
-
remediation: found.length > 0 ? undefined : `Expected one of: ${relativePaths.join(", ")}.`,
|
|
9446
|
-
nextCommand: found.length > 0 ? undefined : nextCommand
|
|
9447
|
-
};
|
|
9448
|
-
}
|
|
9449
|
-
function sdkInitializationCheck(cwd) {
|
|
9450
|
-
const candidates = ["src", "app", "agent_harness"];
|
|
9451
|
-
let detected = false;
|
|
9452
|
-
for (const candidate of candidates) {
|
|
9453
|
-
const path = join9(cwd, candidate);
|
|
9454
|
-
if (!existsSync11(path))
|
|
9455
|
-
continue;
|
|
9456
|
-
try {
|
|
9457
|
-
const stat = statSync(path);
|
|
9458
|
-
detected ||= stat.isDirectory() || stat.isFile();
|
|
9459
|
-
} catch {}
|
|
9460
|
-
}
|
|
9461
|
-
return {
|
|
9462
|
-
id: "sdk_initialization_detected",
|
|
9463
|
-
group: "harness",
|
|
9464
|
-
title: "SDK initialization is detectable",
|
|
9465
|
-
status: detected ? "warning" : "skipped",
|
|
9466
|
-
severity: "warning",
|
|
9467
|
-
evidence: [{ label: "status", value: detected ? "code present, instrumentation not verified" : "no source candidates" }],
|
|
9468
|
-
remediation: "Use the generated integration guide to add Moda SDK instrumentation.",
|
|
9469
|
-
nextCommand: "moda init"
|
|
9470
|
-
};
|
|
9471
|
-
}
|
|
9472
|
-
function validationEventPathCheck(ingestUrl) {
|
|
9473
|
-
return {
|
|
9474
|
-
id: "validation_event_path_defined",
|
|
9475
|
-
group: "cloud",
|
|
9476
|
-
title: "Validation event path is defined",
|
|
9477
|
-
status: "pass",
|
|
9478
|
-
severity: "info",
|
|
9479
|
-
evidence: [{ label: "path", value: `${ingestUrl.replace(/\/+$/, "")}/v1/otel/validation` }]
|
|
9480
|
-
};
|
|
9481
|
-
}
|
|
9482
|
-
function offlineCloudChecks(isOffline) {
|
|
9483
|
-
const status = isOffline ? "skipped" : "warning";
|
|
9484
|
-
const evidence = [{ label: "mode", value: isOffline ? "offline" : "missing api key" }];
|
|
9485
|
-
const remediation = isOffline ? "Rerun without --offline to check Moda Cloud." : "Configure an API key before checking cloud data flow.";
|
|
9486
|
-
const nextCommand = isOffline ? "moda doctor --online" : "moda init";
|
|
9487
|
-
return [
|
|
9488
|
-
{
|
|
9489
|
-
id: "cloud_api_reachable",
|
|
9490
|
-
group: "cloud",
|
|
9491
|
-
title: "Moda Cloud API is reachable",
|
|
9492
|
-
status,
|
|
9493
|
-
severity: "warning",
|
|
9494
|
-
evidence,
|
|
9495
|
-
remediation,
|
|
9496
|
-
nextCommand
|
|
9497
|
-
},
|
|
9498
|
-
{
|
|
9499
|
-
id: "ingest_endpoint_reachable",
|
|
9500
|
-
group: "cloud",
|
|
9501
|
-
title: "Moda ingest endpoint is reachable",
|
|
9502
|
-
status,
|
|
9503
|
-
severity: "warning",
|
|
9504
|
-
evidence,
|
|
9505
|
-
remediation,
|
|
9506
|
-
nextCommand
|
|
9507
|
-
},
|
|
9508
|
-
{
|
|
9509
|
-
id: "recent_data_flow",
|
|
9510
|
-
group: "cloud",
|
|
9511
|
-
title: "Recent traces/logs/runs exist",
|
|
9512
|
-
status,
|
|
9513
|
-
severity: "warning",
|
|
9514
|
-
evidence,
|
|
9515
|
-
remediation,
|
|
9516
|
-
nextCommand
|
|
9517
|
-
},
|
|
9518
|
-
{
|
|
9519
|
-
id: "cloud_data_flow",
|
|
9520
|
-
group: "cloud",
|
|
9521
|
-
title: "Moda Cloud data flow",
|
|
9522
|
-
status,
|
|
9523
|
-
severity: "warning",
|
|
9524
|
-
evidence,
|
|
9525
|
-
remediation,
|
|
9526
|
-
nextCommand
|
|
9527
|
-
}
|
|
9528
|
-
];
|
|
9529
|
-
}
|
|
9530
|
-
async function cloudDataFlowChecks(ingestUrl, apiKey) {
|
|
9531
|
-
let overview;
|
|
9532
|
-
let cloudApiCheck;
|
|
9533
|
-
try {
|
|
9534
|
-
overview = await callDataAPI("/overview?days_back=1");
|
|
9535
|
-
cloudApiCheck = {
|
|
9536
|
-
id: "cloud_api_reachable",
|
|
9537
|
-
group: "cloud",
|
|
9538
|
-
title: "Moda Cloud API is reachable",
|
|
9539
|
-
status: "pass",
|
|
9540
|
-
severity: "info",
|
|
9541
|
-
evidence: [{ label: "overview", value: "reachable" }]
|
|
9542
|
-
};
|
|
9543
|
-
} catch (err) {
|
|
9544
|
-
const error = err instanceof Error ? err.message : String(err);
|
|
9545
|
-
return [
|
|
9546
|
-
{
|
|
9547
|
-
id: "cloud_api_reachable",
|
|
9548
|
-
group: "cloud",
|
|
9549
|
-
title: "Moda Cloud API is reachable",
|
|
9550
|
-
status: "warning",
|
|
9551
|
-
severity: "warning",
|
|
9552
|
-
evidence: [{ label: "error", value: error }],
|
|
9553
|
-
remediation: "Check API key and base URL.",
|
|
9554
|
-
nextCommand: "moda doctor --offline"
|
|
9555
|
-
},
|
|
9556
|
-
{
|
|
9557
|
-
id: "ingest_endpoint_reachable",
|
|
9558
|
-
group: "cloud",
|
|
9559
|
-
title: "Moda ingest endpoint is reachable",
|
|
9560
|
-
status: "skipped",
|
|
9561
|
-
severity: "warning",
|
|
9562
|
-
evidence: [{ label: "reason", value: "cloud API unreachable" }]
|
|
9563
|
-
},
|
|
9564
|
-
{
|
|
9565
|
-
id: "recent_data_flow",
|
|
9566
|
-
group: "cloud",
|
|
9567
|
-
title: "Recent traces/logs/runs exist",
|
|
9568
|
-
status: "skipped",
|
|
9569
|
-
severity: "warning",
|
|
9570
|
-
evidence: [{ label: "reason", value: "overview unavailable" }]
|
|
9571
|
-
},
|
|
9572
|
-
{
|
|
9573
|
-
id: "cloud_data_flow",
|
|
9574
|
-
group: "cloud",
|
|
9575
|
-
title: "Moda Cloud data flow",
|
|
9576
|
-
status: "warning",
|
|
9577
|
-
severity: "warning",
|
|
9578
|
-
evidence: [{ label: "error", value: error }],
|
|
9579
|
-
remediation: "Check API key, base URL, and whether data has reached Moda.",
|
|
9580
|
-
nextCommand: "moda doctor --offline"
|
|
9581
|
-
}
|
|
9582
|
-
];
|
|
9583
|
-
}
|
|
9584
|
-
const ingestCheck = await ingestReachabilityCheck(ingestUrl, apiKey);
|
|
9585
|
-
const recentCheck = recentDataFlowCheck(overview);
|
|
9586
|
-
const cloudStatus = cloudApiCheck.status === "pass" && ingestCheck.status === "pass" && recentCheck.status === "pass" ? "pass" : "warning";
|
|
9587
|
-
return [
|
|
9588
|
-
cloudApiCheck,
|
|
9589
|
-
ingestCheck,
|
|
9590
|
-
recentCheck,
|
|
9591
|
-
{
|
|
9592
|
-
id: "cloud_data_flow",
|
|
9593
|
-
group: "cloud",
|
|
9594
|
-
title: "Moda Cloud data flow",
|
|
9595
|
-
status: cloudStatus,
|
|
9596
|
-
severity: cloudStatus === "pass" ? "info" : "warning",
|
|
9597
|
-
evidence: [
|
|
9598
|
-
{ label: "cloud_api", value: cloudApiCheck.status },
|
|
9599
|
-
{ label: "ingest", value: ingestCheck.status },
|
|
9600
|
-
{ label: "recent_data", value: recentCheck.status }
|
|
9601
|
-
],
|
|
9602
|
-
remediation: cloudStatus === "pass" ? undefined : "Check API key, ingest URL, and whether data has reached Moda.",
|
|
9603
|
-
nextCommand: cloudStatus === "pass" ? undefined : "moda doctor --offline"
|
|
9604
|
-
}
|
|
9605
|
-
];
|
|
9606
|
-
}
|
|
9607
|
-
async function ingestReachabilityCheck(ingestUrl, apiKey) {
|
|
9608
|
-
const url = `${ingestUrl.replace(/\/+$/, "")}/`;
|
|
9609
|
-
try {
|
|
9610
|
-
const response = await fetch(url, {
|
|
9611
|
-
method: "GET",
|
|
9612
|
-
headers: { Authorization: `Bearer ${apiKey}` },
|
|
9613
|
-
signal: AbortSignal.timeout(1e4)
|
|
9614
|
-
});
|
|
9615
|
-
return {
|
|
9616
|
-
id: "ingest_endpoint_reachable",
|
|
9617
|
-
group: "cloud",
|
|
9618
|
-
title: "Moda ingest endpoint is reachable",
|
|
9619
|
-
status: "pass",
|
|
9620
|
-
severity: "info",
|
|
9621
|
-
evidence: [
|
|
9622
|
-
{ label: "url", value: url },
|
|
9623
|
-
{ label: "http_status", value: String(response.status) }
|
|
9624
|
-
]
|
|
9625
|
-
};
|
|
9626
|
-
} catch (err) {
|
|
9627
|
-
return {
|
|
9628
|
-
id: "ingest_endpoint_reachable",
|
|
9629
|
-
group: "cloud",
|
|
9630
|
-
title: "Moda ingest endpoint is reachable",
|
|
9631
|
-
status: "warning",
|
|
9632
|
-
severity: "warning",
|
|
9633
|
-
evidence: [{ label: "error", value: err instanceof Error ? err.message : String(err) }],
|
|
9634
|
-
remediation: "Check the configured ingest URL and network reachability.",
|
|
9635
|
-
nextCommand: "moda profiles create"
|
|
9636
|
-
};
|
|
9637
|
-
}
|
|
9638
|
-
}
|
|
9639
|
-
function recentDataFlowCheck(overview) {
|
|
9640
|
-
const data = isRecord4(overview) ? overview : {};
|
|
9641
|
-
const conversations = numberAt(data, ["conversations", "total"]);
|
|
9642
|
-
const toolFailures = numberAt(data, ["tool_failures", "total"]);
|
|
9643
|
-
const recentActivity = Array.isArray(data.recent_activity) ? data.recent_activity : [];
|
|
9644
|
-
const lastEventAt = lastActivityTimestamp(recentActivity);
|
|
9645
|
-
const hasData = conversations > 0 || toolFailures > 0 || Boolean(lastEventAt);
|
|
9646
|
-
return {
|
|
9647
|
-
id: "recent_data_flow",
|
|
9648
|
-
group: "cloud",
|
|
9649
|
-
title: "Recent traces/logs/runs exist",
|
|
9650
|
-
status: hasData ? "pass" : "warning",
|
|
9651
|
-
severity: hasData ? "info" : "warning",
|
|
9652
|
-
evidence: [
|
|
9653
|
-
{ label: "conversations", value: String(conversations) },
|
|
9654
|
-
{ label: "tool_failures", value: String(toolFailures) },
|
|
9655
|
-
{ label: "last_event_at", value: lastEventAt ?? "none" }
|
|
9656
|
-
],
|
|
9657
|
-
remediation: hasData ? undefined : "No recent Moda data was visible in the overview window.",
|
|
9658
|
-
nextCommand: hasData ? undefined : "moda hooks install --agent=all"
|
|
9659
|
-
};
|
|
9660
|
-
}
|
|
9661
|
-
function numberAt(value, path) {
|
|
9662
|
-
let cursor = value;
|
|
9663
|
-
for (const key of path) {
|
|
9664
|
-
if (!isRecord4(cursor))
|
|
9665
|
-
return 0;
|
|
9666
|
-
cursor = cursor[key];
|
|
9667
|
-
}
|
|
9668
|
-
return typeof cursor === "number" && Number.isFinite(cursor) ? cursor : 0;
|
|
9669
|
-
}
|
|
9670
|
-
function lastActivityTimestamp(items) {
|
|
9671
|
-
const timestamps = items.map((item) => isRecord4(item) && typeof item.timestamp === "string" ? item.timestamp : undefined).filter((item) => Boolean(item)).sort();
|
|
9672
|
-
return timestamps.at(-1);
|
|
9673
|
-
}
|
|
9674
|
-
function summarizeChecks(checks) {
|
|
9675
|
-
const fail = checks.filter((check) => check.status === "fail").length;
|
|
9676
|
-
const warning = checks.filter((check) => check.status === "warning").length;
|
|
9677
|
-
const skipped = checks.filter((check) => check.status === "skipped").length;
|
|
9678
|
-
const pass = checks.filter((check) => check.status === "pass").length;
|
|
9679
|
-
return {
|
|
9680
|
-
status: fail > 0 ? "fail" : warning > 0 ? "warning" : "pass",
|
|
9681
|
-
pass,
|
|
9682
|
-
warning,
|
|
9683
|
-
fail,
|
|
9684
|
-
skipped
|
|
9685
|
-
};
|
|
9686
|
-
}
|
|
9687
|
-
function nextCommandsForChecks(checks) {
|
|
9688
|
-
return [...new Set(checks.filter((check) => check.status === "fail" || check.status === "warning").map((check) => check.nextCommand).filter((command) => Boolean(command)))];
|
|
9689
|
-
}
|
|
9690
|
-
function firstBrokenCheck(checks) {
|
|
9691
|
-
return checks.find((check) => check.status === "fail") ?? checks.find((check) => check.status === "warning");
|
|
9692
|
-
}
|
|
9693
|
-
function checkStatus(checks, id) {
|
|
9694
|
-
return checks.find((check) => check.id === id)?.status ?? "skipped";
|
|
9695
|
-
}
|
|
9696
|
-
function doctorAgentWriteOptions(report, command) {
|
|
9697
|
-
const broken = report.checks.filter((check) => check.status === "fail" || check.status === "warning");
|
|
9698
|
-
return {
|
|
9699
|
-
summary: `${command} completed with status ${report.summary.status}: ${report.summary.fail} failing checks and ${report.summary.warning} warnings.`,
|
|
9700
|
-
findings: broken.slice(0, 16).map((check) => ({
|
|
9701
|
-
id: check.id,
|
|
9702
|
-
title: check.title,
|
|
9703
|
-
status: check.status,
|
|
9704
|
-
severity: check.severity,
|
|
9705
|
-
description: check.remediation ?? `Doctor check ${check.id} is ${check.status}.`,
|
|
9706
|
-
evidence_ref_ids: [`doctor:${check.id}`]
|
|
9707
|
-
})),
|
|
9708
|
-
evidenceRefs: report.checks.map((check) => ({
|
|
9709
|
-
id: `doctor:${check.id}`,
|
|
9710
|
-
kind: "doctor_check",
|
|
9711
|
-
label: check.title,
|
|
9712
|
-
status: check.status
|
|
9713
|
-
})),
|
|
9714
|
-
recommendedActions: report.fixPlan.slice(0, 16).map((item, index) => ({
|
|
9715
|
-
id: `doctor_action_${index + 1}_${item.checkId}`,
|
|
9716
|
-
title: item.action,
|
|
9717
|
-
command: item.command,
|
|
9718
|
-
rationale: `Doctor check ${item.checkId} is not passing.`,
|
|
9719
|
-
mutability: item.command ? agentMutabilityForCommand(item.command) : "unknown",
|
|
9720
|
-
requires_approval: item.command ? agentCommandRequiresApproval(item.command) : true
|
|
9721
|
-
})),
|
|
9722
|
-
nextCommands: report.nextCommands.slice(0, 16).map((nextCommand) => ({
|
|
9723
|
-
command: nextCommand,
|
|
9724
|
-
purpose: agentPurposeForCommand(nextCommand),
|
|
9725
|
-
mutability: agentMutabilityForCommand(nextCommand),
|
|
9726
|
-
requires_approval: agentCommandRequiresApproval(nextCommand)
|
|
9727
|
-
})),
|
|
9728
|
-
warnings: broken.filter((check) => check.status === "warning").slice(0, 16).map((check) => `${check.title}: ${check.remediation ?? check.status}`)
|
|
9729
|
-
};
|
|
9730
|
-
}
|
|
9731
|
-
function agentMutabilityForCommand(command) {
|
|
9732
|
-
if (/\bsync\b/.test(command))
|
|
9733
|
-
return "sync";
|
|
9734
|
-
if (/\b(init|install|approve|pull|gen|promote|remove)\b/.test(command))
|
|
9735
|
-
return "write";
|
|
9736
|
-
return "read";
|
|
9737
|
-
}
|
|
9738
|
-
function agentCommandRequiresApproval(command) {
|
|
9739
|
-
return agentMutabilityForCommand(command) !== "read" && !command.includes("--dry-run");
|
|
9740
|
-
}
|
|
9741
|
-
function agentPurposeForCommand(command) {
|
|
9742
|
-
if (command.includes("doctor"))
|
|
9743
|
-
return "Re-run Moda diagnostics.";
|
|
9744
|
-
if (command.includes("harness approve"))
|
|
9745
|
-
return "Approve the validated harness report after review.";
|
|
9746
|
-
if (command.includes("harness sync"))
|
|
9747
|
-
return "Sync harness state to Moda Cloud.";
|
|
9748
|
-
if (command.includes("harness scan"))
|
|
9749
|
-
return "Refresh the local harness manifest.";
|
|
9750
|
-
if (command.includes("init"))
|
|
9751
|
-
return "Run Moda onboarding.";
|
|
9752
|
-
return "Inspect or repair Moda setup.";
|
|
9753
|
-
}
|
|
9754
|
-
function stableGraphHash(graph) {
|
|
9755
|
-
return createHash5("sha256").update(JSON.stringify(sortJson4({
|
|
9756
|
-
...graph,
|
|
9757
|
-
scan: {
|
|
9758
|
-
...graph.scan,
|
|
9759
|
-
createdAt: undefined,
|
|
9760
|
-
scannerVersion: undefined,
|
|
9761
|
-
skipped: undefined
|
|
9762
|
-
}
|
|
9763
|
-
}))).digest("hex");
|
|
9764
|
-
}
|
|
9765
|
-
function sortJson4(value) {
|
|
9766
|
-
if (Array.isArray(value))
|
|
9767
|
-
return value.map((item) => sortJson4(item));
|
|
9768
|
-
if (value && typeof value === "object") {
|
|
9769
|
-
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => [key, sortJson4(item)]));
|
|
9770
|
-
}
|
|
9771
|
-
return value;
|
|
9772
|
-
}
|
|
9773
|
-
function isRecord4(value) {
|
|
9774
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
9775
|
-
}
|
|
9776
|
-
|
|
9777
|
-
export { ApiError, callDataAPI, callControlAPI, getApiRequestCount, resetApiRequestCount, runPromptsCommand, initPrompts, runPromptSync, claudeSkillToCursorRule, installRules, detectAgents, SKILL_SOURCE_INTEGRATION_BUNDLED, readSkillsManifest, upsertSkillManifestRecord, runSkillSync, runSkillsCommand, shouldColorTerminalOutput, terminalStyles, stripAnsi, renderMarkdownForTerminal, HARNESS_REPORT_PATH, HARNESS_REPORT_APPROVAL_PATH, validateHarnessReport, readHarnessReport, hashHarnessReport, hashStableJson, startRemoteAnalyze, pollRemoteAnalyzeUntilDone, renderInitBanner, renderInitStep, renderInitNote, renderInitSetupPlan, renderInitComplete, runHarnessCommand, describeCodingAgentEvent, isDeterministicLocalAnalyst, detectAvailableCodingAgents, codingAgentDisplayName, runCodingAgent, summarizeHarness, readHarnessSyncState, describeSyncIdentity, describeSyncTenantLabel, graphForSync, runDoctorCommand, runStatusCommand, buildDoctorReport };
|
|
6980
|
+
export { ApiError, callDataAPI, callControlAPI, getApiRequestCount, resetApiRequestCount, shouldColorTerminalOutput, terminalStyles, stripAnsi, renderMarkdownForTerminal, HARNESS_REPORT_APPROVAL_SCHEMA_VERSION, HARNESS_REPORT_PATH, HARNESS_REPORT_APPROVAL_PATH, validateHarnessReport, readHarnessReport, hashHarnessReport, hashStableJson, startRemoteAnalyze, pollRemoteAnalyzeUntilDone, renderDoctorHuman, renderStatusHuman, renderInitBanner, renderInitStep, renderInitNote, renderInitSetupPlan, renderInitComplete, runHarnessCommand, describeCodingAgentEvent, isDeterministicLocalAnalyst, detectAvailableCodingAgents, codingAgentDisplayName, runCodingAgent, scanHarness, summarizeHarness, buildSourceSnapshot, readHarnessSyncState, describeSyncIdentity, describeSyncTenantLabel, graphForSync };
|