@moda-ai/cli 1.28.1 → 1.28.3
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 +331 -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,12 @@ 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 } : {},
|
|
634
899
|
...cases.length > 0 ? { cases } : {}
|
|
635
900
|
}, {
|
|
636
901
|
status: envelopeStatus,
|
|
637
902
|
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"}`,
|
|
903
|
+
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
904
|
confidence: "high"
|
|
640
905
|
},
|
|
641
906
|
findings,
|
|
@@ -845,13 +1110,14 @@ async function runFixDetail(context, profileOptions, tenantId, fixId) {
|
|
|
845
1110
|
context
|
|
846
1111
|
});
|
|
847
1112
|
}
|
|
1113
|
+
const fidelityCopy = fixVerifiedFidelityCopy(fix);
|
|
848
1114
|
if (context.outputMode === "human") {
|
|
849
|
-
process.stderr.write(`MODA-FIX-${fix.shortRef} ${fix.status} ${fix.fixType}${fix.statusReason ? ` — ${fix.statusReason}` : ""}
|
|
1115
|
+
process.stderr.write(`MODA-FIX-${fix.shortRef} ${fix.status} ${fix.fixType}${fidelityCopy ? ` — ${fidelityCopy}` : ""}${fix.statusReason ? ` — ${fix.statusReason}` : ""}
|
|
850
1116
|
`);
|
|
851
1117
|
}
|
|
852
1118
|
context.output.writeData(fix, {
|
|
853
1119
|
summary: {
|
|
854
|
-
text: `Fix MODA-FIX-${fix.shortRef} is ${fix.status}${fix.statusReason ? ` (${fix.statusReason})` : ""}
|
|
1120
|
+
text: `Fix MODA-FIX-${fix.shortRef} is ${fix.status}${fix.statusReason ? ` (${fix.statusReason})` : ""}.${fidelityCopy ? ` ${fidelityCopy}.` : ""}`,
|
|
855
1121
|
confidence: "high"
|
|
856
1122
|
},
|
|
857
1123
|
findings: gateFindings(fix),
|
|
@@ -867,18 +1133,7 @@ async function driveAdvance(args) {
|
|
|
867
1133
|
if (row.status === lastStatus)
|
|
868
1134
|
return;
|
|
869
1135
|
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
|
-
}
|
|
1136
|
+
announceFixTransition(args.context, row);
|
|
882
1137
|
};
|
|
883
1138
|
announce(fix);
|
|
884
1139
|
while (ADVANCING_STATUSES.has(fix.status)) {
|
|
@@ -889,7 +1144,7 @@ async function driveAdvance(args) {
|
|
|
889
1144
|
announce(fix);
|
|
890
1145
|
if (!ADVANCING_STATUSES.has(fix.status))
|
|
891
1146
|
break;
|
|
892
|
-
if (
|
|
1147
|
+
if (isRestingProposed(fix)) {
|
|
893
1148
|
break;
|
|
894
1149
|
}
|
|
895
1150
|
await sleep(fix.status === "GATING" ? args.pollIntervalMs : Math.min(STEP_DELAY_MS, args.pollIntervalMs));
|
|
@@ -922,6 +1177,23 @@ async function fetchPacketFailSoft(tenantId, fixId, profileOptions) {
|
|
|
922
1177
|
return null;
|
|
923
1178
|
}
|
|
924
1179
|
}
|
|
1180
|
+
function isRestingProposed(fix) {
|
|
1181
|
+
return fix.status === "PROPOSED" && /gate (pending|unavailable)/i.test(fix.statusReason ?? "");
|
|
1182
|
+
}
|
|
1183
|
+
function announceFixTransition(context, row) {
|
|
1184
|
+
const line = `fix MODA-FIX-${row.shortRef}: ${row.status}${row.statusReason ? ` — ${row.statusReason}` : ""}`;
|
|
1185
|
+
if (context.outputMode === "human") {
|
|
1186
|
+
process.stderr.write(`${line}
|
|
1187
|
+
`);
|
|
1188
|
+
} else if (context.outputMode === "agent-stream") {
|
|
1189
|
+
context.output.writeEvent({
|
|
1190
|
+
event: "progress",
|
|
1191
|
+
phase: "fix",
|
|
1192
|
+
message: line,
|
|
1193
|
+
elapsed_ms: Date.now() - context.startedAt
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
925
1197
|
function isArtifactFix(fix) {
|
|
926
1198
|
return fix.fixType === "TOOL_SCHEMA" || fix.fixType === "SKILL";
|
|
927
1199
|
}
|
|
@@ -1058,18 +1330,36 @@ function materializePacketCandidates(packet, fixId, human) {
|
|
|
1058
1330
|
summarySuffix: summaryParts.length > 0 ? ` ${summaryParts.join(" ")}` : ""
|
|
1059
1331
|
};
|
|
1060
1332
|
}
|
|
1333
|
+
function baselineFidelityDescriptor(gate) {
|
|
1334
|
+
const fidelity = gate?.baselineFidelity;
|
|
1335
|
+
const base = fidelity?.prompt === "tenant" ? "tenant prompt baseline" : fidelity?.prompt === "generic" ? "standard-agent baseline" : null;
|
|
1336
|
+
if (fidelity?.skills === "observed") {
|
|
1337
|
+
return `${base ?? "baseline"} · skill recovered from production receipts`;
|
|
1338
|
+
}
|
|
1339
|
+
return base;
|
|
1340
|
+
}
|
|
1341
|
+
function verifiedFidelityCopy(gate) {
|
|
1342
|
+
const descriptor = baselineFidelityDescriptor(gate);
|
|
1343
|
+
return descriptor ? `Verified — ${descriptor}` : null;
|
|
1344
|
+
}
|
|
1345
|
+
function fixVerifiedFidelityCopy(fix) {
|
|
1346
|
+
if (fix.status !== "VERIFIED")
|
|
1347
|
+
return null;
|
|
1348
|
+
return verifiedFidelityCopy(fix.gateResult ?? null);
|
|
1349
|
+
}
|
|
1061
1350
|
function gateFindings(fix) {
|
|
1062
1351
|
const gateResult = fix.gateResult ?? null;
|
|
1063
|
-
if (!gateResult)
|
|
1352
|
+
if (!gateResult || gateResult.pending === true)
|
|
1064
1353
|
return [];
|
|
1065
1354
|
const verdict = gateResult.verdict ?? fix.status;
|
|
1355
|
+
const fidelity = baselineFidelityDescriptor(gateResult);
|
|
1066
1356
|
return [
|
|
1067
1357
|
{
|
|
1068
1358
|
id: "fix:gate",
|
|
1069
1359
|
title: `gate ${verdict}: ${gateResult.reason ?? fix.statusReason ?? ""}`.trim(),
|
|
1070
1360
|
status: verdict,
|
|
1071
1361
|
severity: verdict === "pass" ? "info" : verdict === "fail" ? "error" : "warning",
|
|
1072
|
-
description: formatGateScoreboard(gateResult)
|
|
1362
|
+
description: `${formatGateScoreboard(gateResult)}${fidelity ? `; baseline fidelity: ${fidelity}` : ""}`
|
|
1073
1363
|
}
|
|
1074
1364
|
];
|
|
1075
1365
|
}
|
|
@@ -1087,6 +1377,10 @@ function printHumanGateVerdict(fix, gateRunId, gateResult, cases) {
|
|
|
1087
1377
|
w(`Fix: MODA-FIX-${fix.shortRef} (${fix.status})`);
|
|
1088
1378
|
w(`Gate run: ${gateRunId}`);
|
|
1089
1379
|
w(`Verdict: ${gateResult?.verdict ?? "none"}${gateResult?.reason ? ` — ${gateResult.reason}` : ""}`);
|
|
1380
|
+
const fidelity = baselineFidelityDescriptor(gateResult);
|
|
1381
|
+
if (fidelity) {
|
|
1382
|
+
w(`Baseline: ${fidelity}`);
|
|
1383
|
+
}
|
|
1090
1384
|
if (gateResult) {
|
|
1091
1385
|
w(`Score: ${formatGateScoreboard(gateResult)}`);
|
|
1092
1386
|
}
|
|
@@ -4811,7 +5105,7 @@ function buildManifest(commands) {
|
|
|
4811
5105
|
exit_codes: [
|
|
4812
5106
|
{ code: 0, name: "ok", meaning: "Command completed successfully." },
|
|
4813
5107
|
{ code: 1, name: "error", meaning: "Command, input, API, network, or unexpected failure." },
|
|
4814
|
-
{ code: 3, name: "degraded", meaning: "Degraded result: `moda ask` local fallback,
|
|
5108
|
+
{ 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
5109
|
{ code: 4, name: "auth_required", meaning: "Run login_command, then resume." },
|
|
4816
5110
|
{ code: 5, name: "input_required", meaning: "Choose from input_request choices, then resume." },
|
|
4817
5111
|
{ code: 130, name: "cancelled", meaning: "Interrupted or output consumer closed." }
|
|
@@ -4882,7 +5176,7 @@ function event(name, when, terminal, deprecated = false) {
|
|
|
4882
5176
|
function exitCodesForCommand(command) {
|
|
4883
5177
|
if (command === "ask")
|
|
4884
5178
|
return [0, 1, 3, 4, 5, 130];
|
|
4885
|
-
if (command === "fix")
|
|
5179
|
+
if (command === "fix" || command === "fixes")
|
|
4886
5180
|
return [0, 1, 3, 4, 130];
|
|
4887
5181
|
if (command === "init" || command === "harness")
|
|
4888
5182
|
return [0, 1, 4, 5, 130];
|
|
@@ -5055,6 +5349,12 @@ Commands:
|
|
|
5055
5349
|
|
|
5056
5350
|
Fixes (a fix with proof):
|
|
5057
5351
|
fixes Ranked Fix queue (--status=S --limit=N --cursor=TOKEN)
|
|
5352
|
+
fixes draft-batch Campaign mode: batch-draft fixes for the top-ranked problems
|
|
5353
|
+
(--limit=N, 1-25, default 10 — batches cap at 25: LLM spend guard)
|
|
5354
|
+
fixes drive Round-robin one advance per active fix per pass until all rest
|
|
5355
|
+
(--status=S narrows the server-side scan window to one advancing
|
|
5356
|
+
status, --timeout=ms default 2h, --pass-interval=ms default 20s;
|
|
5357
|
+
exit 3 when the timeout lapses with fixes still advancing)
|
|
5058
5358
|
fix <fix_id> One Fix: status + gate result; --packet prints moda.fix_packet.v1
|
|
5059
5359
|
(tool/skill candidates are also written under .moda/fixes/<ref>/);
|
|
5060
5360
|
--wait drives a still-running pipeline to rest
|
|
@@ -6704,11 +7004,17 @@ var commandRegistry = createCommandRegistry([
|
|
|
6704
7004
|
},
|
|
6705
7005
|
{
|
|
6706
7006
|
name: "fixes",
|
|
6707
|
-
description: 'List the ranked queue of Fixes ("a fix with proof")
|
|
6708
|
-
examples: [
|
|
7007
|
+
description: 'List the ranked queue of Fixes ("a fix with proof"); campaign mode batch-drafts and drives them',
|
|
7008
|
+
examples: [
|
|
7009
|
+
"moda fixes",
|
|
7010
|
+
"moda fixes --status=VERIFIED",
|
|
7011
|
+
"moda fixes draft-batch --limit=10",
|
|
7012
|
+
"moda fixes drive",
|
|
7013
|
+
"moda fixes drive --status=SCOPING"
|
|
7014
|
+
],
|
|
6709
7015
|
auth: "none",
|
|
6710
7016
|
outputModes: ["human", "json", "agent"],
|
|
6711
|
-
mutability: "
|
|
7017
|
+
mutability: "write",
|
|
6712
7018
|
validateConfigBeforeRun: false,
|
|
6713
7019
|
resetApiRequestCountBeforeRun: false,
|
|
6714
7020
|
telemetry: "result",
|
package/package.json
CHANGED