@moda-ai/cli 1.26.0 → 1.27.1
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-
|
|
11
|
+
} from "./cli-cjqzrp9k.js";
|
|
9
12
|
import {
|
|
10
13
|
ApiError,
|
|
11
14
|
HARNESS_REPORT_APPROVAL_PATH,
|
|
@@ -359,6 +362,909 @@ 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 mark-applied <fix_id> [--note=TEXT]",
|
|
395
|
+
" moda fix dismiss <fix_id> --reason=TEXT"
|
|
396
|
+
].join(`
|
|
397
|
+
`);
|
|
398
|
+
async function runFixesCommand(context) {
|
|
399
|
+
validateConfig();
|
|
400
|
+
const profileOptions = profileOptionsFor(context);
|
|
401
|
+
const tenantId = requireTenantId(context.flags, profileOptions);
|
|
402
|
+
const query = new URLSearchParams;
|
|
403
|
+
if (context.flags.status)
|
|
404
|
+
query.set("status", context.flags.status);
|
|
405
|
+
if (context.flags.limit) {
|
|
406
|
+
const limit = Number(context.flags.limit);
|
|
407
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
|
|
408
|
+
throw new CliInputError(`--limit must be an integer between 1 and 100 (got ${context.flags.limit})`, FIX_USAGE);
|
|
409
|
+
}
|
|
410
|
+
query.set("limit", String(limit));
|
|
411
|
+
}
|
|
412
|
+
if (context.flags.cursor)
|
|
413
|
+
query.set("cursor", context.flags.cursor);
|
|
414
|
+
const suffix = query.toString() ? `?${query.toString()}` : "";
|
|
415
|
+
const data = await callControlAPI(`${fixesBase(tenantId)}${suffix}`, { method: "GET" }, profileOptions);
|
|
416
|
+
const fixes = data.fixes ?? [];
|
|
417
|
+
if (context.outputMode === "human") {
|
|
418
|
+
if (fixes.length === 0) {
|
|
419
|
+
process.stderr.write("No fixes in the queue. Draft one with `moda fix start <problem_id> --wait`.\n");
|
|
420
|
+
} else {
|
|
421
|
+
for (const fix of fixes) {
|
|
422
|
+
process.stderr.write(`MODA-FIX-${fix.shortRef} ${fix.status} ${fix.fixType} ${fix.id} ${fix.statusReason ?? ""}
|
|
423
|
+
`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
context.output.writeData(data, {
|
|
428
|
+
summary: {
|
|
429
|
+
text: `${fixes.length} fix(es)${context.flags.status ? ` with status ${context.flags.status}` : ""} in the ranked queue.`,
|
|
430
|
+
confidence: "high"
|
|
431
|
+
},
|
|
432
|
+
nextCommands: fixes.slice(0, 1).map((fix) => ({
|
|
433
|
+
command: `moda fix ${fix.id}`,
|
|
434
|
+
purpose: "Open the top-ranked fix (status, gate result, next actions).",
|
|
435
|
+
mutability: "read",
|
|
436
|
+
requires_approval: false
|
|
437
|
+
}))
|
|
438
|
+
});
|
|
439
|
+
return EXIT_OK;
|
|
440
|
+
}
|
|
441
|
+
async function runFixCommand(context) {
|
|
442
|
+
validateConfig();
|
|
443
|
+
const profileOptions = profileOptionsFor(context);
|
|
444
|
+
const tenantId = requireTenantId(context.flags, profileOptions);
|
|
445
|
+
const sub = context.positional;
|
|
446
|
+
if (!sub) {
|
|
447
|
+
throw new CliInputError("A fix id or subcommand is required", FIX_USAGE);
|
|
448
|
+
}
|
|
449
|
+
switch (sub) {
|
|
450
|
+
case "start":
|
|
451
|
+
return runFixStart(context, profileOptions, tenantId);
|
|
452
|
+
case "verify":
|
|
453
|
+
return runFixVerify(context, profileOptions, tenantId);
|
|
454
|
+
case "checkout":
|
|
455
|
+
return runFixCheckout(context, profileOptions, tenantId);
|
|
456
|
+
case "submit":
|
|
457
|
+
return runFixSubmit(context, profileOptions, tenantId);
|
|
458
|
+
case "mark-applied":
|
|
459
|
+
return runFixMarkApplied(context, profileOptions, tenantId);
|
|
460
|
+
case "dismiss":
|
|
461
|
+
return runFixDismiss(context, profileOptions, tenantId);
|
|
462
|
+
default:
|
|
463
|
+
return runFixDetail(context, profileOptions, tenantId, sub);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
async function runFixStart(context, profileOptions, tenantId) {
|
|
467
|
+
const problemId = (context.positionals[1] || context.flags["problem-id"] || "").trim();
|
|
468
|
+
if (!problemId) {
|
|
469
|
+
throw new CliInputError("<problem_id> is required", "Usage: moda fix start <problem_id> [--type=auto|prompt] [--wait] [--timeout=ms] [--poll-interval=ms]");
|
|
470
|
+
}
|
|
471
|
+
const fixType = context.flags.type;
|
|
472
|
+
if (fixType !== undefined && fixType !== "auto" && fixType !== "prompt") {
|
|
473
|
+
throw new CliInputError(`--type must be 'auto' or 'prompt'; got '${fixType}'`);
|
|
474
|
+
}
|
|
475
|
+
const created = await callControlAPI(`/tenants/${encodeURIComponent(tenantId)}/problems/${encodeURIComponent(problemId)}/fixes`, { method: "POST", body: JSON.stringify(fixType ? { fixType } : {}) }, profileOptions, { timeoutMs: 120000, retries: 0 });
|
|
476
|
+
if (context.flags.wait !== "true") {
|
|
477
|
+
if (context.outputMode === "human") {
|
|
478
|
+
process.stderr.write(`Fix MODA-FIX-${created.shortRef} (${created.id}) drafted for problem ${created.problemId} — status ${created.status}.
|
|
479
|
+
`);
|
|
480
|
+
}
|
|
481
|
+
context.output.writeData(created, {
|
|
482
|
+
summary: {
|
|
483
|
+
text: `Fix MODA-FIX-${created.shortRef} drafted (status ${created.status}). Re-run with --wait to drive the pipeline.`,
|
|
484
|
+
confidence: "high"
|
|
485
|
+
},
|
|
486
|
+
nextCommands: [
|
|
487
|
+
{
|
|
488
|
+
command: `moda fix start ${problemId} --wait`,
|
|
489
|
+
purpose: "Drive the pipeline (scope → propose → gate) to a resting status.",
|
|
490
|
+
mutability: "write",
|
|
491
|
+
requires_approval: false
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
command: `moda fix ${created.id}`,
|
|
495
|
+
purpose: "Inspect the fix status and gate result.",
|
|
496
|
+
mutability: "read",
|
|
497
|
+
requires_approval: false
|
|
498
|
+
}
|
|
499
|
+
]
|
|
500
|
+
});
|
|
501
|
+
return EXIT_OK;
|
|
502
|
+
}
|
|
503
|
+
const timeoutMs = parsePositiveInt(context.flags.timeout, DEFAULT_WAIT_TIMEOUT_MS, MAX_WAIT_TIMEOUT_MS);
|
|
504
|
+
const pollIntervalMs = parsePositiveInt(context.flags["poll-interval"], DEFAULT_POLL_INTERVAL_MS, MAX_POLL_INTERVAL_MS);
|
|
505
|
+
const fix = await driveAdvance({
|
|
506
|
+
tenantId,
|
|
507
|
+
fixId: created.id,
|
|
508
|
+
initial: created,
|
|
509
|
+
timeoutMs,
|
|
510
|
+
pollIntervalMs,
|
|
511
|
+
profileOptions,
|
|
512
|
+
context
|
|
513
|
+
});
|
|
514
|
+
const packet = await fetchPacketFailSoft(tenantId, created.id, profileOptions);
|
|
515
|
+
if (context.outputMode === "human") {
|
|
516
|
+
process.stderr.write(`
|
|
517
|
+
Fix MODA-FIX-${fix.shortRef} rested at ${fix.status}${fix.statusReason ? ` — ${fix.statusReason}` : ""}
|
|
518
|
+
`);
|
|
519
|
+
}
|
|
520
|
+
context.output.writeData({ fix, ...packet ? { packet } : {} }, {
|
|
521
|
+
summary: {
|
|
522
|
+
text: `Fix MODA-FIX-${fix.shortRef} reached ${fix.status}${fix.statusReason ? ` (${fix.statusReason})` : ""}.`,
|
|
523
|
+
confidence: "high"
|
|
524
|
+
},
|
|
525
|
+
findings: gateFindings(fix),
|
|
526
|
+
nextCommands: nextCommandsFor(fix)
|
|
527
|
+
});
|
|
528
|
+
return EXIT_OK;
|
|
529
|
+
}
|
|
530
|
+
async function runFixVerify(context, profileOptions, tenantId) {
|
|
531
|
+
const usage = "Usage: moda fix verify <fix_id> [--prompt-file=PATH] [--no-wait] [--timeout=ms] [--poll-interval=ms]";
|
|
532
|
+
const fixId = (context.positionals[1] || context.flags["fix-id"] || "").trim();
|
|
533
|
+
if (!fixId) {
|
|
534
|
+
throw new CliInputError("<fix_id> is required", usage);
|
|
535
|
+
}
|
|
536
|
+
let candidateContent;
|
|
537
|
+
const promptFile = context.flags["prompt-file"];
|
|
538
|
+
if (promptFile) {
|
|
539
|
+
const absPath = isAbsolute(promptFile) ? promptFile : resolve(process.cwd(), promptFile);
|
|
540
|
+
if (!existsSync(absPath)) {
|
|
541
|
+
throw new CliInputError(`--prompt-file not found: ${promptFile}`, usage);
|
|
542
|
+
}
|
|
543
|
+
candidateContent = readFileSync(absPath, "utf8");
|
|
544
|
+
if (candidateContent.trim().length < 40) {
|
|
545
|
+
throw new CliInputError("--prompt-file content is shorter than 40 characters — refusing to gate a truncated candidate");
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
const enqueue = await callControlAPI(`${fixesBase(tenantId)}/${encodeURIComponent(fixId)}/verify`, { method: "POST", body: JSON.stringify(candidateContent ? { candidateContent } : {}) }, profileOptions, { timeoutMs: 120000, retries: 0 });
|
|
549
|
+
const noWait = context.flags.wait === "false" || context.flags["no-wait"] === "true";
|
|
550
|
+
if (noWait) {
|
|
551
|
+
context.output.writeData({ fixId, gateRunId: enqueue.gateRunId, status: "GATING" }, {
|
|
552
|
+
summary: {
|
|
553
|
+
text: `Gate + control runs enqueued (gate run ${enqueue.gateRunId}). Resume with \`moda fix ${fixId} --wait\`.`,
|
|
554
|
+
confidence: "high"
|
|
555
|
+
},
|
|
556
|
+
nextCommands: [
|
|
557
|
+
{
|
|
558
|
+
command: `moda fix ${fixId} --wait`,
|
|
559
|
+
purpose: "Drive the advance-on-poll gate to a verdict.",
|
|
560
|
+
mutability: "write",
|
|
561
|
+
requires_approval: false
|
|
562
|
+
}
|
|
563
|
+
]
|
|
564
|
+
});
|
|
565
|
+
return EXIT_OK;
|
|
566
|
+
}
|
|
567
|
+
const timeoutMs = parsePositiveInt(context.flags.timeout, DEFAULT_WAIT_TIMEOUT_MS, MAX_WAIT_TIMEOUT_MS);
|
|
568
|
+
const pollIntervalMs = parsePositiveInt(context.flags["poll-interval"], DEFAULT_POLL_INTERVAL_MS, MAX_POLL_INTERVAL_MS);
|
|
569
|
+
const deadline = Date.now() + timeoutMs;
|
|
570
|
+
let fix = await advanceFix(tenantId, fixId, profileOptions);
|
|
571
|
+
while (fix.status === "GATING") {
|
|
572
|
+
if (Date.now() >= deadline) {
|
|
573
|
+
throw new Error(`Timed out after ${timeoutMs}ms waiting for gate run ${enqueue.gateRunId} to finish`);
|
|
574
|
+
}
|
|
575
|
+
if (context.outputMode === "human") {
|
|
576
|
+
process.stderr.write(`Waiting for gate run (${fix.statusReason || "running"})...
|
|
577
|
+
`);
|
|
578
|
+
}
|
|
579
|
+
await sleep(pollIntervalMs);
|
|
580
|
+
fix = await advanceFix(tenantId, fixId, profileOptions);
|
|
581
|
+
}
|
|
582
|
+
const warnings = [];
|
|
583
|
+
if (fix.gateRunId && fix.gateRunId !== enqueue.gateRunId) {
|
|
584
|
+
warnings.push(`The reported verdict is for gate run ${fix.gateRunId}, not the one this command enqueued (${enqueue.gateRunId}) — a concurrent verify superseded it.`);
|
|
585
|
+
}
|
|
586
|
+
const gateResult = fix.gateResult ?? null;
|
|
587
|
+
const verdict = gateResult?.verdict ?? "";
|
|
588
|
+
let exitCode;
|
|
589
|
+
let envelopeStatus;
|
|
590
|
+
if (fix.status === "VERIFIED" && verdict === "pass") {
|
|
591
|
+
exitCode = EXIT_OK;
|
|
592
|
+
envelopeStatus = "success";
|
|
593
|
+
} else if (fix.status === "GATE_FAILED" || verdict === "fail") {
|
|
594
|
+
exitCode = EXIT_ERROR;
|
|
595
|
+
envelopeStatus = "error";
|
|
596
|
+
} else {
|
|
597
|
+
exitCode = EXIT_DEGRADED;
|
|
598
|
+
envelopeStatus = "degraded";
|
|
599
|
+
}
|
|
600
|
+
let cases = [];
|
|
601
|
+
if (exitCode === EXIT_ERROR && fix.replaySetId) {
|
|
602
|
+
try {
|
|
603
|
+
const payload = await fetchLatestRun(tenantId, String(fix.replaySetId), enqueue.gateRunId, profileOptions);
|
|
604
|
+
if (payload.run?.runId === enqueue.gateRunId) {
|
|
605
|
+
cases = payload.cases;
|
|
606
|
+
}
|
|
607
|
+
} catch {
|
|
608
|
+
warnings.push("Could not load per-case gate results — see the replay run in the dashboard.");
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
if (context.outputMode === "human") {
|
|
612
|
+
printHumanGateVerdict(fix, enqueue.gateRunId, gateResult, cases);
|
|
613
|
+
}
|
|
614
|
+
const findings = gateFindings(fix);
|
|
615
|
+
for (const row of cases.slice(0, PER_CASE_FINDINGS_CAP)) {
|
|
616
|
+
const baseline = passLabel(row.prodPass);
|
|
617
|
+
const candidate = passLabel(row.proposedPass);
|
|
618
|
+
findings.push({
|
|
619
|
+
id: `case:${row.caseId}`,
|
|
620
|
+
title: `${row.caseId}: baseline=${baseline} candidate=${candidate}`,
|
|
621
|
+
status: candidate,
|
|
622
|
+
severity: baseline === "pass" && candidate === "fail" ? "error" : candidate === "fail" ? "warning" : "info"
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
context.output.writeData({
|
|
626
|
+
fixId: fix.id,
|
|
627
|
+
shortRef: fix.shortRef,
|
|
628
|
+
status: fix.status,
|
|
629
|
+
gateRunId: enqueue.gateRunId,
|
|
630
|
+
verdict: verdict || null,
|
|
631
|
+
gateResult,
|
|
632
|
+
...cases.length > 0 ? { cases } : {}
|
|
633
|
+
}, {
|
|
634
|
+
status: envelopeStatus,
|
|
635
|
+
summary: {
|
|
636
|
+
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"}`,
|
|
637
|
+
confidence: "high"
|
|
638
|
+
},
|
|
639
|
+
findings,
|
|
640
|
+
warnings,
|
|
641
|
+
nextCommands: nextCommandsFor(fix)
|
|
642
|
+
});
|
|
643
|
+
return exitCode;
|
|
644
|
+
}
|
|
645
|
+
async function runFixCheckout(context, profileOptions, tenantId) {
|
|
646
|
+
const fixId = (context.positionals[1] || context.flags["fix-id"] || "").trim();
|
|
647
|
+
if (!fixId) {
|
|
648
|
+
throw new CliInputError("<fix_id> is required", "Usage: moda fix checkout <fix_id>");
|
|
649
|
+
}
|
|
650
|
+
const fix = await getFix(tenantId, fixId, profileOptions);
|
|
651
|
+
if (isArtifactFix(fix)) {
|
|
652
|
+
throw new CliInputError(`Fix MODA-FIX-${fix.shortRef} is a ${fix.fixType} fix — its candidate is applied in your own stack, not checked out. Run \`moda fix ${fixId} --packet\` for the candidate (written as local artifacts), apply it, then confirm with \`moda fix mark-applied ${fixId}\`.`);
|
|
653
|
+
}
|
|
654
|
+
if (!fix.targetSourcePath) {
|
|
655
|
+
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.`);
|
|
656
|
+
}
|
|
657
|
+
const candidateVersionId = fix.candidateRef?.promptVersionId ?? "";
|
|
658
|
+
if (!candidateVersionId) {
|
|
659
|
+
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>\`.`);
|
|
660
|
+
}
|
|
661
|
+
if (!fix.promptId) {
|
|
662
|
+
throw new CliInputError(`Fix MODA-FIX-${fix.shortRef} has no routed prompt — the candidate content cannot be resolved.`);
|
|
663
|
+
}
|
|
664
|
+
const prompt = await callControlAPI(`/prompts/${encodeURIComponent(String(fix.promptId))}`, { method: "GET" }, profileOptions);
|
|
665
|
+
const version = (prompt.versions ?? []).find((row) => row.id === candidateVersionId);
|
|
666
|
+
if (!version || typeof version.content !== "string" || !version.content) {
|
|
667
|
+
throw new Error(`Candidate prompt version ${candidateVersionId} not found in the registry`);
|
|
668
|
+
}
|
|
669
|
+
const relPath = safeTargetSourcePath(String(fix.targetSourcePath));
|
|
670
|
+
const absPath = join(process.cwd(), relPath);
|
|
671
|
+
assertRealpathContained(absPath, relPath);
|
|
672
|
+
const writeStatus = writeContainedFile(absPath, relPath, version.content);
|
|
673
|
+
const branch = `moda/fix/${String(fix.shortRef).toLowerCase()}`;
|
|
674
|
+
const magicWord = `Fixes MODA-FIX-${fix.shortRef}`;
|
|
675
|
+
if (context.outputMode === "human") {
|
|
676
|
+
process.stderr.write([
|
|
677
|
+
`${writeStatus} ${relPath} (candidate ${candidateVersionId})`,
|
|
678
|
+
"",
|
|
679
|
+
"Next steps (the CLI never runs git — branching and committing are yours):",
|
|
680
|
+
` 1. git checkout -b ${branch}`,
|
|
681
|
+
` 2. Review the diff. Re-gate any edits: moda fix verify ${fix.id} --prompt-file=${relPath}`,
|
|
682
|
+
` 3. Hand back: moda fix submit ${fix.id} --pr (or --local-ref=${branch})`,
|
|
683
|
+
` A hand-authored PR body must end with the magic word: ${magicWord}`,
|
|
684
|
+
""
|
|
685
|
+
].join(`
|
|
686
|
+
`));
|
|
687
|
+
}
|
|
688
|
+
context.output.writeData({
|
|
689
|
+
fixId: fix.id,
|
|
690
|
+
shortRef: fix.shortRef,
|
|
691
|
+
path: relPath,
|
|
692
|
+
writeStatus,
|
|
693
|
+
branch,
|
|
694
|
+
magicWord,
|
|
695
|
+
candidateVersionId
|
|
696
|
+
}, {
|
|
697
|
+
summary: {
|
|
698
|
+
text: `Candidate for MODA-FIX-${fix.shortRef} ${writeStatus === "unchanged" ? "already matches" : "written to"} ${relPath}. Create branch ${branch} yourself — the CLI never runs git.`,
|
|
699
|
+
confidence: "high"
|
|
700
|
+
},
|
|
701
|
+
artifacts: [
|
|
702
|
+
{
|
|
703
|
+
kind: "fix_candidate",
|
|
704
|
+
path: relPath,
|
|
705
|
+
description: `Candidate content for fix MODA-FIX-${fix.shortRef} (prompt version ${candidateVersionId}).`
|
|
706
|
+
}
|
|
707
|
+
],
|
|
708
|
+
nextCommands: [
|
|
709
|
+
{
|
|
710
|
+
command: `moda fix verify ${fix.id} --prompt-file=${relPath}`,
|
|
711
|
+
purpose: "Re-gate the (possibly edited) local candidate on the frozen holdout.",
|
|
712
|
+
mutability: "write",
|
|
713
|
+
requires_approval: false
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
command: `moda fix submit ${fix.id} --pr`,
|
|
717
|
+
purpose: `Open the draft fix PR (branch ${branch}, magic word "${magicWord}").`,
|
|
718
|
+
mutability: "write",
|
|
719
|
+
requires_approval: true
|
|
720
|
+
}
|
|
721
|
+
]
|
|
722
|
+
});
|
|
723
|
+
return EXIT_OK;
|
|
724
|
+
}
|
|
725
|
+
async function runFixSubmit(context, profileOptions, tenantId) {
|
|
726
|
+
const usage = "Usage: moda fix submit <fix_id> --pr | --local-ref=<branch>";
|
|
727
|
+
const fixId = (context.positionals[1] || context.flags["fix-id"] || "").trim();
|
|
728
|
+
if (!fixId) {
|
|
729
|
+
throw new CliInputError("<fix_id> is required", usage);
|
|
730
|
+
}
|
|
731
|
+
const wantPr = context.flags.pr === "true";
|
|
732
|
+
const localRef = (context.flags["local-ref"] ?? "").trim();
|
|
733
|
+
if (wantPr === (localRef !== "")) {
|
|
734
|
+
throw new CliInputError("Pass exactly one of --pr or --local-ref=<branch>", usage);
|
|
735
|
+
}
|
|
736
|
+
const result = await callControlAPI(`${fixesBase(tenantId)}/${encodeURIComponent(fixId)}/submit`, {
|
|
737
|
+
method: "POST",
|
|
738
|
+
body: JSON.stringify(wantPr ? { channel: "pr" } : { channel: "local_ref", localRef })
|
|
739
|
+
}, profileOptions, { timeoutMs: 120000, retries: 0 });
|
|
740
|
+
if (context.outputMode === "human") {
|
|
741
|
+
process.stderr.write(result.prUrl ? `Draft PR open: ${result.prUrl} (${result.prBranch ?? ""}) — merging it confirms the fix.
|
|
742
|
+
` : `Fix recorded as ${result.status}${result.prBranch ? ` on ${result.prBranch}` : ""}.
|
|
743
|
+
`);
|
|
744
|
+
}
|
|
745
|
+
context.output.writeData({ fixId, ...result }, {
|
|
746
|
+
summary: {
|
|
747
|
+
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.`,
|
|
748
|
+
confidence: "high"
|
|
749
|
+
},
|
|
750
|
+
nextCommands: [
|
|
751
|
+
{
|
|
752
|
+
command: `moda fix ${fixId}`,
|
|
753
|
+
purpose: "Track delivery and confirmation status.",
|
|
754
|
+
mutability: "read",
|
|
755
|
+
requires_approval: false
|
|
756
|
+
}
|
|
757
|
+
]
|
|
758
|
+
});
|
|
759
|
+
return EXIT_OK;
|
|
760
|
+
}
|
|
761
|
+
async function runFixMarkApplied(context, profileOptions, tenantId) {
|
|
762
|
+
const usage = "Usage: moda fix mark-applied <fix_id> [--note=TEXT]";
|
|
763
|
+
const fixId = (context.positionals[1] || context.flags["fix-id"] || "").trim();
|
|
764
|
+
if (!fixId) {
|
|
765
|
+
throw new CliInputError("<fix_id> is required", usage);
|
|
766
|
+
}
|
|
767
|
+
const note = (context.flags.note ?? "").trim();
|
|
768
|
+
if (note.length > 600) {
|
|
769
|
+
throw new CliInputError(`--note must be 600 characters or fewer (got ${note.length})`, usage);
|
|
770
|
+
}
|
|
771
|
+
const fix = await callControlAPI(`${fixesBase(tenantId)}/${encodeURIComponent(fixId)}/mark-applied`, { method: "POST", body: JSON.stringify(note ? { note } : {}) }, profileOptions, { timeoutMs: 60000, retries: 0 });
|
|
772
|
+
if (context.outputMode === "human") {
|
|
773
|
+
process.stderr.write(fix.duplicate === true ? `Fix MODA-FIX-${fix.shortRef} was already confirmed (${fix.status}) — duplicate no-op; the mark_fixed feedback was re-posted.
|
|
774
|
+
` : `Fix MODA-FIX-${fix.shortRef} marked applied (${fix.status}) — mark_fixed feedback posted, monitoring for recurrence.
|
|
775
|
+
`);
|
|
776
|
+
}
|
|
777
|
+
context.output.writeData(fix, {
|
|
778
|
+
summary: {
|
|
779
|
+
text: fix.duplicate === true ? `Fix MODA-FIX-${fix.shortRef} was already confirmed (${fix.status}); mark-applied was a duplicate no-op.` : `Fix MODA-FIX-${fix.shortRef} marked applied — status ${fix.status}; the problem enters fixed-monitoring.`,
|
|
780
|
+
confidence: "high"
|
|
781
|
+
},
|
|
782
|
+
nextCommands: [
|
|
783
|
+
{
|
|
784
|
+
command: `moda fix ${fix.id}`,
|
|
785
|
+
purpose: "Track confirmation monitoring (HELD after 14 clean days; REGRESSED on reopen).",
|
|
786
|
+
mutability: "read",
|
|
787
|
+
requires_approval: false
|
|
788
|
+
}
|
|
789
|
+
]
|
|
790
|
+
});
|
|
791
|
+
return EXIT_OK;
|
|
792
|
+
}
|
|
793
|
+
async function runFixDismiss(context, profileOptions, tenantId) {
|
|
794
|
+
const usage = "Usage: moda fix dismiss <fix_id> --reason=TEXT";
|
|
795
|
+
const fixId = (context.positionals[1] || context.flags["fix-id"] || "").trim();
|
|
796
|
+
if (!fixId) {
|
|
797
|
+
throw new CliInputError("<fix_id> is required", usage);
|
|
798
|
+
}
|
|
799
|
+
const reason = context.flags.reason;
|
|
800
|
+
if (!reason || !reason.trim()) {
|
|
801
|
+
throw new CliInputError("--reason=TEXT is required (it feeds problem feedback)", usage);
|
|
802
|
+
}
|
|
803
|
+
const fix = await callControlAPI(`${fixesBase(tenantId)}/${encodeURIComponent(fixId)}/dismiss`, { method: "POST", body: JSON.stringify({ reason }) }, profileOptions, { timeoutMs: 60000, retries: 0 });
|
|
804
|
+
if (context.outputMode === "human") {
|
|
805
|
+
process.stderr.write(`Fix MODA-FIX-${fix.shortRef} dismissed (${fix.status}).
|
|
806
|
+
`);
|
|
807
|
+
}
|
|
808
|
+
context.output.writeData(fix, {
|
|
809
|
+
summary: {
|
|
810
|
+
text: `Fix MODA-FIX-${fix.shortRef} dismissed — the reason was recorded as problem feedback.`,
|
|
811
|
+
confidence: "high"
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
return EXIT_OK;
|
|
815
|
+
}
|
|
816
|
+
async function runFixDetail(context, profileOptions, tenantId, fixId) {
|
|
817
|
+
if (context.flags.packet === "true") {
|
|
818
|
+
const packet = await callControlAPI(`${fixesBase(tenantId)}/${encodeURIComponent(fixId)}/packet`, { method: "GET" }, profileOptions);
|
|
819
|
+
const extras = materializePacketCandidates(packet, fixId, context.outputMode === "human");
|
|
820
|
+
context.output.writeData(packet, {
|
|
821
|
+
summary: {
|
|
822
|
+
text: `moda.fix_packet.v1 for fix ${fixId}.${extras.summarySuffix}`,
|
|
823
|
+
confidence: "high"
|
|
824
|
+
},
|
|
825
|
+
...extras.findings.length > 0 ? { findings: extras.findings } : {},
|
|
826
|
+
...extras.artifacts.length > 0 ? { artifacts: extras.artifacts } : {},
|
|
827
|
+
...extras.warnings.length > 0 ? { warnings: extras.warnings } : {},
|
|
828
|
+
...extras.nextCommands.length > 0 ? { nextCommands: extras.nextCommands } : {}
|
|
829
|
+
});
|
|
830
|
+
return EXIT_OK;
|
|
831
|
+
}
|
|
832
|
+
let fix = await getFix(tenantId, fixId, profileOptions);
|
|
833
|
+
if (context.flags.wait === "true" && ADVANCING_STATUSES.has(fix.status)) {
|
|
834
|
+
const timeoutMs = parsePositiveInt(context.flags.timeout, DEFAULT_WAIT_TIMEOUT_MS, MAX_WAIT_TIMEOUT_MS);
|
|
835
|
+
const pollIntervalMs = parsePositiveInt(context.flags["poll-interval"], DEFAULT_POLL_INTERVAL_MS, MAX_POLL_INTERVAL_MS);
|
|
836
|
+
fix = await driveAdvance({
|
|
837
|
+
tenantId,
|
|
838
|
+
fixId,
|
|
839
|
+
initial: fix,
|
|
840
|
+
timeoutMs,
|
|
841
|
+
pollIntervalMs,
|
|
842
|
+
profileOptions,
|
|
843
|
+
context
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
if (context.outputMode === "human") {
|
|
847
|
+
process.stderr.write(`MODA-FIX-${fix.shortRef} ${fix.status} ${fix.fixType}${fix.statusReason ? ` — ${fix.statusReason}` : ""}
|
|
848
|
+
`);
|
|
849
|
+
}
|
|
850
|
+
context.output.writeData(fix, {
|
|
851
|
+
summary: {
|
|
852
|
+
text: `Fix MODA-FIX-${fix.shortRef} is ${fix.status}${fix.statusReason ? ` (${fix.statusReason})` : ""}.`,
|
|
853
|
+
confidence: "high"
|
|
854
|
+
},
|
|
855
|
+
findings: gateFindings(fix),
|
|
856
|
+
nextCommands: nextCommandsFor(fix)
|
|
857
|
+
});
|
|
858
|
+
return EXIT_OK;
|
|
859
|
+
}
|
|
860
|
+
async function driveAdvance(args) {
|
|
861
|
+
const deadline = Date.now() + args.timeoutMs;
|
|
862
|
+
let fix = args.initial;
|
|
863
|
+
let lastStatus = "";
|
|
864
|
+
const announce = (row) => {
|
|
865
|
+
if (row.status === lastStatus)
|
|
866
|
+
return;
|
|
867
|
+
lastStatus = row.status;
|
|
868
|
+
const line = `fix MODA-FIX-${row.shortRef}: ${row.status}${row.statusReason ? ` — ${row.statusReason}` : ""}`;
|
|
869
|
+
if (args.context.outputMode === "human") {
|
|
870
|
+
process.stderr.write(`${line}
|
|
871
|
+
`);
|
|
872
|
+
} else if (args.context.outputMode === "agent-stream") {
|
|
873
|
+
args.context.output.writeEvent({
|
|
874
|
+
event: "progress",
|
|
875
|
+
phase: "fix",
|
|
876
|
+
message: line,
|
|
877
|
+
elapsed_ms: Date.now() - args.context.startedAt
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
};
|
|
881
|
+
announce(fix);
|
|
882
|
+
while (ADVANCING_STATUSES.has(fix.status)) {
|
|
883
|
+
if (Date.now() >= deadline) {
|
|
884
|
+
throw new Error(`Timed out after ${args.timeoutMs}ms waiting for fix ${args.fixId} (status ${fix.status})`);
|
|
885
|
+
}
|
|
886
|
+
fix = await advanceFix(args.tenantId, args.fixId, args.profileOptions);
|
|
887
|
+
announce(fix);
|
|
888
|
+
if (!ADVANCING_STATUSES.has(fix.status))
|
|
889
|
+
break;
|
|
890
|
+
if (fix.status === "PROPOSED" && /gate (pending|unavailable)/i.test(fix.statusReason ?? "")) {
|
|
891
|
+
break;
|
|
892
|
+
}
|
|
893
|
+
await sleep(fix.status === "GATING" ? args.pollIntervalMs : Math.min(STEP_DELAY_MS, args.pollIntervalMs));
|
|
894
|
+
}
|
|
895
|
+
return fix;
|
|
896
|
+
}
|
|
897
|
+
function profileOptionsFor(context) {
|
|
898
|
+
return { profile: context.profile, env: context.env };
|
|
899
|
+
}
|
|
900
|
+
function requireTenantId(flags, profileOptions) {
|
|
901
|
+
const tenantId = flags["tenant-id"] || resolveTenantId(profileOptions);
|
|
902
|
+
if (!tenantId) {
|
|
903
|
+
throw new CliInputError("Missing tenant id. Run `moda init` or pass --tenant-id=<id>.");
|
|
904
|
+
}
|
|
905
|
+
return tenantId;
|
|
906
|
+
}
|
|
907
|
+
function fixesBase(tenantId) {
|
|
908
|
+
return `/tenants/${encodeURIComponent(tenantId)}/fixes`;
|
|
909
|
+
}
|
|
910
|
+
async function getFix(tenantId, fixId, profileOptions) {
|
|
911
|
+
return await callControlAPI(`${fixesBase(tenantId)}/${encodeURIComponent(fixId)}`, { method: "GET" }, profileOptions);
|
|
912
|
+
}
|
|
913
|
+
async function advanceFix(tenantId, fixId, profileOptions) {
|
|
914
|
+
return await callControlAPI(`${fixesBase(tenantId)}/${encodeURIComponent(fixId)}/advance`, { method: "POST" }, profileOptions, { timeoutMs: ADVANCE_TIMEOUT_MS, retries: 0 });
|
|
915
|
+
}
|
|
916
|
+
async function fetchPacketFailSoft(tenantId, fixId, profileOptions) {
|
|
917
|
+
try {
|
|
918
|
+
return await callControlAPI(`${fixesBase(tenantId)}/${encodeURIComponent(fixId)}/packet`, { method: "GET" }, profileOptions);
|
|
919
|
+
} catch {
|
|
920
|
+
return null;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
function isArtifactFix(fix) {
|
|
924
|
+
return fix.fixType === "TOOL_SCHEMA" || fix.fixType === "SKILL";
|
|
925
|
+
}
|
|
926
|
+
function materializePacketCandidates(packet, fixId, human) {
|
|
927
|
+
const findings = [];
|
|
928
|
+
const artifacts = [];
|
|
929
|
+
const warnings = [];
|
|
930
|
+
const nextCommands = [];
|
|
931
|
+
const summaryParts = [];
|
|
932
|
+
const tool = packet.candidate_tool;
|
|
933
|
+
const skill = packet.candidate_skill;
|
|
934
|
+
const hasTool = typeof tool?.proposed_description === "string" && tool.proposed_description !== "";
|
|
935
|
+
const hasSkill = typeof skill?.skill_md === "string" && skill.skill_md !== "";
|
|
936
|
+
if (!hasTool && !hasSkill) {
|
|
937
|
+
return { findings, artifacts, warnings, nextCommands, summarySuffix: "" };
|
|
938
|
+
}
|
|
939
|
+
const rawRef = typeof packet.short_ref === "string" && packet.short_ref.trim() ? packet.short_ref : fixId;
|
|
940
|
+
const ref = rawRef.replace(/[^A-Za-z0-9._-]/g, "").replace(/^\.+/, "").slice(0, 64) || "fix";
|
|
941
|
+
const w = (line) => process.stderr.write(`${line}
|
|
942
|
+
`);
|
|
943
|
+
const block = (label, text) => {
|
|
944
|
+
w(` ${label}:`);
|
|
945
|
+
for (const line of (text || "(none)").split(`
|
|
946
|
+
`))
|
|
947
|
+
w(` ${line}`);
|
|
948
|
+
};
|
|
949
|
+
const writeCandidateFile = (name, content, kind, description) => {
|
|
950
|
+
const relPath = safeTargetSourcePath(`.moda/fixes/${ref}/${name}`);
|
|
951
|
+
const absPath = join(process.cwd(), relPath);
|
|
952
|
+
try {
|
|
953
|
+
assertRealpathContained(absPath, relPath);
|
|
954
|
+
writeContainedFile(absPath, relPath, content);
|
|
955
|
+
artifacts.push({ kind, path: relPath, description });
|
|
956
|
+
return relPath;
|
|
957
|
+
} catch (error) {
|
|
958
|
+
warnings.push(`Could not write ${relPath}: ${error instanceof Error ? error.message : String(error)} — the full candidate text is still in the packet payload.`);
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
961
|
+
};
|
|
962
|
+
if (hasTool && tool) {
|
|
963
|
+
const toolName = typeof tool.tool_name === "string" && tool.tool_name ? tool.tool_name : "unknown-tool";
|
|
964
|
+
const source = typeof tool.current_description_source === "string" ? tool.current_description_source : "";
|
|
965
|
+
const paramNotes = (Array.isArray(tool.param_notes) ? tool.param_notes : []).filter((note) => typeof note === "string" && note !== "");
|
|
966
|
+
const rationale = typeof tool.rationale === "string" ? tool.rationale : "";
|
|
967
|
+
const currentPath = writeCandidateFile("tool-description.current.md", typeof tool.current_description === "string" ? tool.current_description : "", "fix_tool_description_current", `Current description of tool '${toolName}'${source ? ` (from ${source})` : ""} — the diff base.`);
|
|
968
|
+
const proposedPath = writeCandidateFile("tool-description.proposed.md", tool.proposed_description ?? "", "fix_tool_description_proposed", `Proposed description for tool '${toolName}' — apply it to your tool definition, then \`moda fix mark-applied ${fixId}\`.`);
|
|
969
|
+
findings.push({
|
|
970
|
+
id: "fix:candidate_tool",
|
|
971
|
+
title: `tool '${toolName}' description candidate${source ? ` (current from ${source})` : ""}`,
|
|
972
|
+
status: "proposed",
|
|
973
|
+
severity: "info",
|
|
974
|
+
...rationale || paramNotes.length > 0 ? {
|
|
975
|
+
description: [rationale, ...paramNotes.map((note) => `param note: ${note}`)].filter(Boolean).join(`
|
|
976
|
+
`)
|
|
977
|
+
} : {}
|
|
978
|
+
});
|
|
979
|
+
summaryParts.push(`Includes a tool-description candidate for '${toolName}'.`);
|
|
980
|
+
if (human) {
|
|
981
|
+
w("");
|
|
982
|
+
w(`Tool-description candidate — '${toolName}'${source ? ` (current description from ${source})` : ""}`);
|
|
983
|
+
block("current ", tool.current_description ?? "");
|
|
984
|
+
block("proposed", tool.proposed_description ?? "");
|
|
985
|
+
if (paramNotes.length > 0)
|
|
986
|
+
block("param notes", paramNotes.join(`
|
|
987
|
+
`));
|
|
988
|
+
if (rationale)
|
|
989
|
+
block("rationale", rationale);
|
|
990
|
+
if (currentPath && proposedPath) {
|
|
991
|
+
w(` diff: diff ${currentPath} ${proposedPath}`);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
if (hasSkill && skill) {
|
|
996
|
+
const skillName = typeof skill.skill_name === "string" && skill.skill_name ? skill.skill_name : "moda-fix-skill";
|
|
997
|
+
const skillPath = writeCandidateFile("SKILL.md", skill.skill_md ?? "", "fix_candidate_skill", `Drafted SKILL.md for '${skillName}' — install it (for example .claude/skills/${skillName}/SKILL.md), then \`moda fix mark-applied ${fixId}\`.`);
|
|
998
|
+
findings.push({
|
|
999
|
+
id: "fix:candidate_skill",
|
|
1000
|
+
title: `skill '${skillName}' drafted (SKILL.md, ${(skill.skill_md ?? "").length} chars)`,
|
|
1001
|
+
status: "proposed",
|
|
1002
|
+
severity: "info",
|
|
1003
|
+
description: `Install the skill in your agent, then confirm with \`moda fix mark-applied ${fixId}\`. Gating on the replay holdout stays pending until the skill is registered with the skill harness.`
|
|
1004
|
+
});
|
|
1005
|
+
summaryParts.push(`Includes a drafted SKILL.md candidate ('${skillName}').`);
|
|
1006
|
+
if (human) {
|
|
1007
|
+
w("");
|
|
1008
|
+
w(`Skill candidate — '${skillName}'`);
|
|
1009
|
+
if (skillPath)
|
|
1010
|
+
w(` SKILL.md written to ${skillPath}`);
|
|
1011
|
+
w(` install it (for example .claude/skills/${skillName}/SKILL.md), then confirm:`);
|
|
1012
|
+
w(` moda fix mark-applied ${fixId} --note="installed skill ${skillName}"`);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
nextCommands.push({
|
|
1016
|
+
command: `moda fix mark-applied ${fixId} --note="where/how you applied it"`,
|
|
1017
|
+
purpose: "Confirm you applied the candidate in your own stack (no PR) — flips the fix to SHIPPED and starts recurrence monitoring.",
|
|
1018
|
+
mutability: "write",
|
|
1019
|
+
requires_approval: true
|
|
1020
|
+
});
|
|
1021
|
+
return {
|
|
1022
|
+
findings,
|
|
1023
|
+
artifacts,
|
|
1024
|
+
warnings,
|
|
1025
|
+
nextCommands,
|
|
1026
|
+
summarySuffix: summaryParts.length > 0 ? ` ${summaryParts.join(" ")}` : ""
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
function gateFindings(fix) {
|
|
1030
|
+
const gateResult = fix.gateResult ?? null;
|
|
1031
|
+
if (!gateResult)
|
|
1032
|
+
return [];
|
|
1033
|
+
const verdict = gateResult.verdict ?? fix.status;
|
|
1034
|
+
return [
|
|
1035
|
+
{
|
|
1036
|
+
id: "fix:gate",
|
|
1037
|
+
title: `gate ${verdict}: ${gateResult.reason ?? fix.statusReason ?? ""}`.trim(),
|
|
1038
|
+
status: verdict,
|
|
1039
|
+
severity: verdict === "pass" ? "info" : verdict === "fail" ? "error" : "warning",
|
|
1040
|
+
description: formatGateScoreboard(gateResult)
|
|
1041
|
+
}
|
|
1042
|
+
];
|
|
1043
|
+
}
|
|
1044
|
+
function formatGateScoreboard(gate) {
|
|
1045
|
+
const holdout = gate.holdout ?? {};
|
|
1046
|
+
const repair = gate.repair ?? {};
|
|
1047
|
+
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}`;
|
|
1048
|
+
}
|
|
1049
|
+
function printHumanGateVerdict(fix, gateRunId, gateResult, cases) {
|
|
1050
|
+
const w = (line) => process.stderr.write(`${line}
|
|
1051
|
+
`);
|
|
1052
|
+
w("");
|
|
1053
|
+
w("Fix gate verdict");
|
|
1054
|
+
w("----------------");
|
|
1055
|
+
w(`Fix: MODA-FIX-${fix.shortRef} (${fix.status})`);
|
|
1056
|
+
w(`Gate run: ${gateRunId}`);
|
|
1057
|
+
w(`Verdict: ${gateResult?.verdict ?? "none"}${gateResult?.reason ? ` — ${gateResult.reason}` : ""}`);
|
|
1058
|
+
if (gateResult) {
|
|
1059
|
+
w(`Score: ${formatGateScoreboard(gateResult)}`);
|
|
1060
|
+
}
|
|
1061
|
+
if (cases.length > 0) {
|
|
1062
|
+
w("");
|
|
1063
|
+
w("Per-case (holdout is the verdict; repair rows are training cases):");
|
|
1064
|
+
for (const row of cases) {
|
|
1065
|
+
w(` ${row.caseId}: baseline=${passLabel(row.prodPass)} candidate=${passLabel(row.proposedPass)}`);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
w("");
|
|
1069
|
+
}
|
|
1070
|
+
function passLabel(value) {
|
|
1071
|
+
if (value === null || value === undefined)
|
|
1072
|
+
return "abstained";
|
|
1073
|
+
return value ? "pass" : "fail";
|
|
1074
|
+
}
|
|
1075
|
+
function nextCommandsFor(fix) {
|
|
1076
|
+
const packetCommand = {
|
|
1077
|
+
command: `moda fix ${fix.id} --packet`,
|
|
1078
|
+
purpose: "Read the packet — the tool/skill candidate is written as local artifacts to apply in your own stack.",
|
|
1079
|
+
mutability: "write",
|
|
1080
|
+
requires_approval: false
|
|
1081
|
+
};
|
|
1082
|
+
const markAppliedCommand = {
|
|
1083
|
+
command: `moda fix mark-applied ${fix.id} --note="where/how you applied it"`,
|
|
1084
|
+
purpose: "Confirm you applied the candidate in your own stack (no PR) — flips the fix to SHIPPED and starts recurrence monitoring.",
|
|
1085
|
+
mutability: "write",
|
|
1086
|
+
requires_approval: true
|
|
1087
|
+
};
|
|
1088
|
+
const skillGatePending = fix.fixType === "SKILL" && fix.candidateRef?.kind === "skill" && !fix.candidateRef?.skillId;
|
|
1089
|
+
switch (fix.status) {
|
|
1090
|
+
case "VERIFIED":
|
|
1091
|
+
if (isArtifactFix(fix)) {
|
|
1092
|
+
return [packetCommand, markAppliedCommand];
|
|
1093
|
+
}
|
|
1094
|
+
return [
|
|
1095
|
+
{
|
|
1096
|
+
command: `moda fix submit ${fix.id} --pr`,
|
|
1097
|
+
purpose: "Open the draft fix PR (merge with the magic word to confirm).",
|
|
1098
|
+
mutability: "write",
|
|
1099
|
+
requires_approval: true
|
|
1100
|
+
},
|
|
1101
|
+
{
|
|
1102
|
+
command: `moda fix checkout ${fix.id}`,
|
|
1103
|
+
purpose: "Write the verified candidate locally instead of opening a PR.",
|
|
1104
|
+
mutability: "write",
|
|
1105
|
+
requires_approval: false
|
|
1106
|
+
}
|
|
1107
|
+
];
|
|
1108
|
+
case "PROPOSED":
|
|
1109
|
+
if (skillGatePending) {
|
|
1110
|
+
return [packetCommand, markAppliedCommand];
|
|
1111
|
+
}
|
|
1112
|
+
return [
|
|
1113
|
+
{
|
|
1114
|
+
command: `moda fix verify ${fix.id}`,
|
|
1115
|
+
purpose: "Gate the proposed candidate on the frozen holdout.",
|
|
1116
|
+
mutability: "write",
|
|
1117
|
+
requires_approval: false
|
|
1118
|
+
}
|
|
1119
|
+
];
|
|
1120
|
+
case "GATE_FAILED":
|
|
1121
|
+
if (isArtifactFix(fix)) {
|
|
1122
|
+
return [
|
|
1123
|
+
{
|
|
1124
|
+
command: `moda fix verify ${fix.id}`,
|
|
1125
|
+
purpose: "Re-gate the stored candidate on the frozen holdout.",
|
|
1126
|
+
mutability: "write",
|
|
1127
|
+
requires_approval: false
|
|
1128
|
+
},
|
|
1129
|
+
packetCommand
|
|
1130
|
+
];
|
|
1131
|
+
}
|
|
1132
|
+
return [
|
|
1133
|
+
{
|
|
1134
|
+
command: `moda fix checkout ${fix.id}`,
|
|
1135
|
+
purpose: "Check out the candidate, edit it, and re-gate (fail-to-pass loop).",
|
|
1136
|
+
mutability: "write",
|
|
1137
|
+
requires_approval: false
|
|
1138
|
+
},
|
|
1139
|
+
{
|
|
1140
|
+
command: `moda fix verify ${fix.id} --prompt-file=<path>`,
|
|
1141
|
+
purpose: "Re-gate an edited local candidate on the frozen holdout.",
|
|
1142
|
+
mutability: "write",
|
|
1143
|
+
requires_approval: false
|
|
1144
|
+
}
|
|
1145
|
+
];
|
|
1146
|
+
case "GATE_INCONCLUSIVE":
|
|
1147
|
+
return [
|
|
1148
|
+
{
|
|
1149
|
+
command: `moda fix verify ${fix.id}`,
|
|
1150
|
+
purpose: "Re-run the gate — the last run was inconclusive (never a pass).",
|
|
1151
|
+
mutability: "write",
|
|
1152
|
+
requires_approval: false
|
|
1153
|
+
}
|
|
1154
|
+
];
|
|
1155
|
+
case "UNROUTED":
|
|
1156
|
+
case "NO_EVIDENCE":
|
|
1157
|
+
case "BLOCKED_COVERAGE":
|
|
1158
|
+
case "NO_HEADROOM":
|
|
1159
|
+
return [
|
|
1160
|
+
{
|
|
1161
|
+
command: `moda fix ${fix.id} --packet`,
|
|
1162
|
+
purpose: "Read the investigation packet — this fix is deliverable as diagnosis.",
|
|
1163
|
+
mutability: "read",
|
|
1164
|
+
requires_approval: false
|
|
1165
|
+
}
|
|
1166
|
+
];
|
|
1167
|
+
case "DRAFT":
|
|
1168
|
+
case "SCOPING":
|
|
1169
|
+
case "PROPOSING":
|
|
1170
|
+
case "GATING":
|
|
1171
|
+
return [
|
|
1172
|
+
{
|
|
1173
|
+
command: `moda fix ${fix.id} --wait`,
|
|
1174
|
+
purpose: "Drive the advance-on-poll pipeline to its next resting status.",
|
|
1175
|
+
mutability: "write",
|
|
1176
|
+
requires_approval: false
|
|
1177
|
+
}
|
|
1178
|
+
];
|
|
1179
|
+
default:
|
|
1180
|
+
return [
|
|
1181
|
+
{
|
|
1182
|
+
command: `moda fix ${fix.id}`,
|
|
1183
|
+
purpose: "Track the fix status.",
|
|
1184
|
+
mutability: "read",
|
|
1185
|
+
requires_approval: false
|
|
1186
|
+
}
|
|
1187
|
+
];
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
function assertRealpathContained(absPath, relPath) {
|
|
1191
|
+
const root = realpathSync(process.cwd());
|
|
1192
|
+
const within = (p) => p === root || p.startsWith(root + sep);
|
|
1193
|
+
const segments = absPath.slice(root.length).split(sep).filter(Boolean);
|
|
1194
|
+
let probe = root;
|
|
1195
|
+
for (const segment of segments) {
|
|
1196
|
+
probe = join(probe, segment);
|
|
1197
|
+
let stat;
|
|
1198
|
+
try {
|
|
1199
|
+
stat = lstatSync(probe);
|
|
1200
|
+
} catch {
|
|
1201
|
+
break;
|
|
1202
|
+
}
|
|
1203
|
+
if (stat.isSymbolicLink()) {
|
|
1204
|
+
throw new CliInputError(`Refusing to write through a symlink: ${relPath}`);
|
|
1205
|
+
}
|
|
1206
|
+
if (stat.isFile() && stat.nlink > 1) {
|
|
1207
|
+
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.`);
|
|
1208
|
+
}
|
|
1209
|
+
if (!within(realpathSync(probe))) {
|
|
1210
|
+
throw new CliInputError(`Refusing to write outside the repo: ${relPath} resolves past the working tree`);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
function writeContainedFile(absPath, relPath, content) {
|
|
1215
|
+
mkdirSync(dirname(absPath), { recursive: true });
|
|
1216
|
+
const existedBefore = existsSync(absPath);
|
|
1217
|
+
let fd;
|
|
1218
|
+
try {
|
|
1219
|
+
fd = openSync(absPath, fsConstants.O_RDWR | fsConstants.O_CREAT | O_NOFOLLOW, 420);
|
|
1220
|
+
} catch (error) {
|
|
1221
|
+
if (error.code === "ELOOP") {
|
|
1222
|
+
throw new CliInputError(`Refusing to write through a symlink: ${relPath}`);
|
|
1223
|
+
}
|
|
1224
|
+
throw error;
|
|
1225
|
+
}
|
|
1226
|
+
try {
|
|
1227
|
+
const stat = fstatSync(fd);
|
|
1228
|
+
if (!stat.isFile()) {
|
|
1229
|
+
throw new CliInputError(`Refusing to write ${relPath}: not a regular file`);
|
|
1230
|
+
}
|
|
1231
|
+
if (stat.nlink > 1) {
|
|
1232
|
+
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.`);
|
|
1233
|
+
}
|
|
1234
|
+
if (readFileSync(fd, "utf8") === content) {
|
|
1235
|
+
return "unchanged";
|
|
1236
|
+
}
|
|
1237
|
+
ftruncateSync(fd, 0);
|
|
1238
|
+
writeSync(fd, content, 0, "utf8");
|
|
1239
|
+
return existedBefore ? "updated" : "wrote";
|
|
1240
|
+
} finally {
|
|
1241
|
+
closeSync(fd);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
function safeTargetSourcePath(raw) {
|
|
1245
|
+
const trimmed = raw.trim();
|
|
1246
|
+
if (!trimmed || trimmed.includes("\x00")) {
|
|
1247
|
+
throw new CliInputError(`Refusing to write an invalid target source path: ${JSON.stringify(raw).slice(0, 120)}`);
|
|
1248
|
+
}
|
|
1249
|
+
if (isAbsolute(trimmed) || /^[A-Za-z]:[\\/]/.test(trimmed)) {
|
|
1250
|
+
throw new CliInputError(`Refusing to write outside the repo: target source path is absolute (${trimmed})`);
|
|
1251
|
+
}
|
|
1252
|
+
const segments = trimmed.split(/[\\/]+/).filter(Boolean);
|
|
1253
|
+
if (segments.length === 0 || segments.some((segment) => segment === "..")) {
|
|
1254
|
+
throw new CliInputError(`Refusing to write outside the repo: target source path contains '..' (${trimmed})`);
|
|
1255
|
+
}
|
|
1256
|
+
return segments.join("/");
|
|
1257
|
+
}
|
|
1258
|
+
function parsePositiveInt(raw, fallback, max) {
|
|
1259
|
+
const parsed = Number.parseInt(raw ?? "", 10);
|
|
1260
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
1261
|
+
return fallback;
|
|
1262
|
+
return Math.min(parsed, max);
|
|
1263
|
+
}
|
|
1264
|
+
function sleep(ms) {
|
|
1265
|
+
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
1266
|
+
}
|
|
1267
|
+
|
|
362
1268
|
// src/sync.ts
|
|
363
1269
|
async function runSyncCommand(context) {
|
|
364
1270
|
const resource = context.positionals[0] ?? "harness";
|
|
@@ -534,8 +1440,8 @@ async function sendCliUsageTelemetry(input) {
|
|
|
534
1440
|
}
|
|
535
1441
|
|
|
536
1442
|
// src/update-check.ts
|
|
537
|
-
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
538
|
-
import { join } from "node:path";
|
|
1443
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
1444
|
+
import { join as join2 } from "node:path";
|
|
539
1445
|
var UPDATE_CHECK_FLAG = "no-update-check";
|
|
540
1446
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
541
1447
|
var REGISTRY_TIMEOUT_MS = 1500;
|
|
@@ -549,11 +1455,11 @@ function isUpdateCheckDisabled(flags, env = process.env) {
|
|
|
549
1455
|
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
1456
|
}
|
|
551
1457
|
function cachePath(options = {}) {
|
|
552
|
-
return
|
|
1458
|
+
return join2(getModaDir(options), "update-check.json");
|
|
553
1459
|
}
|
|
554
1460
|
function readCache(options = {}) {
|
|
555
1461
|
try {
|
|
556
|
-
const parsed = JSON.parse(
|
|
1462
|
+
const parsed = JSON.parse(readFileSync2(cachePath(options), "utf8"));
|
|
557
1463
|
if (parsed && typeof parsed === "object" && typeof parsed.checked_at === "number" && typeof parsed.latest === "string") {
|
|
558
1464
|
return parsed;
|
|
559
1465
|
}
|
|
@@ -562,7 +1468,7 @@ function readCache(options = {}) {
|
|
|
562
1468
|
}
|
|
563
1469
|
function writeCache(cache, options = {}) {
|
|
564
1470
|
try {
|
|
565
|
-
|
|
1471
|
+
mkdirSync2(getModaDir(options), { recursive: true });
|
|
566
1472
|
writeFileSync(cachePath(options), `${JSON.stringify(cache, null, 2)}
|
|
567
1473
|
`, "utf8");
|
|
568
1474
|
} catch {}
|
|
@@ -1124,8 +2030,8 @@ function renderJsonSchemas() {
|
|
|
1124
2030
|
|
|
1125
2031
|
// src/keys.ts
|
|
1126
2032
|
import { createHash } from "node:crypto";
|
|
1127
|
-
import { existsSync, readFileSync as
|
|
1128
|
-
import { join as
|
|
2033
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
|
|
2034
|
+
import { join as join3 } from "node:path";
|
|
1129
2035
|
var KEY_LIFECYCLE_ENDPOINT_REQUIREMENTS = {
|
|
1130
2036
|
list: "GET /api/cli/keys?profile=<profile>",
|
|
1131
2037
|
create: "POST /api/cli/keys",
|
|
@@ -1192,14 +2098,14 @@ function writeLocalApiKeySecret(descriptor, apiKey, options = {}) {
|
|
|
1192
2098
|
return path;
|
|
1193
2099
|
}
|
|
1194
2100
|
function keyDescriptorStorePath(options = {}) {
|
|
1195
|
-
return
|
|
2101
|
+
return join3(getModaPaths(options).dir, "keys.json");
|
|
1196
2102
|
}
|
|
1197
2103
|
function loadLocalKeyDescriptors(options = {}) {
|
|
1198
2104
|
const path = keyDescriptorStorePath(options);
|
|
1199
2105
|
try {
|
|
1200
|
-
if (!
|
|
2106
|
+
if (!existsSync2(path))
|
|
1201
2107
|
return [];
|
|
1202
|
-
const parsed = JSON.parse(
|
|
2108
|
+
const parsed = JSON.parse(readFileSync3(path, "utf8"));
|
|
1203
2109
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1204
2110
|
return [];
|
|
1205
2111
|
}
|
|
@@ -1547,12 +2453,12 @@ async function runKeysCommand(context) {
|
|
|
1547
2453
|
}
|
|
1548
2454
|
|
|
1549
2455
|
// src/agent.ts
|
|
1550
|
-
import { existsSync as
|
|
1551
|
-
import { join as
|
|
2456
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4, statSync } from "node:fs";
|
|
2457
|
+
import { join as join4, relative, resolve as resolve2 } from "node:path";
|
|
1552
2458
|
var MODA_DIR = ".moda";
|
|
1553
|
-
var HARNESS_PATH =
|
|
1554
|
-
var HARNESS_REPORT_MARKDOWN_PATH =
|
|
1555
|
-
var HARNESS_SUMMARY_PATH =
|
|
2459
|
+
var HARNESS_PATH = join4(MODA_DIR, "harness.json");
|
|
2460
|
+
var HARNESS_REPORT_MARKDOWN_PATH = join4(MODA_DIR, "HARNESS_REPORT.md");
|
|
2461
|
+
var HARNESS_SUMMARY_PATH = join4(MODA_DIR, "HARNESS.md");
|
|
1556
2462
|
var MAX_AGENT_EVIDENCE_BYTES = 24000;
|
|
1557
2463
|
async function runAgentCommand(context) {
|
|
1558
2464
|
const subcommand2 = context.positionals[0] ?? "context";
|
|
@@ -1674,7 +2580,7 @@ async function buildAgentContext(context) {
|
|
|
1674
2580
|
},
|
|
1675
2581
|
report: reportState.state,
|
|
1676
2582
|
sync: syncState ? {
|
|
1677
|
-
path:
|
|
2583
|
+
path: join4(MODA_DIR, "sync-state.json"),
|
|
1678
2584
|
exists: true,
|
|
1679
2585
|
source: syncState.source,
|
|
1680
2586
|
updatedAt: syncState.updatedAt,
|
|
@@ -1682,7 +2588,7 @@ async function buildAgentContext(context) {
|
|
|
1682
2588
|
remoteHarnessId: syncState.remote.harnessId,
|
|
1683
2589
|
remoteVersionId: syncState.remote.versionId
|
|
1684
2590
|
} : {
|
|
1685
|
-
path:
|
|
2591
|
+
path: join4(MODA_DIR, "sync-state.json"),
|
|
1686
2592
|
exists: false
|
|
1687
2593
|
}
|
|
1688
2594
|
},
|
|
@@ -1732,18 +2638,18 @@ function summarizeAgentContext(payload, noun) {
|
|
|
1732
2638
|
return `Local Moda ${noun} is healthy with ${agents} runtime agent(s).`;
|
|
1733
2639
|
}
|
|
1734
2640
|
function readHarnessGraphIfPresent(cwd) {
|
|
1735
|
-
const path =
|
|
1736
|
-
if (!
|
|
2641
|
+
const path = join4(cwd, HARNESS_PATH);
|
|
2642
|
+
if (!existsSync3(path))
|
|
1737
2643
|
return;
|
|
1738
2644
|
try {
|
|
1739
|
-
return JSON.parse(
|
|
2645
|
+
return JSON.parse(readFileSync4(path, "utf8"));
|
|
1740
2646
|
} catch {
|
|
1741
2647
|
return;
|
|
1742
2648
|
}
|
|
1743
2649
|
}
|
|
1744
2650
|
function readHarnessReportState(cwd) {
|
|
1745
|
-
const path =
|
|
1746
|
-
if (!
|
|
2651
|
+
const path = join4(cwd, HARNESS_REPORT_PATH);
|
|
2652
|
+
if (!existsSync3(path)) {
|
|
1747
2653
|
return {
|
|
1748
2654
|
state: {
|
|
1749
2655
|
path: HARNESS_REPORT_PATH,
|
|
@@ -1787,11 +2693,11 @@ function readHarnessReportState(cwd) {
|
|
|
1787
2693
|
}
|
|
1788
2694
|
}
|
|
1789
2695
|
function readApproval(cwd) {
|
|
1790
|
-
const path =
|
|
1791
|
-
if (!
|
|
2696
|
+
const path = join4(cwd, HARNESS_REPORT_APPROVAL_PATH);
|
|
2697
|
+
if (!existsSync3(path))
|
|
1792
2698
|
return;
|
|
1793
2699
|
try {
|
|
1794
|
-
return JSON.parse(
|
|
2700
|
+
return JSON.parse(readFileSync4(path, "utf8"));
|
|
1795
2701
|
} catch {
|
|
1796
2702
|
return;
|
|
1797
2703
|
}
|
|
@@ -1867,9 +2773,9 @@ function existingArtifacts(cwd) {
|
|
|
1867
2773
|
[HARNESS_REPORT_PATH, "harness_report", "Cited harness analyst report."],
|
|
1868
2774
|
[HARNESS_REPORT_MARKDOWN_PATH, "harness_report_markdown", "Human-readable harness analyst report."],
|
|
1869
2775
|
[HARNESS_SUMMARY_PATH, "harness_summary", "Human-readable harness graph summary."],
|
|
1870
|
-
[
|
|
1871
|
-
[
|
|
1872
|
-
].flatMap(([path, kind, description]) =>
|
|
2776
|
+
[join4(MODA_DIR, "sync-state.json"), "sync_state", "Last local harness sync state."],
|
|
2777
|
+
[join4(MODA_DIR, "prompts.yml"), "prompt_manifest", "Local prompt discovery manifest."]
|
|
2778
|
+
].flatMap(([path, kind, description]) => existsSync3(join4(cwd, path)) ? [{ path, kind, description }] : []);
|
|
1873
2779
|
}
|
|
1874
2780
|
function expandEvidenceRef(ref, loaded, cwd) {
|
|
1875
2781
|
if (ref.startsWith("doctor:")) {
|
|
@@ -1950,9 +2856,9 @@ function readCitationExcerpt(cwd, citation) {
|
|
|
1950
2856
|
if (!citation.path || citation.type !== "file" && citation.type !== "generated_doc")
|
|
1951
2857
|
return citation.excerpt;
|
|
1952
2858
|
const absolute = safeResolve(cwd, citation.path);
|
|
1953
|
-
if (!absolute || !
|
|
2859
|
+
if (!absolute || !existsSync3(absolute))
|
|
1954
2860
|
return citation.excerpt;
|
|
1955
|
-
const text =
|
|
2861
|
+
const text = readFileSync4(absolute, "utf8");
|
|
1956
2862
|
if (citation.line && citation.line > 0) {
|
|
1957
2863
|
return text.split(/\r?\n/)[citation.line - 1]?.slice(0, 1000) ?? citation.excerpt;
|
|
1958
2864
|
}
|
|
@@ -1963,10 +2869,10 @@ function readBoundedModaArtifact(cwd, artifactPath) {
|
|
|
1963
2869
|
throw new CliInputError(`Artifact '${artifactPath}' is outside .moda and cannot be expanded directly.`, "Use a citation evidence ref for source-file evidence.");
|
|
1964
2870
|
}
|
|
1965
2871
|
const absolute = safeResolve(cwd, artifactPath);
|
|
1966
|
-
if (!absolute || !
|
|
2872
|
+
if (!absolute || !existsSync3(absolute))
|
|
1967
2873
|
throw missingEvidence(`artifact:${artifactPath}`);
|
|
1968
2874
|
const size = statSync(absolute).size;
|
|
1969
|
-
const text =
|
|
2875
|
+
const text = readFileSync4(absolute, "utf8");
|
|
1970
2876
|
return {
|
|
1971
2877
|
path: artifactPath,
|
|
1972
2878
|
bytes: size,
|
|
@@ -1975,7 +2881,7 @@ function readBoundedModaArtifact(cwd, artifactPath) {
|
|
|
1975
2881
|
};
|
|
1976
2882
|
}
|
|
1977
2883
|
function safeResolve(cwd, path) {
|
|
1978
|
-
const absolute =
|
|
2884
|
+
const absolute = resolve2(cwd, path);
|
|
1979
2885
|
const rel = relative(cwd, absolute);
|
|
1980
2886
|
if (rel.startsWith("..") || rel === ".." || rel.startsWith("/"))
|
|
1981
2887
|
return;
|
|
@@ -2009,8 +2915,8 @@ function purposeForCommand(command) {
|
|
|
2009
2915
|
}
|
|
2010
2916
|
|
|
2011
2917
|
// src/production-intelligence.ts
|
|
2012
|
-
import { existsSync as
|
|
2013
|
-
import { join as
|
|
2918
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5 } from "node:fs";
|
|
2919
|
+
import { join as join5 } from "node:path";
|
|
2014
2920
|
|
|
2015
2921
|
// src/ask-stream.ts
|
|
2016
2922
|
class AskStreamError extends Error {
|
|
@@ -3430,12 +4336,12 @@ function loadHarnessHints(cwd) {
|
|
|
3430
4336
|
skillPaths: new Map,
|
|
3431
4337
|
entrypointPaths: []
|
|
3432
4338
|
};
|
|
3433
|
-
const harnessPath =
|
|
3434
|
-
if (!
|
|
4339
|
+
const harnessPath = join5(cwd, ".moda", "harness.json");
|
|
4340
|
+
if (!existsSync4(harnessPath))
|
|
3435
4341
|
return hints;
|
|
3436
4342
|
hints.reportPath = ".moda/harness.json";
|
|
3437
4343
|
try {
|
|
3438
|
-
const parsed = JSON.parse(
|
|
4344
|
+
const parsed = JSON.parse(readFileSync5(harnessPath, "utf8"));
|
|
3439
4345
|
const graph = asRecord(parsed);
|
|
3440
4346
|
const artifacts = Array.isArray(graph?.artifacts) ? graph.artifacts : [];
|
|
3441
4347
|
for (const item of artifacts) {
|
|
@@ -3463,11 +4369,11 @@ function loadHarnessHints(cwd) {
|
|
|
3463
4369
|
return hints;
|
|
3464
4370
|
}
|
|
3465
4371
|
function localHarnessSummary(cwd) {
|
|
3466
|
-
const path =
|
|
3467
|
-
if (!
|
|
4372
|
+
const path = join5(cwd, ".moda", "harness.json");
|
|
4373
|
+
if (!existsSync4(path))
|
|
3468
4374
|
return null;
|
|
3469
4375
|
try {
|
|
3470
|
-
const parsed = JSON.parse(
|
|
4376
|
+
const parsed = JSON.parse(readFileSync5(path, "utf8"));
|
|
3471
4377
|
return {
|
|
3472
4378
|
harnessId: parsed.harnessId,
|
|
3473
4379
|
graphHash: parsed.graphHash,
|
|
@@ -3478,16 +4384,16 @@ function localHarnessSummary(cwd) {
|
|
|
3478
4384
|
}
|
|
3479
4385
|
}
|
|
3480
4386
|
function localSyncBrief(cwd, relativePath, label) {
|
|
3481
|
-
const path =
|
|
3482
|
-
if (!
|
|
4387
|
+
const path = join5(cwd, relativePath);
|
|
4388
|
+
if (!existsSync4(path)) {
|
|
3483
4389
|
return { status: "missing", summary: `${label} state not found`, path: relativePath };
|
|
3484
4390
|
}
|
|
3485
4391
|
return { status: "present", summary: `${label} state exists locally`, path: relativePath };
|
|
3486
4392
|
}
|
|
3487
4393
|
function localSkillBrief(cwd) {
|
|
3488
|
-
const claudeSkill =
|
|
3489
|
-
const cursorRules =
|
|
3490
|
-
if (
|
|
4394
|
+
const claudeSkill = join5(cwd, ".claude", "skills");
|
|
4395
|
+
const cursorRules = join5(cwd, ".cursor", "rules");
|
|
4396
|
+
if (existsSync4(claudeSkill) || existsSync4(cursorRules)) {
|
|
3491
4397
|
return { status: "present", summary: "Local agent skill/rule directory exists" };
|
|
3492
4398
|
}
|
|
3493
4399
|
return { status: "missing", summary: "No local agent skill/rule directory found" };
|
|
@@ -3728,7 +4634,7 @@ function buildManifest(commands) {
|
|
|
3728
4634
|
exit_codes: [
|
|
3729
4635
|
{ code: 0, name: "ok", meaning: "Command completed successfully." },
|
|
3730
4636
|
{ code: 1, name: "error", meaning: "Command, input, API, network, or unexpected failure." },
|
|
3731
|
-
{ code: 3, name: "degraded", meaning: "
|
|
4637
|
+
{ 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
4638
|
{ code: 4, name: "auth_required", meaning: "Run login_command, then resume." },
|
|
3733
4639
|
{ code: 5, name: "input_required", meaning: "Choose from input_request choices, then resume." },
|
|
3734
4640
|
{ code: 130, name: "cancelled", meaning: "Interrupted or output consumer closed." }
|
|
@@ -3799,6 +4705,8 @@ function event(name, when, terminal, deprecated = false) {
|
|
|
3799
4705
|
function exitCodesForCommand(command) {
|
|
3800
4706
|
if (command === "ask")
|
|
3801
4707
|
return [0, 1, 3, 4, 5, 130];
|
|
4708
|
+
if (command === "fix")
|
|
4709
|
+
return [0, 1, 3, 4, 130];
|
|
3802
4710
|
if (command === "init" || command === "harness")
|
|
3803
4711
|
return [0, 1, 4, 5, 130];
|
|
3804
4712
|
return [0, 1, 4, 130];
|
|
@@ -3806,7 +4714,7 @@ function exitCodesForCommand(command) {
|
|
|
3806
4714
|
|
|
3807
4715
|
// src/cli.ts
|
|
3808
4716
|
import { fileURLToPath } from "url";
|
|
3809
|
-
import { realpathSync } from "fs";
|
|
4717
|
+
import { realpathSync as realpathSync2 } from "fs";
|
|
3810
4718
|
function parseArgs(argv) {
|
|
3811
4719
|
const args = argv.slice(2);
|
|
3812
4720
|
let command = "";
|
|
@@ -3967,6 +4875,21 @@ Commands:
|
|
|
3967
4875
|
skills sync Push local .claude/skills/**/SKILL.md up to Moda
|
|
3968
4876
|
skills sync --dry-run Show what would be pushed without writing
|
|
3969
4877
|
|
|
4878
|
+
Fixes (a fix with proof):
|
|
4879
|
+
fixes Ranked Fix queue (--status=S --limit=N --cursor=TOKEN)
|
|
4880
|
+
fix <fix_id> One Fix: status + gate result; --packet prints moda.fix_packet.v1
|
|
4881
|
+
(tool/skill candidates are also written under .moda/fixes/<ref>/);
|
|
4882
|
+
--wait drives a still-running pipeline to rest
|
|
4883
|
+
fix start <problem_id> Draft a Fix from a Problem (--type=auto|prompt); --wait drives
|
|
4884
|
+
scope -> propose -> gate and prints the packet
|
|
4885
|
+
fix verify <fix_id> Re-gate on the frozen holdout; --prompt-file=P gates local content
|
|
4886
|
+
(exit 0 pass / 1 fail with per-case findings / 3 degraded-inconclusive)
|
|
4887
|
+
fix checkout <fix_id> Write the candidate at its target source path (never runs git)
|
|
4888
|
+
fix submit <fix_id> Deliver: --pr opens a draft PR | --local-ref=<branch> records your branch
|
|
4889
|
+
fix mark-applied <fix_id> Confirm a candidate you applied in your own stack (--note=TEXT);
|
|
4890
|
+
no PR needed — flips to SHIPPED and starts monitoring
|
|
4891
|
+
fix dismiss <fix_id> Reject with --reason=TEXT (feeds problem feedback)
|
|
4892
|
+
|
|
3970
4893
|
Options:
|
|
3971
4894
|
--help Show this help message
|
|
3972
4895
|
--version Show version number
|
|
@@ -4162,7 +5085,11 @@ Examples:
|
|
|
4162
5085
|
moda prompts sync
|
|
4163
5086
|
moda prompts promote support.triage --label=prod --version=pver_abc123
|
|
4164
5087
|
moda prompts ab --baseline=prompts/agent.prompt.md --candidate=prompts/agent-v2.prompt.md --conversations=conv_1,conv_2
|
|
4165
|
-
moda skills pull
|
|
5088
|
+
moda skills pull
|
|
5089
|
+
moda fixes
|
|
5090
|
+
moda fix start <problem_id> --wait
|
|
5091
|
+
moda fix verify <fix_id> --prompt-file=prompts/agent.prompt.md
|
|
5092
|
+
moda fix mark-applied <fix_id> --note="tool description updated in our agent config"`);
|
|
4166
5093
|
}
|
|
4167
5094
|
function printCommandHelp(definition) {
|
|
4168
5095
|
const lines = [];
|
|
@@ -4407,7 +5334,7 @@ async function runTailCommand(params, context) {
|
|
|
4407
5334
|
break;
|
|
4408
5335
|
if (params.max_events !== undefined && emitted >= params.max_events)
|
|
4409
5336
|
break;
|
|
4410
|
-
await new Promise((
|
|
5337
|
+
await new Promise((resolve3) => setTimeout(resolve3, intervalMs));
|
|
4411
5338
|
}
|
|
4412
5339
|
}
|
|
4413
5340
|
async function attachWorldState(rows) {
|
|
@@ -5025,7 +5952,7 @@ var commandRegistry = createCommandRegistry([
|
|
|
5025
5952
|
resetApiRequestCountBeforeRun: true,
|
|
5026
5953
|
telemetry: "result",
|
|
5027
5954
|
handler: async (context) => {
|
|
5028
|
-
const { runInit } = await import("./index-
|
|
5955
|
+
const { runInit } = await import("./index-fwxpt51s.js");
|
|
5029
5956
|
if (context.outputMode === "agent-stream") {
|
|
5030
5957
|
context.output.writeEvent({
|
|
5031
5958
|
event: "started",
|
|
@@ -5430,6 +6357,42 @@ var commandRegistry = createCommandRegistry([
|
|
|
5430
6357
|
handler: async (context) => ({
|
|
5431
6358
|
exitCode: await runSkillsCommand(context)
|
|
5432
6359
|
})
|
|
6360
|
+
},
|
|
6361
|
+
{
|
|
6362
|
+
name: "fixes",
|
|
6363
|
+
description: 'List the ranked queue of Fixes ("a fix with proof") for this tenant',
|
|
6364
|
+
examples: ["moda fixes", "moda fixes --status=VERIFIED", "moda fixes --limit=10"],
|
|
6365
|
+
auth: "none",
|
|
6366
|
+
outputModes: ["human", "json", "agent"],
|
|
6367
|
+
mutability: "read",
|
|
6368
|
+
validateConfigBeforeRun: false,
|
|
6369
|
+
resetApiRequestCountBeforeRun: false,
|
|
6370
|
+
telemetry: "result",
|
|
6371
|
+
handler: async (context) => ({
|
|
6372
|
+
exitCode: await runFixesCommand(context)
|
|
6373
|
+
})
|
|
6374
|
+
},
|
|
6375
|
+
{
|
|
6376
|
+
name: "fix",
|
|
6377
|
+
description: 'Draft, verify, check out, submit, and dismiss Fixes ("a fix with proof")',
|
|
6378
|
+
examples: [
|
|
6379
|
+
"moda fix start <problem_id> --wait",
|
|
6380
|
+
"moda fix <fix_id> --packet",
|
|
6381
|
+
"moda fix verify <fix_id> --prompt-file=prompts/agent.prompt.md",
|
|
6382
|
+
"moda fix checkout <fix_id>",
|
|
6383
|
+
"moda fix submit <fix_id> --pr",
|
|
6384
|
+
'moda fix mark-applied <fix_id> --note="tool description updated in our agent config"',
|
|
6385
|
+
'moda fix dismiss <fix_id> --reason="not actionable"'
|
|
6386
|
+
],
|
|
6387
|
+
auth: "none",
|
|
6388
|
+
outputModes: ["human", "json", "agent"],
|
|
6389
|
+
mutability: "write",
|
|
6390
|
+
validateConfigBeforeRun: false,
|
|
6391
|
+
resetApiRequestCountBeforeRun: false,
|
|
6392
|
+
telemetry: "result",
|
|
6393
|
+
handler: async (context) => ({
|
|
6394
|
+
exitCode: await runFixCommand(context)
|
|
6395
|
+
})
|
|
5433
6396
|
}
|
|
5434
6397
|
]);
|
|
5435
6398
|
function classifyCliError(error) {
|
|
@@ -5677,7 +6640,7 @@ function installSignalHandlers() {
|
|
|
5677
6640
|
async function main() {
|
|
5678
6641
|
process.exitCode = await dispatchParsedCommand(parseArgs(process.argv));
|
|
5679
6642
|
}
|
|
5680
|
-
var isMain = process.argv[1] && fileURLToPath(import.meta.url) ===
|
|
6643
|
+
var isMain = process.argv[1] && fileURLToPath(import.meta.url) === realpathSync2(process.argv[1]);
|
|
5681
6644
|
if (isMain) {
|
|
5682
6645
|
installSignalHandlers();
|
|
5683
6646
|
main().catch((error) => {
|