@moda-ai/cli 1.28.0 → 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 +363 -27
- 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
|
}
|
|
@@ -933,9 +1205,11 @@ function materializePacketCandidates(packet, fixId, human) {
|
|
|
933
1205
|
const summaryParts = [];
|
|
934
1206
|
const tool = packet.candidate_tool;
|
|
935
1207
|
const skill = packet.candidate_skill;
|
|
1208
|
+
const skillEdit = packet.candidate_skill_edit;
|
|
936
1209
|
const hasTool = typeof tool?.proposed_description === "string" && tool.proposed_description !== "";
|
|
937
1210
|
const hasSkill = typeof skill?.skill_md === "string" && skill.skill_md !== "";
|
|
938
|
-
|
|
1211
|
+
const hasSkillEdit = typeof skillEdit?.proposed_skill_md === "string" && skillEdit.proposed_skill_md !== "";
|
|
1212
|
+
if (!hasTool && !hasSkill && !hasSkillEdit) {
|
|
939
1213
|
return { findings, artifacts, warnings, nextCommands, summarySuffix: "" };
|
|
940
1214
|
}
|
|
941
1215
|
const rawRef = typeof packet.short_ref === "string" && packet.short_ref.trim() ? packet.short_ref : fixId;
|
|
@@ -1014,6 +1288,34 @@ function materializePacketCandidates(packet, fixId, human) {
|
|
|
1014
1288
|
w(` moda fix mark-applied ${fixId} --note="installed skill ${skillName}"`);
|
|
1015
1289
|
}
|
|
1016
1290
|
}
|
|
1291
|
+
if (hasSkillEdit && skillEdit) {
|
|
1292
|
+
const skillKey = typeof skillEdit.skill_key === "string" && skillEdit.skill_key ? skillEdit.skill_key : typeof skillEdit.skill_name === "string" && skillEdit.skill_name ? skillEdit.skill_name : "moda-skill";
|
|
1293
|
+
const changeSummary = typeof skillEdit.change_summary === "string" ? skillEdit.change_summary : "";
|
|
1294
|
+
const currentPath = writeCandidateFile("SKILL.current.md", typeof skillEdit.current_skill_md === "string" ? skillEdit.current_skill_md : "", "fix_skill_edit_current", `Current SKILL.md of your synced skill '${skillKey}' — the diff base.`);
|
|
1295
|
+
const proposedPath = writeCandidateFile("SKILL.proposed.md", skillEdit.proposed_skill_md ?? "", "fix_skill_edit_proposed", `Proposed SKILL.md for '${skillKey}' (minimal edit) — apply it to the existing skill in your repo, then \`moda fix mark-applied ${fixId}\`.`);
|
|
1296
|
+
findings.push({
|
|
1297
|
+
id: "fix:candidate_skill_edit",
|
|
1298
|
+
title: `skill '${skillKey}' edit proposed${changeSummary ? `: ${changeSummary}` : " (minimal delta to the existing skill)"}`,
|
|
1299
|
+
status: "proposed",
|
|
1300
|
+
severity: "info",
|
|
1301
|
+
description: `This amends the EXISTING synced skill '${skillKey}' — a minimal delta, not a new skill. Apply the proposed SKILL.md in your repo, then confirm with \`moda fix mark-applied ${fixId}\`.`
|
|
1302
|
+
});
|
|
1303
|
+
summaryParts.push(`Includes a skill-edit candidate amending existing skill '${skillKey}'.`);
|
|
1304
|
+
if (human) {
|
|
1305
|
+
w("");
|
|
1306
|
+
w(`Skill-edit candidate — amends existing skill '${skillKey}'`);
|
|
1307
|
+
if (changeSummary)
|
|
1308
|
+
block("change summary", changeSummary);
|
|
1309
|
+
if (currentPath)
|
|
1310
|
+
w(` current SKILL.md written to ${currentPath}`);
|
|
1311
|
+
if (proposedPath)
|
|
1312
|
+
w(` proposed SKILL.md written to ${proposedPath}`);
|
|
1313
|
+
if (currentPath && proposedPath)
|
|
1314
|
+
w(` diff: diff ${currentPath} ${proposedPath}`);
|
|
1315
|
+
w(` apply the edit to the existing skill in your repo, then confirm:`);
|
|
1316
|
+
w(` moda fix mark-applied ${fixId} --note="amended skill ${skillKey}"`);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1017
1319
|
nextCommands.push({
|
|
1018
1320
|
command: `moda fix mark-applied ${fixId} --note="where/how you applied it"`,
|
|
1019
1321
|
purpose: "Confirm you applied the candidate in your own stack (no PR) — flips the fix to SHIPPED and starts recurrence monitoring.",
|
|
@@ -1028,18 +1330,36 @@ function materializePacketCandidates(packet, fixId, human) {
|
|
|
1028
1330
|
summarySuffix: summaryParts.length > 0 ? ` ${summaryParts.join(" ")}` : ""
|
|
1029
1331
|
};
|
|
1030
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
|
+
}
|
|
1031
1350
|
function gateFindings(fix) {
|
|
1032
1351
|
const gateResult = fix.gateResult ?? null;
|
|
1033
|
-
if (!gateResult)
|
|
1352
|
+
if (!gateResult || gateResult.pending === true)
|
|
1034
1353
|
return [];
|
|
1035
1354
|
const verdict = gateResult.verdict ?? fix.status;
|
|
1355
|
+
const fidelity = baselineFidelityDescriptor(gateResult);
|
|
1036
1356
|
return [
|
|
1037
1357
|
{
|
|
1038
1358
|
id: "fix:gate",
|
|
1039
1359
|
title: `gate ${verdict}: ${gateResult.reason ?? fix.statusReason ?? ""}`.trim(),
|
|
1040
1360
|
status: verdict,
|
|
1041
1361
|
severity: verdict === "pass" ? "info" : verdict === "fail" ? "error" : "warning",
|
|
1042
|
-
description: formatGateScoreboard(gateResult)
|
|
1362
|
+
description: `${formatGateScoreboard(gateResult)}${fidelity ? `; baseline fidelity: ${fidelity}` : ""}`
|
|
1043
1363
|
}
|
|
1044
1364
|
];
|
|
1045
1365
|
}
|
|
@@ -1057,6 +1377,10 @@ function printHumanGateVerdict(fix, gateRunId, gateResult, cases) {
|
|
|
1057
1377
|
w(`Fix: MODA-FIX-${fix.shortRef} (${fix.status})`);
|
|
1058
1378
|
w(`Gate run: ${gateRunId}`);
|
|
1059
1379
|
w(`Verdict: ${gateResult?.verdict ?? "none"}${gateResult?.reason ? ` — ${gateResult.reason}` : ""}`);
|
|
1380
|
+
const fidelity = baselineFidelityDescriptor(gateResult);
|
|
1381
|
+
if (fidelity) {
|
|
1382
|
+
w(`Baseline: ${fidelity}`);
|
|
1383
|
+
}
|
|
1060
1384
|
if (gateResult) {
|
|
1061
1385
|
w(`Score: ${formatGateScoreboard(gateResult)}`);
|
|
1062
1386
|
}
|
|
@@ -1087,7 +1411,7 @@ function nextCommandsFor(fix) {
|
|
|
1087
1411
|
mutability: "write",
|
|
1088
1412
|
requires_approval: true
|
|
1089
1413
|
};
|
|
1090
|
-
const skillGatePending = fix.fixType === "SKILL" && fix.candidateRef?.kind === "skill" && !fix.candidateRef?.skillId;
|
|
1414
|
+
const skillGatePending = fix.fixType === "SKILL" && (fix.candidateRef?.kind === "skill_edit" || fix.candidateRef?.kind === "skill" && !fix.candidateRef?.skillId);
|
|
1091
1415
|
switch (fix.status) {
|
|
1092
1416
|
case "VERIFIED":
|
|
1093
1417
|
if (isArtifactFix(fix)) {
|
|
@@ -4781,7 +5105,7 @@ function buildManifest(commands) {
|
|
|
4781
5105
|
exit_codes: [
|
|
4782
5106
|
{ code: 0, name: "ok", meaning: "Command completed successfully." },
|
|
4783
5107
|
{ code: 1, name: "error", meaning: "Command, input, API, network, or unexpected failure." },
|
|
4784
|
-
{ 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." },
|
|
4785
5109
|
{ code: 4, name: "auth_required", meaning: "Run login_command, then resume." },
|
|
4786
5110
|
{ code: 5, name: "input_required", meaning: "Choose from input_request choices, then resume." },
|
|
4787
5111
|
{ code: 130, name: "cancelled", meaning: "Interrupted or output consumer closed." }
|
|
@@ -4852,7 +5176,7 @@ function event(name, when, terminal, deprecated = false) {
|
|
|
4852
5176
|
function exitCodesForCommand(command) {
|
|
4853
5177
|
if (command === "ask")
|
|
4854
5178
|
return [0, 1, 3, 4, 5, 130];
|
|
4855
|
-
if (command === "fix")
|
|
5179
|
+
if (command === "fix" || command === "fixes")
|
|
4856
5180
|
return [0, 1, 3, 4, 130];
|
|
4857
5181
|
if (command === "init" || command === "harness")
|
|
4858
5182
|
return [0, 1, 4, 5, 130];
|
|
@@ -5025,6 +5349,12 @@ Commands:
|
|
|
5025
5349
|
|
|
5026
5350
|
Fixes (a fix with proof):
|
|
5027
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)
|
|
5028
5358
|
fix <fix_id> One Fix: status + gate result; --packet prints moda.fix_packet.v1
|
|
5029
5359
|
(tool/skill candidates are also written under .moda/fixes/<ref>/);
|
|
5030
5360
|
--wait drives a still-running pipeline to rest
|
|
@@ -6674,11 +7004,17 @@ var commandRegistry = createCommandRegistry([
|
|
|
6674
7004
|
},
|
|
6675
7005
|
{
|
|
6676
7006
|
name: "fixes",
|
|
6677
|
-
description: 'List the ranked queue of Fixes ("a fix with proof")
|
|
6678
|
-
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
|
+
],
|
|
6679
7015
|
auth: "none",
|
|
6680
7016
|
outputModes: ["human", "json", "agent"],
|
|
6681
|
-
mutability: "
|
|
7017
|
+
mutability: "write",
|
|
6682
7018
|
validateConfigBeforeRun: false,
|
|
6683
7019
|
resetApiRequestCountBeforeRun: false,
|
|
6684
7020
|
telemetry: "result",
|
package/package.json
CHANGED