@moda-ai/cli 1.26.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/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-xf5nx5hc.js";
11
+ } from "./cli-cjqzrp9k.js";
9
12
  import {
10
13
  ApiError,
11
14
  HARNESS_REPORT_APPROVAL_PATH,
@@ -359,6 +362,724 @@ function embeddedWindowSize(conversation) {
359
362
  return Math.min(center - from, to - center);
360
363
  }
361
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
+
362
1083
  // src/sync.ts
363
1084
  async function runSyncCommand(context) {
364
1085
  const resource = context.positionals[0] ?? "harness";
@@ -534,8 +1255,8 @@ async function sendCliUsageTelemetry(input) {
534
1255
  }
535
1256
 
536
1257
  // src/update-check.ts
537
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
538
- 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";
539
1260
  var UPDATE_CHECK_FLAG = "no-update-check";
540
1261
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
541
1262
  var REGISTRY_TIMEOUT_MS = 1500;
@@ -549,11 +1270,11 @@ function isUpdateCheckDisabled(flags, env = process.env) {
549
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";
550
1271
  }
551
1272
  function cachePath(options = {}) {
552
- return join(getModaDir(options), "update-check.json");
1273
+ return join2(getModaDir(options), "update-check.json");
553
1274
  }
554
1275
  function readCache(options = {}) {
555
1276
  try {
556
- const parsed = JSON.parse(readFileSync(cachePath(options), "utf8"));
1277
+ const parsed = JSON.parse(readFileSync2(cachePath(options), "utf8"));
557
1278
  if (parsed && typeof parsed === "object" && typeof parsed.checked_at === "number" && typeof parsed.latest === "string") {
558
1279
  return parsed;
559
1280
  }
@@ -562,7 +1283,7 @@ function readCache(options = {}) {
562
1283
  }
563
1284
  function writeCache(cache, options = {}) {
564
1285
  try {
565
- mkdirSync(getModaDir(options), { recursive: true });
1286
+ mkdirSync2(getModaDir(options), { recursive: true });
566
1287
  writeFileSync(cachePath(options), `${JSON.stringify(cache, null, 2)}
567
1288
  `, "utf8");
568
1289
  } catch {}
@@ -1124,8 +1845,8 @@ function renderJsonSchemas() {
1124
1845
 
1125
1846
  // src/keys.ts
1126
1847
  import { createHash } from "node:crypto";
1127
- import { existsSync, readFileSync as readFileSync2 } from "node:fs";
1128
- import { join as join2 } from "node:path";
1848
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
1849
+ import { join as join3 } from "node:path";
1129
1850
  var KEY_LIFECYCLE_ENDPOINT_REQUIREMENTS = {
1130
1851
  list: "GET /api/cli/keys?profile=<profile>",
1131
1852
  create: "POST /api/cli/keys",
@@ -1192,14 +1913,14 @@ function writeLocalApiKeySecret(descriptor, apiKey, options = {}) {
1192
1913
  return path;
1193
1914
  }
1194
1915
  function keyDescriptorStorePath(options = {}) {
1195
- return join2(getModaPaths(options).dir, "keys.json");
1916
+ return join3(getModaPaths(options).dir, "keys.json");
1196
1917
  }
1197
1918
  function loadLocalKeyDescriptors(options = {}) {
1198
1919
  const path = keyDescriptorStorePath(options);
1199
1920
  try {
1200
- if (!existsSync(path))
1921
+ if (!existsSync2(path))
1201
1922
  return [];
1202
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
1923
+ const parsed = JSON.parse(readFileSync3(path, "utf8"));
1203
1924
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1204
1925
  return [];
1205
1926
  }
@@ -1547,12 +2268,12 @@ async function runKeysCommand(context) {
1547
2268
  }
1548
2269
 
1549
2270
  // src/agent.ts
1550
- import { existsSync as existsSync2, readFileSync as readFileSync3, statSync } from "node:fs";
1551
- import { join as join3, relative, resolve } from "node:path";
2271
+ import { existsSync as existsSync3, readFileSync as readFileSync4, statSync } from "node:fs";
2272
+ import { join as join4, relative, resolve as resolve2 } from "node:path";
1552
2273
  var MODA_DIR = ".moda";
1553
- var HARNESS_PATH = join3(MODA_DIR, "harness.json");
1554
- var HARNESS_REPORT_MARKDOWN_PATH = join3(MODA_DIR, "HARNESS_REPORT.md");
1555
- var HARNESS_SUMMARY_PATH = join3(MODA_DIR, "HARNESS.md");
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");
1556
2277
  var MAX_AGENT_EVIDENCE_BYTES = 24000;
1557
2278
  async function runAgentCommand(context) {
1558
2279
  const subcommand2 = context.positionals[0] ?? "context";
@@ -1674,7 +2395,7 @@ async function buildAgentContext(context) {
1674
2395
  },
1675
2396
  report: reportState.state,
1676
2397
  sync: syncState ? {
1677
- path: join3(MODA_DIR, "sync-state.json"),
2398
+ path: join4(MODA_DIR, "sync-state.json"),
1678
2399
  exists: true,
1679
2400
  source: syncState.source,
1680
2401
  updatedAt: syncState.updatedAt,
@@ -1682,7 +2403,7 @@ async function buildAgentContext(context) {
1682
2403
  remoteHarnessId: syncState.remote.harnessId,
1683
2404
  remoteVersionId: syncState.remote.versionId
1684
2405
  } : {
1685
- path: join3(MODA_DIR, "sync-state.json"),
2406
+ path: join4(MODA_DIR, "sync-state.json"),
1686
2407
  exists: false
1687
2408
  }
1688
2409
  },
@@ -1732,18 +2453,18 @@ function summarizeAgentContext(payload, noun) {
1732
2453
  return `Local Moda ${noun} is healthy with ${agents} runtime agent(s).`;
1733
2454
  }
1734
2455
  function readHarnessGraphIfPresent(cwd) {
1735
- const path = join3(cwd, HARNESS_PATH);
1736
- if (!existsSync2(path))
2456
+ const path = join4(cwd, HARNESS_PATH);
2457
+ if (!existsSync3(path))
1737
2458
  return;
1738
2459
  try {
1739
- return JSON.parse(readFileSync3(path, "utf8"));
2460
+ return JSON.parse(readFileSync4(path, "utf8"));
1740
2461
  } catch {
1741
2462
  return;
1742
2463
  }
1743
2464
  }
1744
2465
  function readHarnessReportState(cwd) {
1745
- const path = join3(cwd, HARNESS_REPORT_PATH);
1746
- if (!existsSync2(path)) {
2466
+ const path = join4(cwd, HARNESS_REPORT_PATH);
2467
+ if (!existsSync3(path)) {
1747
2468
  return {
1748
2469
  state: {
1749
2470
  path: HARNESS_REPORT_PATH,
@@ -1787,11 +2508,11 @@ function readHarnessReportState(cwd) {
1787
2508
  }
1788
2509
  }
1789
2510
  function readApproval(cwd) {
1790
- const path = join3(cwd, HARNESS_REPORT_APPROVAL_PATH);
1791
- if (!existsSync2(path))
2511
+ const path = join4(cwd, HARNESS_REPORT_APPROVAL_PATH);
2512
+ if (!existsSync3(path))
1792
2513
  return;
1793
2514
  try {
1794
- return JSON.parse(readFileSync3(path, "utf8"));
2515
+ return JSON.parse(readFileSync4(path, "utf8"));
1795
2516
  } catch {
1796
2517
  return;
1797
2518
  }
@@ -1867,9 +2588,9 @@ function existingArtifacts(cwd) {
1867
2588
  [HARNESS_REPORT_PATH, "harness_report", "Cited harness analyst report."],
1868
2589
  [HARNESS_REPORT_MARKDOWN_PATH, "harness_report_markdown", "Human-readable harness analyst report."],
1869
2590
  [HARNESS_SUMMARY_PATH, "harness_summary", "Human-readable harness graph summary."],
1870
- [join3(MODA_DIR, "sync-state.json"), "sync_state", "Last local harness sync state."],
1871
- [join3(MODA_DIR, "prompts.yml"), "prompt_manifest", "Local prompt discovery manifest."]
1872
- ].flatMap(([path, kind, description]) => existsSync2(join3(cwd, path)) ? [{ 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 }] : []);
1873
2594
  }
1874
2595
  function expandEvidenceRef(ref, loaded, cwd) {
1875
2596
  if (ref.startsWith("doctor:")) {
@@ -1950,9 +2671,9 @@ function readCitationExcerpt(cwd, citation) {
1950
2671
  if (!citation.path || citation.type !== "file" && citation.type !== "generated_doc")
1951
2672
  return citation.excerpt;
1952
2673
  const absolute = safeResolve(cwd, citation.path);
1953
- if (!absolute || !existsSync2(absolute))
2674
+ if (!absolute || !existsSync3(absolute))
1954
2675
  return citation.excerpt;
1955
- const text = readFileSync3(absolute, "utf8");
2676
+ const text = readFileSync4(absolute, "utf8");
1956
2677
  if (citation.line && citation.line > 0) {
1957
2678
  return text.split(/\r?\n/)[citation.line - 1]?.slice(0, 1000) ?? citation.excerpt;
1958
2679
  }
@@ -1963,10 +2684,10 @@ function readBoundedModaArtifact(cwd, artifactPath) {
1963
2684
  throw new CliInputError(`Artifact '${artifactPath}' is outside .moda and cannot be expanded directly.`, "Use a citation evidence ref for source-file evidence.");
1964
2685
  }
1965
2686
  const absolute = safeResolve(cwd, artifactPath);
1966
- if (!absolute || !existsSync2(absolute))
2687
+ if (!absolute || !existsSync3(absolute))
1967
2688
  throw missingEvidence(`artifact:${artifactPath}`);
1968
2689
  const size = statSync(absolute).size;
1969
- const text = readFileSync3(absolute, "utf8");
2690
+ const text = readFileSync4(absolute, "utf8");
1970
2691
  return {
1971
2692
  path: artifactPath,
1972
2693
  bytes: size,
@@ -1975,7 +2696,7 @@ function readBoundedModaArtifact(cwd, artifactPath) {
1975
2696
  };
1976
2697
  }
1977
2698
  function safeResolve(cwd, path) {
1978
- const absolute = resolve(cwd, path);
2699
+ const absolute = resolve2(cwd, path);
1979
2700
  const rel = relative(cwd, absolute);
1980
2701
  if (rel.startsWith("..") || rel === ".." || rel.startsWith("/"))
1981
2702
  return;
@@ -2009,8 +2730,8 @@ function purposeForCommand(command) {
2009
2730
  }
2010
2731
 
2011
2732
  // src/production-intelligence.ts
2012
- import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
2013
- import { join as join4 } from "node:path";
2733
+ import { existsSync as existsSync4, readFileSync as readFileSync5 } from "node:fs";
2734
+ import { join as join5 } from "node:path";
2014
2735
 
2015
2736
  // src/ask-stream.ts
2016
2737
  class AskStreamError extends Error {
@@ -3430,12 +4151,12 @@ function loadHarnessHints(cwd) {
3430
4151
  skillPaths: new Map,
3431
4152
  entrypointPaths: []
3432
4153
  };
3433
- const harnessPath = join4(cwd, ".moda", "harness.json");
3434
- if (!existsSync3(harnessPath))
4154
+ const harnessPath = join5(cwd, ".moda", "harness.json");
4155
+ if (!existsSync4(harnessPath))
3435
4156
  return hints;
3436
4157
  hints.reportPath = ".moda/harness.json";
3437
4158
  try {
3438
- const parsed = JSON.parse(readFileSync4(harnessPath, "utf8"));
4159
+ const parsed = JSON.parse(readFileSync5(harnessPath, "utf8"));
3439
4160
  const graph = asRecord(parsed);
3440
4161
  const artifacts = Array.isArray(graph?.artifacts) ? graph.artifacts : [];
3441
4162
  for (const item of artifacts) {
@@ -3463,11 +4184,11 @@ function loadHarnessHints(cwd) {
3463
4184
  return hints;
3464
4185
  }
3465
4186
  function localHarnessSummary(cwd) {
3466
- const path = join4(cwd, ".moda", "harness.json");
3467
- if (!existsSync3(path))
4187
+ const path = join5(cwd, ".moda", "harness.json");
4188
+ if (!existsSync4(path))
3468
4189
  return null;
3469
4190
  try {
3470
- const parsed = JSON.parse(readFileSync4(path, "utf8"));
4191
+ const parsed = JSON.parse(readFileSync5(path, "utf8"));
3471
4192
  return {
3472
4193
  harnessId: parsed.harnessId,
3473
4194
  graphHash: parsed.graphHash,
@@ -3478,16 +4199,16 @@ function localHarnessSummary(cwd) {
3478
4199
  }
3479
4200
  }
3480
4201
  function localSyncBrief(cwd, relativePath, label) {
3481
- const path = join4(cwd, relativePath);
3482
- if (!existsSync3(path)) {
4202
+ const path = join5(cwd, relativePath);
4203
+ if (!existsSync4(path)) {
3483
4204
  return { status: "missing", summary: `${label} state not found`, path: relativePath };
3484
4205
  }
3485
4206
  return { status: "present", summary: `${label} state exists locally`, path: relativePath };
3486
4207
  }
3487
4208
  function localSkillBrief(cwd) {
3488
- const claudeSkill = join4(cwd, ".claude", "skills");
3489
- const cursorRules = join4(cwd, ".cursor", "rules");
3490
- if (existsSync3(claudeSkill) || existsSync3(cursorRules)) {
4209
+ const claudeSkill = join5(cwd, ".claude", "skills");
4210
+ const cursorRules = join5(cwd, ".cursor", "rules");
4211
+ if (existsSync4(claudeSkill) || existsSync4(cursorRules)) {
3491
4212
  return { status: "present", summary: "Local agent skill/rule directory exists" };
3492
4213
  }
3493
4214
  return { status: "missing", summary: "No local agent skill/rule directory found" };
@@ -3728,7 +4449,7 @@ function buildManifest(commands) {
3728
4449
  exit_codes: [
3729
4450
  { code: 0, name: "ok", meaning: "Command completed successfully." },
3730
4451
  { code: 1, name: "error", meaning: "Command, input, API, network, or unexpected failure." },
3731
- { code: 3, name: "degraded", meaning: "Reserved for `moda ask` local fallback." },
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)." },
3732
4453
  { code: 4, name: "auth_required", meaning: "Run login_command, then resume." },
3733
4454
  { code: 5, name: "input_required", meaning: "Choose from input_request choices, then resume." },
3734
4455
  { code: 130, name: "cancelled", meaning: "Interrupted or output consumer closed." }
@@ -3799,6 +4520,8 @@ function event(name, when, terminal, deprecated = false) {
3799
4520
  function exitCodesForCommand(command) {
3800
4521
  if (command === "ask")
3801
4522
  return [0, 1, 3, 4, 5, 130];
4523
+ if (command === "fix")
4524
+ return [0, 1, 3, 4, 130];
3802
4525
  if (command === "init" || command === "harness")
3803
4526
  return [0, 1, 4, 5, 130];
3804
4527
  return [0, 1, 4, 130];
@@ -3806,7 +4529,7 @@ function exitCodesForCommand(command) {
3806
4529
 
3807
4530
  // src/cli.ts
3808
4531
  import { fileURLToPath } from "url";
3809
- import { realpathSync } from "fs";
4532
+ import { realpathSync as realpathSync2 } from "fs";
3810
4533
  function parseArgs(argv) {
3811
4534
  const args = argv.slice(2);
3812
4535
  let command = "";
@@ -3967,6 +4690,18 @@ Commands:
3967
4690
  skills sync Push local .claude/skills/**/SKILL.md up to Moda
3968
4691
  skills sync --dry-run Show what would be pushed without writing
3969
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
+
3970
4705
  Options:
3971
4706
  --help Show this help message
3972
4707
  --version Show version number
@@ -4162,7 +4897,10 @@ Examples:
4162
4897
  moda prompts sync
4163
4898
  moda prompts promote support.triage --label=prod --version=pver_abc123
4164
4899
  moda prompts ab --baseline=prompts/agent.prompt.md --candidate=prompts/agent-v2.prompt.md --conversations=conv_1,conv_2
4165
- 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`);
4166
4904
  }
4167
4905
  function printCommandHelp(definition) {
4168
4906
  const lines = [];
@@ -4407,7 +5145,7 @@ async function runTailCommand(params, context) {
4407
5145
  break;
4408
5146
  if (params.max_events !== undefined && emitted >= params.max_events)
4409
5147
  break;
4410
- await new Promise((resolve2) => setTimeout(resolve2, intervalMs));
5148
+ await new Promise((resolve3) => setTimeout(resolve3, intervalMs));
4411
5149
  }
4412
5150
  }
4413
5151
  async function attachWorldState(rows) {
@@ -5025,7 +5763,7 @@ var commandRegistry = createCommandRegistry([
5025
5763
  resetApiRequestCountBeforeRun: true,
5026
5764
  telemetry: "result",
5027
5765
  handler: async (context) => {
5028
- const { runInit } = await import("./index-efv643ec.js");
5766
+ const { runInit } = await import("./index-fwxpt51s.js");
5029
5767
  if (context.outputMode === "agent-stream") {
5030
5768
  context.output.writeEvent({
5031
5769
  event: "started",
@@ -5430,6 +6168,41 @@ var commandRegistry = createCommandRegistry([
5430
6168
  handler: async (context) => ({
5431
6169
  exitCode: await runSkillsCommand(context)
5432
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
+ })
5433
6206
  }
5434
6207
  ]);
5435
6208
  function classifyCliError(error) {
@@ -5677,7 +6450,7 @@ function installSignalHandlers() {
5677
6450
  async function main() {
5678
6451
  process.exitCode = await dispatchParsedCommand(parseArgs(process.argv));
5679
6452
  }
5680
- var isMain = process.argv[1] && fileURLToPath(import.meta.url) === realpathSync(process.argv[1]);
6453
+ var isMain = process.argv[1] && fileURLToPath(import.meta.url) === realpathSync2(process.argv[1]);
5681
6454
  if (isMain) {
5682
6455
  installSignalHandlers();
5683
6456
  main().catch((error) => {