@moda-ai/cli 1.27.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
@@ -391,6 +391,7 @@ var FIX_USAGE = [
391
391
  " moda fix verify <fix_id> [--prompt-file=PATH] [--no-wait] [--timeout=ms]",
392
392
  " moda fix checkout <fix_id>",
393
393
  " moda fix submit <fix_id> --pr | --local-ref=<branch>",
394
+ " moda fix mark-applied <fix_id> [--note=TEXT]",
394
395
  " moda fix dismiss <fix_id> --reason=TEXT"
395
396
  ].join(`
396
397
  `);
@@ -454,6 +455,8 @@ async function runFixCommand(context) {
454
455
  return runFixCheckout(context, profileOptions, tenantId);
455
456
  case "submit":
456
457
  return runFixSubmit(context, profileOptions, tenantId);
458
+ case "mark-applied":
459
+ return runFixMarkApplied(context, profileOptions, tenantId);
457
460
  case "dismiss":
458
461
  return runFixDismiss(context, profileOptions, tenantId);
459
462
  default:
@@ -645,6 +648,9 @@ async function runFixCheckout(context, profileOptions, tenantId) {
645
648
  throw new CliInputError("<fix_id> is required", "Usage: moda fix checkout <fix_id>");
646
649
  }
647
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
+ }
648
654
  if (!fix.targetSourcePath) {
649
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.`);
650
656
  }
@@ -752,6 +758,38 @@ async function runFixSubmit(context, profileOptions, tenantId) {
752
758
  });
753
759
  return EXIT_OK;
754
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
+ }
755
793
  async function runFixDismiss(context, profileOptions, tenantId) {
756
794
  const usage = "Usage: moda fix dismiss <fix_id> --reason=TEXT";
757
795
  const fixId = (context.positionals[1] || context.flags["fix-id"] || "").trim();
@@ -778,8 +816,16 @@ async function runFixDismiss(context, profileOptions, tenantId) {
778
816
  async function runFixDetail(context, profileOptions, tenantId, fixId) {
779
817
  if (context.flags.packet === "true") {
780
818
  const packet = await callControlAPI(`${fixesBase(tenantId)}/${encodeURIComponent(fixId)}/packet`, { method: "GET" }, profileOptions);
819
+ const extras = materializePacketCandidates(packet, fixId, context.outputMode === "human");
781
820
  context.output.writeData(packet, {
782
- summary: { text: `moda.fix_packet.v1 for fix ${fixId}.`, confidence: "high" }
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 } : {}
783
829
  });
784
830
  return EXIT_OK;
785
831
  }
@@ -841,6 +887,9 @@ async function driveAdvance(args) {
841
887
  announce(fix);
842
888
  if (!ADVANCING_STATUSES.has(fix.status))
843
889
  break;
890
+ if (fix.status === "PROPOSED" && /gate (pending|unavailable)/i.test(fix.statusReason ?? "")) {
891
+ break;
892
+ }
844
893
  await sleep(fix.status === "GATING" ? args.pollIntervalMs : Math.min(STEP_DELAY_MS, args.pollIntervalMs));
845
894
  }
846
895
  return fix;
@@ -871,6 +920,112 @@ async function fetchPacketFailSoft(tenantId, fixId, profileOptions) {
871
920
  return null;
872
921
  }
873
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
+ }
874
1029
  function gateFindings(fix) {
875
1030
  const gateResult = fix.gateResult ?? null;
876
1031
  if (!gateResult)
@@ -918,8 +1073,24 @@ function passLabel(value) {
918
1073
  return value ? "pass" : "fail";
919
1074
  }
920
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;
921
1089
  switch (fix.status) {
922
1090
  case "VERIFIED":
1091
+ if (isArtifactFix(fix)) {
1092
+ return [packetCommand, markAppliedCommand];
1093
+ }
923
1094
  return [
924
1095
  {
925
1096
  command: `moda fix submit ${fix.id} --pr`,
@@ -935,6 +1106,9 @@ function nextCommandsFor(fix) {
935
1106
  }
936
1107
  ];
937
1108
  case "PROPOSED":
1109
+ if (skillGatePending) {
1110
+ return [packetCommand, markAppliedCommand];
1111
+ }
938
1112
  return [
939
1113
  {
940
1114
  command: `moda fix verify ${fix.id}`,
@@ -944,6 +1118,17 @@ function nextCommandsFor(fix) {
944
1118
  }
945
1119
  ];
946
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
+ }
947
1132
  return [
948
1133
  {
949
1134
  command: `moda fix checkout ${fix.id}`,
@@ -4692,7 +4877,8 @@ Commands:
4692
4877
 
4693
4878
  Fixes (a fix with proof):
4694
4879
  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;
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>/);
4696
4882
  --wait drives a still-running pipeline to rest
4697
4883
  fix start <problem_id> Draft a Fix from a Problem (--type=auto|prompt); --wait drives
4698
4884
  scope -> propose -> gate and prints the packet
@@ -4700,6 +4886,8 @@ Commands:
4700
4886
  (exit 0 pass / 1 fail with per-case findings / 3 degraded-inconclusive)
4701
4887
  fix checkout <fix_id> Write the candidate at its target source path (never runs git)
4702
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
4703
4891
  fix dismiss <fix_id> Reject with --reason=TEXT (feeds problem feedback)
4704
4892
 
4705
4893
  Options:
@@ -4900,7 +5088,8 @@ Examples:
4900
5088
  moda skills pull
4901
5089
  moda fixes
4902
5090
  moda fix start <problem_id> --wait
4903
- moda fix verify <fix_id> --prompt-file=prompts/agent.prompt.md`);
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"`);
4904
5093
  }
