@moda-ai/cli 1.25.0 → 1.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -24
- package/dist/{cli-vc7zddj1.js → cli-59yacef3.js} +14 -1
- package/dist/{cli-8s69sg7f.js → cli-9xw5bq1j.js} +1 -1
- package/dist/{cli-kqvjt12y.js → cli-cjqzrp9k.js} +127 -46
- package/dist/{cli-jntzpem2.js → cli-g931ntph.js} +2 -2
- package/dist/{cli-tdttb4cv.js → cli-pq6rte0w.js} +2 -2
- package/dist/cli.js +1473 -73
- package/dist/{harness-github-actions-rjyc9a7z.js → harness-github-actions-ct8fwzdr.js} +2 -2
- package/dist/{index-fm9ey3ks.js → index-fwxpt51s.js} +5 -5
- package/dist/{provision-dphktm75.js → provision-r0cq3g2v.js} +3 -3
- package/package.json +1 -1
- package/skills/integration/index.json +2 -2
- package/skills/moda-cli/SKILL.md +80 -12
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
DEFAULT_POLL_INTERVAL_MS,
|
|
4
|
+
DEFAULT_WAIT_TIMEOUT_MS,
|
|
3
5
|
buildDoctorReport,
|
|
6
|
+
fetchLatestRun,
|
|
4
7
|
runDoctorCommand,
|
|
5
8
|
runPromptsCommand,
|
|
6
9
|
runSkillsCommand,
|
|
7
10
|
runStatusCommand
|
|
8
|
-
} from "./cli-
|
|
11
|
+
} from "./cli-cjqzrp9k.js";
|
|
9
12
|
import {
|
|
10
13
|
ApiError,
|
|
11
14
|
HARNESS_REPORT_APPROVAL_PATH,
|
|
@@ -26,7 +29,7 @@ import {
|
|
|
26
29
|
summarizeHarness,
|
|
27
30
|
terminalStyles,
|
|
28
31
|
validateHarnessReport
|
|
29
|
-
} from "./cli-
|
|
32
|
+
} from "./cli-g931ntph.js";
|
|
30
33
|
import {
|
|
31
34
|
authFetch,
|
|
32
35
|
clearAuthSession,
|
|
@@ -34,7 +37,7 @@ import {
|
|
|
34
37
|
loadAuthSession,
|
|
35
38
|
login,
|
|
36
39
|
revokeCliSessionToken
|
|
37
|
-
} from "./cli-
|
|
40
|
+
} from "./cli-9xw5bq1j.js";
|
|
38
41
|
import {
|
|
39
42
|
AGENT_ERROR_CODES,
|
|
40
43
|
CliAccessError,
|
|
@@ -79,7 +82,7 @@ import {
|
|
|
79
82
|
validateConfig,
|
|
80
83
|
wasOutputCancelled,
|
|
81
84
|
writeSecretJsonFile
|
|
82
|
-
} from "./cli-
|
|
85
|
+
} from "./cli-59yacef3.js";
|
|
83
86
|
|
|
84
87
|
// src/cli.ts
|
|
85
88
|
import { z as z2 } from "zod";
|
|
@@ -92,7 +95,11 @@ var OverviewSchema = z.object({
|
|
|
92
95
|
});
|
|
93
96
|
var ClustersSchema = z.object({
|
|
94
97
|
parent_id: z.string().optional(),
|
|
95
|
-
time_range: z.enum(["all", "1h", "3d", "7d", "24h", "30d", "90d"]).default("all").optional()
|
|
98
|
+
time_range: z.enum(["all", "1h", "3d", "7d", "24h", "30d", "90d"]).default("all").optional(),
|
|
99
|
+
search: z.string().min(2).max(200).optional(),
|
|
100
|
+
node_id: z.string().min(1).max(512).optional(),
|
|
101
|
+
mode: z.enum(["fuzzy", "semantic", "hybrid"]).optional(),
|
|
102
|
+
limit: z.number().min(1).max(50).optional()
|
|
96
103
|
});
|
|
97
104
|
var ClusterConversationsSchema = z.object({
|
|
98
105
|
node_id: z.string(),
|
|
@@ -114,7 +121,14 @@ var ConversationsSchema = z.object({
|
|
|
114
121
|
var WorldStateSchema = z.object({
|
|
115
122
|
conversation_id: z.string(),
|
|
116
123
|
summary_only: boolFlag(),
|
|
117
|
-
event_limit: z.number().min(1).max(5000).default(2000).optional()
|
|
124
|
+
event_limit: z.number().min(1).max(5000).default(2000).optional(),
|
|
125
|
+
snapshot: boolFlag(),
|
|
126
|
+
msg_index: z.number().min(0).max(1e5).optional(),
|
|
127
|
+
block_index: z.number().min(0).max(1e5).optional(),
|
|
128
|
+
segment_index: z.number().min(0).max(65535).optional(),
|
|
129
|
+
replay: boolFlag(),
|
|
130
|
+
format: z.enum(["frames", "fold-inputs"]).optional(),
|
|
131
|
+
message_count: z.number().min(1).max(1e6).optional()
|
|
118
132
|
});
|
|
119
133
|
var ContextSchema = z.object({
|
|
120
134
|
conversation_id: z.string(),
|
|
@@ -135,6 +149,73 @@ var ProblemsSchema = z.object({
|
|
|
135
149
|
days_back: z.number().min(1).max(90).default(30).optional(),
|
|
136
150
|
limit: z.number().min(1).max(25).default(25).optional()
|
|
137
151
|
});
|
|
152
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
153
|
+
var PROBLEM_FAMILIES = [
|
|
154
|
+
"tool_failure",
|
|
155
|
+
"emotion",
|
|
156
|
+
"laziness",
|
|
157
|
+
"hallucination",
|
|
158
|
+
"prm_dip"
|
|
159
|
+
];
|
|
160
|
+
var PROBLEM_DOORS = ["rubric_match", "local_causal_chain"];
|
|
161
|
+
var ProblemSchema = z.object({
|
|
162
|
+
id: z.string().min(1),
|
|
163
|
+
evidence: boolFlag(),
|
|
164
|
+
reports: boolFlag(),
|
|
165
|
+
conversations: boolFlag(),
|
|
166
|
+
feedback: boolFlag(),
|
|
167
|
+
limit: z.number().min(1).max(50).optional(),
|
|
168
|
+
cursor: z.string().optional(),
|
|
169
|
+
family: z.enum(PROBLEM_FAMILIES).optional(),
|
|
170
|
+
door: z.enum(PROBLEM_DOORS).optional()
|
|
171
|
+
});
|
|
172
|
+
var PROBLEM_FEEDBACK_ACTIONS = [
|
|
173
|
+
"mark_fixed",
|
|
174
|
+
"dismiss",
|
|
175
|
+
"flag_attribution",
|
|
176
|
+
"rename"
|
|
177
|
+
];
|
|
178
|
+
var ProblemFeedbackSchema = z.object({
|
|
179
|
+
id: z.string().min(1),
|
|
180
|
+
action: z.enum(PROBLEM_FEEDBACK_ACTIONS),
|
|
181
|
+
reason: z.string().max(2000).optional(),
|
|
182
|
+
attribution_id: z.string().max(64).optional(),
|
|
183
|
+
new_name: z.string().max(255).optional(),
|
|
184
|
+
actor: z.string().max(255).optional()
|
|
185
|
+
});
|
|
186
|
+
var EMOTION_FAMILIES = [
|
|
187
|
+
"frustration",
|
|
188
|
+
"sadness",
|
|
189
|
+
"confusion",
|
|
190
|
+
"anxiety",
|
|
191
|
+
"trust",
|
|
192
|
+
"positive"
|
|
193
|
+
];
|
|
194
|
+
var EmotionsSchema = z.object({
|
|
195
|
+
days_back: z.number().min(1).max(90).default(7).optional(),
|
|
196
|
+
limit: z.number().min(1).max(20).default(10).optional(),
|
|
197
|
+
offset: z.number().min(0).max(1e4).optional(),
|
|
198
|
+
family: z.enum(EMOTION_FAMILIES).optional()
|
|
199
|
+
});
|
|
200
|
+
var HallucinationsSchema = z.object({
|
|
201
|
+
days_back: z.number().min(1).max(90).default(7).optional(),
|
|
202
|
+
limit: z.number().min(1).max(20).default(10).optional(),
|
|
203
|
+
offset: z.number().min(0).max(1e4).optional(),
|
|
204
|
+
conversation_id: z.string().max(200).optional(),
|
|
205
|
+
kind: z.enum(["contradicted", "verified"]).optional()
|
|
206
|
+
});
|
|
207
|
+
var StepScoresSchema = z.object({
|
|
208
|
+
conversation_id: z.string().min(1)
|
|
209
|
+
});
|
|
210
|
+
var TAIL_SIGNALS = ["conversations", "emotions", "all"];
|
|
211
|
+
var TailSchema = z.object({
|
|
212
|
+
interval: z.number().min(5).max(3600).default(15).optional(),
|
|
213
|
+
signal: z.enum(TAIL_SIGNALS).default("conversations").optional(),
|
|
214
|
+
once: boolFlag(),
|
|
215
|
+
limit: z.number().min(1).max(50).default(20).optional(),
|
|
216
|
+
max_events: z.number().min(1).max(1e5).optional(),
|
|
217
|
+
full_scan: boolFlag()
|
|
218
|
+
});
|
|
138
219
|
var ToolFailureDetailSchema = z.object({
|
|
139
220
|
tool_name: z.string(),
|
|
140
221
|
subtype: z.string().optional(),
|
|
@@ -158,7 +239,7 @@ var SearchSchema = z.object({
|
|
|
158
239
|
user_id: z.string().optional(),
|
|
159
240
|
time_range: z.enum(TIME_RANGES).default("all").optional(),
|
|
160
241
|
limit: z.number().min(1).max(100).default(20).optional(),
|
|
161
|
-
include_tool_io:
|
|
242
|
+
include_tool_io: boolFlag()
|
|
162
243
|
});
|
|
163
244
|
var FEEDBACK_CATEGORIES = [
|
|
164
245
|
"bad_cluster_label",
|
|
@@ -281,6 +362,724 @@ function embeddedWindowSize(conversation) {
|
|
|
281
362
|
return Math.min(center - from, to - center);
|
|
282
363
|
}
|
|
283
364
|
|
|
365
|
+
// src/fix.ts
|
|
366
|
+
import {
|
|
367
|
+
closeSync,
|
|
368
|
+
constants as fsConstants,
|
|
369
|
+
existsSync,
|
|
370
|
+
fstatSync,
|
|
371
|
+
ftruncateSync,
|
|
372
|
+
lstatSync,
|
|
373
|
+
mkdirSync,
|
|
374
|
+
openSync,
|
|
375
|
+
readFileSync,
|
|
376
|
+
realpathSync,
|
|
377
|
+
writeSync
|
|
378
|
+
} from "node:fs";
|
|
379
|
+
import { dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
380
|
+
var O_NOFOLLOW = fsConstants.O_NOFOLLOW ?? 0;
|
|
381
|
+
var ADVANCE_TIMEOUT_MS = 300000;
|
|
382
|
+
var STEP_DELAY_MS = 2000;
|
|
383
|
+
var MAX_WAIT_TIMEOUT_MS = 24 * 60 * 60 * 1000;
|
|
384
|
+
var MAX_POLL_INTERVAL_MS = 120000;
|
|
385
|
+
var PER_CASE_FINDINGS_CAP = 50;
|
|
386
|
+
var ADVANCING_STATUSES = new Set(["DRAFT", "SCOPING", "PROPOSING", "PROPOSED", "GATING"]);
|
|
387
|
+
var FIX_USAGE = [
|
|
388
|
+
"Usage: moda fixes [--status=S] [--limit=N] [--cursor=TOKEN]",
|
|
389
|
+
" moda fix <fix_id> [--packet] [--wait]",
|
|
390
|
+
" moda fix start <problem_id> [--type=auto|prompt] [--wait] [--timeout=ms]",
|
|
391
|
+
" moda fix verify <fix_id> [--prompt-file=PATH] [--no-wait] [--timeout=ms]",
|
|
392
|
+
" moda fix checkout <fix_id>",
|
|
393
|
+
" moda fix submit <fix_id> --pr | --local-ref=<branch>",
|
|
394
|
+
" moda fix dismiss <fix_id> --reason=TEXT"
|
|
395
|
+
].join(`
|
|
396
|
+
`);
|
|
397
|
+
async function runFixesCommand(context) {
|
|
398
|
+
validateConfig();
|
|
399
|
+
const profileOptions = profileOptionsFor(context);
|
|
400
|
+
const tenantId = requireTenantId(context.flags, profileOptions);
|
|
401
|
+
const query = new URLSearchParams;
|
|
402
|
+
if (context.flags.status)
|
|
403
|
+
query.set("status", context.flags.status);
|
|
404
|
+
if (context.flags.limit) {
|
|
405
|
+
const limit = Number(context.flags.limit);
|
|
406
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
|
|
407
|
+
throw new CliInputError(`--limit must be an integer between 1 and 100 (got ${context.flags.limit})`, FIX_USAGE);
|
|
408
|
+
}
|
|
409
|
+
query.set("limit", String(limit));
|
|
410
|
+
}
|
|
411
|
+
if (context.flags.cursor)
|
|
412
|
+
query.set("cursor", context.flags.cursor);
|
|
413
|
+
const suffix = query.toString() ? `?${query.toString()}` : "";
|
|
414
|
+
const data = await callControlAPI(`${fixesBase(tenantId)}${suffix}`, { method: "GET" }, profileOptions);
|
|
415
|
+
const fixes = data.fixes ?? [];
|
|
416
|
+
if (context.outputMode === "human") {
|
|
417
|
+
if (fixes.length === 0) {
|
|
418
|
+
process.stderr.write("No fixes in the queue. Draft one with `moda fix start <problem_id> --wait`.\n");
|
|
419
|
+
} else {
|
|
420
|
+
for (const fix of fixes) {
|
|
421
|
+
process.stderr.write(`MODA-FIX-${fix.shortRef} ${fix.status} ${fix.fixType} ${fix.id} ${fix.statusReason ?? ""}
|
|
422
|
+
`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
context.output.writeData(data, {
|
|
427
|
+
summary: {
|
|
428
|
+
text: `${fixes.length} fix(es)${context.flags.status ? ` with status ${context.flags.status}` : ""} in the ranked queue.`,
|
|
429
|
+
confidence: "high"
|
|
430
|
+
},
|
|
431
|
+
nextCommands: fixes.slice(0, 1).map((fix) => ({
|
|
432
|
+
command: `moda fix ${fix.id}`,
|
|
433
|
+
purpose: "Open the top-ranked fix (status, gate result, next actions).",
|
|
434
|
+
mutability: "read",
|
|
435
|
+
requires_approval: false
|
|
436
|
+
}))
|
|
437
|
+
});
|
|
438
|
+
return EXIT_OK;
|
|
439
|
+
}
|
|
440
|
+
async function runFixCommand(context) {
|
|
441
|
+
validateConfig();
|
|
442
|
+
const profileOptions = profileOptionsFor(context);
|
|
443
|
+
const tenantId = requireTenantId(context.flags, profileOptions);
|
|
444
|
+
const sub = context.positional;
|
|
445
|
+
if (!sub) {
|
|
446
|
+
throw new CliInputError("A fix id or subcommand is required", FIX_USAGE);
|
|
447
|
+
}
|
|
448
|
+
switch (sub) {
|
|
449
|
+
case "start":
|
|
450
|
+
return runFixStart(context, profileOptions, tenantId);
|
|
451
|
+
case "verify":
|
|
452
|
+
return runFixVerify(context, profileOptions, tenantId);
|
|
453
|
+
case "checkout":
|
|
454
|
+
return runFixCheckout(context, profileOptions, tenantId);
|
|
455
|
+
case "submit":
|
|
456
|
+
return runFixSubmit(context, profileOptions, tenantId);
|
|
457
|
+
case "dismiss":
|
|
458
|
+
return runFixDismiss(context, profileOptions, tenantId);
|
|
459
|
+
default:
|
|
460
|
+
return runFixDetail(context, profileOptions, tenantId, sub);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
async function runFixStart(context, profileOptions, tenantId) {
|
|
464
|
+
const problemId = (context.positionals[1] || context.flags["problem-id"] || "").trim();
|
|
465
|
+
if (!problemId) {
|
|
466
|
+
throw new CliInputError("<problem_id> is required", "Usage: moda fix start <problem_id> [--type=auto|prompt] [--wait] [--timeout=ms] [--poll-interval=ms]");
|
|
467
|
+
}
|
|
468
|
+
const fixType = context.flags.type;
|
|
469
|
+
if (fixType !== undefined && fixType !== "auto" && fixType !== "prompt") {
|
|
470
|
+
throw new CliInputError(`--type must be 'auto' or 'prompt'; got '${fixType}'`);
|
|
471
|
+
}
|
|
472
|
+
const created = await callControlAPI(`/tenants/${encodeURIComponent(tenantId)}/problems/${encodeURIComponent(problemId)}/fixes`, { method: "POST", body: JSON.stringify(fixType ? { fixType } : {}) }, profileOptions, { timeoutMs: 120000, retries: 0 });
|
|
473
|
+
if (context.flags.wait !== "true") {
|
|
474
|
+
if (context.outputMode === "human") {
|
|
475
|
+
process.stderr.write(`Fix MODA-FIX-${created.shortRef} (${created.id}) drafted for problem ${created.problemId} — status ${created.status}.
|
|
476
|
+
`);
|
|
477
|
+
}
|
|
478
|
+
context.output.writeData(created, {
|
|
479
|
+
summary: {
|
|
480
|
+
text: `Fix MODA-FIX-${created.shortRef} drafted (status ${created.status}). Re-run with --wait to drive the pipeline.`,
|
|
481
|
+
confidence: "high"
|
|
482
|
+
},
|
|
483
|
+
nextCommands: [
|
|
484
|
+
{
|
|
485
|
+
command: `moda fix start ${problemId} --wait`,
|
|
486
|
+
purpose: "Drive the pipeline (scope → propose → gate) to a resting status.",
|
|
487
|
+
mutability: "write",
|
|
488
|
+
requires_approval: false
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
command: `moda fix ${created.id}`,
|
|
492
|
+
purpose: "Inspect the fix status and gate result.",
|
|
493
|
+
mutability: "read",
|
|
494
|
+
requires_approval: false
|
|
495
|
+
}
|
|
496
|
+
]
|
|
497
|
+
});
|
|
498
|
+
return EXIT_OK;
|
|
499
|
+
}
|
|
500
|
+
const timeoutMs = parsePositiveInt(context.flags.timeout, DEFAULT_WAIT_TIMEOUT_MS, MAX_WAIT_TIMEOUT_MS);
|
|
501
|
+
const pollIntervalMs = parsePositiveInt(context.flags["poll-interval"], DEFAULT_POLL_INTERVAL_MS, MAX_POLL_INTERVAL_MS);
|
|
502
|
+
const fix = await driveAdvance({
|
|
503
|
+
tenantId,
|
|
504
|
+
fixId: created.id,
|
|
505
|
+
initial: created,
|
|
506
|
+
timeoutMs,
|
|
507
|
+
pollIntervalMs,
|
|
508
|
+
profileOptions,
|
|
509
|
+
context
|
|
510
|
+
});
|
|
511
|
+
const packet = await fetchPacketFailSoft(tenantId, created.id, profileOptions);
|
|
512
|
+
if (context.outputMode === "human") {
|
|
513
|
+
process.stderr.write(`
|
|
514
|
+
Fix MODA-FIX-${fix.shortRef} rested at ${fix.status}${fix.statusReason ? ` — ${fix.statusReason}` : ""}
|
|
515
|
+
`);
|
|
516
|
+
}
|
|
517
|
+
context.output.writeData({ fix, ...packet ? { packet } : {} }, {
|
|
518
|
+
summary: {
|
|
519
|
+
text: `Fix MODA-FIX-${fix.shortRef} reached ${fix.status}${fix.statusReason ? ` (${fix.statusReason})` : ""}.`,
|
|
520
|
+
confidence: "high"
|
|
521
|
+
},
|
|
522
|
+
findings: gateFindings(fix),
|
|
523
|
+
nextCommands: nextCommandsFor(fix)
|
|
524
|
+
});
|
|
525
|
+
return EXIT_OK;
|
|
526
|
+
}
|
|
527
|
+
async function runFixVerify(context, profileOptions, tenantId) {
|
|
528
|
+
const usage = "Usage: moda fix verify <fix_id> [--prompt-file=PATH] [--no-wait] [--timeout=ms] [--poll-interval=ms]";
|
|
529
|
+
const fixId = (context.positionals[1] || context.flags["fix-id"] || "").trim();
|
|
530
|
+
if (!fixId) {
|
|
531
|
+
throw new CliInputError("<fix_id> is required", usage);
|
|
532
|
+
}
|
|
533
|
+
let candidateContent;
|
|
534
|
+
const promptFile = context.flags["prompt-file"];
|
|
535
|
+
if (promptFile) {
|
|
536
|
+
const absPath = isAbsolute(promptFile) ? promptFile : resolve(process.cwd(), promptFile);
|
|
537
|
+
if (!existsSync(absPath)) {
|
|
538
|
+
throw new CliInputError(`--prompt-file not found: ${promptFile}`, usage);
|
|
539
|
+
}
|
|
540
|
+
candidateContent = readFileSync(absPath, "utf8");
|
|
541
|
+
if (candidateContent.trim().length < 40) {
|
|
542
|
+
throw new CliInputError("--prompt-file content is shorter than 40 characters — refusing to gate a truncated candidate");
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
const enqueue = await callControlAPI(`${fixesBase(tenantId)}/${encodeURIComponent(fixId)}/verify`, { method: "POST", body: JSON.stringify(candidateContent ? { candidateContent } : {}) }, profileOptions, { timeoutMs: 120000, retries: 0 });
|
|
546
|
+
const noWait = context.flags.wait === "false" || context.flags["no-wait"] === "true";
|
|
547
|
+
if (noWait) {
|
|
548
|
+
context.output.writeData({ fixId, gateRunId: enqueue.gateRunId, status: "GATING" }, {
|
|
549
|
+
summary: {
|
|
550
|
+
text: `Gate + control runs enqueued (gate run ${enqueue.gateRunId}). Resume with \`moda fix ${fixId} --wait\`.`,
|
|
551
|
+
confidence: "high"
|
|
552
|
+
},
|
|
553
|
+
nextCommands: [
|
|
554
|
+
{
|
|
555
|
+
command: `moda fix ${fixId} --wait`,
|
|
556
|
+
purpose: "Drive the advance-on-poll gate to a verdict.",
|
|
557
|
+
mutability: "write",
|
|
558
|
+
requires_approval: false
|
|
559
|
+
}
|
|
560
|
+
]
|
|
561
|
+
});
|
|
562
|
+
return EXIT_OK;
|
|
563
|
+
}
|
|
564
|
+
const timeoutMs = parsePositiveInt(context.flags.timeout, DEFAULT_WAIT_TIMEOUT_MS, MAX_WAIT_TIMEOUT_MS);
|
|
565
|
+
const pollIntervalMs = parsePositiveInt(context.flags["poll-interval"], DEFAULT_POLL_INTERVAL_MS, MAX_POLL_INTERVAL_MS);
|
|
566
|
+
const deadline = Date.now() + timeoutMs;
|
|
567
|
+
let fix = await advanceFix(tenantId, fixId, profileOptions);
|
|
568
|
+
while (fix.status === "GATING") {
|
|
569
|
+
if (Date.now() >= deadline) {
|
|
570
|
+
throw new Error(`Timed out after ${timeoutMs}ms waiting for gate run ${enqueue.gateRunId} to finish`);
|
|
571
|
+
}
|
|
572
|
+
if (context.outputMode === "human") {
|
|
573
|
+
process.stderr.write(`Waiting for gate run (${fix.statusReason || "running"})...
|
|
574
|
+
`);
|
|
575
|
+
}
|
|
576
|
+
await sleep(pollIntervalMs);
|
|
577
|
+
fix = await advanceFix(tenantId, fixId, profileOptions);
|
|
578
|
+
}
|
|
579
|
+
const warnings = [];
|
|
580
|
+
if (fix.gateRunId && fix.gateRunId !== enqueue.gateRunId) {
|
|
581
|
+
warnings.push(`The reported verdict is for gate run ${fix.gateRunId}, not the one this command enqueued (${enqueue.gateRunId}) — a concurrent verify superseded it.`);
|
|
582
|
+
}
|
|
583
|
+
const gateResult = fix.gateResult ?? null;
|
|
584
|
+
const verdict = gateResult?.verdict ?? "";
|
|
585
|
+
let exitCode;
|
|
586
|
+
let envelopeStatus;
|
|
587
|
+
if (fix.status === "VERIFIED" && verdict === "pass") {
|
|
588
|
+
exitCode = EXIT_OK;
|
|
589
|
+
envelopeStatus = "success";
|
|
590
|
+
} else if (fix.status === "GATE_FAILED" || verdict === "fail") {
|
|
591
|
+
exitCode = EXIT_ERROR;
|
|
592
|
+
envelopeStatus = "error";
|
|
593
|
+
} else {
|
|
594
|
+
exitCode = EXIT_DEGRADED;
|
|
595
|
+
envelopeStatus = "degraded";
|
|
596
|
+
}
|
|
597
|
+
let cases = [];
|
|
598
|
+
if (exitCode === EXIT_ERROR && fix.replaySetId) {
|
|
599
|
+
try {
|
|
600
|
+
const payload = await fetchLatestRun(tenantId, String(fix.replaySetId), enqueue.gateRunId, profileOptions);
|
|
601
|
+
if (payload.run?.runId === enqueue.gateRunId) {
|
|
602
|
+
cases = payload.cases;
|
|
603
|
+
}
|
|
604
|
+
} catch {
|
|
605
|
+
warnings.push("Could not load per-case gate results — see the replay run in the dashboard.");
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
if (context.outputMode === "human") {
|
|
609
|
+
printHumanGateVerdict(fix, enqueue.gateRunId, gateResult, cases);
|
|
610
|
+
}
|
|
611
|
+
const findings = gateFindings(fix);
|
|
612
|
+
for (const row of cases.slice(0, PER_CASE_FINDINGS_CAP)) {
|
|
613
|
+
const baseline = passLabel(row.prodPass);
|
|
614
|
+
const candidate = passLabel(row.proposedPass);
|
|
615
|
+
findings.push({
|
|
616
|
+
id: `case:${row.caseId}`,
|
|
617
|
+
title: `${row.caseId}: baseline=${baseline} candidate=${candidate}`,
|
|
618
|
+
status: candidate,
|
|
619
|
+
severity: baseline === "pass" && candidate === "fail" ? "error" : candidate === "fail" ? "warning" : "info"
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
context.output.writeData({
|
|
623
|
+
fixId: fix.id,
|
|
624
|
+
shortRef: fix.shortRef,
|
|
625
|
+
status: fix.status,
|
|
626
|
+
gateRunId: enqueue.gateRunId,
|
|
627
|
+
verdict: verdict || null,
|
|
628
|
+
gateResult,
|
|
629
|
+
...cases.length > 0 ? { cases } : {}
|
|
630
|
+
}, {
|
|
631
|
+
status: envelopeStatus,
|
|
632
|
+
summary: {
|
|
633
|
+
text: verdict === "pass" ? `Gate passed on the frozen holdout: ${gateResult?.reason ?? ""}`.trim() : verdict === "fail" ? `Gate failed: ${gateResult?.reason ?? fix.statusReason ?? "candidate did not beat baseline"}` : `Gate degraded (${fix.status}): ${gateResult?.reason ?? fix.statusReason ?? "no verdict"}`,
|
|
634
|
+
confidence: "high"
|
|
635
|
+
},
|
|
636
|
+
findings,
|
|
637
|
+
warnings,
|
|
638
|
+
nextCommands: nextCommandsFor(fix)
|
|
639
|
+
});
|
|
640
|
+
return exitCode;
|
|
641
|
+
}
|
|
642
|
+
async function runFixCheckout(context, profileOptions, tenantId) {
|
|
643
|
+
const fixId = (context.positionals[1] || context.flags["fix-id"] || "").trim();
|
|
644
|
+
if (!fixId) {
|
|
645
|
+
throw new CliInputError("<fix_id> is required", "Usage: moda fix checkout <fix_id>");
|
|
646
|
+
}
|
|
647
|
+
const fix = await getFix(tenantId, fixId, profileOptions);
|
|
648
|
+
if (!fix.targetSourcePath) {
|
|
649
|
+
throw new CliInputError(`Fix MODA-FIX-${fix.shortRef} has no target source path — ${fix.fixType} fixes are packet-only. Run \`moda fix ${fixId} --packet\` for the diagnosis packet.`);
|
|
650
|
+
}
|
|
651
|
+
const candidateVersionId = fix.candidateRef?.promptVersionId ?? "";
|
|
652
|
+
if (!candidateVersionId) {
|
|
653
|
+
throw new CliInputError(`Fix MODA-FIX-${fix.shortRef} has no registered candidate version yet (status ${fix.status}). Run \`moda fix start ${fix.problemId} --wait\` to propose one, or edit the target locally and gate it with \`moda fix verify ${fixId} --prompt-file=<path>\`.`);
|
|
654
|
+
}
|
|
655
|
+
if (!fix.promptId) {
|
|
656
|
+
throw new CliInputError(`Fix MODA-FIX-${fix.shortRef} has no routed prompt — the candidate content cannot be resolved.`);
|
|
657
|
+
}
|
|
658
|
+
const prompt = await callControlAPI(`/prompts/${encodeURIComponent(String(fix.promptId))}`, { method: "GET" }, profileOptions);
|
|
659
|
+
const version = (prompt.versions ?? []).find((row) => row.id === candidateVersionId);
|
|
660
|
+
if (!version || typeof version.content !== "string" || !version.content) {
|
|
661
|
+
throw new Error(`Candidate prompt version ${candidateVersionId} not found in the registry`);
|
|
662
|
+
}
|
|
663
|
+
const relPath = safeTargetSourcePath(String(fix.targetSourcePath));
|
|
664
|
+
const absPath = join(process.cwd(), relPath);
|
|
665
|
+
assertRealpathContained(absPath, relPath);
|
|
666
|
+
const writeStatus = writeContainedFile(absPath, relPath, version.content);
|
|
667
|
+
const branch = `moda/fix/${String(fix.shortRef).toLowerCase()}`;
|
|
668
|
+
const magicWord = `Fixes MODA-FIX-${fix.shortRef}`;
|
|
669
|
+
if (context.outputMode === "human") {
|
|
670
|
+
process.stderr.write([
|
|
671
|
+
`${writeStatus} ${relPath} (candidate ${candidateVersionId})`,
|
|
672
|
+
"",
|
|
673
|
+
"Next steps (the CLI never runs git — branching and committing are yours):",
|
|
674
|
+
` 1. git checkout -b ${branch}`,
|
|
675
|
+
` 2. Review the diff. Re-gate any edits: moda fix verify ${fix.id} --prompt-file=${relPath}`,
|
|
676
|
+
` 3. Hand back: moda fix submit ${fix.id} --pr (or --local-ref=${branch})`,
|
|
677
|
+
` A hand-authored PR body must end with the magic word: ${magicWord}`,
|
|
678
|
+
""
|
|
679
|
+
].join(`
|
|
680
|
+
`));
|
|
681
|
+
}
|
|
682
|
+
context.output.writeData({
|
|
683
|
+
fixId: fix.id,
|
|
684
|
+
shortRef: fix.shortRef,
|
|
685
|
+
path: relPath,
|
|
686
|
+
writeStatus,
|
|
687
|
+
branch,
|
|
688
|
+
magicWord,
|
|
689
|
+
candidateVersionId
|
|
690
|
+
}, {
|
|
691
|
+
summary: {
|
|
692
|
+
text: `Candidate for MODA-FIX-${fix.shortRef} ${writeStatus === "unchanged" ? "already matches" : "written to"} ${relPath}. Create branch ${branch} yourself — the CLI never runs git.`,
|
|
693
|
+
confidence: "high"
|
|
694
|
+
},
|
|
695
|
+
artifacts: [
|
|
696
|
+
{
|
|
697
|
+
kind: "fix_candidate",
|
|
698
|
+
path: relPath,
|
|
699
|
+
description: `Candidate content for fix MODA-FIX-${fix.shortRef} (prompt version ${candidateVersionId}).`
|
|
700
|
+
}
|
|
701
|
+
],
|
|
702
|
+
nextCommands: [
|
|
703
|
+
{
|
|
704
|
+
command: `moda fix verify ${fix.id} --prompt-file=${relPath}`,
|
|
705
|
+
purpose: "Re-gate the (possibly edited) local candidate on the frozen holdout.",
|
|
706
|
+
mutability: "write",
|
|
707
|
+
requires_approval: false
|
|
708
|
+
},
|
|
709
|
+
{
|
|
710
|
+
command: `moda fix submit ${fix.id} --pr`,
|
|
711
|
+
purpose: `Open the draft fix PR (branch ${branch}, magic word "${magicWord}").`,
|
|
712
|
+
mutability: "write",
|
|
713
|
+
requires_approval: true
|
|
714
|
+
}
|
|
715
|
+
]
|
|
716
|
+
});
|
|
717
|
+
return EXIT_OK;
|
|
718
|
+
}
|
|
719
|
+
async function runFixSubmit(context, profileOptions, tenantId) {
|
|
720
|
+
const usage = "Usage: moda fix submit <fix_id> --pr | --local-ref=<branch>";
|
|
721
|
+
const fixId = (context.positionals[1] || context.flags["fix-id"] || "").trim();
|
|
722
|
+
if (!fixId) {
|
|
723
|
+
throw new CliInputError("<fix_id> is required", usage);
|
|
724
|
+
}
|
|
725
|
+
const wantPr = context.flags.pr === "true";
|
|
726
|
+
const localRef = (context.flags["local-ref"] ?? "").trim();
|
|
727
|
+
if (wantPr === (localRef !== "")) {
|
|
728
|
+
throw new CliInputError("Pass exactly one of --pr or --local-ref=<branch>", usage);
|
|
729
|
+
}
|
|
730
|
+
const result = await callControlAPI(`${fixesBase(tenantId)}/${encodeURIComponent(fixId)}/submit`, {
|
|
731
|
+
method: "POST",
|
|
732
|
+
body: JSON.stringify(wantPr ? { channel: "pr" } : { channel: "local_ref", localRef })
|
|
733
|
+
}, profileOptions, { timeoutMs: 120000, retries: 0 });
|
|
734
|
+
if (context.outputMode === "human") {
|
|
735
|
+
process.stderr.write(result.prUrl ? `Draft PR open: ${result.prUrl} (${result.prBranch ?? ""}) — merging it confirms the fix.
|
|
736
|
+
` : `Fix recorded as ${result.status}${result.prBranch ? ` on ${result.prBranch}` : ""}.
|
|
737
|
+
`);
|
|
738
|
+
}
|
|
739
|
+
context.output.writeData({ fixId, ...result }, {
|
|
740
|
+
summary: {
|
|
741
|
+
text: result.prUrl ? `Draft fix PR open at ${result.prUrl}. Merging with the magic word confirms the fix and starts monitoring.` : `Fix ${fixId} recorded as ${result.status}${result.prBranch ? ` on branch ${result.prBranch}` : ""}. Land the branch with the magic word in the PR body to confirm the fix.`,
|
|
742
|
+
confidence: "high"
|
|
743
|
+
},
|
|
744
|
+
nextCommands: [
|
|
745
|
+
{
|
|
746
|
+
command: `moda fix ${fixId}`,
|
|
747
|
+
purpose: "Track delivery and confirmation status.",
|
|
748
|
+
mutability: "read",
|
|
749
|
+
requires_approval: false
|
|
750
|
+
}
|
|
751
|
+
]
|
|
752
|
+
});
|
|
753
|
+
return EXIT_OK;
|
|
754
|
+
}
|
|
755
|
+
async function runFixDismiss(context, profileOptions, tenantId) {
|
|
756
|
+
const usage = "Usage: moda fix dismiss <fix_id> --reason=TEXT";
|
|
757
|
+
const fixId = (context.positionals[1] || context.flags["fix-id"] || "").trim();
|
|
758
|
+
if (!fixId) {
|
|
759
|
+
throw new CliInputError("<fix_id> is required", usage);
|
|
760
|
+
}
|
|
761
|
+
const reason = context.flags.reason;
|
|
762
|
+
if (!reason || !reason.trim()) {
|
|
763
|
+
throw new CliInputError("--reason=TEXT is required (it feeds problem feedback)", usage);
|
|
764
|
+
}
|
|
765
|
+
const fix = await callControlAPI(`${fixesBase(tenantId)}/${encodeURIComponent(fixId)}/dismiss`, { method: "POST", body: JSON.stringify({ reason }) }, profileOptions, { timeoutMs: 60000, retries: 0 });
|
|
766
|
+
if (context.outputMode === "human") {
|
|
767
|
+
process.stderr.write(`Fix MODA-FIX-${fix.shortRef} dismissed (${fix.status}).
|
|
768
|
+
`);
|
|
769
|
+
}
|
|
770
|
+
context.output.writeData(fix, {
|
|
771
|
+
summary: {
|
|
772
|
+
text: `Fix MODA-FIX-${fix.shortRef} dismissed — the reason was recorded as problem feedback.`,
|
|
773
|
+
confidence: "high"
|
|
774
|
+
}
|
|
775
|
+
});
|
|
776
|
+
return EXIT_OK;
|
|
777
|
+
}
|
|
778
|
+
async function runFixDetail(context, profileOptions, tenantId, fixId) {
|
|
779
|
+
if (context.flags.packet === "true") {
|
|
780
|
+
const packet = await callControlAPI(`${fixesBase(tenantId)}/${encodeURIComponent(fixId)}/packet`, { method: "GET" }, profileOptions);
|
|
781
|
+
context.output.writeData(packet, {
|
|
782
|
+
summary: { text: `moda.fix_packet.v1 for fix ${fixId}.`, confidence: "high" }
|
|
783
|
+
});
|
|
784
|
+
return EXIT_OK;
|
|
785
|
+
}
|
|
786
|
+
let fix = await getFix(tenantId, fixId, profileOptions);
|
|
787
|
+
if (context.flags.wait === "true" && ADVANCING_STATUSES.has(fix.status)) {
|
|
788
|
+
const timeoutMs = parsePositiveInt(context.flags.timeout, DEFAULT_WAIT_TIMEOUT_MS, MAX_WAIT_TIMEOUT_MS);
|
|
789
|
+
const pollIntervalMs = parsePositiveInt(context.flags["poll-interval"], DEFAULT_POLL_INTERVAL_MS, MAX_POLL_INTERVAL_MS);
|
|
790
|
+
fix = await driveAdvance({
|
|
791
|
+
tenantId,
|
|
792
|
+
fixId,
|
|
793
|
+
initial: fix,
|
|
794
|
+
timeoutMs,
|
|
795
|
+
pollIntervalMs,
|
|
796
|
+
profileOptions,
|
|
797
|
+
context
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
if (context.outputMode === "human") {
|
|
801
|
+
process.stderr.write(`MODA-FIX-${fix.shortRef} ${fix.status} ${fix.fixType}${fix.statusReason ? ` — ${fix.statusReason}` : ""}
|
|
802
|
+
`);
|
|
803
|
+
}
|
|
804
|
+
context.output.writeData(fix, {
|
|
805
|
+
summary: {
|
|
806
|
+
text: `Fix MODA-FIX-${fix.shortRef} is ${fix.status}${fix.statusReason ? ` (${fix.statusReason})` : ""}.`,
|
|
807
|
+
confidence: "high"
|
|
808
|
+
},
|
|
809
|
+
findings: gateFindings(fix),
|
|
810
|
+
nextCommands: nextCommandsFor(fix)
|
|
811
|
+
});
|
|
812
|
+
return EXIT_OK;
|
|
813
|
+
}
|
|
814
|
+
async function driveAdvance(args) {
|
|
815
|
+
const deadline = Date.now() + args.timeoutMs;
|
|
816
|
+
let fix = args.initial;
|
|
817
|
+
let lastStatus = "";
|
|
818
|
+
const announce = (row) => {
|
|
819
|
+
if (row.status === lastStatus)
|
|
820
|
+
return;
|
|
821
|
+
lastStatus = row.status;
|
|
822
|
+
const line = `fix MODA-FIX-${row.shortRef}: ${row.status}${row.statusReason ? ` — ${row.statusReason}` : ""}`;
|
|
823
|
+
if (args.context.outputMode === "human") {
|
|
824
|
+
process.stderr.write(`${line}
|
|
825
|
+
`);
|
|
826
|
+
} else if (args.context.outputMode === "agent-stream") {
|
|
827
|
+
args.context.output.writeEvent({
|
|
828
|
+
event: "progress",
|
|
829
|
+
phase: "fix",
|
|
830
|
+
message: line,
|
|
831
|
+
elapsed_ms: Date.now() - args.context.startedAt
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
};
|
|
835
|
+
announce(fix);
|
|
836
|
+
while (ADVANCING_STATUSES.has(fix.status)) {
|
|
837
|
+
if (Date.now() >= deadline) {
|
|
838
|
+
throw new Error(`Timed out after ${args.timeoutMs}ms waiting for fix ${args.fixId} (status ${fix.status})`);
|
|
839
|
+
}
|
|
840
|
+
fix = await advanceFix(args.tenantId, args.fixId, args.profileOptions);
|
|
841
|
+
announce(fix);
|
|
842
|
+
if (!ADVANCING_STATUSES.has(fix.status))
|
|
843
|
+
break;
|
|
844
|
+
await sleep(fix.status === "GATING" ? args.pollIntervalMs : Math.min(STEP_DELAY_MS, args.pollIntervalMs));
|
|
845
|
+
}
|
|
846
|
+
return fix;
|
|
847
|
+
}
|
|
848
|
+
function profileOptionsFor(context) {
|
|
849
|
+
return { profile: context.profile, env: context.env };
|
|
850
|
+
}
|
|
851
|
+
function requireTenantId(flags, profileOptions) {
|
|
852
|
+
const tenantId = flags["tenant-id"] || resolveTenantId(profileOptions);
|
|
853
|
+
if (!tenantId) {
|
|
854
|
+
throw new CliInputError("Missing tenant id. Run `moda init` or pass --tenant-id=<id>.");
|
|
855
|
+
}
|
|
856
|
+
return tenantId;
|
|
857
|
+
}
|
|
858
|
+
function fixesBase(tenantId) {
|
|
859
|
+
return `/tenants/${encodeURIComponent(tenantId)}/fixes`;
|
|
860
|
+
}
|
|
861
|
+
async function getFix(tenantId, fixId, profileOptions) {
|
|
862
|
+
return await callControlAPI(`${fixesBase(tenantId)}/${encodeURIComponent(fixId)}`, { method: "GET" }, profileOptions);
|
|
863
|
+
}
|
|
864
|
+
async function advanceFix(tenantId, fixId, profileOptions) {
|
|
865
|
+
return await callControlAPI(`${fixesBase(tenantId)}/${encodeURIComponent(fixId)}/advance`, { method: "POST" }, profileOptions, { timeoutMs: ADVANCE_TIMEOUT_MS, retries: 0 });
|
|
866
|
+
}
|
|
867
|
+
async function fetchPacketFailSoft(tenantId, fixId, profileOptions) {
|
|
868
|
+
try {
|
|
869
|
+
return await callControlAPI(`${fixesBase(tenantId)}/${encodeURIComponent(fixId)}/packet`, { method: "GET" }, profileOptions);
|
|
870
|
+
} catch {
|
|
871
|
+
return null;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
function gateFindings(fix) {
|
|
875
|
+
const gateResult = fix.gateResult ?? null;
|
|
876
|
+
if (!gateResult)
|
|
877
|
+
return [];
|
|
878
|
+
const verdict = gateResult.verdict ?? fix.status;
|
|
879
|
+
return [
|
|
880
|
+
{
|
|
881
|
+
id: "fix:gate",
|
|
882
|
+
title: `gate ${verdict}: ${gateResult.reason ?? fix.statusReason ?? ""}`.trim(),
|
|
883
|
+
status: verdict,
|
|
884
|
+
severity: verdict === "pass" ? "info" : verdict === "fail" ? "error" : "warning",
|
|
885
|
+
description: formatGateScoreboard(gateResult)
|
|
886
|
+
}
|
|
887
|
+
];
|
|
888
|
+
}
|
|
889
|
+
function formatGateScoreboard(gate) {
|
|
890
|
+
const holdout = gate.holdout ?? {};
|
|
891
|
+
const repair = gate.repair ?? {};
|
|
892
|
+
return `holdout: candidate ${holdout.candidate ?? 0}/${holdout.n ?? 0} vs baseline ${holdout.baseline ?? 0}/${holdout.n ?? 0} ` + `(wins ${holdout.wins ?? 0}, losses ${holdout.losses ?? 0}); ` + `training cases (repair): candidate ${repair.candidate ?? 0}/${repair.n ?? 0} vs baseline ${repair.baseline ?? 0}/${repair.n ?? 0}; ` + `noise floor ${gate.noiseFloor ?? 0}; abstained ${gate.abstained ?? 0}`;
|
|
893
|
+
}
|
|
894
|
+
function printHumanGateVerdict(fix, gateRunId, gateResult, cases) {
|
|
895
|
+
const w = (line) => process.stderr.write(`${line}
|
|
896
|
+
`);
|
|
897
|
+
w("");
|
|
898
|
+
w("Fix gate verdict");
|
|
899
|
+
w("----------------");
|
|
900
|
+
w(`Fix: MODA-FIX-${fix.shortRef} (${fix.status})`);
|
|
901
|
+
w(`Gate run: ${gateRunId}`);
|
|
902
|
+
w(`Verdict: ${gateResult?.verdict ?? "none"}${gateResult?.reason ? ` — ${gateResult.reason}` : ""}`);
|
|
903
|
+
if (gateResult) {
|
|
904
|
+
w(`Score: ${formatGateScoreboard(gateResult)}`);
|
|
905
|
+
}
|
|
906
|
+
if (cases.length > 0) {
|
|
907
|
+
w("");
|
|
908
|
+
w("Per-case (holdout is the verdict; repair rows are training cases):");
|
|
909
|
+
for (const row of cases) {
|
|
910
|
+
w(` ${row.caseId}: baseline=${passLabel(row.prodPass)} candidate=${passLabel(row.proposedPass)}`);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
w("");
|
|
914
|
+
}
|
|
915
|
+
function passLabel(value) {
|
|
916
|
+
if (value === null || value === undefined)
|
|
917
|
+
return "abstained";
|
|
918
|
+
return value ? "pass" : "fail";
|
|
919
|
+
}
|
|
920
|
+
function nextCommandsFor(fix) {
|
|
921
|
+
switch (fix.status) {
|
|
922
|
+
case "VERIFIED":
|
|
923
|
+
return [
|
|
924
|
+
{
|
|
925
|
+
command: `moda fix submit ${fix.id} --pr`,
|
|
926
|
+
purpose: "Open the draft fix PR (merge with the magic word to confirm).",
|
|
927
|
+
mutability: "write",
|
|
928
|
+
requires_approval: true
|
|
929
|
+
},
|
|
930
|
+
{
|
|
931
|
+
command: `moda fix checkout ${fix.id}`,
|
|
932
|
+
purpose: "Write the verified candidate locally instead of opening a PR.",
|
|
933
|
+
mutability: "write",
|
|
934
|
+
requires_approval: false
|
|
935
|
+
}
|
|
936
|
+
];
|
|
937
|
+
case "PROPOSED":
|
|
938
|
+
return [
|
|
939
|
+
{
|
|
940
|
+
command: `moda fix verify ${fix.id}`,
|
|
941
|
+
purpose: "Gate the proposed candidate on the frozen holdout.",
|
|
942
|
+
mutability: "write",
|
|
943
|
+
requires_approval: false
|
|
944
|
+
}
|
|
945
|
+
];
|
|
946
|
+
case "GATE_FAILED":
|
|
947
|
+
return [
|
|
948
|
+
{
|
|
949
|
+
command: `moda fix checkout ${fix.id}`,
|
|
950
|
+
purpose: "Check out the candidate, edit it, and re-gate (fail-to-pass loop).",
|
|
951
|
+
mutability: "write",
|
|
952
|
+
requires_approval: false
|
|
953
|
+
},
|
|
954
|
+
{
|
|
955
|
+
command: `moda fix verify ${fix.id} --prompt-file=<path>`,
|
|
956
|
+
purpose: "Re-gate an edited local candidate on the frozen holdout.",
|
|
957
|
+
mutability: "write",
|
|
958
|
+
requires_approval: false
|
|
959
|
+
}
|
|
960
|
+
];
|
|
961
|
+
case "GATE_INCONCLUSIVE":
|
|
962
|
+
return [
|
|
963
|
+
{
|
|
964
|
+
command: `moda fix verify ${fix.id}`,
|
|
965
|
+
purpose: "Re-run the gate — the last run was inconclusive (never a pass).",
|
|
966
|
+
mutability: "write",
|
|
967
|
+
requires_approval: false
|
|
968
|
+
}
|
|
969
|
+
];
|
|
970
|
+
case "UNROUTED":
|
|
971
|
+
case "NO_EVIDENCE":
|
|
972
|
+
case "BLOCKED_COVERAGE":
|
|
973
|
+
case "NO_HEADROOM":
|
|
974
|
+
return [
|
|
975
|
+
{
|
|
976
|
+
command: `moda fix ${fix.id} --packet`,
|
|
977
|
+
purpose: "Read the investigation packet — this fix is deliverable as diagnosis.",
|
|
978
|
+
mutability: "read",
|
|
979
|
+
requires_approval: false
|
|
980
|
+
}
|
|
981
|
+
];
|
|
982
|
+
case "DRAFT":
|
|
983
|
+
case "SCOPING":
|
|
984
|
+
case "PROPOSING":
|
|
985
|
+
case "GATING":
|
|
986
|
+
return [
|
|
987
|
+
{
|
|
988
|
+
command: `moda fix ${fix.id} --wait`,
|
|
989
|
+
purpose: "Drive the advance-on-poll pipeline to its next resting status.",
|
|
990
|
+
mutability: "write",
|
|
991
|
+
requires_approval: false
|
|
992
|
+
}
|
|
993
|
+
];
|
|
994
|
+
default:
|
|
995
|
+
return [
|
|
996
|
+
{
|
|
997
|
+
command: `moda fix ${fix.id}`,
|
|
998
|
+
purpose: "Track the fix status.",
|
|
999
|
+
mutability: "read",
|
|
1000
|
+
requires_approval: false
|
|
1001
|
+
}
|
|
1002
|
+
];
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
function assertRealpathContained(absPath, relPath) {
|
|
1006
|
+
const root = realpathSync(process.cwd());
|
|
1007
|
+
const within = (p) => p === root || p.startsWith(root + sep);
|
|
1008
|
+
const segments = absPath.slice(root.length).split(sep).filter(Boolean);
|
|
1009
|
+
let probe = root;
|
|
1010
|
+
for (const segment of segments) {
|
|
1011
|
+
probe = join(probe, segment);
|
|
1012
|
+
let stat;
|
|
1013
|
+
try {
|
|
1014
|
+
stat = lstatSync(probe);
|
|
1015
|
+
} catch {
|
|
1016
|
+
break;
|
|
1017
|
+
}
|
|
1018
|
+
if (stat.isSymbolicLink()) {
|
|
1019
|
+
throw new CliInputError(`Refusing to write through a symlink: ${relPath}`);
|
|
1020
|
+
}
|
|
1021
|
+
if (stat.isFile() && stat.nlink > 1) {
|
|
1022
|
+
throw new CliInputError(`Refusing to write through a hard link (${stat.nlink} names share this file): ${relPath} — replace it with an independent copy (cp then rm the link) and re-run.`);
|
|
1023
|
+
}
|
|
1024
|
+
if (!within(realpathSync(probe))) {
|
|
1025
|
+
throw new CliInputError(`Refusing to write outside the repo: ${relPath} resolves past the working tree`);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
function writeContainedFile(absPath, relPath, content) {
|
|
1030
|
+
mkdirSync(dirname(absPath), { recursive: true });
|
|
1031
|
+
const existedBefore = existsSync(absPath);
|
|
1032
|
+
let fd;
|
|
1033
|
+
try {
|
|
1034
|
+
fd = openSync(absPath, fsConstants.O_RDWR | fsConstants.O_CREAT | O_NOFOLLOW, 420);
|
|
1035
|
+
} catch (error) {
|
|
1036
|
+
if (error.code === "ELOOP") {
|
|
1037
|
+
throw new CliInputError(`Refusing to write through a symlink: ${relPath}`);
|
|
1038
|
+
}
|
|
1039
|
+
throw error;
|
|
1040
|
+
}
|
|
1041
|
+
try {
|
|
1042
|
+
const stat = fstatSync(fd);
|
|
1043
|
+
if (!stat.isFile()) {
|
|
1044
|
+
throw new CliInputError(`Refusing to write ${relPath}: not a regular file`);
|
|
1045
|
+
}
|
|
1046
|
+
if (stat.nlink > 1) {
|
|
1047
|
+
throw new CliInputError(`Refusing to write through a hard link (${stat.nlink} names share this file): ${relPath} — replace it with an independent copy (cp then rm the link) and re-run.`);
|
|
1048
|
+
}
|
|
1049
|
+
if (readFileSync(fd, "utf8") === content) {
|
|
1050
|
+
return "unchanged";
|
|
1051
|
+
}
|
|
1052
|
+
ftruncateSync(fd, 0);
|
|
1053
|
+
writeSync(fd, content, 0, "utf8");
|
|
1054
|
+
return existedBefore ? "updated" : "wrote";
|
|
1055
|
+
} finally {
|
|
1056
|
+
closeSync(fd);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
function safeTargetSourcePath(raw) {
|
|
1060
|
+
const trimmed = raw.trim();
|
|
1061
|
+
if (!trimmed || trimmed.includes("\x00")) {
|
|
1062
|
+
throw new CliInputError(`Refusing to write an invalid target source path: ${JSON.stringify(raw).slice(0, 120)}`);
|
|
1063
|
+
}
|
|
1064
|
+
if (isAbsolute(trimmed) || /^[A-Za-z]:[\\/]/.test(trimmed)) {
|
|
1065
|
+
throw new CliInputError(`Refusing to write outside the repo: target source path is absolute (${trimmed})`);
|
|
1066
|
+
}
|
|
1067
|
+
const segments = trimmed.split(/[\\/]+/).filter(Boolean);
|
|
1068
|
+
if (segments.length === 0 || segments.some((segment) => segment === "..")) {
|
|
1069
|
+
throw new CliInputError(`Refusing to write outside the repo: target source path contains '..' (${trimmed})`);
|
|
1070
|
+
}
|
|
1071
|
+
return segments.join("/");
|
|
1072
|
+
}
|
|
1073
|
+
function parsePositiveInt(raw, fallback, max) {
|
|
1074
|
+
const parsed = Number.parseInt(raw ?? "", 10);
|
|
1075
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
1076
|
+
return fallback;
|
|
1077
|
+
return Math.min(parsed, max);
|
|
1078
|
+
}
|
|
1079
|
+
function sleep(ms) {
|
|
1080
|
+
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
1081
|
+
}
|
|
1082
|
+
|
|
284
1083
|
// src/sync.ts
|
|
285
1084
|
async function runSyncCommand(context) {
|
|
286
1085
|
const resource = context.positionals[0] ?? "harness";
|
|
@@ -456,8 +1255,8 @@ async function sendCliUsageTelemetry(input) {
|
|
|
456
1255
|
}
|
|
457
1256
|
|
|
458
1257
|
// src/update-check.ts
|
|
459
|
-
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
460
|
-
import { join } from "node:path";
|
|
1258
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
1259
|
+
import { join as join2 } from "node:path";
|
|
461
1260
|
var UPDATE_CHECK_FLAG = "no-update-check";
|
|
462
1261
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
463
1262
|
var REGISTRY_TIMEOUT_MS = 1500;
|
|
@@ -471,11 +1270,11 @@ function isUpdateCheckDisabled(flags, env = process.env) {
|
|
|
471
1270
|
return flags[UPDATE_CHECK_FLAG] === "true" || env.MODA_CLI_UPDATE_CHECK === "0" || env.DO_NOT_TRACK === "1" || env.CI !== undefined && env.CI !== "" && env.CI !== "false" && env.CI !== "0";
|
|
472
1271
|
}
|
|
473
1272
|
function cachePath(options = {}) {
|
|
474
|
-
return
|
|
1273
|
+
return join2(getModaDir(options), "update-check.json");
|
|
475
1274
|
}
|
|
476
1275
|
function readCache(options = {}) {
|
|
477
1276
|
try {
|
|
478
|
-
const parsed = JSON.parse(
|
|
1277
|
+
const parsed = JSON.parse(readFileSync2(cachePath(options), "utf8"));
|
|
479
1278
|
if (parsed && typeof parsed === "object" && typeof parsed.checked_at === "number" && typeof parsed.latest === "string") {
|
|
480
1279
|
return parsed;
|
|
481
1280
|
}
|
|
@@ -484,7 +1283,7 @@ function readCache(options = {}) {
|
|
|
484
1283
|
}
|
|
485
1284
|
function writeCache(cache, options = {}) {
|
|
486
1285
|
try {
|
|
487
|
-
|
|
1286
|
+
mkdirSync2(getModaDir(options), { recursive: true });
|
|
488
1287
|
writeFileSync(cachePath(options), `${JSON.stringify(cache, null, 2)}
|
|
489
1288
|
`, "utf8");
|
|
490
1289
|
} catch {}
|
|
@@ -1046,8 +1845,8 @@ function renderJsonSchemas() {
|
|
|
1046
1845
|
|
|
1047
1846
|
// src/keys.ts
|
|
1048
1847
|
import { createHash } from "node:crypto";
|
|
1049
|
-
import { existsSync, readFileSync as
|
|
1050
|
-
import { join as
|
|
1848
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
|
|
1849
|
+
import { join as join3 } from "node:path";
|
|
1051
1850
|
var KEY_LIFECYCLE_ENDPOINT_REQUIREMENTS = {
|
|
1052
1851
|
list: "GET /api/cli/keys?profile=<profile>",
|
|
1053
1852
|
create: "POST /api/cli/keys",
|
|
@@ -1114,14 +1913,14 @@ function writeLocalApiKeySecret(descriptor, apiKey, options = {}) {
|
|
|
1114
1913
|
return path;
|
|
1115
1914
|
}
|
|
1116
1915
|
function keyDescriptorStorePath(options = {}) {
|
|
1117
|
-
return
|
|
1916
|
+
return join3(getModaPaths(options).dir, "keys.json");
|
|
1118
1917
|
}
|
|
1119
1918
|
function loadLocalKeyDescriptors(options = {}) {
|
|
1120
1919
|
const path = keyDescriptorStorePath(options);
|
|
1121
1920
|
try {
|
|
1122
|
-
if (!
|
|
1921
|
+
if (!existsSync2(path))
|
|
1123
1922
|
return [];
|
|
1124
|
-
const parsed = JSON.parse(
|
|
1923
|
+
const parsed = JSON.parse(readFileSync3(path, "utf8"));
|
|
1125
1924
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1126
1925
|
return [];
|
|
1127
1926
|
}
|
|
@@ -1173,6 +1972,10 @@ var ONLINE_PROBE_TIMEOUT_MS = 5000;
|
|
|
1173
1972
|
function subcommand(context) {
|
|
1174
1973
|
return context.positionals[0];
|
|
1175
1974
|
}
|
|
1975
|
+
function unknownSubcommandError(parent, attempted, valid) {
|
|
1976
|
+
const message = attempted ? `Unknown ${parent} subcommand '${attempted}'. Valid subcommands: ${valid.join(", ")}.` : `A ${parent} subcommand is required. Valid subcommands: ${valid.join(", ")}.`;
|
|
1977
|
+
return new CliInputError(message, `Usage: moda ${parent} <${valid.join("|")}>`);
|
|
1978
|
+
}
|
|
1176
1979
|
function requirePositional(context, index, usage) {
|
|
1177
1980
|
const value = context.positionals[index];
|
|
1178
1981
|
if (!value)
|
|
@@ -1318,7 +2121,7 @@ async function runAuthCommand(context) {
|
|
|
1318
2121
|
return { exitCode: EXIT_OK };
|
|
1319
2122
|
}
|
|
1320
2123
|
default:
|
|
1321
|
-
throw
|
|
2124
|
+
throw unknownSubcommandError("auth", subcommand(context), ["login", "logout", "status", "token", "whoami"]);
|
|
1322
2125
|
}
|
|
1323
2126
|
}
|
|
1324
2127
|
async function runProfilesCommand(context) {
|
|
@@ -1396,12 +2199,12 @@ async function runProfilesCommand(context) {
|
|
|
1396
2199
|
return { exitCode: 0 };
|
|
1397
2200
|
}
|
|
1398
2201
|
default:
|
|
1399
|
-
throw
|
|
2202
|
+
throw unknownSubcommandError("profiles", subcommand(context), ["list", "use", "create", "doctor"]);
|
|
1400
2203
|
}
|
|
1401
2204
|
}
|
|
1402
2205
|
async function runConfigCommand(context) {
|
|
1403
2206
|
if (subcommand(context) !== "show") {
|
|
1404
|
-
throw
|
|
2207
|
+
throw unknownSubcommandError("config", subcommand(context), ["show"]);
|
|
1405
2208
|
}
|
|
1406
2209
|
const config = loadProfileConfig({ env: context.env });
|
|
1407
2210
|
const profile = getActiveProfile({ profile: context.profile, env: context.env });
|
|
@@ -1460,17 +2263,17 @@ async function runKeysCommand(context) {
|
|
|
1460
2263
|
throw new CliInputError(`moda keys ${action} is waiting on Moda Cloud key lifecycle support for ${keyId}.`, `Required endpoint: ${KEY_LIFECYCLE_ENDPOINT_REQUIREMENTS[action]}`);
|
|
1461
2264
|
}
|
|
1462
2265
|
default:
|
|
1463
|
-
throw
|
|
2266
|
+
throw unknownSubcommandError("keys", subcommand(context), ["list", "create", "rotate", "revoke"]);
|
|
1464
2267
|
}
|
|
1465
2268
|
}
|
|
1466
2269
|
|
|
1467
2270
|
// src/agent.ts
|
|
1468
|
-
import { existsSync as
|
|
1469
|
-
import { join as
|
|
2271
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4, statSync } from "node:fs";
|
|
2272
|
+
import { join as join4, relative, resolve as resolve2 } from "node:path";
|
|
1470
2273
|
var MODA_DIR = ".moda";
|
|
1471
|
-
var HARNESS_PATH =
|
|
1472
|
-
var HARNESS_REPORT_MARKDOWN_PATH =
|
|
1473
|
-
var HARNESS_SUMMARY_PATH =
|
|
2274
|
+
var HARNESS_PATH = join4(MODA_DIR, "harness.json");
|
|
2275
|
+
var HARNESS_REPORT_MARKDOWN_PATH = join4(MODA_DIR, "HARNESS_REPORT.md");
|
|
2276
|
+
var HARNESS_SUMMARY_PATH = join4(MODA_DIR, "HARNESS.md");
|
|
1474
2277
|
var MAX_AGENT_EVIDENCE_BYTES = 24000;
|
|
1475
2278
|
async function runAgentCommand(context) {
|
|
1476
2279
|
const subcommand2 = context.positionals[0] ?? "context";
|
|
@@ -1592,7 +2395,7 @@ async function buildAgentContext(context) {
|
|
|
1592
2395
|
},
|
|
1593
2396
|
report: reportState.state,
|
|
1594
2397
|
sync: syncState ? {
|
|
1595
|
-
path:
|
|
2398
|
+
path: join4(MODA_DIR, "sync-state.json"),
|
|
1596
2399
|
exists: true,
|
|
1597
2400
|
source: syncState.source,
|
|
1598
2401
|
updatedAt: syncState.updatedAt,
|
|
@@ -1600,7 +2403,7 @@ async function buildAgentContext(context) {
|
|
|
1600
2403
|
remoteHarnessId: syncState.remote.harnessId,
|
|
1601
2404
|
remoteVersionId: syncState.remote.versionId
|
|
1602
2405
|
} : {
|
|
1603
|
-
path:
|
|
2406
|
+
path: join4(MODA_DIR, "sync-state.json"),
|
|
1604
2407
|
exists: false
|
|
1605
2408
|
}
|
|
1606
2409
|
},
|
|
@@ -1650,18 +2453,18 @@ function summarizeAgentContext(payload, noun) {
|
|
|
1650
2453
|
return `Local Moda ${noun} is healthy with ${agents} runtime agent(s).`;
|
|
1651
2454
|
}
|
|
1652
2455
|
function readHarnessGraphIfPresent(cwd) {
|
|
1653
|
-
const path =
|
|
1654
|
-
if (!
|
|
2456
|
+
const path = join4(cwd, HARNESS_PATH);
|
|
2457
|
+
if (!existsSync3(path))
|
|
1655
2458
|
return;
|
|
1656
2459
|
try {
|
|
1657
|
-
return JSON.parse(
|
|
2460
|
+
return JSON.parse(readFileSync4(path, "utf8"));
|
|
1658
2461
|
} catch {
|
|
1659
2462
|
return;
|
|
1660
2463
|
}
|
|
1661
2464
|
}
|
|
1662
2465
|
function readHarnessReportState(cwd) {
|
|
1663
|
-
const path =
|
|
1664
|
-
if (!
|
|
2466
|
+
const path = join4(cwd, HARNESS_REPORT_PATH);
|
|
2467
|
+
if (!existsSync3(path)) {
|
|
1665
2468
|
return {
|
|
1666
2469
|
state: {
|
|
1667
2470
|
path: HARNESS_REPORT_PATH,
|
|
@@ -1705,11 +2508,11 @@ function readHarnessReportState(cwd) {
|
|
|
1705
2508
|
}
|
|
1706
2509
|
}
|
|
1707
2510
|
function readApproval(cwd) {
|
|
1708
|
-
const path =
|
|
1709
|
-
if (!
|
|
2511
|
+
const path = join4(cwd, HARNESS_REPORT_APPROVAL_PATH);
|
|
2512
|
+
if (!existsSync3(path))
|
|
1710
2513
|
return;
|
|
1711
2514
|
try {
|
|
1712
|
-
return JSON.parse(
|
|
2515
|
+
return JSON.parse(readFileSync4(path, "utf8"));
|
|
1713
2516
|
} catch {
|
|
1714
2517
|
return;
|
|
1715
2518
|
}
|
|
@@ -1785,9 +2588,9 @@ function existingArtifacts(cwd) {
|
|
|
1785
2588
|
[HARNESS_REPORT_PATH, "harness_report", "Cited harness analyst report."],
|
|
1786
2589
|
[HARNESS_REPORT_MARKDOWN_PATH, "harness_report_markdown", "Human-readable harness analyst report."],
|
|
1787
2590
|
[HARNESS_SUMMARY_PATH, "harness_summary", "Human-readable harness graph summary."],
|
|
1788
|
-
[
|
|
1789
|
-
[
|
|
1790
|
-
].flatMap(([path, kind, description]) =>
|
|
2591
|
+
[join4(MODA_DIR, "sync-state.json"), "sync_state", "Last local harness sync state."],
|
|
2592
|
+
[join4(MODA_DIR, "prompts.yml"), "prompt_manifest", "Local prompt discovery manifest."]
|
|
2593
|
+
].flatMap(([path, kind, description]) => existsSync3(join4(cwd, path)) ? [{ path, kind, description }] : []);
|
|
1791
2594
|
}
|
|
1792
2595
|
function expandEvidenceRef(ref, loaded, cwd) {
|
|
1793
2596
|
if (ref.startsWith("doctor:")) {
|
|
@@ -1868,9 +2671,9 @@ function readCitationExcerpt(cwd, citation) {
|
|
|
1868
2671
|
if (!citation.path || citation.type !== "file" && citation.type !== "generated_doc")
|
|
1869
2672
|
return citation.excerpt;
|
|
1870
2673
|
const absolute = safeResolve(cwd, citation.path);
|
|
1871
|
-
if (!absolute || !
|
|
2674
|
+
if (!absolute || !existsSync3(absolute))
|
|
1872
2675
|
return citation.excerpt;
|
|
1873
|
-
const text =
|
|
2676
|
+
const text = readFileSync4(absolute, "utf8");
|
|
1874
2677
|
if (citation.line && citation.line > 0) {
|
|
1875
2678
|
return text.split(/\r?\n/)[citation.line - 1]?.slice(0, 1000) ?? citation.excerpt;
|
|
1876
2679
|
}
|
|
@@ -1881,10 +2684,10 @@ function readBoundedModaArtifact(cwd, artifactPath) {
|
|
|
1881
2684
|
throw new CliInputError(`Artifact '${artifactPath}' is outside .moda and cannot be expanded directly.`, "Use a citation evidence ref for source-file evidence.");
|
|
1882
2685
|
}
|
|
1883
2686
|
const absolute = safeResolve(cwd, artifactPath);
|
|
1884
|
-
if (!absolute || !
|
|
2687
|
+
if (!absolute || !existsSync3(absolute))
|
|
1885
2688
|
throw missingEvidence(`artifact:${artifactPath}`);
|
|
1886
2689
|
const size = statSync(absolute).size;
|
|
1887
|
-
const text =
|
|
2690
|
+
const text = readFileSync4(absolute, "utf8");
|
|
1888
2691
|
return {
|
|
1889
2692
|
path: artifactPath,
|
|
1890
2693
|
bytes: size,
|
|
@@ -1893,7 +2696,7 @@ function readBoundedModaArtifact(cwd, artifactPath) {
|
|
|
1893
2696
|
};
|
|
1894
2697
|
}
|
|
1895
2698
|
function safeResolve(cwd, path) {
|
|
1896
|
-
const absolute =
|
|
2699
|
+
const absolute = resolve2(cwd, path);
|
|
1897
2700
|
const rel = relative(cwd, absolute);
|
|
1898
2701
|
if (rel.startsWith("..") || rel === ".." || rel.startsWith("/"))
|
|
1899
2702
|
return;
|
|
@@ -1927,8 +2730,8 @@ function purposeForCommand(command) {
|
|
|
1927
2730
|
}
|
|
1928
2731
|
|
|
1929
2732
|
// src/production-intelligence.ts
|
|
1930
|
-
import { existsSync as
|
|
1931
|
-
import { join as
|
|
2733
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5 } from "node:fs";
|
|
2734
|
+
import { join as join5 } from "node:path";
|
|
1932
2735
|
|
|
1933
2736
|
// src/ask-stream.ts
|
|
1934
2737
|
class AskStreamError extends Error {
|
|
@@ -3348,12 +4151,12 @@ function loadHarnessHints(cwd) {
|
|
|
3348
4151
|
skillPaths: new Map,
|
|
3349
4152
|
entrypointPaths: []
|
|
3350
4153
|
};
|
|
3351
|
-
const harnessPath =
|
|
3352
|
-
if (!
|
|
4154
|
+
const harnessPath = join5(cwd, ".moda", "harness.json");
|
|
4155
|
+
if (!existsSync4(harnessPath))
|
|
3353
4156
|
return hints;
|
|
3354
4157
|
hints.reportPath = ".moda/harness.json";
|
|
3355
4158
|
try {
|
|
3356
|
-
const parsed = JSON.parse(
|
|
4159
|
+
const parsed = JSON.parse(readFileSync5(harnessPath, "utf8"));
|
|
3357
4160
|
const graph = asRecord(parsed);
|
|
3358
4161
|
const artifacts = Array.isArray(graph?.artifacts) ? graph.artifacts : [];
|
|
3359
4162
|
for (const item of artifacts) {
|
|
@@ -3381,11 +4184,11 @@ function loadHarnessHints(cwd) {
|
|
|
3381
4184
|
return hints;
|
|
3382
4185
|
}
|
|
3383
4186
|
function localHarnessSummary(cwd) {
|
|
3384
|
-
const path =
|
|
3385
|
-
if (!
|
|
4187
|
+
const path = join5(cwd, ".moda", "harness.json");
|
|
4188
|
+
if (!existsSync4(path))
|
|
3386
4189
|
return null;
|
|
3387
4190
|
try {
|
|
3388
|
-
const parsed = JSON.parse(
|
|
4191
|
+
const parsed = JSON.parse(readFileSync5(path, "utf8"));
|
|
3389
4192
|
return {
|
|
3390
4193
|
harnessId: parsed.harnessId,
|
|
3391
4194
|
graphHash: parsed.graphHash,
|
|
@@ -3396,16 +4199,16 @@ function localHarnessSummary(cwd) {
|
|
|
3396
4199
|
}
|
|
3397
4200
|
}
|
|
3398
4201
|
function localSyncBrief(cwd, relativePath, label) {
|
|
3399
|
-
const path =
|
|
3400
|
-
if (!
|
|
4202
|
+
const path = join5(cwd, relativePath);
|
|
4203
|
+
if (!existsSync4(path)) {
|
|
3401
4204
|
return { status: "missing", summary: `${label} state not found`, path: relativePath };
|
|
3402
4205
|
}
|
|
3403
4206
|
return { status: "present", summary: `${label} state exists locally`, path: relativePath };
|
|
3404
4207
|
}
|
|
3405
4208
|
function localSkillBrief(cwd) {
|
|
3406
|
-
const claudeSkill =
|
|
3407
|
-
const cursorRules =
|
|
3408
|
-
if (
|
|
4209
|
+
const claudeSkill = join5(cwd, ".claude", "skills");
|
|
4210
|
+
const cursorRules = join5(cwd, ".cursor", "rules");
|
|
4211
|
+
if (existsSync4(claudeSkill) || existsSync4(cursorRules)) {
|
|
3409
4212
|
return { status: "present", summary: "Local agent skill/rule directory exists" };
|
|
3410
4213
|
}
|
|
3411
4214
|
return { status: "missing", summary: "No local agent skill/rule directory found" };
|
|
@@ -3646,7 +4449,7 @@ function buildManifest(commands) {
|
|
|
3646
4449
|
exit_codes: [
|
|
3647
4450
|
{ code: 0, name: "ok", meaning: "Command completed successfully." },
|
|
3648
4451
|
{ code: 1, name: "error", meaning: "Command, input, API, network, or unexpected failure." },
|
|
3649
|
-
{ code: 3, name: "degraded", meaning: "
|
|
4452
|
+
{ code: 3, name: "degraded", meaning: "Degraded result: `moda ask` local fallback, or `moda fix verify` gate inconclusive/blocked-coverage (never treat as a pass)." },
|
|
3650
4453
|
{ code: 4, name: "auth_required", meaning: "Run login_command, then resume." },
|
|
3651
4454
|
{ code: 5, name: "input_required", meaning: "Choose from input_request choices, then resume." },
|
|
3652
4455
|
{ code: 130, name: "cancelled", meaning: "Interrupted or output consumer closed." }
|
|
@@ -3717,6 +4520,8 @@ function event(name, when, terminal, deprecated = false) {
|
|
|
3717
4520
|
function exitCodesForCommand(command) {
|
|
3718
4521
|
if (command === "ask")
|
|
3719
4522
|
return [0, 1, 3, 4, 5, 130];
|
|
4523
|
+
if (command === "fix")
|
|
4524
|
+
return [0, 1, 3, 4, 130];
|
|
3720
4525
|
if (command === "init" || command === "harness")
|
|
3721
4526
|
return [0, 1, 4, 5, 130];
|
|
3722
4527
|
return [0, 1, 4, 130];
|
|
@@ -3724,7 +4529,7 @@ function exitCodesForCommand(command) {
|
|
|
3724
4529
|
|
|
3725
4530
|
// src/cli.ts
|
|
3726
4531
|
import { fileURLToPath } from "url";
|
|
3727
|
-
import { realpathSync } from "fs";
|
|
4532
|
+
import { realpathSync as realpathSync2 } from "fs";
|
|
3728
4533
|
function parseArgs(argv) {
|
|
3729
4534
|
const args = argv.slice(2);
|
|
3730
4535
|
let command = "";
|
|
@@ -3855,10 +4660,16 @@ Commands:
|
|
|
3855
4660
|
world-state <conversation_id> Get a conversation's world state (slots/threads/events)
|
|
3856
4661
|
context <conversation_id> Get windowed conversation context
|
|
3857
4662
|
audit <conversation_id|trace_id> Raw span/trace audit (spans, hierarchy, orphans, duplicates)
|
|
3858
|
-
frustrations Get user frustration detections
|
|
4663
|
+
frustrations Get user frustration detections (legacy single-family; see emotions)
|
|
4664
|
+
emotions Multi-family emotion detections (frustration, sadness, confusion, anxiety, trust, positive)
|
|
4665
|
+
hallucinations Grounding detections: contradicted/verified outputs with rule breakdown
|
|
3859
4666
|
tool-failures Get tool failure overview
|
|
3860
4667
|
tool-failure-detail <tool_name> Get per-tool failure detail
|
|
3861
4668
|
problems Rank cross-signal Problems by root cause (what to fix first)
|
|
4669
|
+
problem <problem_id> Open one Problem: dossier, or --evidence/--reports/--conversations/--feedback
|
|
4670
|
+
problem-feedback <problem_id> Mark a Problem fixed, dismiss, rename, or flag a bad attribution
|
|
4671
|
+
step-scores <conversation_id> Graph-PRM step scores (per-segment curves, first bad step, rollup)
|
|
4672
|
+
tail Live-tail new conversations/detections as NDJSON (one line per item)
|
|
3862
4673
|
feedback "<note>" Flag wrong/missing data or CLI quirks to the Moda team
|
|
3863
4674
|
|
|
3864
4675
|
Prompt management:
|
|
@@ -3879,6 +4690,18 @@ Commands:
|
|
|
3879
4690
|
skills sync Push local .claude/skills/**/SKILL.md up to Moda
|
|
3880
4691
|
skills sync --dry-run Show what would be pushed without writing
|
|
3881
4692
|
|
|
4693
|
+
Fixes (a fix with proof):
|
|
4694
|
+
fixes Ranked Fix queue (--status=S --limit=N --cursor=TOKEN)
|
|
4695
|
+
fix <fix_id> One Fix: status + gate result; --packet prints moda.fix_packet.v1;
|
|
4696
|
+
--wait drives a still-running pipeline to rest
|
|
4697
|
+
fix start <problem_id> Draft a Fix from a Problem (--type=auto|prompt); --wait drives
|
|
4698
|
+
scope -> propose -> gate and prints the packet
|
|
4699
|
+
fix verify <fix_id> Re-gate on the frozen holdout; --prompt-file=P gates local content
|
|
4700
|
+
(exit 0 pass / 1 fail with per-case findings / 3 degraded-inconclusive)
|
|
4701
|
+
fix checkout <fix_id> Write the candidate at its target source path (never runs git)
|
|
4702
|
+
fix submit <fix_id> Deliver: --pr opens a draft PR | --local-ref=<branch> records your branch
|
|
4703
|
+
fix dismiss <fix_id> Reject with --reason=TEXT (feeds problem feedback)
|
|
4704
|
+
|
|
3882
4705
|
Options:
|
|
3883
4706
|
--help Show this help message
|
|
3884
4707
|
--version Show version number
|
|
@@ -3924,6 +4747,52 @@ Search flags:
|
|
|
3924
4747
|
World-state flags:
|
|
3925
4748
|
--summary-only Return segment summaries only (omit the event stream)
|
|
3926
4749
|
--event-limit=N Max world-state events to return (1-5000, default 2000)
|
|
4750
|
+
--snapshot Folded slots at an exact tick (what the agent believed at that turn)
|
|
4751
|
+
--msg-index=N --block-index=M Snapshot tick (defaults 0; --segment-index=K scopes to a segment)
|
|
4752
|
+
--replay State-over-time replay; frames mode REQUIRES --message-count=N
|
|
4753
|
+
--format=frames|fold-inputs Replay format (default frames)
|
|
4754
|
+
--message-count=N Number of frames to fold (frames mode)
|
|
4755
|
+
|
|
4756
|
+
Emotions flags:
|
|
4757
|
+
--family=F frustration|sadness|confusion|anxiety|trust|positive
|
|
4758
|
+
--days-back=N --limit=N Window 1-90 (default 7); page size 1-20 (default 10)
|
|
4759
|
+
--offset=N Skip N detections (0-10000)
|
|
4760
|
+
|
|
4761
|
+
Hallucinations flags:
|
|
4762
|
+
--kind=contradicted|verified Narrow the detections list
|
|
4763
|
+
--conversation-id=ID Scope summary + list to one conversation
|
|
4764
|
+
--days-back=N --limit=N Window 1-90 (default 7); page size 1-20 (default 10)
|
|
4765
|
+
|
|
4766
|
+
Problem flags (moda problem <id>):
|
|
4767
|
+
--evidence|--reports|--conversations|--feedback
|
|
4768
|
+
Open one sub-resource page (at most one)
|
|
4769
|
+
--limit=N --cursor=TOKEN Keyset paging (1-50; pass next_cursor back verbatim)
|
|
4770
|
+
--family=F --door=D Filter --conversations (families: tool_failure|emotion|laziness|hallucination|prm_dip)
|
|
4771
|
+
|
|
4772
|
+
Problem-feedback flags:
|
|
4773
|
+
--action=A mark_fixed|dismiss|flag_attribution|rename (required)
|
|
4774
|
+
--reason=TEXT Required for dismiss and flag_attribution
|
|
4775
|
+
--attribution-id=UUID Required for flag_attribution (ids come from problem --evidence)
|
|
4776
|
+
--new-name=NAME Required for rename
|
|
4777
|
+
|
|
4778
|
+
Tail flags:
|
|
4779
|
+
--signal=S conversations|emotions|all (default conversations)
|
|
4780
|
+
--interval=N Poll every N seconds (5-3600, default 15)
|
|
4781
|
+
--once One poll, then exit (baseline page)
|
|
4782
|
+
--limit=N --max-events=N Page size per poll; stop after N stdout records
|
|
4783
|
+
(metadata included; a slot is reserved for the
|
|
4784
|
+
emotions coverage record, which falls back to
|
|
4785
|
+
stderr if the bound leaves no room)
|
|
4786
|
+
--full-scan Emotions: scan the whole window instead of the
|
|
4787
|
+
top-scoring prefix (many more requests). /emotions
|
|
4788
|
+
is score-ranked with no cursor, so the default
|
|
4789
|
+
follows the highest-scoring detections and emits a
|
|
4790
|
+
tail_coverage line stating exactly what it saw.
|
|
4791
|
+
|
|
4792
|
+
Clusters search flags:
|
|
4793
|
+
--search="query" Find clusters by name/meaning (task-cluster search)
|
|
4794
|
+
--node-id=ID Deterministically resolve one cluster node (deep link)
|
|
4795
|
+
--mode=fuzzy|semantic|hybrid Cluster search mode (default hybrid)
|
|
3927
4796
|
|
|
3928
4797
|
Feedback flags:
|
|
3929
4798
|
--category=CAT bad_cluster_label|mismatched_frustration|missing_data|noisy_data|
|
|
@@ -4009,6 +4878,16 @@ Examples:
|
|
|
4009
4878
|
moda search "billing error" --mode=hybrid
|
|
4010
4879
|
moda search "refund flow" --mode=semantic --time-range=7d --limit=10
|
|
4011
4880
|
moda world-state <conversation_id> --summary-only
|
|
4881
|
+
moda world-state <conversation_id> --snapshot --msg-index=42
|
|
4882
|
+
moda world-state <conversation_id> --replay --message-count=50
|
|
4883
|
+
moda clusters --search="billing disputes"
|
|
4884
|
+
moda emotions --family=confusion --days-back=14
|
|
4885
|
+
moda hallucinations --kind=contradicted
|
|
4886
|
+
moda problems --days-back=7
|
|
4887
|
+
moda problem <problem_id> --evidence --limit=10
|
|
4888
|
+
moda problem-feedback <problem_id> --action=mark_fixed
|
|
4889
|
+
moda step-scores <conversation_id>
|
|
4890
|
+
moda tail --signal=all --interval=30
|
|
4012
4891
|
moda frustrations --days-back=14 --limit=20
|
|
4013
4892
|
moda feedback "cluster label looks wrong" --category=bad_cluster_label --cluster-id=<node_id>
|
|
4014
4893
|
moda context <conversation_id> --window=3
|
|
@@ -4018,7 +4897,29 @@ Examples:
|
|
|
4018
4897
|
moda prompts sync
|
|
4019
4898
|
moda prompts promote support.triage --label=prod --version=pver_abc123
|
|
4020
4899
|
moda prompts ab --baseline=prompts/agent.prompt.md --candidate=prompts/agent-v2.prompt.md --conversations=conv_1,conv_2
|
|
4021
|
-
moda skills pull
|
|
4900
|
+
moda skills pull
|
|
4901
|
+
moda fixes
|
|
4902
|
+
moda fix start <problem_id> --wait
|
|
4903
|
+
moda fix verify <fix_id> --prompt-file=prompts/agent.prompt.md`);
|
|
4904
|
+
}
|
|
4905
|
+
function printCommandHelp(definition) {
|
|
4906
|
+
const lines = [];
|
|
4907
|
+
lines.push(`moda ${definition.name} — ${definition.description}`);
|
|
4908
|
+
if (definition.aliases && definition.aliases.length > 0) {
|
|
4909
|
+
lines.push(`Aliases: ${definition.aliases.join(", ")}`);
|
|
4910
|
+
}
|
|
4911
|
+
if (definition.mutability === "write") {
|
|
4912
|
+
lines.push("This command can mutate state.");
|
|
4913
|
+
}
|
|
4914
|
+
if (definition.examples.length > 0) {
|
|
4915
|
+
lines.push("", "Examples:");
|
|
4916
|
+
for (const example of definition.examples) {
|
|
4917
|
+
lines.push(` ${example}`);
|
|
4918
|
+
}
|
|
4919
|
+
}
|
|
4920
|
+
lines.push("", "Run 'moda --help' for the full command list and common flags", "(--json, --agent, --profile, --days-back, --limit, --time-range, ...).");
|
|
4921
|
+
console.error(lines.join(`
|
|
4922
|
+
`));
|
|
4022
4923
|
}
|
|
4023
4924
|
async function attachWindows(rows, windowSize) {
|
|
4024
4925
|
return Promise.all(rows.map(async (row) => {
|
|
@@ -4051,14 +4952,201 @@ async function attachWindows(rows, windowSize) {
|
|
|
4051
4952
|
}
|
|
4052
4953
|
}));
|
|
4053
4954
|
}
|
|
4054
|
-
async function printWithOptionalWindows(enriched, listKey, includeWindow, windowSize, output) {
|
|
4955
|
+
async function printWithOptionalWindows(enriched, listKey, includeWindow, windowSize, output, warnings) {
|
|
4956
|
+
const writeOptions = warnings && warnings.length > 0 ? { warnings } : undefined;
|
|
4055
4957
|
if (!includeWindow) {
|
|
4056
|
-
output.writeData(enriched);
|
|
4958
|
+
output.writeData(enriched, writeOptions);
|
|
4057
4959
|
return;
|
|
4058
4960
|
}
|
|
4059
4961
|
const list = Array.isArray(enriched[listKey]) ? enriched[listKey] : [];
|
|
4060
4962
|
const withWindows = await attachWindows(list, windowSize);
|
|
4061
|
-
output.writeData({ ...enriched, [listKey]: withWindows });
|
|
4963
|
+
output.writeData({ ...enriched, [listKey]: withWindows }, writeOptions);
|
|
4964
|
+
}
|
|
4965
|
+
function notFoundWarning(entity, id, isMissing) {
|
|
4966
|
+
if (!isMissing)
|
|
4967
|
+
return [];
|
|
4968
|
+
return [
|
|
4969
|
+
`No ${entity} found for id "${id}" — it may not exist in this tenant (the Data API returns an empty result rather than an error for unknown ids).`
|
|
4970
|
+
];
|
|
4971
|
+
}
|
|
4972
|
+
var TAIL_SEEN_MAX = 5000;
|
|
4973
|
+
var EMOTIONS_TAIL_PAGE_SIZE = 20;
|
|
4974
|
+
var EMOTIONS_TAIL_MAX_PAGES = 25;
|
|
4975
|
+
var EMOTIONS_OFFSET_CEILING = 1e4;
|
|
4976
|
+
var EMOTIONS_FULL_SCAN_MAX_PAGES = Math.floor(EMOTIONS_OFFSET_CEILING / EMOTIONS_TAIL_PAGE_SIZE) + 1;
|
|
4977
|
+
var EMOTIONS_SEEN_MAX = EMOTIONS_FULL_SCAN_MAX_PAGES * EMOTIONS_TAIL_PAGE_SIZE + 2000;
|
|
4978
|
+
var CONVERSATIONS_SEEN_MAX = TAIL_SEEN_MAX;
|
|
4979
|
+
var TAIL_LIMITS = {
|
|
4980
|
+
emotionsPageSize: EMOTIONS_TAIL_PAGE_SIZE,
|
|
4981
|
+
emotionsFullScanMaxPages: EMOTIONS_FULL_SCAN_MAX_PAGES,
|
|
4982
|
+
emotionsSeenMax: EMOTIONS_SEEN_MAX,
|
|
4983
|
+
conversationsSeenMax: CONVERSATIONS_SEEN_MAX,
|
|
4984
|
+
independentKeyspaces: true
|
|
4985
|
+
};
|
|
4986
|
+
async function runTailCommand(params, context) {
|
|
4987
|
+
const intervalMs = (params.interval ?? 15) * 1000;
|
|
4988
|
+
const signal = params.signal ?? "conversations";
|
|
4989
|
+
const limit = params.limit ?? 20;
|
|
4990
|
+
const seenConversations = new Set;
|
|
4991
|
+
const seenEmotions = new Set;
|
|
4992
|
+
let emitted = 0;
|
|
4993
|
+
let evictionReported = false;
|
|
4994
|
+
const rememberIn = (seen, capacity, key) => {
|
|
4995
|
+
if (seen.has(key))
|
|
4996
|
+
return false;
|
|
4997
|
+
if (seen.size >= capacity) {
|
|
4998
|
+
const iterator = seen.values();
|
|
4999
|
+
for (let i = 0;i < Math.max(1, Math.floor(capacity / 10)); i++) {
|
|
5000
|
+
const next = iterator.next();
|
|
5001
|
+
if (next.done)
|
|
5002
|
+
break;
|
|
5003
|
+
seen.delete(next.value);
|
|
5004
|
+
}
|
|
5005
|
+
if (!evictionReported) {
|
|
5006
|
+
evictionReported = true;
|
|
5007
|
+
emitMeta({
|
|
5008
|
+
type: "tail_dedupe_evicted",
|
|
5009
|
+
capacity,
|
|
5010
|
+
detail: "Dedupe memory reached capacity and oldest ids were dropped; items seen long ago may re-appear once. Restart the tail, or narrow --signal, if exact-once matters."
|
|
5011
|
+
});
|
|
5012
|
+
}
|
|
5013
|
+
}
|
|
5014
|
+
seen.add(key);
|
|
5015
|
+
return true;
|
|
5016
|
+
};
|
|
5017
|
+
const rememberConversation = (key) => rememberIn(seenConversations, CONVERSATIONS_SEEN_MAX, key);
|
|
5018
|
+
const rememberEmotion = (key) => rememberIn(seenEmotions, EMOTIONS_SEEN_MAX, key);
|
|
5019
|
+
const emitsCoverage = signal === "emotions" || signal === "all";
|
|
5020
|
+
const metaReserve = params.max_events !== undefined && emitsCoverage && params.max_events >= 2 ? 1 : 0;
|
|
5021
|
+
const dataBudget = params.max_events === undefined ? undefined : Math.max(0, params.max_events - metaReserve);
|
|
5022
|
+
const capReached = () => dataBudget !== undefined && emitted >= dataBudget;
|
|
5023
|
+
const emitLine = (item) => {
|
|
5024
|
+
if (capReached())
|
|
5025
|
+
return;
|
|
5026
|
+
console.log(JSON.stringify(item));
|
|
5027
|
+
emitted++;
|
|
5028
|
+
};
|
|
5029
|
+
const emitMeta = (item) => {
|
|
5030
|
+
if (params.max_events !== undefined && emitted >= params.max_events) {
|
|
5031
|
+
safeWriteStderr(`${JSON.stringify(item)}
|
|
5032
|
+
`);
|
|
5033
|
+
return;
|
|
5034
|
+
}
|
|
5035
|
+
console.log(JSON.stringify(item));
|
|
5036
|
+
emitted++;
|
|
5037
|
+
};
|
|
5038
|
+
const pollConversations = async () => {
|
|
5039
|
+
const data = await callDataAPI(`/conversations?time_range=1h&limit=${limit}`);
|
|
5040
|
+
const record = asRecord(data) ?? {};
|
|
5041
|
+
const rows = Array.isArray(record.conversations) ? record.conversations : [];
|
|
5042
|
+
for (const row of [...rows].reverse()) {
|
|
5043
|
+
const conversation = asRecord(row);
|
|
5044
|
+
if (!conversation)
|
|
5045
|
+
continue;
|
|
5046
|
+
const id = asString(conversation.conversation_id) ?? asString(conversation.id);
|
|
5047
|
+
if (!id)
|
|
5048
|
+
continue;
|
|
5049
|
+
const timestamp = asString(conversation.last_timestamp) ?? asString(conversation.timestamp) ?? asString(conversation.last_activity) ?? "";
|
|
5050
|
+
const messageCount = asNumber(conversation.message_count);
|
|
5051
|
+
if (capReached())
|
|
5052
|
+
return;
|
|
5053
|
+
if (!rememberConversation(`c:${id}:${timestamp}:${messageCount ?? ""}`))
|
|
5054
|
+
continue;
|
|
5055
|
+
emitLine({
|
|
5056
|
+
type: "conversation",
|
|
5057
|
+
conversation_id: id,
|
|
5058
|
+
timestamp: timestamp || null,
|
|
5059
|
+
message_count: messageCount,
|
|
5060
|
+
environment: conversation.environment ?? null,
|
|
5061
|
+
cluster_name: conversation.cluster_name ?? null,
|
|
5062
|
+
summary: conversation.summary ?? null
|
|
5063
|
+
});
|
|
5064
|
+
}
|
|
5065
|
+
};
|
|
5066
|
+
const pollEmotions = async () => {
|
|
5067
|
+
const maxPages = params.full_scan === true ? EMOTIONS_FULL_SCAN_MAX_PAGES : EMOTIONS_TAIL_MAX_PAGES;
|
|
5068
|
+
const collected = [];
|
|
5069
|
+
let offset = 0;
|
|
5070
|
+
let pages = 0;
|
|
5071
|
+
let truncated = false;
|
|
5072
|
+
let total = null;
|
|
5073
|
+
for (;; ) {
|
|
5074
|
+
const data = await callDataAPI(`/emotions?days_back=1&limit=${EMOTIONS_TAIL_PAGE_SIZE}&offset=${offset}`);
|
|
5075
|
+
const record = asRecord(data) ?? {};
|
|
5076
|
+
const rows = Array.isArray(record.detections) ? record.detections : [];
|
|
5077
|
+
collected.push(...rows.map((row) => asRecord(row)).filter((row) => row !== null));
|
|
5078
|
+
pages++;
|
|
5079
|
+
const pagination = asRecord(record.pagination) ?? {};
|
|
5080
|
+
total = asNumber(pagination.total) ?? total;
|
|
5081
|
+
const hasMore = pagination.has_more === true;
|
|
5082
|
+
if (!hasMore || rows.length === 0)
|
|
5083
|
+
break;
|
|
5084
|
+
if (pages >= maxPages) {
|
|
5085
|
+
truncated = true;
|
|
5086
|
+
break;
|
|
5087
|
+
}
|
|
5088
|
+
if (offset + EMOTIONS_TAIL_PAGE_SIZE > EMOTIONS_OFFSET_CEILING) {
|
|
5089
|
+
truncated = true;
|
|
5090
|
+
break;
|
|
5091
|
+
}
|
|
5092
|
+
offset += EMOTIONS_TAIL_PAGE_SIZE;
|
|
5093
|
+
}
|
|
5094
|
+
collected.sort((a, b) => (asString(a.detected_at) ?? "").localeCompare(asString(b.detected_at) ?? ""));
|
|
5095
|
+
for (const detection of collected) {
|
|
5096
|
+
if (capReached())
|
|
5097
|
+
break;
|
|
5098
|
+
const detectionId = asString(detection.detection_id);
|
|
5099
|
+
if (!detectionId || !rememberEmotion(`e:${detectionId}`))
|
|
5100
|
+
continue;
|
|
5101
|
+
emitLine({
|
|
5102
|
+
type: "emotion",
|
|
5103
|
+
detection_id: detectionId,
|
|
5104
|
+
conversation_id: detection.conversation_id ?? null,
|
|
5105
|
+
family: detection.family ?? null,
|
|
5106
|
+
primary_cause: detection.primary_cause ?? null,
|
|
5107
|
+
detected_at: detection.detected_at ?? null
|
|
5108
|
+
});
|
|
5109
|
+
}
|
|
5110
|
+
if (truncated || total !== null && collected.length < total) {
|
|
5111
|
+
const scanned = collected.length;
|
|
5112
|
+
const coveragePct = total && total > 0 ? Math.round(scanned / total * 1000) / 10 : null;
|
|
5113
|
+
emitMeta({
|
|
5114
|
+
type: "tail_coverage",
|
|
5115
|
+
signal: "emotions",
|
|
5116
|
+
complete: false,
|
|
5117
|
+
scanned,
|
|
5118
|
+
total,
|
|
5119
|
+
coverage_pct: coveragePct,
|
|
5120
|
+
pages_scanned: pages,
|
|
5121
|
+
detail: `Scanned ${scanned}${total !== null ? ` of ${total}` : ""} detection(s) in the last day` + `${coveragePct !== null ? ` (${coveragePct}%)` : ""}. /emotions is ranked by score with no ` + "time ordering or cursor, so this follows the highest-scoring detections and " + "lower-scoring ones may not appear. Use --full-scan for completeness (many more " + "requests, up to the API offset ceiling of 10000), or `moda emotions --offset=N` to page manually."
|
|
5122
|
+
});
|
|
5123
|
+
} else if (total !== null) {
|
|
5124
|
+
emitMeta({
|
|
5125
|
+
type: "tail_coverage",
|
|
5126
|
+
signal: "emotions",
|
|
5127
|
+
complete: true,
|
|
5128
|
+
scanned: collected.length,
|
|
5129
|
+
total,
|
|
5130
|
+
coverage_pct: 100,
|
|
5131
|
+
pages_scanned: pages,
|
|
5132
|
+
detail: "Scanned the full window for this poll."
|
|
5133
|
+
});
|
|
5134
|
+
}
|
|
5135
|
+
};
|
|
5136
|
+
if (params.once !== true && context.output.mode === "human") {
|
|
5137
|
+
console.error(`Tailing ${signal} every ${intervalMs / 1000}s (Ctrl-C to stop). One JSON line per new item.`);
|
|
5138
|
+
}
|
|
5139
|
+
for (;; ) {
|
|
5140
|
+
if (signal === "conversations" || signal === "all")
|
|
5141
|
+
await pollConversations();
|
|
5142
|
+
if (signal === "emotions" || signal === "all")
|
|
5143
|
+
await pollEmotions();
|
|
5144
|
+
if (params.once === true)
|
|
5145
|
+
break;
|
|
5146
|
+
if (params.max_events !== undefined && emitted >= params.max_events)
|
|
5147
|
+
break;
|
|
5148
|
+
await new Promise((resolve3) => setTimeout(resolve3, intervalMs));
|
|
5149
|
+
}
|
|
4062
5150
|
}
|
|
4063
5151
|
async function attachWorldState(rows) {
|
|
4064
5152
|
return Promise.all(rows.map(async (row) => {
|
|
@@ -4130,6 +5218,23 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
4130
5218
|
case "clusters": {
|
|
4131
5219
|
const args = flagsToArgs(flags);
|
|
4132
5220
|
const params = ClustersSchema.parse(args);
|
|
5221
|
+
if (params.search !== undefined || params.node_id !== undefined) {
|
|
5222
|
+
const query2 = new URLSearchParams;
|
|
5223
|
+
if (params.search !== undefined)
|
|
5224
|
+
query2.set("q", params.search);
|
|
5225
|
+
if (params.node_id !== undefined)
|
|
5226
|
+
query2.set("node_id", params.node_id);
|
|
5227
|
+
if (params.mode)
|
|
5228
|
+
query2.set("mode", params.mode);
|
|
5229
|
+
if (params.limit)
|
|
5230
|
+
query2.set("limit", params.limit.toString());
|
|
5231
|
+
const data2 = await callDataAPI(`/task-clusters/search?${query2.toString()}`);
|
|
5232
|
+
const record = asRecord(data2) ?? {};
|
|
5233
|
+
const matches = Array.isArray(record.matches) ? record.matches : [];
|
|
5234
|
+
const warnings = params.node_id !== undefined && matches.length === 0 ? notFoundWarning("cluster", params.node_id, true) : [];
|
|
5235
|
+
context.output.writeData(data2, warnings.length > 0 ? { warnings } : undefined);
|
|
5236
|
+
break;
|
|
5237
|
+
}
|
|
4133
5238
|
const query = new URLSearchParams;
|
|
4134
5239
|
if (params.parent_id)
|
|
4135
5240
|
query.set("parent_id", params.parent_id);
|
|
@@ -4153,7 +5258,9 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
4153
5258
|
query.set("offset", params.offset.toString());
|
|
4154
5259
|
const queryString = query.toString() ? `?${query.toString()}` : "";
|
|
4155
5260
|
const data = await callDataAPI(`/clusters/${params.node_id}/conversations${queryString}`);
|
|
4156
|
-
|
|
5261
|
+
const clusterRecord = asRecord(data) ?? {};
|
|
5262
|
+
const warnings = notFoundWarning("cluster", params.node_id, clusterRecord.cluster === null || clusterRecord.cluster === undefined);
|
|
5263
|
+
context.output.writeData(data, warnings.length > 0 ? { warnings } : undefined);
|
|
4157
5264
|
break;
|
|
4158
5265
|
}
|
|
4159
5266
|
case "conversations": {
|
|
@@ -4211,7 +5318,16 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
4211
5318
|
query.set("include_tool_io", "true");
|
|
4212
5319
|
const data = await callDataAPI(`/search?${query.toString()}`);
|
|
4213
5320
|
recordSearchTelemetry(context, data);
|
|
4214
|
-
|
|
5321
|
+
const searchRecord = asRecord(data) ?? {};
|
|
5322
|
+
const degradeReason = asString(searchRecord.degrade_reason);
|
|
5323
|
+
const actualMode = asString(searchRecord.search_mode);
|
|
5324
|
+
const warnings = [];
|
|
5325
|
+
if (degradeReason && degradeReason !== "none") {
|
|
5326
|
+
warnings.push(`Search degraded to keyword mode (${degradeReason}): the requested ${params.mode ?? "hybrid"} ranking did not run, and keyword scores are not comparable to semantic scores. Retry, or use --mode=keyword with exact identifiers.`);
|
|
5327
|
+
} else if (params.mode && actualMode && actualMode !== params.mode && params.mode !== "hybrid") {
|
|
5328
|
+
warnings.push(`Search ran in ${actualMode} mode instead of the requested ${params.mode} mode.`);
|
|
5329
|
+
}
|
|
5330
|
+
context.output.writeData(data, warnings.length > 0 ? { warnings } : undefined);
|
|
4215
5331
|
break;
|
|
4216
5332
|
}
|
|
4217
5333
|
case "world-state": {
|
|
@@ -4220,6 +5336,37 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
4220
5336
|
}
|
|
4221
5337
|
const args = flagsToArgs(flags, "conversation_id", positional);
|
|
4222
5338
|
const params = WorldStateSchema.parse(args);
|
|
5339
|
+
if (params.snapshot === true && params.replay === true) {
|
|
5340
|
+
throw new CliInputError("Choose either --snapshot or --replay, not both.", "Usage: moda world-state <conversation_id> [--snapshot --msg-index=N [--block-index=M] [--segment-index=K]] | [--replay --message-count=N [--format=frames|fold-inputs]]");
|
|
5341
|
+
}
|
|
5342
|
+
if (params.snapshot === true) {
|
|
5343
|
+
const query2 = new URLSearchParams;
|
|
5344
|
+
query2.set("msg_index", (params.msg_index ?? 0).toString());
|
|
5345
|
+
if (params.block_index !== undefined)
|
|
5346
|
+
query2.set("block_index", params.block_index.toString());
|
|
5347
|
+
if (params.segment_index !== undefined)
|
|
5348
|
+
query2.set("segment_index", params.segment_index.toString());
|
|
5349
|
+
const data2 = await callDataAPI(`/conversations/${params.conversation_id}/world-state/snapshot?${query2.toString()}`);
|
|
5350
|
+
const snapshotRecord = asRecord(data2) ?? {};
|
|
5351
|
+
const warnings2 = notFoundWarning("world state", params.conversation_id, snapshotRecord.found === false);
|
|
5352
|
+
context.output.writeData(data2, warnings2.length > 0 ? { warnings: warnings2 } : undefined);
|
|
5353
|
+
break;
|
|
5354
|
+
}
|
|
5355
|
+
if (params.replay === true) {
|
|
5356
|
+
const format = params.format ?? "frames";
|
|
5357
|
+
if (format === "frames" && params.message_count === undefined) {
|
|
5358
|
+
throw new CliInputError("--message-count=N is required with --replay in frames mode (the API returns no frames without it). Use --format=fold-inputs for the raw keyframes/events without a count.");
|
|
5359
|
+
}
|
|
5360
|
+
const query2 = new URLSearchParams;
|
|
5361
|
+
query2.set("format", format === "fold-inputs" ? "fold_inputs" : "frames");
|
|
5362
|
+
if (params.message_count !== undefined)
|
|
5363
|
+
query2.set("message_count", params.message_count.toString());
|
|
5364
|
+
const data2 = await callDataAPI(`/conversations/${params.conversation_id}/world-state/replay?${query2.toString()}`);
|
|
5365
|
+
const replayRecord = asRecord(data2) ?? {};
|
|
5366
|
+
const warnings2 = notFoundWarning("world state", params.conversation_id, replayRecord.hasData === false);
|
|
5367
|
+
context.output.writeData(data2, warnings2.length > 0 ? { warnings: warnings2 } : undefined);
|
|
5368
|
+
break;
|
|
5369
|
+
}
|
|
4223
5370
|
const query = new URLSearchParams;
|
|
4224
5371
|
if (params.summary_only === true)
|
|
4225
5372
|
query.set("summary_only", "1");
|
|
@@ -4227,7 +5374,9 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
4227
5374
|
query.set("event_limit", params.event_limit.toString());
|
|
4228
5375
|
const queryString = query.toString() ? `?${query.toString()}` : "";
|
|
4229
5376
|
const data = await callDataAPI(`/conversations/${params.conversation_id}/world-state${queryString}`);
|
|
4230
|
-
|
|
5377
|
+
const wsRecord = asRecord(data) ?? {};
|
|
5378
|
+
const warnings = notFoundWarning("world state", params.conversation_id, wsRecord.hasData === false);
|
|
5379
|
+
context.output.writeData(data, warnings.length > 0 ? { warnings } : undefined);
|
|
4231
5380
|
break;
|
|
4232
5381
|
}
|
|
4233
5382
|
case "context": {
|
|
@@ -4243,7 +5392,9 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
4243
5392
|
query.set("window", params.window.toString());
|
|
4244
5393
|
const queryString = query.toString() ? `?${query.toString()}` : "";
|
|
4245
5394
|
const data = await callDataAPI(`/conversations/${params.conversation_id}/context${queryString}`);
|
|
4246
|
-
|
|
5395
|
+
const ctxRecord = asRecord(data) ?? {};
|
|
5396
|
+
const warnings = notFoundWarning("conversation", params.conversation_id, asNumber(ctxRecord.total_messages) === 0);
|
|
5397
|
+
context.output.writeData(data, warnings.length > 0 ? { warnings } : undefined);
|
|
4247
5398
|
break;
|
|
4248
5399
|
}
|
|
4249
5400
|
case "audit":
|
|
@@ -4305,7 +5456,13 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
4305
5456
|
query.set("offset", params.offset.toString());
|
|
4306
5457
|
const queryString = query.toString() ? `?${query.toString()}` : "";
|
|
4307
5458
|
const data = await callDataAPI(`/tool-failures/${params.tool_name}${queryString}`);
|
|
4308
|
-
|
|
5459
|
+
const tfRecord = asRecord(data) ?? {};
|
|
5460
|
+
const subtypeCount = Array.isArray(tfRecord.subtypes) ? tfRecord.subtypes.length : 0;
|
|
5461
|
+
const exampleCount = Array.isArray(tfRecord.examples) ? tfRecord.examples.length : 0;
|
|
5462
|
+
const warnings = subtypeCount === 0 && exampleCount === 0 ? [
|
|
5463
|
+
`No failures found for tool "${params.tool_name}" in the last ${params.days_back ?? 7} day(s) — the tool may have no failures in this window, or the name may not exist in this tenant. Run \`moda tool-failures\` to list tools with recorded failures, or widen --days-back.`
|
|
5464
|
+
] : [];
|
|
5465
|
+
await printWithOptionalWindows(withToolFailureAnchors(data), "examples", params.include_window === true, params.window ?? 1, context.output, warnings);
|
|
4309
5466
|
break;
|
|
4310
5467
|
}
|
|
4311
5468
|
case "problems": {
|
|
@@ -4325,6 +5482,149 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
4325
5482
|
context.output.writeData(withLink);
|
|
4326
5483
|
break;
|
|
4327
5484
|
}
|
|
5485
|
+
case "problem": {
|
|
5486
|
+
if (!positional) {
|
|
5487
|
+
throw new CliInputError("<problem_id> is required", "Usage: moda problem <problem_id> [--evidence|--reports|--conversations|--feedback] [--limit=N] [--cursor=TOKEN] [--family=F] [--door=D]");
|
|
5488
|
+
}
|
|
5489
|
+
const args = flagsToArgs(flags, "id", positional);
|
|
5490
|
+
const params = ProblemSchema.parse(args);
|
|
5491
|
+
const views = ["evidence", "reports", "conversations", "feedback"].filter((view2) => params[view2] === true);
|
|
5492
|
+
if (views.length > 1) {
|
|
5493
|
+
throw new CliInputError(`Choose at most one of --evidence, --reports, --conversations, --feedback (got ${views.map((v) => `--${v}`).join(" ")}).`, "Usage: moda problem <problem_id> [--evidence|--reports|--conversations|--feedback]");
|
|
5494
|
+
}
|
|
5495
|
+
const view = views[0];
|
|
5496
|
+
if (view === undefined) {
|
|
5497
|
+
const data2 = await callDataAPI(`/problems/${encodeURIComponent(params.id)}`);
|
|
5498
|
+
const record = asRecord(data2) ?? {};
|
|
5499
|
+
const warnings = record.found === false ? [
|
|
5500
|
+
`No problem found for id "${params.id}" — it may not exist in this tenant, or may have been merged away (the API returns found:false rather than an error).`
|
|
5501
|
+
] : [];
|
|
5502
|
+
context.output.writeData(data2, warnings.length > 0 ? { warnings } : undefined);
|
|
5503
|
+
break;
|
|
5504
|
+
}
|
|
5505
|
+
if (!UUID_RE.test(params.id)) {
|
|
5506
|
+
throw new CliInputError(`--${view} requires a canonical problem UUID; got "${params.id}". Run \`moda problems\` or \`moda problem <id>\` first to resolve the id.`);
|
|
5507
|
+
}
|
|
5508
|
+
const query = new URLSearchParams;
|
|
5509
|
+
if (params.limit)
|
|
5510
|
+
query.set("limit", params.limit.toString());
|
|
5511
|
+
if (params.cursor)
|
|
5512
|
+
query.set("cursor", params.cursor);
|
|
5513
|
+
if (view === "conversations") {
|
|
5514
|
+
if (params.family)
|
|
5515
|
+
query.set("family", params.family);
|
|
5516
|
+
if (params.door)
|
|
5517
|
+
query.set("door", params.door);
|
|
5518
|
+
}
|
|
5519
|
+
const queryString = query.toString() ? `?${query.toString()}` : "";
|
|
5520
|
+
const data = await callDataAPI(`/problems/${params.id}/${view}${queryString}`);
|
|
5521
|
+
context.output.writeData(data);
|
|
5522
|
+
break;
|
|
5523
|
+
}
|
|
5524
|
+
case "problem-feedback": {
|
|
5525
|
+
if (!positional) {
|
|
5526
|
+
throw new CliInputError("<problem_id> is required", `Usage: moda problem-feedback <problem_id> --action=${PROBLEM_FEEDBACK_ACTIONS.join("|")} [--reason=TEXT] [--attribution-id=UUID] [--new-name=NAME]`);
|
|
5527
|
+
}
|
|
5528
|
+
const params = ProblemFeedbackSchema.parse({
|
|
5529
|
+
id: positional,
|
|
5530
|
+
...flags.action !== undefined ? { action: flags.action } : {},
|
|
5531
|
+
...flags.reason !== undefined ? { reason: flags.reason } : {},
|
|
5532
|
+
...flags["attribution-id"] !== undefined ? { attribution_id: flags["attribution-id"] } : {},
|
|
5533
|
+
...flags["new-name"] !== undefined ? { new_name: flags["new-name"] } : {},
|
|
5534
|
+
...flags.actor !== undefined ? { actor: flags.actor } : {}
|
|
5535
|
+
});
|
|
5536
|
+
if (!UUID_RE.test(params.id)) {
|
|
5537
|
+
throw new CliInputError(`Problem id must be a canonical UUID; got "${params.id}".`);
|
|
5538
|
+
}
|
|
5539
|
+
const reason = params.reason?.trim() ?? "";
|
|
5540
|
+
if ((params.action === "dismiss" || params.action === "flag_attribution") && reason === "") {
|
|
5541
|
+
throw new CliInputError(`--reason is required for --action=${params.action}.`);
|
|
5542
|
+
}
|
|
5543
|
+
if (params.action === "rename" && (params.new_name?.trim() ?? "") === "") {
|
|
5544
|
+
throw new CliInputError("--new-name is required for --action=rename.");
|
|
5545
|
+
}
|
|
5546
|
+
if (params.action === "flag_attribution") {
|
|
5547
|
+
if (params.attribution_id === undefined || params.attribution_id.trim() === "") {
|
|
5548
|
+
throw new CliInputError("--attribution-id=<uuid> is required for --action=flag_attribution. Run `moda problem <problem_id> --evidence` to list attribution ids.");
|
|
5549
|
+
}
|
|
5550
|
+
if (!UUID_RE.test(params.attribution_id)) {
|
|
5551
|
+
throw new CliInputError("--attribution-id must be a UUID.");
|
|
5552
|
+
}
|
|
5553
|
+
}
|
|
5554
|
+
const data = await callDataAPI(`/problems/${params.id}/feedback`, {
|
|
5555
|
+
method: "POST",
|
|
5556
|
+
body: JSON.stringify({
|
|
5557
|
+
action: params.action,
|
|
5558
|
+
...reason !== "" ? { reason } : {},
|
|
5559
|
+
...params.attribution_id ? { attribution_id: params.attribution_id } : {},
|
|
5560
|
+
...params.new_name ? { new_display_name: params.new_name } : {},
|
|
5561
|
+
actor: params.actor ?? "moda-cli"
|
|
5562
|
+
}),
|
|
5563
|
+
headers: { "Content-Type": "application/json" }
|
|
5564
|
+
}, { retries: 0 });
|
|
5565
|
+
context.output.writeData(data);
|
|
5566
|
+
break;
|
|
5567
|
+
}
|
|
5568
|
+
case "emotions": {
|
|
5569
|
+
const args = flagsToArgs(flags);
|
|
5570
|
+
const params = EmotionsSchema.parse(args);
|
|
5571
|
+
const query = new URLSearchParams;
|
|
5572
|
+
if (params.days_back)
|
|
5573
|
+
query.set("days_back", params.days_back.toString());
|
|
5574
|
+
if (params.limit)
|
|
5575
|
+
query.set("limit", params.limit.toString());
|
|
5576
|
+
if (params.offset !== undefined)
|
|
5577
|
+
query.set("offset", params.offset.toString());
|
|
5578
|
+
if (params.family)
|
|
5579
|
+
query.set("family", params.family);
|
|
5580
|
+
const queryString = query.toString() ? `?${query.toString()}` : "";
|
|
5581
|
+
const data = await callDataAPI(`/emotions${queryString}`);
|
|
5582
|
+
context.output.writeData(data);
|
|
5583
|
+
break;
|
|
5584
|
+
}
|
|
5585
|
+
case "hallucinations": {
|
|
5586
|
+
const args = flagsToArgs(flags);
|
|
5587
|
+
if (flags["conversation-id"] !== undefined) {
|
|
5588
|
+
args.conversation_id = flags["conversation-id"];
|
|
5589
|
+
}
|
|
5590
|
+
const params = HallucinationsSchema.parse(args);
|
|
5591
|
+
const query = new URLSearchParams;
|
|
5592
|
+
if (params.days_back)
|
|
5593
|
+
query.set("days_back", params.days_back.toString());
|
|
5594
|
+
if (params.limit)
|
|
5595
|
+
query.set("limit", params.limit.toString());
|
|
5596
|
+
if (params.offset !== undefined)
|
|
5597
|
+
query.set("offset", params.offset.toString());
|
|
5598
|
+
if (params.conversation_id)
|
|
5599
|
+
query.set("conversation_id", params.conversation_id);
|
|
5600
|
+
if (params.kind)
|
|
5601
|
+
query.set("kind", params.kind);
|
|
5602
|
+
const queryString = query.toString() ? `?${query.toString()}` : "";
|
|
5603
|
+
const data = await callDataAPI(`/hallucinations${queryString}`);
|
|
5604
|
+
context.output.writeData(data);
|
|
5605
|
+
break;
|
|
5606
|
+
}
|
|
5607
|
+
case "step-scores": {
|
|
5608
|
+
if (!positional) {
|
|
5609
|
+
throw new CliInputError("<conversation_id> is required", "Usage: moda step-scores <conversation_id>");
|
|
5610
|
+
}
|
|
5611
|
+
const params = StepScoresSchema.parse({ conversation_id: positional });
|
|
5612
|
+
const data = await callDataAPI(`/conversations/${encodeURIComponent(params.conversation_id)}/step-scores`);
|
|
5613
|
+
const record = asRecord(data) ?? {};
|
|
5614
|
+
const steps = Array.isArray(record.steps) ? record.steps : [];
|
|
5615
|
+
const segments = Array.isArray(record.segments) ? record.segments : [];
|
|
5616
|
+
const warnings = steps.length === 0 && segments.length === 0 ? [
|
|
5617
|
+
`No step scores found for conversation "${params.conversation_id}" — it may not exist in this tenant, or has not been scored yet.`
|
|
5618
|
+
] : [];
|
|
5619
|
+
context.output.writeData(data, warnings.length > 0 ? { warnings } : undefined);
|
|
5620
|
+
break;
|
|
5621
|
+
}
|
|
5622
|
+
case "tail": {
|
|
5623
|
+
const args = flagsToArgs(flags);
|
|
5624
|
+
const params = TailSchema.parse(args);
|
|
5625
|
+
await runTailCommand(params, context);
|
|
5626
|
+
break;
|
|
5627
|
+
}
|
|
4328
5628
|
case "feedback": {
|
|
4329
5629
|
const note = positionals.join(" ").trim();
|
|
4330
5630
|
if (note === "") {
|
|
@@ -4463,7 +5763,7 @@ var commandRegistry = createCommandRegistry([
|
|
|
4463
5763
|
resetApiRequestCountBeforeRun: true,
|
|
4464
5764
|
telemetry: "result",
|
|
4465
5765
|
handler: async (context) => {
|
|
4466
|
-
const { runInit } = await import("./index-
|
|
5766
|
+
const { runInit } = await import("./index-fwxpt51s.js");
|
|
4467
5767
|
if (context.outputMode === "agent-stream") {
|
|
4468
5768
|
context.output.writeEvent({
|
|
4469
5769
|
event: "started",
|
|
@@ -4504,7 +5804,7 @@ var commandRegistry = createCommandRegistry([
|
|
|
4504
5804
|
resetApiRequestCountBeforeRun: true,
|
|
4505
5805
|
telemetry: "result-and-error",
|
|
4506
5806
|
handler: async (context) => {
|
|
4507
|
-
const { runProvision } = await import("./provision-
|
|
5807
|
+
const { runProvision } = await import("./provision-r0cq3g2v.js");
|
|
4508
5808
|
return runProvision(context, {
|
|
4509
5809
|
resumeCommand: buildResumeCommand("provision", context.rawFlags)
|
|
4510
5810
|
});
|
|
@@ -4775,6 +6075,65 @@ var commandRegistry = createCommandRegistry([
|
|
|
4775
6075
|
examples: ["moda problems", "moda problems --days-back=7 --limit=10"],
|
|
4776
6076
|
...dataApiDefaults
|
|
4777
6077
|
}),
|
|
6078
|
+
legacyCommand({
|
|
6079
|
+
name: "problem",
|
|
6080
|
+
description: "Open one Problem: dossier, or --evidence/--reports/--conversations/--feedback pages",
|
|
6081
|
+
examples: [
|
|
6082
|
+
"moda problem <problem_id>",
|
|
6083
|
+
"moda problem <problem_id> --evidence --limit=10",
|
|
6084
|
+
"moda problem <problem_id> --reports",
|
|
6085
|
+
"moda problem <problem_id> --conversations --family=tool_failure",
|
|
6086
|
+
"moda problem <problem_id> --feedback"
|
|
6087
|
+
],
|
|
6088
|
+
...dataApiDefaults
|
|
6089
|
+
}),
|
|
6090
|
+
legacyCommand({
|
|
6091
|
+
name: "problem-feedback",
|
|
6092
|
+
description: "Submit Problem feedback: mark fixed, dismiss, rename, or flag a bad attribution",
|
|
6093
|
+
examples: [
|
|
6094
|
+
"moda problem-feedback <problem_id> --action=mark_fixed",
|
|
6095
|
+
'moda problem-feedback <problem_id> --action=dismiss --reason="not actionable"',
|
|
6096
|
+
'moda problem-feedback <problem_id> --action=rename --new-name="Better title"',
|
|
6097
|
+
'moda problem-feedback <problem_id> --action=flag_attribution --attribution-id=<uuid> --reason="wrong conversation"'
|
|
6098
|
+
],
|
|
6099
|
+
...dataApiDefaults,
|
|
6100
|
+
mutability: "write"
|
|
6101
|
+
}),
|
|
6102
|
+
legacyCommand({
|
|
6103
|
+
name: "emotions",
|
|
6104
|
+
description: "Multi-family emotion detections (frustration, sadness, confusion, anxiety, trust, positive)",
|
|
6105
|
+
examples: [
|
|
6106
|
+
"moda emotions --days-back=7",
|
|
6107
|
+
"moda emotions --family=confusion --limit=20"
|
|
6108
|
+
],
|
|
6109
|
+
...dataApiDefaults
|
|
6110
|
+
}),
|
|
6111
|
+
legacyCommand({
|
|
6112
|
+
name: "hallucinations",
|
|
6113
|
+
description: "Grounding detections: contradicted/verified outputs with rule breakdown",
|
|
6114
|
+
examples: [
|
|
6115
|
+
"moda hallucinations --days-back=30",
|
|
6116
|
+
"moda hallucinations --kind=contradicted --limit=20",
|
|
6117
|
+
"moda hallucinations --conversation-id=<id>"
|
|
6118
|
+
],
|
|
6119
|
+
...dataApiDefaults
|
|
6120
|
+
}),
|
|
6121
|
+
legacyCommand({
|
|
6122
|
+
name: "step-scores",
|
|
6123
|
+
description: "Graph-PRM step scores for a conversation (per-segment curves, first bad step, rollup)",
|
|
6124
|
+
examples: ["moda step-scores <conversation_id>"],
|
|
6125
|
+
...dataApiDefaults
|
|
6126
|
+
}),
|
|
6127
|
+
legacyCommand({
|
|
6128
|
+
name: "tail",
|
|
6129
|
+
description: "Live-tail new conversations and detections as NDJSON (one JSON line per item)",
|
|
6130
|
+
examples: [
|
|
6131
|
+
"moda tail",
|
|
6132
|
+
"moda tail --signal=emotions --interval=30",
|
|
6133
|
+
"moda tail --once"
|
|
6134
|
+
],
|
|
6135
|
+
...dataApiDefaults
|
|
6136
|
+
}),
|
|
4778
6137
|
legacyCommand({
|
|
4779
6138
|
name: "feedback",
|
|
4780
6139
|
description: "Flag wrong/missing data or CLI quirks to the Moda team",
|
|
@@ -4809,6 +6168,41 @@ var commandRegistry = createCommandRegistry([
|
|
|
4809
6168
|
handler: async (context) => ({
|
|
4810
6169
|
exitCode: await runSkillsCommand(context)
|
|
4811
6170
|
})
|
|
6171
|
+
},
|
|
6172
|
+
{
|
|
6173
|
+
name: "fixes",
|
|
6174
|
+
description: 'List the ranked queue of Fixes ("a fix with proof") for this tenant',
|
|
6175
|
+
examples: ["moda fixes", "moda fixes --status=VERIFIED", "moda fixes --limit=10"],
|
|
6176
|
+
auth: "none",
|
|
6177
|
+
outputModes: ["human", "json", "agent"],
|
|
6178
|
+
mutability: "read",
|
|
6179
|
+
validateConfigBeforeRun: false,
|
|
6180
|
+
resetApiRequestCountBeforeRun: false,
|
|
6181
|
+
telemetry: "result",
|
|
6182
|
+
handler: async (context) => ({
|
|
6183
|
+
exitCode: await runFixesCommand(context)
|
|
6184
|
+
})
|
|
6185
|
+
},
|
|
6186
|
+
{
|
|
6187
|
+
name: "fix",
|
|
6188
|
+
description: 'Draft, verify, check out, submit, and dismiss Fixes ("a fix with proof")',
|
|
6189
|
+
examples: [
|
|
6190
|
+
"moda fix start <problem_id> --wait",
|
|
6191
|
+
"moda fix <fix_id> --packet",
|
|
6192
|
+
"moda fix verify <fix_id> --prompt-file=prompts/agent.prompt.md",
|
|
6193
|
+
"moda fix checkout <fix_id>",
|
|
6194
|
+
"moda fix submit <fix_id> --pr",
|
|
6195
|
+
'moda fix dismiss <fix_id> --reason="not actionable"'
|
|
6196
|
+
],
|
|
6197
|
+
auth: "none",
|
|
6198
|
+
outputModes: ["human", "json", "agent"],
|
|
6199
|
+
mutability: "write",
|
|
6200
|
+
validateConfigBeforeRun: false,
|
|
6201
|
+
resetApiRequestCountBeforeRun: false,
|
|
6202
|
+
telemetry: "result",
|
|
6203
|
+
handler: async (context) => ({
|
|
6204
|
+
exitCode: await runFixCommand(context)
|
|
6205
|
+
})
|
|
4812
6206
|
}
|
|
4813
6207
|
]);
|
|
4814
6208
|
function classifyCliError(error) {
|
|
@@ -4888,7 +6282,12 @@ async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
|
|
|
4888
6282
|
return 0;
|
|
4889
6283
|
}
|
|
4890
6284
|
if (command === "--help" || command === "-h" || flags.help === "true") {
|
|
4891
|
-
|
|
6285
|
+
const helpTarget = command && command !== "--help" && command !== "-h" ? commandRegistry.find(command) : undefined;
|
|
6286
|
+
if (helpTarget) {
|
|
6287
|
+
printCommandHelp(helpTarget);
|
|
6288
|
+
} else {
|
|
6289
|
+
printHelp();
|
|
6290
|
+
}
|
|
4892
6291
|
return 0;
|
|
4893
6292
|
}
|
|
4894
6293
|
if (command === "--json-schemas" || flags["json-schemas"] === "true") {
|
|
@@ -5051,7 +6450,7 @@ function installSignalHandlers() {
|
|
|
5051
6450
|
async function main() {
|
|
5052
6451
|
process.exitCode = await dispatchParsedCommand(parseArgs(process.argv));
|
|
5053
6452
|
}
|
|
5054
|
-
var isMain = process.argv[1] && fileURLToPath(import.meta.url) ===
|
|
6453
|
+
var isMain = process.argv[1] && fileURLToPath(import.meta.url) === realpathSync2(process.argv[1]);
|
|
5055
6454
|
if (isMain) {
|
|
5056
6455
|
installSignalHandlers();
|
|
5057
6456
|
main().catch((error) => {
|
|
@@ -5069,5 +6468,6 @@ export {
|
|
|
5069
6468
|
commandRegistry,
|
|
5070
6469
|
classifyCliError,
|
|
5071
6470
|
buildResumeCommand,
|
|
6471
|
+
TAIL_LIMITS,
|
|
5072
6472
|
CliInputError
|
|
5073
6473
|
};
|