@moda-ai/cli 1.28.6 → 1.29.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.
@@ -124,7 +124,7 @@ function resetApiRequestCount() {
124
124
  }
125
125
 
126
126
  // src/harness-remote.ts
127
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
127
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
128
128
  import { join as join3 } from "node:path";
129
129
 
130
130
  // src/markdown-terminal.ts
@@ -542,7 +542,7 @@ function createHarnessReport(options) {
542
542
  unknowns: limitedUnknowns
543
543
  };
544
544
  }
545
- function validateHarnessReport(report, rootDir) {
545
+ function validateHarnessReport(report, rootDir, options) {
546
546
  const checks = [];
547
547
  const errors = [];
548
548
  const warnings = [];
@@ -713,6 +713,7 @@ function validateHarnessReport(report, rootDir) {
713
713
  }
714
714
  }
715
715
  }
716
+ validateDeterministicFloor(report, options?.deterministicFloor, check, warn);
716
717
  return {
717
718
  schemaVersion: HARNESS_REPORT_VALIDATION_SCHEMA_VERSION,
718
719
  reportId: report?.reportId ?? "unknown",
@@ -724,6 +725,27 @@ function validateHarnessReport(report, rootDir) {
724
725
  warnings
725
726
  };
726
727
  }
728
+ function validateDeterministicFloor(report, floor, check, warn) {
729
+ if (!floor)
730
+ return;
731
+ if (process.env.MODA_HARNESS_FLOOR_CHECK === "0")
732
+ return;
733
+ const graph = isRecord(report?.graph) ? report.graph : {};
734
+ const toolArtifactCount = Array.isArray(graph.artifacts) ? graph.artifacts.filter((artifact) => isRecord(artifact) && artifact.type === "tool").length : 0;
735
+ const providerCount = safeArrayLength(graph.modelProviders);
736
+ const runtimeAgentCount = safeArrayLength(graph.runtimeAgents);
737
+ if (typeof floor.toolCandidates === "number" && floor.toolCandidates >= 5) {
738
+ const minimumTools = Math.ceil(0.5 * floor.toolCandidates);
739
+ check("floor_tools", toolArtifactCount >= minimumTools, `Deterministic scan found ${floor.toolCandidates} high-confidence tool definition candidate(s); the report graph must include at least ${minimumTools} tool artifact(s) but has ${toolArtifactCount}.`);
740
+ }
741
+ const providerPackages = Array.isArray(floor.providerPackages) ? floor.providerPackages.filter((name) => typeof name === "string" && name.length > 0) : [];
742
+ if (providerPackages.length > 0) {
743
+ check("floor_providers", providerCount > 0, `Manifest dependencies include model-provider package(s) ${providerPackages.join(", ")}, but the report graph.modelProviders is empty.`);
744
+ }
745
+ if (typeof floor.llmCallSites === "number" && floor.llmCallSites > 0 && runtimeAgentCount === 0) {
746
+ warn("floor_agents", `Deterministic scan found ${floor.llmCallSites} LLM call site(s) but the report graph.runtimeAgents is empty; sync will be blocked by the zero-agent gate.`);
747
+ }
748
+ }
727
749
  async function approveHarnessReport(options) {
728
750
  const report = readHarnessReport(options.reportPath);
729
751
  const validation = validateHarnessReport(report, options.rootDir);
@@ -760,9 +782,9 @@ async function approveHarnessReport(options) {
760
782
  writeJson(join(options.rootDir, HARNESS_REPORT_APPROVAL_PATH), approval);
761
783
  return { report, validation, approval };
762
784
  }
763
- function loadHarnessReportForSync(rootDir, reportPath) {
785
+ function loadHarnessReportForSync(rootDir, reportPath, options) {
764
786
  const report = readHarnessReport(reportPath);
765
- const validation = validateHarnessReport(report, rootDir);
787
+ const validation = validateHarnessReport(report, rootDir, options);
766
788
  if (validation.status !== "pass") {
767
789
  throw new CliInputError("Harness report validation failed; cannot sync.", validation.errors.slice(0, 5).join(`
768
790
  `));
@@ -1753,11 +1775,11 @@ import {
1753
1775
  existsSync as existsSync3,
1754
1776
  lstatSync as lstatSync3,
1755
1777
  mkdirSync as mkdirSync3,
1756
- readFileSync as readFileSync3,
1778
+ readFileSync as readFileSync4,
1757
1779
  readdirSync,
1758
1780
  writeFileSync as writeFileSync3
1759
1781
  } from "node:fs";
1760
- import { basename as basename4, delimiter, dirname as dirname4, join as join2, relative as relative3, resolve as resolve4, sep } from "node:path";
1782
+ import { basename as basename4, delimiter, dirname as dirname4, join as join2, relative as relative3, resolve as resolve5, sep } from "node:path";
1761
1783
  import { confirm as confirm2, select } from "@inquirer/prompts";
1762
1784
 
1763
1785
  // src/analyst-tui.ts
@@ -2400,7 +2422,7 @@ var LINE_PATTERNS = [
2400
2422
  kind: "tool_definition",
2401
2423
  signal: "python.tool_decorator",
2402
2424
  confidence: "high",
2403
- pattern: /^\s*@(tool|function_tool|mcp\.tool|app\.tool)\b/,
2425
+ pattern: /^\s*@(tool|function_tool|register_as_tool|\w+\.tool)\b/,
2404
2426
  extensions: PY
2405
2427
  },
2406
2428
  {
@@ -2764,6 +2786,173 @@ function writeHarnessCandidatesReport(report, outPath) {
2764
2786
  return target;
2765
2787
  }
2766
2788
 
2789
+ // src/harness-candidate-promotion.ts
2790
+ import { readFileSync as readFileSync3 } from "node:fs";
2791
+ import { resolve as resolve4 } from "node:path";
2792
+ var PROMOTED_CONFIDENCE = {
2793
+ high: 0.85,
2794
+ medium: 0.6
2795
+ };
2796
+ var MAX_DECORATOR_LOOKAHEAD_LINES = 12;
2797
+ var PY_DEF_NAME = /^\s*(?:async\s+)?def\s+([A-Za-z_]\w*)\s*\(/;
2798
+ var PY_CLASS_NAME = /\bclass\s+([A-Za-z_]\w*)\s*\(/;
2799
+ var JS_ASSIGNMENT_NAME = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=/;
2800
+ var JS_PROPERTY_NAME = /^\s*(?:public\s+|readonly\s+)?([A-Za-z_$][\w$]*)\s*:\s*(?:new\s+)?(?:[\w.$]*\.)?(?:tool|functionTool|createTool|dynamicTool|DynamicStructuredTool)\s*\(/;
2801
+ var JS_TRAILING_ASSIGNMENT_NAME = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:new\s+)?$/;
2802
+ var REGISTRY_CALL_NAME = /\b(?:register_tool|registerTool|add_tool)\s*\(\s*["']?([A-Za-z_$][\w$]*)/;
2803
+ var PROMPT_ASSIGNMENT_NAME = /^\s*(?:export\s+)?(?:(?:const|let|var)\s+)?([A-Za-z_$][\w$]*)\s*[:=]/;
2804
+ var MODEL_STRING = /["'`]([\w.:/-]*(?:gpt-|o[134](?:-mini)?\b|claude|gemini|llama|mistral|mixtral|deepseek|qwen|grok|sonnet|opus|haiku|command-r|glm|gemma)[\w.:/-]*)["'`]/i;
2805
+ function candidateEvidence(candidate) {
2806
+ return [{
2807
+ path: candidate.path,
2808
+ line: candidate.startLine,
2809
+ reason: `deterministic detector ${candidate.signal}`
2810
+ }];
2811
+ }
2812
+ function pathStem(path) {
2813
+ return path.replace(/\.[^.]+$/, "");
2814
+ }
2815
+ function readFileLines(rootDir, path, cache) {
2816
+ const cached = cache.get(path);
2817
+ if (cached !== undefined)
2818
+ return cached;
2819
+ let lines;
2820
+ try {
2821
+ lines = readFileSync3(resolve4(rootDir, path), "utf8").split(/\r?\n/);
2822
+ } catch {
2823
+ lines = null;
2824
+ }
2825
+ cache.set(path, lines);
2826
+ return lines;
2827
+ }
2828
+ function extractToolName(candidate, lines) {
2829
+ const line = lines[candidate.startLine - 1] ?? "";
2830
+ if (candidate.signal === "python.tool_decorator") {
2831
+ const end = Math.min(lines.length, candidate.startLine + MAX_DECORATOR_LOOKAHEAD_LINES);
2832
+ for (let i = candidate.startLine;i < end; i += 1) {
2833
+ const match = PY_DEF_NAME.exec(lines[i]);
2834
+ if (match)
2835
+ return match[1];
2836
+ }
2837
+ return;
2838
+ }
2839
+ if (candidate.signal === "python.base_tool_subclass") {
2840
+ return PY_CLASS_NAME.exec(line)?.[1];
2841
+ }
2842
+ if (candidate.signal === "tool_registry.registration") {
2843
+ return REGISTRY_CALL_NAME.exec(line)?.[1];
2844
+ }
2845
+ return JS_ASSIGNMENT_NAME.exec(line)?.[1] ?? JS_PROPERTY_NAME.exec(line)?.[1] ?? (candidate.startLine >= 2 ? JS_TRAILING_ASSIGNMENT_NAME.exec(lines[candidate.startLine - 2])?.[1] : undefined);
2846
+ }
2847
+ function promoteToolCandidate(candidate, rootDir, confidence, fileCache) {
2848
+ const lines = readFileLines(rootDir, candidate.path, fileCache);
2849
+ if (!lines)
2850
+ return;
2851
+ const name = extractToolName(candidate, lines);
2852
+ if (!name)
2853
+ return;
2854
+ return {
2855
+ id: stableId2("tool", `${candidate.path.replace(/\.[^./]+$/, "")}:${name}`),
2856
+ type: "tool",
2857
+ name: titleFromId2(name),
2858
+ scope: "shared",
2859
+ sourcePath: candidate.path,
2860
+ usedByAgentIds: [],
2861
+ confidence,
2862
+ evidence: candidateEvidence(candidate),
2863
+ body: extractToolBody({ path: candidate.path, rootDir }, { name, reason: candidate.signal })
2864
+ };
2865
+ }
2866
+ function promotePromptFileCandidate(candidate, confidence) {
2867
+ return {
2868
+ id: stableId2("prompt", candidate.path),
2869
+ type: "prompt",
2870
+ name: promptNameFromPath(candidate.path),
2871
+ scope: "shared",
2872
+ sourcePath: candidate.path,
2873
+ usedByAgentIds: [],
2874
+ confidence,
2875
+ evidence: candidateEvidence(candidate)
2876
+ };
2877
+ }
2878
+ function promotePromptLiteralCandidate(candidate, rootDir, confidence, fileCache) {
2879
+ const lines = readFileLines(rootDir, candidate.path, fileCache);
2880
+ const assignedName = lines ? PROMPT_ASSIGNMENT_NAME.exec(lines[candidate.startLine - 1] ?? "")?.[1] ?? (candidate.startLine >= 2 ? PROMPT_ASSIGNMENT_NAME.exec(lines[candidate.startLine - 2] ?? "")?.[1] : undefined) : undefined;
2881
+ return {
2882
+ id: stableId2("prompt", `${pathStem(candidate.path)}:L${candidate.startLine}`),
2883
+ type: "prompt",
2884
+ name: assignedName ? titleFromId2(assignedName) : `Inline Prompt (${candidate.path}:${candidate.startLine})`,
2885
+ scope: "shared",
2886
+ usedByAgentIds: [],
2887
+ confidence,
2888
+ evidence: candidateEvidence(candidate)
2889
+ };
2890
+ }
2891
+ function promoteModelConfigCandidate(candidate, rootDir, confidence, fileCache) {
2892
+ const lines = readFileLines(rootDir, candidate.path, fileCache);
2893
+ const sourceLine = lines?.[candidate.startLine - 1] ?? "";
2894
+ const model = MODEL_STRING.exec(sourceLine)?.[1] ?? MODEL_STRING.exec(candidate.excerpt)?.[1];
2895
+ if (!model)
2896
+ return;
2897
+ return {
2898
+ id: stableId2("model_config", `${pathStem(candidate.path)}:L${candidate.startLine}`),
2899
+ type: "model_config",
2900
+ name: model,
2901
+ scope: "shared",
2902
+ usedByAgentIds: [],
2903
+ confidence,
2904
+ evidence: candidateEvidence(candidate)
2905
+ };
2906
+ }
2907
+ function mergePromotedDuplicate(existing, next) {
2908
+ for (const entry of next.evidence) {
2909
+ const already = existing.evidence.some((candidate) => candidate.path === entry.path && candidate.line === entry.line && candidate.reason === entry.reason);
2910
+ if (!already)
2911
+ existing.evidence.push(entry);
2912
+ }
2913
+ existing.confidence = Math.max(existing.confidence, next.confidence);
2914
+ if (!existing.body && next.body)
2915
+ existing.body = next.body;
2916
+ }
2917
+ function promoteHarnessCandidates(report, rootDir) {
2918
+ const fileCache = new Map;
2919
+ const byId = new Map;
2920
+ for (const candidate of report.candidates) {
2921
+ const confidence = PROMOTED_CONFIDENCE[candidate.confidence];
2922
+ if (confidence === undefined)
2923
+ continue;
2924
+ let artifact;
2925
+ switch (candidate.kind) {
2926
+ case "tool_definition":
2927
+ artifact = promoteToolCandidate(candidate, rootDir, confidence, fileCache);
2928
+ break;
2929
+ case "prompt_file":
2930
+ artifact = promotePromptFileCandidate(candidate, confidence);
2931
+ break;
2932
+ case "prompt_literal":
2933
+ if (candidate.confidence === "high") {
2934
+ artifact = promotePromptLiteralCandidate(candidate, rootDir, confidence, fileCache);
2935
+ }
2936
+ break;
2937
+ case "model_config":
2938
+ if (candidate.confidence === "high") {
2939
+ artifact = promoteModelConfigCandidate(candidate, rootDir, confidence, fileCache);
2940
+ }
2941
+ break;
2942
+ default:
2943
+ break;
2944
+ }
2945
+ if (!artifact)
2946
+ continue;
2947
+ const existing = byId.get(artifact.id);
2948
+ if (existing)
2949
+ mergePromotedDuplicate(existing, artifact);
2950
+ else
2951
+ byId.set(artifact.id, artifact);
2952
+ }
2953
+ return [...byId.values()];
2954
+ }
2955
+
2767
2956
  // src/harness-sync-limits.ts
2768
2957
  var MAX_HARNESS_SYNC_GRAPH_BYTES = 2000000;
2769
2958
  function assertHarnessGraphSyncSize(graph) {
@@ -3103,7 +3292,7 @@ var REPORT_JSON_END = "MODA_HARNESS_REPORT_JSON_END";
3103
3292
  var EXTERNAL_ANALYST_ADAPTERS = ["claude", "codex", "cursor"];
3104
3293
  async function runHarnessCommand(context) {
3105
3294
  const subcommand = context.positionals[0] ?? "scan";
3106
- const rootDir = resolve4(context.cwd, context.flags.path ?? ".");
3295
+ const rootDir = resolve5(context.cwd, context.flags.path ?? ".");
3107
3296
  const selectedAgentId = selectedRuntimeAgentId(context.flags);
3108
3297
  const tenant = resolveHarnessTenant(context.flags, context);
3109
3298
  const scanPlan = buildCommandScanPlan(context, rootDir);
@@ -3148,7 +3337,9 @@ async function runHarnessCommand(context) {
3148
3337
  }
3149
3338
  if (context.flags["from-report"] || context.flags.report) {
3150
3339
  const reportPath = resolveHarnessReportPath(rootDir, context.flags["from-report"] ?? context.flags.report);
3151
- const { report, validation, approval, approved } = loadHarnessReportForSync(rootDir, reportPath);
3340
+ const { report, validation, approval, approved } = loadHarnessReportForSync(rootDir, reportPath, {
3341
+ deterministicFloor: computeDeterministicFloor(rootDir)
3342
+ });
3152
3343
  const reportHash = hashHarnessReport(report);
3153
3344
  if (!approved && !context.dryRun) {
3154
3345
  throw new CliInputError("Harness report is not approved; refusing to sync.", "Run `moda harness validate-report`, review the report, then run `moda harness approve --yes` before syncing.");
@@ -3342,11 +3533,13 @@ async function runHarnessCommand(context) {
3342
3533
  elapsed_ms: Date.now() - context.startedAt
3343
3534
  });
3344
3535
  if (context.flags["github-actions"] === "true") {
3345
- const { runGithubActionsAnalyze } = await import("./harness-github-actions-wd1ahhxz.js");
3536
+ const { runGithubActionsAnalyze } = await import("./harness-github-actions-p8xa3akx.js");
3346
3537
  const result = await runGithubActionsAnalyze(rootDir, {
3347
3538
  writeReport: (report2) => {
3348
3539
  const normalized = normalizeHarnessReport(report2);
3349
- const validation2 = validateHarnessReport(normalized, rootDir);
3540
+ const validation2 = validateHarnessReport(normalized, rootDir, {
3541
+ deterministicFloor: computeDeterministicFloor(rootDir)
3542
+ });
3350
3543
  writeHarnessReportArtifacts({
3351
3544
  rootDir,
3352
3545
  report: normalized,
@@ -3434,7 +3627,9 @@ async function runHarnessCommand(context) {
3434
3627
  case "validate-report": {
3435
3628
  const reportPath = resolveHarnessReportPath(rootDir, context.positionals[1] ?? context.flags.report);
3436
3629
  const report = readHarnessReport(reportPath);
3437
- const validation = validateHarnessReport(report, rootDir);
3630
+ const validation = validateHarnessReport(report, rootDir, {
3631
+ deterministicFloor: computeDeterministicFloor(rootDir)
3632
+ });
3438
3633
  if (!context.dryRun) {
3439
3634
  writeHarnessReportArtifacts({
3440
3635
  rootDir,
@@ -3479,7 +3674,7 @@ async function runHarnessCommand(context) {
3479
3674
  throw new CliInputError("Missing output path for --out.", "Usage: moda harness candidates --out=<file>");
3480
3675
  }
3481
3676
  if (outFlag && !context.dryRun) {
3482
- written.push(writeHarnessCandidatesReport(report, resolve4(context.cwd, outFlag)));
3677
+ written.push(writeHarnessCandidatesReport(report, resolve5(context.cwd, outFlag)));
3483
3678
  }
3484
3679
  writeHumanProgress(context, `Harness candidates: ${report.stats.candidateCount} candidates across ${report.stats.filesWithCandidates} files (${report.stats.filesScanned} scanned, ${report.stats.filesPerSecond.toLocaleString()} files/sec).`);
3485
3680
  context.output.writeData({ scanPlan, ...report, written, dryRun: context.dryRun });
@@ -3740,7 +3935,9 @@ ${result.stderr}`, 1200);
3740
3935
  elapsed_ms: Date.now() - options.context.startedAt
3741
3936
  });
3742
3937
  analystTui?.setValidation("running");
3743
- const validation = validateHarnessReport(report, options.rootDir);
3938
+ const validation = validateHarnessReport(report, options.rootDir, {
3939
+ deterministicFloor: computeDeterministicFloor(options.rootDir)
3940
+ });
3744
3941
  analystTui?.setValidation(validation.status === "pass" ? "pass" : "fail");
3745
3942
  if (!analystTui)
3746
3943
  writeHumanProgress(options.context, `Moda Analyst: validation ${validation.status}.`);
@@ -3790,7 +3987,7 @@ function buildExternalAnalystEnv(env) {
3790
3987
  return next;
3791
3988
  }
3792
3989
  function readExternalAnalystReport(rootDir, result, adapter) {
3793
- const reportPath = resolve4(rootDir, HARNESS_REPORT_PATH);
3990
+ const reportPath = resolve5(rootDir, HARNESS_REPORT_PATH);
3794
3991
  if (existsSync3(reportPath))
3795
3992
  return repairExternalAnalystReport(readHarnessReport(reportPath), rootDir);
3796
3993
  const output = [
@@ -3805,7 +4002,7 @@ function readExternalAnalystReport(rootDir, result, adapter) {
3805
4002
  return parsed;
3806
4003
  let debugHint = "";
3807
4004
  try {
3808
- const debugPath = resolve4(rootDir, MODA_DIR, "analyst-debug.log");
4005
+ const debugPath = resolve5(rootDir, MODA_DIR, "analyst-debug.log");
3809
4006
  mkdirSync3(dirname4(debugPath), { recursive: true });
3810
4007
  writeFileSync3(debugPath, [
3811
4008
  `# analyst=${adapter} status=${result.status} signal=${result.signal} timedOut=${result.timedOut} maxBufferExceeded=${result.maxBufferExceeded}`,
@@ -3865,10 +4062,56 @@ function parseHarnessReportCandidate(candidate) {
3865
4062
  function repairExternalAnalystReport(report, rootDir) {
3866
4063
  const normalized = normalizeHarnessReport(report);
3867
4064
  repairCitationLines(normalized, rootDir);
4065
+ mergeDeterministicArtifacts(normalized.graph, rootDir);
3868
4066
  hydratePromptAndToolBodies(normalized.graph, rootDir);
3869
4067
  repairReportSummaryCounts(normalized);
3870
4068
  return normalized;
3871
4069
  }
4070
+ function mergeDeterministicArtifacts(graph, rootDir) {
4071
+ if (process.env.MODA_HARNESS_DETERMINISTIC_MERGE === "0")
4072
+ return { added: 0 };
4073
+ let promoted;
4074
+ try {
4075
+ promoted = promoteHarnessCandidates(detectHarnessCandidates(rootDir), rootDir);
4076
+ } catch {
4077
+ return { added: 0 };
4078
+ }
4079
+ let added = 0;
4080
+ for (const artifact of promoted) {
4081
+ if (graphCoversPromotedArtifact(graph, artifact))
4082
+ continue;
4083
+ graph.artifacts.push(artifact);
4084
+ added += 1;
4085
+ }
4086
+ return { added };
4087
+ }
4088
+ function artifactSourceKey(artifact) {
4089
+ if (artifact.sourcePath)
4090
+ return normalizePath3(artifact.sourcePath);
4091
+ const pinned = Array.isArray(artifact.evidence) ? artifact.evidence.find((entry) => typeof entry.path === "string" && entry.path.length > 0) : undefined;
4092
+ return pinned ? normalizePath3(pinned.path) : undefined;
4093
+ }
4094
+ function graphCoversPromotedArtifact(graph, promoted) {
4095
+ const promotedName = promoted.name.toLowerCase();
4096
+ const promotedLines = promoted.evidence.filter((entry) => typeof entry.line === "number");
4097
+ const promotedSource = artifactSourceKey(promoted);
4098
+ const isWholeFilePrompt = promoted.type === "prompt" && Boolean(promoted.sourcePath);
4099
+ return graph.artifacts.some((existing) => {
4100
+ if (existing.id === promoted.id)
4101
+ return true;
4102
+ if (existing.type === promoted.type && existing.name?.toLowerCase() === promotedName) {
4103
+ const existingSource = artifactSourceKey(existing);
4104
+ if (!existingSource || !promotedSource || existingSource === promotedSource)
4105
+ return true;
4106
+ }
4107
+ if (existing.type === promoted.type && Array.isArray(existing.evidence) && existing.evidence.some((entry) => typeof entry.line === "number" && promotedLines.some((line) => line.path === entry.path && line.line === entry.line))) {
4108
+ return true;
4109
+ }
4110
+ if (isWholeFilePrompt && existing.sourcePath === promoted.sourcePath)
4111
+ return true;
4112
+ return false;
4113
+ });
4114
+ }
3872
4115
  function repairReportSummaryCounts(report) {
3873
4116
  const graph = report.graph;
3874
4117
  report.summary = {
@@ -3892,12 +4135,12 @@ function repairCitationLines(report, rootDir) {
3892
4135
  if (citation.type !== "file" && citation.type !== "generated_doc" || !citation.path || !citation.excerpt)
3893
4136
  continue;
3894
4137
  const excerpt = citation.excerpt;
3895
- const candidatePath = resolve4(rootDir, citation.path);
4138
+ const candidatePath = resolve5(rootDir, citation.path);
3896
4139
  if (!existsSync3(candidatePath))
3897
4140
  continue;
3898
4141
  let text;
3899
4142
  try {
3900
- text = readFileSync3(candidatePath, "utf8");
4143
+ text = readFileSync4(candidatePath, "utf8");
3901
4144
  } catch {
3902
4145
  continue;
3903
4146
  }
@@ -4634,7 +4877,7 @@ function renderAnalystCandidateLines(report) {
4634
4877
  const lines = [
4635
4878
  "",
4636
4879
  `Deterministic candidate inventory (${report.candidates.length} candidates across ${report.stats.filesWithCandidates} files, scanned in ${report.stats.durationMs}ms):`,
4637
- "Each entry is path:line [detector] excerpt. Verify each candidate against the real file, then represent every confirmed tool/prompt/eval/model-config as a graph artifact pinned to that location. Candidates are pattern matches — drop false positives, and investigate what patterns cannot see (agent topology, relationships, prompts assembled at runtime). Do not cite this list as evidence; cite the files."
4880
+ "Each entry is path:line [detector] excerpt. Candidates are pattern matches — verify each against the real file and drop false positives. Verified deterministic candidates are also auto-merged into the graph after your run, so prioritize (1) agents, topology, and relationships, (2) verifying or refuting these candidates (list refuted ones in unknowns), and (3) artifacts the patterns cannot see (runtime agents, prompts assembled at runtime, cross-file wiring) — rather than re-listing every candidate as an artifact. Do not cite this list as evidence; cite the files."
4638
4881
  ];
4639
4882
  for (const [kind, label] of Object.entries(SEED_CANDIDATE_KIND_LABELS)) {
4640
4883
  const bucket = byKind.get(kind);
@@ -4786,7 +5029,7 @@ function truncateText(value, maxLength) {
4786
5029
  return compact.length > maxLength ? `${compact.slice(0, maxLength - 1)}...` : compact;
4787
5030
  }
4788
5031
  function scanHarness(options = {}) {
4789
- const rootDir = resolve4(options.rootDir ?? process.cwd());
5032
+ const rootDir = resolve5(options.rootDir ?? process.cwd());
4790
5033
  const skipped = [];
4791
5034
  const files = inventoryFiles(rootDir, skipped);
4792
5035
  const annotations = files.flatMap((file) => parseAnnotations(file));
@@ -4888,7 +5131,7 @@ function collectSnapshotInputs(rootDir, maxFiles, maxTextBytes) {
4888
5131
  let text = "";
4889
5132
  if (isTextFile(path)) {
4890
5133
  try {
4891
- text = readFileSync3(current, "utf8");
5134
+ text = readFileSync4(current, "utf8");
4892
5135
  inventoryTextBytes += Buffer.byteLength(text, "utf8");
4893
5136
  } catch {
4894
5137
  return;
@@ -4901,7 +5144,7 @@ function collectSnapshotInputs(rootDir, maxFiles, maxTextBytes) {
4901
5144
  return inputs;
4902
5145
  }
4903
5146
  function buildSourceSnapshot(rootDir) {
4904
- const root = resolve4(rootDir);
5147
+ const root = resolve5(rootDir);
4905
5148
  const maxFiles = numericEnv("MODA_HARNESS_MAX_FILES", DEFAULT_MAX_INVENTORY_FILES);
4906
5149
  const maxTextBytes = numericEnv("MODA_HARNESS_MAX_TEXT_BYTES", DEFAULT_MAX_INVENTORY_TEXT_BYTES);
4907
5150
  const inputs = collectSnapshotInputs(root, maxFiles, maxTextBytes);
@@ -5128,7 +5371,7 @@ function readHarnessSyncState(rootDir) {
5128
5371
  if (!existsSync3(path))
5129
5372
  return;
5130
5373
  try {
5131
- const parsed = JSON.parse(readFileSync3(path, "utf8"));
5374
+ const parsed = JSON.parse(readFileSync4(path, "utf8"));
5132
5375
  if (!isRecord3(parsed) || parsed.schemaVersion !== "sync_state.v0.1") {
5133
5376
  return;
5134
5377
  }
@@ -5245,7 +5488,7 @@ function findFresherHarnessReports(rootDir) {
5245
5488
  return fresher.sort((a, b) => b.mtimeMs - a.mtimeMs);
5246
5489
  }
5247
5490
  function warnFresherSubdirHarnessReports(context, rootDir, explicitReport) {
5248
- const explicitReportPath = explicitReport && explicitReport !== "true" ? normalizePath3(relative3(rootDir, resolve4(rootDir, explicitReport))) : undefined;
5491
+ const explicitReportPath = explicitReport && explicitReport !== "true" ? normalizePath3(relative3(rootDir, resolve5(rootDir, explicitReport))) : undefined;
5249
5492
  if (explicitReportPath && explicitReportPath !== normalizePath3(HARNESS_REPORT_PATH))
5250
5493
  return;
5251
5494
  let fresher;
@@ -5262,7 +5505,7 @@ function warnFresherSubdirHarnessReports(context, rootDir, explicitReport) {
5262
5505
  writeHarnessNotice(context, `A fresher harness report exists at ${newest.path} (this directory has ${rootState}). If you meant that report, re-run with --path=${subdir}.`);
5263
5506
  }
5264
5507
  function readHarnessGraph(manifestPath) {
5265
- return JSON.parse(readFileSync3(manifestPath, "utf8"));
5508
+ return JSON.parse(readFileSync4(manifestPath, "utf8"));
5266
5509
  }
5267
5510
  function writeHarnessArtifacts(graph, rootDir, options = {}) {
5268
5511
  const modaDir = join2(rootDir, MODA_DIR);
@@ -5439,7 +5682,7 @@ function explainAgent(graph, agent) {
5439
5682
  }
5440
5683
  function inventoryFiles(rootDir, skipped, options = {}) {
5441
5684
  const files = [];
5442
- const rootReal = resolve4(rootDir);
5685
+ const rootReal = resolve5(rootDir);
5443
5686
  const maxFiles = numericEnv("MODA_HARNESS_MAX_FILES", DEFAULT_MAX_INVENTORY_FILES);
5444
5687
  const maxTextBytes = numericEnv("MODA_HARNESS_MAX_TEXT_BYTES", DEFAULT_MAX_INVENTORY_TEXT_BYTES);
5445
5688
  let textBytes = 0;
@@ -5493,7 +5736,7 @@ function inventoryFiles(rootDir, skipped, options = {}) {
5493
5736
  recordSkipped(skipped, { path: rel, reason: "scan text budget exceeded" });
5494
5737
  } else {
5495
5738
  try {
5496
- item.text = readFileSync3(current, "utf8");
5739
+ item.text = readFileSync4(current, "utf8");
5497
5740
  textBytes += Buffer.byteLength(item.text, "utf8");
5498
5741
  } catch {
5499
5742
  recordSkipped(skipped, { path: rel, reason: "text read failed" });
@@ -5800,7 +6043,7 @@ function maybeAddPromptArtifact(graph, file) {
5800
6043
  if (!isPrompt)
5801
6044
  return;
5802
6045
  const id = stableId2("prompt", file.path);
5803
- const text = file.text ?? readFileSync3(file.absPath, "utf8");
6046
+ const text = file.text ?? readFileSync4(file.absPath, "utf8");
5804
6047
  upsertArtifact(graph, {
5805
6048
  id,
5806
6049
  type: "prompt",
@@ -6027,12 +6270,27 @@ function detectToolDefinitions(file) {
6027
6270
  const pyDecorated = /@function_tool[^\n]*\n(?:[ \t]*@[^\n]*\n)*[ \t]*(?:async[ \t]+)?def[ \t]+([A-Za-z_]\w*)/g;
6028
6271
  for (const match of text.matchAll(pyDecorated))
6029
6272
  found.set(match[1], "@function_tool-decorated function");
6273
+ const pyToolDecorated = /@((?:[A-Za-z_]\w*\.)?tool|register_as_tool)\b[^\n]*\n(?:[ \t]*@[^\n]*\n)*[ \t]*(?:async[ \t]+)?def[ \t]+([A-Za-z_]\w*)/g;
6274
+ for (const match of text.matchAll(pyToolDecorated))
6275
+ found.set(match[2], `@${match[1]}-decorated function`);
6276
+ const pyToolClass = /class[ \t]+([A-Za-z_]\w*)\s*\(\s*(?:[A-Za-z_]\w*\.)?(BaseTool|StructuredTool)\b/g;
6277
+ for (const match of text.matchAll(pyToolClass))
6278
+ found.set(match[1], `${match[2]} subclass`);
6030
6279
  const tsFactory = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:[\w.]*\.)?(?:tool|functionTool|createTool|dynamicTool)\s*\(/g;
6031
6280
  for (const match of text.matchAll(tsFactory))
6032
6281
  found.set(match[1], "tool factory assignment");
6033
- const tsNewTool = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*new\s+(?:Dynamic|Structured)?Tool\b/g;
6282
+ const tsNewTool = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*new\s+(?:DynamicStructured|Dynamic|Structured)?Tool\b/g;
6034
6283
  for (const match of text.matchAll(tsNewTool))
6035
6284
  found.set(match[1], "tool constructor assignment");
6285
+ const tsInlineToolCtor = /(?:^|[(,[]|return\s)\s*(?:new\s+)?(?:DynamicStructured|Dynamic|Structured)Tool\s*\(\s*\{[^{}]*?\bname\s*:\s*["'`]([\w .:-]+)["'`]/gm;
6286
+ for (const match of text.matchAll(tsInlineToolCtor))
6287
+ found.set(match[1], "tool constructor call");
6288
+ const registryCall = /\b(register_tool|registerTool|add_tool|ToolRegistry)\s*\(\s*\[?\s*(?:name\s*=\s*)?(?:["']([\w .:-]+)["']|([A-Za-z_$][\w$]*))/g;
6289
+ for (const match of text.matchAll(registryCall)) {
6290
+ const name = match[2] ?? match[3];
6291
+ if (name)
6292
+ found.set(name, `${match[1]} registration`);
6293
+ }
6036
6294
  for (const match of text.matchAll(HOSTED_TOOL_CALL))
6037
6295
  found.set(match[1], `hosted tool ${match[1]}`);
6038
6296
  return [...found.entries()].map(([name, reason]) => ({ name, reason }));
@@ -6048,7 +6306,7 @@ function maybeAddToolArtifact(graph, file) {
6048
6306
  for (const tool of tools) {
6049
6307
  const body = extractToolBody(file, tool);
6050
6308
  upsertArtifact(graph, {
6051
- id: stableId2("tool", `${file.path}:${tool.name}`),
6309
+ id: stableId2("tool", `${file.path.replace(/\.[^./]+$/, "")}:${tool.name}`),
6052
6310
  type: "tool",
6053
6311
  name: titleFromId2(tool.name),
6054
6312
  scope: "agent",
@@ -6156,6 +6414,55 @@ var FRAMEWORK_CATALOG = [
6156
6414
  kind: "agent_framework",
6157
6415
  packageNames: ["pydantic-ai", "pydantic_ai"],
6158
6416
  patterns: [/pydantic_ai/i]
6417
+ },
6418
+ {
6419
+ id: "framework_langgraph",
6420
+ name: "LangGraph",
6421
+ kind: "agent_framework",
6422
+ packageNames: ["langgraph", "@langchain/langgraph"],
6423
+ patterns: [/\blanggraph\b/i, /\bStateGraph\s*\(/]
6424
+ },
6425
+ {
6426
+ id: "framework_autogen",
6427
+ name: "AutoGen",
6428
+ kind: "agent_framework",
6429
+ packageNames: ["autogen", "pyautogen", "autogen-agentchat"],
6430
+ patterns: [/\bautogen\b/i]
6431
+ },
6432
+ {
6433
+ id: "framework_dspy",
6434
+ name: "DSPy",
6435
+ kind: "agent_framework",
6436
+ packageNames: ["dspy", "dspy-ai"],
6437
+ patterns: [/\bdspy\b/i]
6438
+ },
6439
+ {
6440
+ id: "framework_semantic_kernel",
6441
+ name: "Semantic Kernel",
6442
+ kind: "agent_framework",
6443
+ packageNames: ["semantic-kernel"],
6444
+ patterns: [/semantic[-_]?kernel/i]
6445
+ },
6446
+ {
6447
+ id: "framework_smolagents",
6448
+ name: "Smolagents",
6449
+ kind: "agent_framework",
6450
+ packageNames: ["smolagents"],
6451
+ patterns: [/\bsmolagents\b/i]
6452
+ },
6453
+ {
6454
+ id: "framework_llama_index",
6455
+ name: "LlamaIndex",
6456
+ kind: "agent_framework",
6457
+ packageNames: ["llama-index", "llamaindex"],
6458
+ patterns: [/llama[-_]?index/i]
6459
+ },
6460
+ {
6461
+ id: "framework_claude_agent_sdk",
6462
+ name: "Claude Agent SDK",
6463
+ kind: "agent_framework",
6464
+ packageNames: ["@anthropic-ai/claude-agent-sdk", "claude-agent-sdk"],
6465
+ patterns: [/claude[-_]agent[-_]sdk/i]
6159
6466
  }
6160
6467
  ];
6161
6468
  var PROVIDER_CATALOG = [
@@ -6206,6 +6513,46 @@ var PROVIDER_CATALOG = [
6206
6513
  packageNames: ["ollama"],
6207
6514
  envVars: ["OLLAMA_HOST"],
6208
6515
  patterns: [/\bollama\b/i]
6516
+ },
6517
+ {
6518
+ id: "provider_litellm",
6519
+ name: "LiteLLM",
6520
+ kind: "model_provider",
6521
+ packageNames: ["litellm"],
6522
+ envVars: [],
6523
+ patterns: [/\blitellm\b/i]
6524
+ },
6525
+ {
6526
+ id: "provider_aws_bedrock",
6527
+ name: "AWS Bedrock",
6528
+ kind: "model_provider",
6529
+ packageNames: ["@aws-sdk/client-bedrock-runtime"],
6530
+ envVars: ["AWS_BEARER_TOKEN_BEDROCK"],
6531
+ patterns: [/bedrock-runtime/i, /\bBedrockRuntimeClient\b/, /\bInvokeModelCommand\b/, /\binvoke_model\s*\(/]
6532
+ },
6533
+ {
6534
+ id: "provider_openrouter",
6535
+ name: "OpenRouter",
6536
+ kind: "model_provider",
6537
+ packageNames: ["openrouter", "@openrouter/ai-sdk-provider"],
6538
+ envVars: ["OPENROUTER_API_KEY"],
6539
+ patterns: [/openrouter\.ai/i, /\bopenrouter\b/i, /OPENROUTER_API_KEY/]
6540
+ },
6541
+ {
6542
+ id: "provider_xai",
6543
+ name: "xAI",
6544
+ kind: "model_provider",
6545
+ packageNames: ["xai-sdk"],
6546
+ envVars: ["XAI_API_KEY"],
6547
+ patterns: [/\bgrok-[\w.-]+/i, /api\.x\.ai/i, /XAI_API_KEY/]
6548
+ },
6549
+ {
6550
+ id: "provider_groq",
6551
+ name: "Groq",
6552
+ kind: "model_provider",
6553
+ packageNames: ["groq", "groq-sdk"],
6554
+ envVars: ["GROQ_API_KEY"],
6555
+ patterns: [/\bgroq\b/i]
6209
6556
  }
6210
6557
  ];
6211
6558
  function detectAgentFrameworks(files) {
@@ -6247,6 +6594,59 @@ function detectModelProviders(files) {
6247
6594
  }
6248
6595
  return providers.sort((a, b) => a.id.localeCompare(b.id));
6249
6596
  }
6597
+ function computeDeterministicFloor(rootDir) {
6598
+ try {
6599
+ const candidateReport = detectHarnessCandidates(rootDir);
6600
+ let toolCandidates = 0;
6601
+ let promptCandidates = 0;
6602
+ let llmCallSites = 0;
6603
+ for (const candidate of candidateReport.candidates) {
6604
+ if (candidate.kind === "tool_definition" && candidate.confidence === "high") {
6605
+ toolCandidates += 1;
6606
+ } else if ((candidate.kind === "prompt_file" || candidate.kind === "prompt_literal") && candidate.confidence === "high") {
6607
+ promptCandidates += 1;
6608
+ } else if (candidate.kind === "llm_call_site") {
6609
+ llmCallSites += 1;
6610
+ }
6611
+ }
6612
+ return {
6613
+ toolCandidates,
6614
+ promptCandidates,
6615
+ llmCallSites,
6616
+ providerPackages: detectProviderPackagesInManifests(rootDir)
6617
+ };
6618
+ } catch {
6619
+ return { toolCandidates: 0, promptCandidates: 0, llmCallSites: 0, providerPackages: [] };
6620
+ }
6621
+ }
6622
+ function detectProviderPackagesInManifests(rootDir) {
6623
+ const present = new Set;
6624
+ const packageJsonPath = join2(rootDir, "package.json");
6625
+ if (existsSync3(packageJsonPath)) {
6626
+ try {
6627
+ const deps = packageDependencyNames(readFileSync4(packageJsonPath, "utf8"));
6628
+ for (const provider of PROVIDER_CATALOG) {
6629
+ for (const name of provider.packageNames) {
6630
+ if (deps.has(name))
6631
+ present.add(name);
6632
+ }
6633
+ }
6634
+ } catch {}
6635
+ }
6636
+ const pyprojectPath = join2(rootDir, "pyproject.toml");
6637
+ if (existsSync3(pyprojectPath)) {
6638
+ try {
6639
+ const deps = pyprojectDependencyNames(readFileSync4(pyprojectPath, "utf8"));
6640
+ for (const provider of PROVIDER_CATALOG) {
6641
+ for (const name of provider.packageNames) {
6642
+ if (deps.has(name.toLowerCase()))
6643
+ present.add(name);
6644
+ }
6645
+ }
6646
+ } catch {}
6647
+ }
6648
+ return [...present].sort();
6649
+ }
6250
6650
  function detectionEvidence(files, packageNames, patterns) {
6251
6651
  const evidence = [];
6252
6652
  for (const file of files) {
@@ -6328,7 +6728,7 @@ function pyprojectDependencyNames(text) {
6328
6728
  }
6329
6729
  function modelHintsForProvider(providerId, files) {
6330
6730
  const hints = new Set;
6331
- const pattern = providerId === "provider_anthropic" ? /\bclaude-[A-Za-z0-9_.-]+/g : providerId === "provider_openai" ? /\bgpt-[A-Za-z0-9_.-]+/g : providerId === "provider_google" ? /\bgemini-[A-Za-z0-9_.-]+/g : null;
6731
+ const pattern = providerId === "provider_anthropic" ? /\bclaude-[A-Za-z0-9_.-]+/g : providerId === "provider_openai" ? /\bgpt-[A-Za-z0-9_.-]+/g : providerId === "provider_google" ? /\bgemini-[A-Za-z0-9_.-]+/g : providerId === "provider_xai" ? /\bgrok-[A-Za-z0-9_.-]+/g : null;
6332
6732
  if (!pattern)
6333
6733
  return [];
6334
6734
  for (const file of files) {
@@ -6619,7 +7019,7 @@ function upsertDeployment(graph, next) {
6619
7019
  }
6620
7020
  function contentHashForAnnotatedSource(sourcePath, annotation) {
6621
7021
  const bodyAnnotation = annotation;
6622
- const rootCandidate = resolve4(annotation.rootDir, sourcePath);
7022
+ const rootCandidate = resolve5(annotation.rootDir, sourcePath);
6623
7023
  const candidates = [
6624
7024
  rootCandidate,
6625
7025
  ...annotatedSourceCandidates(sourcePath, bodyAnnotation).filter((c) => c !== rootCandidate)
@@ -6653,7 +7053,7 @@ function readSmallTextFileForHash(absPath, rootDir) {
6653
7053
  if (!isTextFile(rel))
6654
7054
  return;
6655
7055
  try {
6656
- return readFileSync3(absPath, "utf8");
7056
+ return readFileSync4(absPath, "utf8");
6657
7057
  } catch {
6658
7058
  return;
6659
7059
  }
@@ -6773,7 +7173,7 @@ function readRemoteRunState(rootDir) {
6773
7173
  if (!existsSync4(path))
6774
7174
  return;
6775
7175
  try {
6776
- const parsed = JSON.parse(readFileSync4(path, "utf8"));
7176
+ const parsed = JSON.parse(readFileSync5(path, "utf8"));
6777
7177
  if (!parsed || typeof parsed.run_id !== "string" || parsed.run_id.length === 0)
6778
7178
  return;
6779
7179
  return {
@@ -6928,7 +7328,9 @@ function finalizeRemoteAnalyzeResult(options) {
6928
7328
  if (options.snapshotTruncated) {
6929
7329
  report = stampRemoteSnapshotTruncation(report);
6930
7330
  }
6931
- const validation = validateHarnessReport(report, rootDir);
7331
+ const validation = validateHarnessReport(report, rootDir, {
7332
+ deterministicFloor: computeDeterministicFloor(rootDir)
7333
+ });
6932
7334
  context.output.writeEvent({
6933
7335
  event: "progress",
6934
7336
  phase: "validation",
@@ -11,7 +11,7 @@ import {
11
11
  renderStatusHuman,
12
12
  scanHarness,
13
13
  validateHarnessReport
14
- } from "./cli-793dhrgb.js";
14
+ } from "./cli-351a5yc4.js";
15
15
  import {
16
16
  isAuthSessionValid,
17
17
  loadAuthSession
package/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  runPromptsCommand,
9
9
  runSkillsCommand,
10
10
  runStatusCommand
11
- } from "./cli-n5qz81m0.js";
11
+ } from "./cli-3w6hbpr3.js";
12
12
  import {
13
13
  ApiError,
14
14
  HARNESS_REPORT_APPROVAL_PATH,
@@ -29,7 +29,7 @@ import {
29
29
  summarizeHarness,
30
30
  terminalStyles,
31
31
  validateHarnessReport
32
- } from "./cli-793dhrgb.js";
32
+ } from "./cli-351a5yc4.js";
33
33
  import {
34
34
  authFetch,
35
35
  clearAuthSession,
@@ -6496,7 +6496,7 @@ var commandRegistry = createCommandRegistry([
6496
6496
  resetApiRequestCountBeforeRun: true,
6497
6497
  telemetry: "result",
6498
6498
  handler: async (context) => {
6499
- const { runInit } = await import("./index-djg29jhz.js");
6499
+ const { runInit } = await import("./index-8ksqhqmr.js");
6500
6500
  if (context.outputMode === "agent-stream") {
6501
6501
  context.output.writeEvent({
6502
6502
  event: "started",
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  buildSourceSnapshot
3
- } from "./cli-793dhrgb.js";
3
+ } from "./cli-351a5yc4.js";
4
4
  import"./cli-59yacef3.js";
5
5
 
6
6
  // src/harness-github-actions.ts
@@ -9,7 +9,7 @@ import {
9
9
  runPromptSync,
10
10
  runSkillSync,
11
11
  upsertSkillManifestRecord
12
- } from "./cli-n5qz81m0.js";
12
+ } from "./cli-3w6hbpr3.js";
13
13
  import {
14
14
  codingAgentDisplayName,
15
15
  describeCodingAgentEvent,
@@ -27,7 +27,7 @@ import {
27
27
  runHarnessCommand,
28
28
  startRemoteAnalyze,
29
29
  stripAnsi
30
- } from "./cli-793dhrgb.js";
30
+ } from "./cli-351a5yc4.js";
31
31
  import {
32
32
  selectTenantAndCreateKey
33
33
  } from "./cli-pq6rte0w.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moda-ai/cli",
3
- "version": "1.28.6",
3
+ "version": "1.29.0",
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-28T19:57:51.453Z",
4
- "cli_version": "1.28.6",
3
+ "bundled_at": "2026-08-28T23:14:50.005Z",
4
+ "cli_version": "1.29.0",
5
5
  "skills": [
6
6
  {
7
7
  "id": "integration-node-anthropic",