4905
5094
  function printCommandHelp(definition) {
4906
5095
  const lines = [];
@@ -6192,6 +6381,7 @@ var commandRegistry = createCommandRegistry([
6192
6381
  "moda fix verify <fix_id> --prompt-file=prompts/agent.prompt.md",
6193
6382
  "moda fix checkout <fix_id>",
6194
6383
  "moda fix submit <fix_id> --pr",
6384
+ 'moda fix mark-applied <fix_id> --note="tool description updated in our agent config"',
6195
6385
  'moda fix dismiss <fix_id> --reason="not actionable"'
6196
6386
  ],
6197
6387
  auth: "none",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moda-ai/cli",
3
- "version": "1.27.0",
3
+ "version": "1.27.1",
4
4
  "description": "CLI for Moda - AI agent analytics and observability",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema_version": "moda.skill_index.v1",
3
- "bundled_at": "2026-08-17T23:28:03.271Z",
4
- "cli_version": "1.27.0",
3
+ "bundled_at": "2026-08-19T08:07:44.284Z",
4
+ "cli_version": "1.27.1",
5
5
  "skills": [
6
6
  {
7
7
  "id": "integration-node-anthropic",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: moda-cli
3
- version: 2.3.0
3
+ version: 2.4.0
4
4
  description: Query Moda's AI agent conversation analytics and manage code-first prompt versions from the terminal — semantic/keyword/hybrid message search, overview KPIs, topic clusters, message context, user frustration detections, tool failures, and moda prompts status/sync/promote. Use when the user asks about moda, modaflows, conversation analytics, prompt management, user frustrations, agent observability, tool failure debugging, wants to find conversations or tool calls about a topic, or wants to investigate how their AI agent is performing.
5
5
  ---
6
6
 
@@ -621,23 +621,35 @@ replay **gate** that proves it on held-out production evidence. Use this loop
621
621
  whenever you are asked to fix a detected Problem, or before hand-editing
622
622
  prompt/skill files in a repo that talks to Moda.
623
623
 
624
+ Fixes are typed by where the evidence points: `PROMPT` (a managed prompt
625
+ revision — the checkout/verify/submit loop below), `TOOL_SCHEMA` (a proposed
626
+ rewrite of one tool's description), and `SKILL` (a drafted SKILL.md for an
627
+ agent-behavior problem). Tool and skill candidates are not repo files: read
628
+ them from the packet (they are also written locally under
629
+ `.moda/fixes/<SHORTREF>/`), apply them in your own stack, then confirm with
630
+ `moda fix mark-applied` — no GitHub integration required.
631
+
624
632
  The fail-to-pass loop:
625
633
 
626
634
  ```bash
627
635
  moda fixes # ranked queue of fixes with proof
628
636
  moda fix <fix_id> --packet # moda.fix_packet.v1: cause, target
629
637
  # file, evidence links, verify cmd,
630
- # handback + magic word
638
+ # handback + magic word; tool/skill
639
+ # candidates land in .moda/fixes/
631
640
  moda fix start <problem_id> --wait # draft from a Problem and drive
632
641
  # scope -> propose -> gate
633
- moda fix checkout <fix_id> # write the candidate at its target
634
- # source path (NEVER runs git)
642
+ moda fix checkout <fix_id> # PROMPT fixes: write the candidate
643
+ # at its target source path
644
+ # (NEVER runs git)
635
645
  # ...edit the target file...
636
646
  moda fix verify <fix_id> --prompt-file=<path> # re-gate local content on the
637
- # frozen holdout
647
+ # frozen holdout (PROMPT only)
638
648
  # iterate edit -> verify until exit 0, then hand back:
639
649
  moda fix submit <fix_id> --pr # draft PR via the Moda GitHub App
640
650
  moda fix submit <fix_id> --local-ref=<branch> # or record your own branch
651
+ moda fix mark-applied <fix_id> --note="…" # tool/skill/no-PR path: confirm a
652
+ # candidate you applied yourself
641
653
  moda fix dismiss <fix_id> --reason="why" # reject (feeds problem feedback)
642
654
  ```
643
655
 
@@ -660,6 +672,15 @@ Rules that keep the proof honest:
660
672
  - Fix PRs end with the magic word `Fixes MODA-FIX-<SHORTREF>` in the body —
661
673
  Moda-authored PRs carry it automatically; hand-authored PRs must include it
662
674
  so the merge confirms the fix and starts production monitoring.
675
+ - `mark-applied` is the confirmation for changes shipped without a PR (tool
676
+ descriptions, installed skills, hand-applied prompts): it flips the fix to
677
+ `SHIPPED`, records your `--note`, and starts the same monitoring the merge
678
+ webhook would. It is idempotent — repeating it is a duplicate no-op.
679
+ - `TOOL_SCHEMA` fixes gate like prompt fixes (both arms pin the same current
680
+ prompt; the proposed description is the only delta). `SKILL` fixes rest at
681
+ `PROPOSED` until the skill is registered with the Moda skill harness — an
682
+ inline drafted SKILL.md cannot enter the gate, so deliver it via the packet
683
+ and `mark-applied`.
663
684
  - The pipeline is advance-on-poll: `--wait` (on `start`, `verify`, or
664
685
  `moda fix <fix_id> --wait`) drives it; a fix left `GATING` will not finish
665
686
  on its own.
@@ -842,11 +863,12 @@ npx: `npx -p @moda-ai/cli moda <command>`.
842
863
  | `moda prompts sync` | Upload changed prompt versions and update lockfile |
843
864
  | `moda prompts promote <key>` | Move a remote prompt label |
844
865
  | `moda fixes` | Ranked queue of Fixes ("a fix with proof"); `--status=S`, `--limit=N`, `--cursor=TOKEN` |
845
- | `moda fix <fix_id>` | One Fix: status + gate result; `--packet` prints `moda.fix_packet.v1`; `--wait` drives a running pipeline to rest |
866
+ | `moda fix <fix_id>` | One Fix: status + gate result; `--packet` prints `moda.fix_packet.v1` and writes tool/skill candidates under `.moda/fixes/<ref>/`; `--wait` drives a running pipeline to rest |
846
867
  | `moda fix start <problem_id>` | Draft a Fix from a Problem (`--type=auto\|prompt`); `--wait` drives scope → propose → gate |
847
- | `moda fix verify <fix_id>` | Re-gate on the frozen holdout; `--prompt-file=P` gates local content (exit `0` pass / `1` fail with per-case findings / `3` degraded — never a pass) |
848
- | `moda fix checkout <fix_id>` | Write the candidate at its target source path (never runs git) |
868
+ | `moda fix verify <fix_id>` | Re-gate on the frozen holdout; `--prompt-file=P` gates local content, PROMPT fixes only (exit `0` pass / `1` fail with per-case findings / `3` degraded — never a pass) |
869
+ | `moda fix checkout <fix_id>` | Write the candidate at its target source path (never runs git; PROMPT fixes only) |
849
870
  | `moda fix submit <fix_id>` | Deliver: `--pr` opens a draft PR \| `--local-ref=<branch>` records your branch |
871
+ | `moda fix mark-applied <fix_id>` | Confirm a candidate you applied in your own stack (`--note=TEXT`, ≤600 chars); no PR needed — flips to SHIPPED and starts monitoring |
850
872
  | `moda fix dismiss <fix_id>` | Reject with `--reason=TEXT` (feeds problem feedback) |
851
873
 
852
874
  `moda search` flags: `--mode=keyword|semantic|hybrid` (default `hybrid`),