@moda-ai/cli 1.28.1 → 1.28.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +356 -25
- package/package.json +1 -1
- package/skills/integration/index.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -385,9 +385,16 @@ var STEP_DELAY_MS = 2000;
|
|
|
385
385
|
var MAX_WAIT_TIMEOUT_MS = 24 * 60 * 60 * 1000;
|
|
386
386
|
var MAX_POLL_INTERVAL_MS = 120000;
|
|
387
387
|
var PER_CASE_FINDINGS_CAP = 50;
|
|
388
|
+
var DRIVE_PASS_INTERVAL_MS = 20000;
|
|
389
|
+
var DRIVE_LIST_LIMIT = 100;
|
|
390
|
+
var DRIVE_LIST_MAX_PAGES = 10;
|
|
391
|
+
var DRAFT_BATCH_LIMIT_MAX = 25;
|
|
392
|
+
var DRAFT_BATCH_TIMEOUT_MS = 300000;
|
|
388
393
|
var ADVANCING_STATUSES = new Set(["DRAFT", "SCOPING", "PROPOSING", "PROPOSED", "GATING"]);
|
|
389
394
|
var FIX_USAGE = [
|
|
390
395
|
"Usage: moda fixes [--status=S] [--limit=N] [--cursor=TOKEN]",
|
|
396
|
+
" moda fixes draft-batch [--limit=N] (N is 1-25, default 10)",
|
|
397
|
+
" moda fixes drive [--status=S] [--timeout=ms] [--pass-interval=ms]",
|
|
391
398
|
" moda fix <fix_id> [--packet] [--wait]",
|
|
392
399
|
" moda fix start <problem_id> [--type=auto|prompt] [--wait] [--timeout=ms]",
|
|
393
400
|
" moda fix verify <fix_id> [--prompt-file=PATH] [--no-wait] [--timeout=ms]",
|
|
@@ -401,6 +408,16 @@ async function runFixesCommand(context) {
|
|
|
401
408
|
validateConfig();
|
|
402
409
|
const profileOptions = profileOptionsFor(context);
|
|
403
410
|
const tenantId = requireTenantId(context.flags, profileOptions);
|
|
411
|
+
switch (context.positional) {
|
|
412
|
+
case "draft-batch":
|
|
413
|
+
return runFixesDraftBatch(context, profileOptions, tenantId);
|
|
414
|
+
case "drive":
|
|
415
|
+
return runFixesDrive(context, profileOptions, tenantId);
|
|
416
|
+
case undefined:
|
|
417
|
+
break;
|
|
418
|
+
default:
|
|
419
|
+
throw new CliInputError(`Unknown fixes subcommand '${context.positional}'`, FIX_USAGE);
|
|
420
|
+
}
|
|
404
421
|
const query = new URLSearchParams;
|
|
405
422
|
if (context.flags.status)
|
|
406
423
|
query.set("status", context.flags.status);
|
|
@@ -440,6 +457,251 @@ async function runFixesCommand(context) {
|
|
|
440
457
|
});
|
|
441
458
|
return EXIT_OK;
|
|
442
459
|
}
|
|
460
|
+
async function runFixesDraftBatch(context, profileOptions, tenantId) {
|
|
461
|
+
const usage = "Usage: moda fixes draft-batch [--limit=N] (N is 1-25, default 10)";
|
|
462
|
+
let limit;
|
|
463
|
+
if (context.flags.limit) {
|
|
464
|
+
const parsed = Number(context.flags.limit);
|
|
465
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > DRAFT_BATCH_LIMIT_MAX) {
|
|
466
|
+
throw new CliInputError(`--limit must be an integer between 1 and ${DRAFT_BATCH_LIMIT_MAX} (got ${context.flags.limit}) — batches cap at ${DRAFT_BATCH_LIMIT_MAX} (LLM spend guard)`, usage);
|
|
467
|
+
}
|
|
468
|
+
limit = parsed;
|
|
469
|
+
}
|
|
470
|
+
const result = await callControlAPI(`${fixesBase(tenantId)}/draft-batch`, { method: "POST", body: JSON.stringify(limit !== undefined ? { limit } : {}) }, profileOptions, { timeoutMs: DRAFT_BATCH_TIMEOUT_MS, retries: 0 });
|
|
471
|
+
const drafted = result.drafted ?? [];
|
|
472
|
+
const skipped = result.skipped ?? [];
|
|
473
|
+
if (context.outputMode === "human") {
|
|
474
|
+
const w = (line) => process.stderr.write(`${line}
|
|
475
|
+
`);
|
|
476
|
+
w(`Drafted ${drafted.length} fix(es) (batch cap ${DRAFT_BATCH_LIMIT_MAX} — LLM spend guard):`);
|
|
477
|
+
for (const fix of drafted) {
|
|
478
|
+
w(` MODA-FIX-${fix.shortRef} ${fix.status} problem ${fix.problemId}`);
|
|
479
|
+
}
|
|
480
|
+
if (skipped.length > 0) {
|
|
481
|
+
w(`Skipped ${skipped.length} problem(s):`);
|
|
482
|
+
for (const skip of skipped) {
|
|
483
|
+
w(` ${skip.problemId}: ${skip.reason}`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
if (drafted.length > 0) {
|
|
487
|
+
w("Next: moda fixes drive");
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
context.output.writeData({
|
|
491
|
+
drafted: drafted.map((fix) => ({
|
|
492
|
+
fixId: fix.id,
|
|
493
|
+
shortRef: fix.shortRef,
|
|
494
|
+
problemId: fix.problemId,
|
|
495
|
+
status: fix.status,
|
|
496
|
+
problemRank: fix.problemRank ?? null
|
|
497
|
+
})),
|
|
498
|
+
skipped
|
|
499
|
+
}, {
|
|
500
|
+
summary: {
|
|
501
|
+
text: `Drafted ${drafted.length} fix(es) for the top-ranked problems; skipped ${skipped.length} (reasons in data.skipped). Batches cap at ${DRAFT_BATCH_LIMIT_MAX} — an LLM spend guard; run another batch after this one lands.`,
|
|
502
|
+
confidence: "high"
|
|
503
|
+
},
|
|
504
|
+
nextCommands: drafted.length > 0 ? [
|
|
505
|
+
{
|
|
506
|
+
command: "moda fixes drive",
|
|
507
|
+
purpose: "Round-robin advance every drafted fix (scope → propose → gate) until all rest.",
|
|
508
|
+
mutability: "write",
|
|
509
|
+
requires_approval: false
|
|
510
|
+
}
|
|
511
|
+
] : []
|
|
512
|
+
});
|
|
513
|
+
return EXIT_OK;
|
|
514
|
+
}
|
|
515
|
+
async function runFixesDrive(context, profileOptions, tenantId) {
|
|
516
|
+
const timeoutMs = parsePositiveInt(context.flags.timeout, DEFAULT_WAIT_TIMEOUT_MS, MAX_WAIT_TIMEOUT_MS);
|
|
517
|
+
const passIntervalMs = parsePositiveInt(context.flags["pass-interval"], DRIVE_PASS_INTERVAL_MS, MAX_POLL_INTERVAL_MS);
|
|
518
|
+
const deadline = Date.now() + timeoutMs;
|
|
519
|
+
const statusFilter = (context.flags.status ?? "").trim().toUpperCase();
|
|
520
|
+
if (statusFilter && !ADVANCING_STATUSES.has(statusFilter)) {
|
|
521
|
+
throw new CliInputError(`--status must be one of the advancing statuses (${[...ADVANCING_STATUSES].join(", ")}) — a resting fix has nothing to drive (got ${context.flags.status})`, FIX_USAGE);
|
|
522
|
+
}
|
|
523
|
+
const statusParam = statusFilter ? `&status=${encodeURIComponent(statusFilter)}` : "";
|
|
524
|
+
const snapshot = [];
|
|
525
|
+
const snapshotWarnings = [];
|
|
526
|
+
let coverageTruncated = false;
|
|
527
|
+
const seenIds = new Set;
|
|
528
|
+
let reorderDetected = false;
|
|
529
|
+
let multiPage = false;
|
|
530
|
+
let scanTruncated = false;
|
|
531
|
+
let cursor = null;
|
|
532
|
+
for (let page = 0;page < DRIVE_LIST_MAX_PAGES; page++) {
|
|
533
|
+
const data = await callControlAPI(`${fixesBase(tenantId)}?limit=${DRIVE_LIST_LIMIT}${statusParam}` + (cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""), { method: "GET" }, profileOptions);
|
|
534
|
+
for (const fix of data.fixes ?? []) {
|
|
535
|
+
if (seenIds.has(fix.id)) {
|
|
536
|
+
if (!reorderDetected) {
|
|
537
|
+
reorderDetected = true;
|
|
538
|
+
coverageTruncated = true;
|
|
539
|
+
snapshotWarnings.push("The fix queue was re-ranked while paging (a page repeated a fix), so rows may have shifted past the page boundary and been skipped — coverage is not guaranteed. Re-run `moda fixes drive` to pick up anything missed.");
|
|
540
|
+
}
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
seenIds.add(fix.id);
|
|
544
|
+
snapshot.push(fix);
|
|
545
|
+
}
|
|
546
|
+
if (data.pagination?.scan_truncated) {
|
|
547
|
+
coverageTruncated = true;
|
|
548
|
+
scanTruncated = true;
|
|
549
|
+
snapshotWarnings.push(statusFilter ? `Status ${statusFilter} alone exceeds the backend list scan window — driving the newest ${snapshot.length}; older ${statusFilter} fixes are not reachable through drive, advance them directly with \`moda fix <id> --wait\`.` : "Fix queue exceeds the backend list scan window — driving the newest fixes only. Re-running unfiltered rescans the same rows; drive each advancing status separately (`moda fixes drive --status SCOPING`, `--status PROPOSED`, …) — a status filter narrows the server-side scan window and reaches older fixes.");
|
|
550
|
+
}
|
|
551
|
+
cursor = data.pagination?.has_more ? data.pagination?.next_cursor ?? null : null;
|
|
552
|
+
if (!cursor)
|
|
553
|
+
break;
|
|
554
|
+
if (!multiPage) {
|
|
555
|
+
multiPage = true;
|
|
556
|
+
coverageTruncated = true;
|
|
557
|
+
snapshotWarnings.push("The queue needed more than one page, and the backend re-ranks it per request — rows can shift across a page boundary and be skipped without any visible duplicate, so this drive cannot prove it covered every advancing fix. Re-run `moda fixes drive` until it exits 0 (single page = provably complete).");
|
|
558
|
+
}
|
|
559
|
+
if (page === DRIVE_LIST_MAX_PAGES - 1) {
|
|
560
|
+
coverageTruncated = true;
|
|
561
|
+
scanTruncated = true;
|
|
562
|
+
snapshotWarnings.push(`Fix queue exceeds ${DRIVE_LIST_LIMIT * DRIVE_LIST_MAX_PAGES} rows — driving the first ${snapshot.length}; narrow with \`moda fixes drive --status <ADVANCING_STATUS>\` to reach the rest (a plain re-run pages the same rank order).`);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
const tracked = new Map;
|
|
566
|
+
for (const fix of snapshot) {
|
|
567
|
+
if (ADVANCING_STATUSES.has(fix.status) && !isRestingProposed(fix)) {
|
|
568
|
+
tracked.set(fix.id, fix);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
if (tracked.size === 0) {
|
|
572
|
+
context.output.writeData({
|
|
573
|
+
fixes: [],
|
|
574
|
+
passes: 0,
|
|
575
|
+
rested: 0,
|
|
576
|
+
stragglers: [],
|
|
577
|
+
timedOut: false,
|
|
578
|
+
coverageTruncated
|
|
579
|
+
}, {
|
|
580
|
+
...coverageTruncated ? { status: "degraded" } : {},
|
|
581
|
+
summary: {
|
|
582
|
+
text: coverageTruncated ? "Nothing advancing inside the scanned window, but the queue scan was truncated — fixes outside it may still be advancing (see warnings)." : "No fixes in an advancing status — nothing to drive.",
|
|
583
|
+
confidence: coverageTruncated ? "medium" : "high"
|
|
584
|
+
},
|
|
585
|
+
...snapshotWarnings.length > 0 ? { warnings: snapshotWarnings } : {},
|
|
586
|
+
nextCommands: [
|
|
587
|
+
{
|
|
588
|
+
command: "moda fixes draft-batch",
|
|
589
|
+
purpose: "Batch-draft fixes for the top-ranked problems (cap 25 — LLM spend guard).",
|
|
590
|
+
mutability: "write",
|
|
591
|
+
requires_approval: true
|
|
592
|
+
}
|
|
593
|
+
]
|
|
594
|
+
});
|
|
595
|
+
return coverageTruncated ? EXIT_DEGRADED : EXIT_OK;
|
|
596
|
+
}
|
|
597
|
+
const isRested = (fix) => !ADVANCING_STATUSES.has(fix.status) || isRestingProposed(fix);
|
|
598
|
+
const activeIds = () => [...tracked.values()].filter((fix) => !isRested(fix)).map((fix) => fix.id);
|
|
599
|
+
const lastAnnounced = new Map;
|
|
600
|
+
const announce = (row) => {
|
|
601
|
+
if (lastAnnounced.get(row.id) === row.status)
|
|
602
|
+
return;
|
|
603
|
+
lastAnnounced.set(row.id, row.status);
|
|
604
|
+
announceFixTransition(context, row);
|
|
605
|
+
};
|
|
606
|
+
for (const fix of tracked.values())
|
|
607
|
+
announce(fix);
|
|
608
|
+
const errors = new Map;
|
|
609
|
+
let errorCount = 0;
|
|
610
|
+
let passes = 0;
|
|
611
|
+
let timedOut = false;
|
|
612
|
+
while (activeIds().length > 0) {
|
|
613
|
+
if (Date.now() >= deadline) {
|
|
614
|
+
timedOut = true;
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
passes += 1;
|
|
618
|
+
for (const fixId of activeIds()) {
|
|
619
|
+
if (Date.now() >= deadline) {
|
|
620
|
+
timedOut = true;
|
|
621
|
+
break;
|
|
622
|
+
}
|
|
623
|
+
try {
|
|
624
|
+
const next = await advanceFix(tenantId, fixId, profileOptions);
|
|
625
|
+
tracked.set(fixId, next);
|
|
626
|
+
errors.delete(fixId);
|
|
627
|
+
announce(next);
|
|
628
|
+
} catch (err) {
|
|
629
|
+
errorCount += 1;
|
|
630
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
631
|
+
errors.set(fixId, message);
|
|
632
|
+
if (context.outputMode === "human") {
|
|
633
|
+
const known = tracked.get(fixId);
|
|
634
|
+
process.stderr.write(`fix MODA-FIX-${known?.shortRef ?? fixId}: advance failed — ${message}
|
|
635
|
+
`);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
if (timedOut || activeIds().length === 0)
|
|
640
|
+
break;
|
|
641
|
+
await sleep(passIntervalMs);
|
|
642
|
+
}
|
|
643
|
+
const landings = [...tracked.values()].map((fix) => ({
|
|
644
|
+
shortRef: fix.shortRef,
|
|
645
|
+
status: fix.status,
|
|
646
|
+
fixType: fix.fixType,
|
|
647
|
+
statusReason: fix.statusReason ?? null
|
|
648
|
+
}));
|
|
649
|
+
const stragglers = [...tracked.values()].filter((fix) => !isRested(fix));
|
|
650
|
+
const warnings = [
|
|
651
|
+
...snapshotWarnings,
|
|
652
|
+
...[...errors.entries()].map(([fixId, message]) => {
|
|
653
|
+
const fix = tracked.get(fixId);
|
|
654
|
+
return `advance failed for MODA-FIX-${fix?.shortRef ?? fixId}: ${message}`;
|
|
655
|
+
})
|
|
656
|
+
];
|
|
657
|
+
if (errorCount > errors.size) {
|
|
658
|
+
warnings.push(`${errorCount} advance error(s) total across ${passes} pass(es) — each was retried on the next pass.`);
|
|
659
|
+
}
|
|
660
|
+
const degraded = timedOut || coverageTruncated;
|
|
661
|
+
const restedCount = tracked.size - stragglers.length;
|
|
662
|
+
const coverageNote = coverageTruncated ? " Coverage was incomplete: the queue scan was truncated, so fixes outside the scanned window were never driven (see warnings)." : "";
|
|
663
|
+
if (context.outputMode === "human") {
|
|
664
|
+
process.stderr.write(timedOut ? `
|
|
665
|
+
Timed out after ${timeoutMs}ms with ${stragglers.length} of ${tracked.size} fix(es) still advancing.${coverageNote}
|
|
666
|
+
` : `
|
|
667
|
+
${coverageTruncated ? `Rested all ${tracked.size} scanned fix(es)` : `All ${tracked.size} fix(es) rested`} after ${passes} pass(es).${coverageNote}
|
|
668
|
+
`);
|
|
669
|
+
}
|
|
670
|
+
context.output.writeData({
|
|
671
|
+
fixes: landings,
|
|
672
|
+
passes,
|
|
673
|
+
rested: restedCount,
|
|
674
|
+
stragglers: stragglers.map((fix) => fix.shortRef),
|
|
675
|
+
timedOut,
|
|
676
|
+
coverageTruncated
|
|
677
|
+
}, {
|
|
678
|
+
...degraded ? { status: "degraded" } : {},
|
|
679
|
+
summary: {
|
|
680
|
+
text: timedOut ? `Drive timed out after ${timeoutMs}ms: ${stragglers.length} of ${tracked.size} fix(es) still advancing (${stragglers.map((fix) => `MODA-FIX-${fix.shortRef}`).join(", ")}). Re-run \`moda fixes drive\` to resume.${coverageNote}` : coverageTruncated ? `Rested all ${tracked.size} scanned fix(es) after ${passes} pass(es), but the queue was NOT swept.${coverageNote}` : `All ${tracked.size} fix(es) rested after ${passes} pass(es).`,
|
|
681
|
+
confidence: "high"
|
|
682
|
+
},
|
|
683
|
+
...warnings.length > 0 ? { warnings } : {},
|
|
684
|
+
nextCommands: [
|
|
685
|
+
timedOut ? {
|
|
686
|
+
command: "moda fixes drive",
|
|
687
|
+
purpose: "Resume driving the stragglers — advance-on-poll picks up where it left off.",
|
|
688
|
+
mutability: "write",
|
|
689
|
+
requires_approval: false
|
|
690
|
+
} : coverageTruncated ? {
|
|
691
|
+
command: !scanTruncated ? "moda fixes drive" : statusFilter ? "moda fix <fix_id> --wait" : "moda fixes drive --status=SCOPING",
|
|
692
|
+
purpose: !scanTruncated ? "The queue was re-ranked mid-page — re-run to pick up any fix that shifted past the boundary." : statusFilter ? `Status ${statusFilter} overflows the scan window — advance the older fixes individually.` : "Drive each advancing status separately to reach fixes outside the unfiltered scan window.",
|
|
693
|
+
mutability: "write",
|
|
694
|
+
requires_approval: false
|
|
695
|
+
} : {
|
|
696
|
+
command: "moda fixes",
|
|
697
|
+
purpose: "Review where every fix landed (VERIFIED fixes are ready to deliver).",
|
|
698
|
+
mutability: "read",
|
|
699
|
+
requires_approval: false
|
|
700
|
+
}
|
|
701
|
+
]
|
|
702
|
+
});
|
|
703
|
+
return degraded ? EXIT_DEGRADED : EXIT_OK;
|
|
704
|
+
}
|
|
443
705
|
async function runFixCommand(context) {
|
|
444
706
|
validateConfig();
|
|
445
707
|
const profileOptions = profileOptionsFor(context);
|
|
@@ -514,14 +776,15 @@ async function runFixStart(context, profileOptions, tenantId) {
|
|
|
514
776
|
context
|
|
515
777
|
});
|
|
516
778
|
const packet = await fetchPacketFailSoft(tenantId, created.id, profileOptions);
|
|
779
|
+
const fidelityCopy = fixVerifiedFidelityCopy(fix);
|
|
517
780
|
if (context.outputMode === "human") {
|
|
518
781
|
process.stderr.write(`
|
|
519
|
-
Fix MODA-FIX-${fix.shortRef} rested at ${fix.status}${fix.statusReason ? ` — ${fix.statusReason}` : ""}
|
|
782
|
+
Fix MODA-FIX-${fix.shortRef} rested at ${fix.status}${fidelityCopy ? ` — ${fidelityCopy}` : ""}${fix.statusReason ? ` — ${fix.statusReason}` : ""}
|
|
520
783
|
`);
|
|
521
784
|
}
|
|
522
785
|
context.output.writeData({ fix, ...packet ? { packet } : {} }, {
|
|
523
786
|
summary: {
|
|
524
|
-
text: `Fix MODA-FIX-${fix.shortRef} reached ${fix.status}${fix.statusReason ? ` (${fix.statusReason})` : ""}
|
|
787
|
+
text: `Fix MODA-FIX-${fix.shortRef} reached ${fix.status}${fix.statusReason ? ` (${fix.statusReason})` : ""}.${fidelityCopy ? ` ${fidelityCopy}.` : ""}`,
|
|
525
788
|
confidence: "high"
|
|
526
789
|
},
|
|
527
790
|
findings: gateFindings(fix),
|
|
@@ -624,6 +887,7 @@ async function runFixVerify(context, profileOptions, tenantId) {
|
|
|
624
887
|
severity: baseline === "pass" && candidate === "fail" ? "error" : candidate === "fail" ? "warning" : "info"
|
|
625
888
|
});
|
|
626
889
|
}
|
|
890
|
+
const fidelityCopy = verifiedFidelityCopy(gateResult);
|
|
627
891
|
context.output.writeData({
|
|
628
892
|
fixId: fix.id,
|
|
629
893
|
shortRef: fix.shortRef,
|
|
@@ -631,11 +895,13 @@ async function runFixVerify(context, profileOptions, tenantId) {
|
|
|
631
895
|
gateRunId: enqueue.gateRunId,
|
|
632
896
|
verdict: verdict || null,
|
|
633
897
|
gateResult,
|
|
898
|
+
...gateResult?.baselineFidelity ? { baselineFidelity: gateResult.baselineFidelity } : {},
|
|
899
|
+
...gateResult?.counterfactual ? { counterfactual: gateResult.counterfactual } : {},
|
|
634
900
|
...cases.length > 0 ? { cases } : {}
|
|
635
901
|
}, {
|
|
636
902
|
status: envelopeStatus,
|
|
637
903
|
summary: {
|
|
638
|
-
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"}`,
|
|
904
|
+
text: verdict === "pass" ? `${fidelityCopy ? `${fidelityCopy}. ` : ""}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"}`,
|
|
639
905
|
confidence: "high"
|
|
640
906
|
},
|
|
641
907
|
findings,
|
|
@@ -845,13 +1111,19 @@ async function runFixDetail(context, profileOptions, tenantId, fixId) {
|
|
|
845
1111
|
context
|
|
846
1112
|
});
|
|
847
1113
|
}
|
|
1114
|
+
const fidelityCopy = fixVerifiedFidelityCopy(fix);
|
|
1115
|
+
const detailCf = counterfactualBlock(fix.gateResult ?? null);
|
|
848
1116
|
if (context.outputMode === "human") {
|
|
849
|
-
process.stderr.write(`MODA-FIX-${fix.shortRef} ${fix.status} ${fix.fixType}${fix.statusReason ? ` — ${fix.statusReason}` : ""}
|
|
1117
|
+
process.stderr.write(`MODA-FIX-${fix.shortRef} ${fix.status} ${fix.fixType}${fidelityCopy ? ` — ${fidelityCopy}` : ""}${fix.statusReason ? ` — ${fix.statusReason}` : ""}
|
|
850
1118
|
`);
|
|
1119
|
+
if (detailCf) {
|
|
1120
|
+
process.stderr.write(`${counterfactualHumanLine(detailCf)}
|
|
1121
|
+
`);
|
|
1122
|
+
}
|
|
851
1123
|
}
|
|
852
1124
|
context.output.writeData(fix, {
|
|
853
1125
|
summary: {
|
|
854
|
-
text: `Fix MODA-FIX-${fix.shortRef} is ${fix.status}${fix.statusReason ? ` (${fix.statusReason})` : ""}
|
|
1126
|
+
text: `Fix MODA-FIX-${fix.shortRef} is ${fix.status}${fix.statusReason ? ` (${fix.statusReason})` : ""}.${fidelityCopy ? ` ${fidelityCopy}.` : ""}`,
|
|
855
1127
|
confidence: "high"
|
|
856
1128
|
},
|
|
857
1129
|
findings: gateFindings(fix),
|
|
@@ -867,18 +1139,7 @@ async function driveAdvance(args) {
|
|
|
867
1139
|
if (row.status === lastStatus)
|
|
868
1140
|
return;
|
|
869
1141
|
lastStatus = row.status;
|
|
870
|
-
|
|
871
|
-
if (args.context.outputMode === "human") {
|
|
872
|
-
process.stderr.write(`${line}
|
|
873
|
-
`);
|
|
874
|
-
} else if (args.context.outputMode === "agent-stream") {
|
|
875
|
-
args.context.output.writeEvent({
|
|
876
|
-
event: "progress",
|
|
877
|
-
phase: "fix",
|
|
878
|
-
message: line,
|
|
879
|
-
elapsed_ms: Date.now() - args.context.startedAt
|
|
880
|
-
});
|
|
881
|
-
}
|
|
1142
|
+
announceFixTransition(args.context, row);
|
|
882
1143
|
};
|
|
883
1144
|
announce(fix);
|
|
884
1145
|
while (ADVANCING_STATUSES.has(fix.status)) {
|
|
@@ -889,7 +1150,7 @@ async function driveAdvance(args) {
|
|
|
889
1150
|
announce(fix);
|
|
890
1151
|
if (!ADVANCING_STATUSES.has(fix.status))
|
|
891
1152
|
break;
|
|
892
|
-
if (
|
|
1153
|
+
if (isRestingProposed(fix)) {
|
|
893
1154
|
break;
|
|
894
1155
|
}
|
|
895
1156
|
await sleep(fix.status === "GATING" ? args.pollIntervalMs : Math.min(STEP_DELAY_MS, args.pollIntervalMs));
|
|
@@ -922,6 +1183,23 @@ async function fetchPacketFailSoft(tenantId, fixId, profileOptions) {
|
|
|
922
1183
|
return null;
|
|
923
1184
|
}
|
|
924
1185
|
}
|
|
1186
|
+
function isRestingProposed(fix) {
|
|
1187
|
+
return fix.status === "PROPOSED" && /gate (pending|unavailable)/i.test(fix.statusReason ?? "");
|
|
1188
|
+
}
|
|
1189
|
+
function announceFixTransition(context, row) {
|
|
1190
|
+
const line = `fix MODA-FIX-${row.shortRef}: ${row.status}${row.statusReason ? ` — ${row.statusReason}` : ""}`;
|
|
1191
|
+
if (context.outputMode === "human") {
|
|
1192
|
+
process.stderr.write(`${line}
|
|
1193
|
+
`);
|
|
1194
|
+
} else if (context.outputMode === "agent-stream") {
|
|
1195
|
+
context.output.writeEvent({
|
|
1196
|
+
event: "progress",
|
|
1197
|
+
phase: "fix",
|
|
1198
|
+
message: line,
|
|
1199
|
+
elapsed_ms: Date.now() - context.startedAt
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
925
1203
|
function isArtifactFix(fix) {
|
|
926
1204
|
return fix.fixType === "TOOL_SCHEMA" || fix.fixType === "SKILL";
|
|
927
1205
|
}
|
|
@@ -1058,18 +1336,51 @@ function materializePacketCandidates(packet, fixId, human) {
|
|
|
1058
1336
|
summarySuffix: summaryParts.length > 0 ? ` ${summaryParts.join(" ")}` : ""
|
|
1059
1337
|
};
|
|
1060
1338
|
}
|
|
1339
|
+
function baselineFidelityDescriptor(gate) {
|
|
1340
|
+
const fidelity = gate?.baselineFidelity;
|
|
1341
|
+
const base = fidelity?.prompt === "tenant" ? "tenant prompt baseline" : fidelity?.prompt === "generic" ? "standard-agent baseline" : null;
|
|
1342
|
+
if (fidelity?.skills === "observed") {
|
|
1343
|
+
return `${base ?? "baseline"} · skill recovered from production receipts`;
|
|
1344
|
+
}
|
|
1345
|
+
return base;
|
|
1346
|
+
}
|
|
1347
|
+
function verifiedFidelityCopy(gate) {
|
|
1348
|
+
const descriptor = baselineFidelityDescriptor(gate);
|
|
1349
|
+
return descriptor ? `Verified — ${descriptor}` : null;
|
|
1350
|
+
}
|
|
1351
|
+
function counterfactualBlock(gate) {
|
|
1352
|
+
if (!gate || gate.pending === true)
|
|
1353
|
+
return null;
|
|
1354
|
+
const cf = gate.counterfactual;
|
|
1355
|
+
if (!cf || typeof cf !== "object" || typeof cf.verdict !== "string" || !cf.verdict)
|
|
1356
|
+
return null;
|
|
1357
|
+
return cf;
|
|
1358
|
+
}
|
|
1359
|
+
function counterfactualSentence(cf) {
|
|
1360
|
+
return `prevented ${cf.wins ?? 0} of ${cf.informative ?? 0} replayed production failures (control still failed in ${cf.informative ?? 0}; regressions ${cf.losses ?? 0})`;
|
|
1361
|
+
}
|
|
1362
|
+
function counterfactualHumanLine(cf) {
|
|
1363
|
+
return `Counterfactual: ${cf.verdict} — ${counterfactualSentence(cf)}; anchors ${cf.anchors ?? 0}, abstained ${cf.abstained ?? 0}`;
|
|
1364
|
+
}
|
|
1365
|
+
function fixVerifiedFidelityCopy(fix) {
|
|
1366
|
+
if (fix.status !== "VERIFIED")
|
|
1367
|
+
return null;
|
|
1368
|
+
return verifiedFidelityCopy(fix.gateResult ?? null);
|
|
1369
|
+
}
|
|
1061
1370
|
function gateFindings(fix) {
|
|
1062
1371
|
const gateResult = fix.gateResult ?? null;
|
|
1063
|
-
if (!gateResult)
|
|
1372
|
+
if (!gateResult || gateResult.pending === true)
|
|
1064
1373
|
return [];
|
|
1065
1374
|
const verdict = gateResult.verdict ?? fix.status;
|
|
1375
|
+
const fidelity = baselineFidelityDescriptor(gateResult);
|
|
1376
|
+
const cf = counterfactualBlock(gateResult);
|
|
1066
1377
|
return [
|
|
1067
1378
|
{
|
|
1068
1379
|
id: "fix:gate",
|
|
1069
1380
|
title: `gate ${verdict}: ${gateResult.reason ?? fix.statusReason ?? ""}`.trim(),
|
|
1070
1381
|
status: verdict,
|
|
1071
1382
|
severity: verdict === "pass" ? "info" : verdict === "fail" ? "error" : "warning",
|
|
1072
|
-
description: formatGateScoreboard(gateResult)
|
|
1383
|
+
description: `${formatGateScoreboard(gateResult)}${fidelity ? `; baseline fidelity: ${fidelity}` : ""}` + `${cf ? `; counterfactual ${cf.verdict}: ${counterfactualSentence(cf)} (anchors ${cf.anchors ?? 0}, abstained ${cf.abstained ?? 0})` : ""}`
|
|
1073
1384
|
}
|
|
1074
1385
|
];
|
|
1075
1386
|
}
|
|
@@ -1087,9 +1398,17 @@ function printHumanGateVerdict(fix, gateRunId, gateResult, cases) {
|
|
|
1087
1398
|
w(`Fix: MODA-FIX-${fix.shortRef} (${fix.status})`);
|
|
1088
1399
|
w(`Gate run: ${gateRunId}`);
|
|
1089
1400
|
w(`Verdict: ${gateResult?.verdict ?? "none"}${gateResult?.reason ? ` — ${gateResult.reason}` : ""}`);
|
|
1401
|
+
const fidelity = baselineFidelityDescriptor(gateResult);
|
|
1402
|
+
if (fidelity) {
|
|
1403
|
+
w(`Baseline: ${fidelity}`);
|
|
1404
|
+
}
|
|
1090
1405
|
if (gateResult) {
|
|
1091
1406
|
w(`Score: ${formatGateScoreboard(gateResult)}`);
|
|
1092
1407
|
}
|
|
1408
|
+
const cf = counterfactualBlock(gateResult);
|
|
1409
|
+
if (cf) {
|
|
1410
|
+
w(counterfactualHumanLine(cf));
|
|
1411
|
+
}
|
|
1093
1412
|
if (cases.length > 0) {
|
|
1094
1413
|
w("");
|
|
1095
1414
|
w("Per-case (holdout is the verdict; repair rows are training cases):");
|
|
@@ -4811,7 +5130,7 @@ function buildManifest(commands) {
|
|
|
4811
5130
|
exit_codes: [
|
|
4812
5131
|
{ code: 0, name: "ok", meaning: "Command completed successfully." },
|
|
4813
5132
|
{ code: 1, name: "error", meaning: "Command, input, API, network, or unexpected failure." },
|
|
4814
|
-
{ code: 3, name: "degraded", meaning: "Degraded result: `moda ask` local fallback,
|
|
5133
|
+
{ code: 3, name: "degraded", meaning: "Degraded result: `moda ask` local fallback, `moda fix verify` gate inconclusive/blocked-coverage (never treat as a pass), or `moda fixes drive` timeout with fixes still advancing." },
|
|
4815
5134
|
{ code: 4, name: "auth_required", meaning: "Run login_command, then resume." },
|
|
4816
5135
|
{ code: 5, name: "input_required", meaning: "Choose from input_request choices, then resume." },
|
|
4817
5136
|
{ code: 130, name: "cancelled", meaning: "Interrupted or output consumer closed." }
|
|
@@ -4882,7 +5201,7 @@ function event(name, when, terminal, deprecated = false) {
|
|
|
4882
5201
|
function exitCodesForCommand(command) {
|
|
4883
5202
|
if (command === "ask")
|
|
4884
5203
|
return [0, 1, 3, 4, 5, 130];
|
|
4885
|
-
if (command === "fix")
|
|
5204
|
+
if (command === "fix" || command === "fixes")
|
|
4886
5205
|
return [0, 1, 3, 4, 130];
|
|
4887
5206
|
if (command === "init" || command === "harness")
|
|
4888
5207
|
return [0, 1, 4, 5, 130];
|
|
@@ -5055,6 +5374,12 @@ Commands:
|
|
|
5055
5374
|
|
|
5056
5375
|
Fixes (a fix with proof):
|
|
5057
5376
|
fixes Ranked Fix queue (--status=S --limit=N --cursor=TOKEN)
|
|
5377
|
+
fixes draft-batch Campaign mode: batch-draft fixes for the top-ranked problems
|
|
5378
|
+
(--limit=N, 1-25, default 10 — batches cap at 25: LLM spend guard)
|
|
5379
|
+
fixes drive Round-robin one advance per active fix per pass until all rest
|
|
5380
|
+
(--status=S narrows the server-side scan window to one advancing
|
|
5381
|
+
status, --timeout=ms default 2h, --pass-interval=ms default 20s;
|
|
5382
|
+
exit 3 when the timeout lapses with fixes still advancing)
|
|
5058
5383
|
fix <fix_id> One Fix: status + gate result; --packet prints moda.fix_packet.v1
|
|
5059
5384
|
(tool/skill candidates are also written under .moda/fixes/<ref>/);
|
|
5060
5385
|
--wait drives a still-running pipeline to rest
|
|
@@ -6704,11 +7029,17 @@ var commandRegistry = createCommandRegistry([
|
|
|
6704
7029
|
},
|
|
6705
7030
|
{
|
|
6706
7031
|
name: "fixes",
|
|
6707
|
-
description: 'List the ranked queue of Fixes ("a fix with proof")
|
|
6708
|
-
examples: [
|
|
7032
|
+
description: 'List the ranked queue of Fixes ("a fix with proof"); campaign mode batch-drafts and drives them',
|
|
7033
|
+
examples: [
|
|
7034
|
+
"moda fixes",
|
|
7035
|
+
"moda fixes --status=VERIFIED",
|
|
7036
|
+
"moda fixes draft-batch --limit=10",
|
|
7037
|
+
"moda fixes drive",
|
|
7038
|
+
"moda fixes drive --status=SCOPING"
|
|
7039
|
+
],
|
|
6709
7040
|
auth: "none",
|
|
6710
7041
|
outputModes: ["human", "json", "agent"],
|
|
6711
|
-
mutability: "
|
|
7042
|
+
mutability: "write",
|
|
6712
7043
|
validateConfigBeforeRun: false,
|
|
6713
7044
|
resetApiRequestCountBeforeRun: false,
|
|
6714
7045
|
telemetry: "result",
|
package/package.json
CHANGED