@moda-ai/cli 1.28.5 → 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,11 +3337,15 @@ 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.");
3155
3346
  }
3347
+ if (!context.dryRun)
3348
+ assertHarnessGraphSyncQuality(report.graph, context.flags);
3156
3349
  const syncGraph2 = graphForReport(report.graph);
3157
3350
  const sourceCommit2 = report.source.sourceCommit ?? getGitCommit(rootDir);
3158
3351
  const localGraphHash2 = hashStableJson(syncGraph2);
@@ -3233,6 +3426,8 @@ async function runHarnessCommand(context) {
3233
3426
  tenantSource: tenant?.source,
3234
3427
  pathWasExplicit: context.flags.path !== undefined
3235
3428
  });
3429
+ if (!context.dryRun)
3430
+ assertHarnessGraphSyncQuality(graph, context.flags);
3236
3431
  const syncGraph = graphForSync(graph);
3237
3432
  const sourceCommit = getGitCommit(rootDir);
3238
3433
  const localGraphHash = hashStableJson2(syncGraph);
@@ -3338,11 +3533,13 @@ async function runHarnessCommand(context) {
3338
3533
  elapsed_ms: Date.now() - context.startedAt
3339
3534
  });
3340
3535
  if (context.flags["github-actions"] === "true") {
3341
- const { runGithubActionsAnalyze } = await import("./harness-github-actions-ct8fwzdr.js");
3536
+ const { runGithubActionsAnalyze } = await import("./harness-github-actions-p8xa3akx.js");
3342
3537
  const result = await runGithubActionsAnalyze(rootDir, {
3343
3538
  writeReport: (report2) => {
3344
3539
  const normalized = normalizeHarnessReport(report2);
3345
- const validation2 = validateHarnessReport(normalized, rootDir);
3540
+ const validation2 = validateHarnessReport(normalized, rootDir, {
3541
+ deterministicFloor: computeDeterministicFloor(rootDir)
3542
+ });
3346
3543
  writeHarnessReportArtifacts({
3347
3544
  rootDir,
3348
3545
  report: normalized,
@@ -3430,7 +3627,9 @@ async function runHarnessCommand(context) {
3430
3627
  case "validate-report": {
3431
3628
  const reportPath = resolveHarnessReportPath(rootDir, context.positionals[1] ?? context.flags.report);
3432
3629
  const report = readHarnessReport(reportPath);
3433
- const validation = validateHarnessReport(report, rootDir);
3630
+ const validation = validateHarnessReport(report, rootDir, {
3631
+ deterministicFloor: computeDeterministicFloor(rootDir)
3632
+ });
3434
3633
  if (!context.dryRun) {
3435
3634
  writeHarnessReportArtifacts({
3436
3635
  rootDir,
@@ -3475,7 +3674,7 @@ async function runHarnessCommand(context) {
3475
3674
  throw new CliInputError("Missing output path for --out.", "Usage: moda harness candidates --out=<file>");
3476
3675
  }
3477
3676
  if (outFlag && !context.dryRun) {
3478
- written.push(writeHarnessCandidatesReport(report, resolve4(context.cwd, outFlag)));
3677
+ written.push(writeHarnessCandidatesReport(report, resolve5(context.cwd, outFlag)));
3479
3678
  }
3480
3679
  writeHumanProgress(context, `Harness candidates: ${report.stats.candidateCount} candidates across ${report.stats.filesWithCandidates} files (${report.stats.filesScanned} scanned, ${report.stats.filesPerSecond.toLocaleString()} files/sec).`);
3481
3680
  context.output.writeData({ scanPlan, ...report, written, dryRun: context.dryRun });
@@ -3669,7 +3868,7 @@ async function runExternalHarnessAnalyst(options) {
3669
3868
  reportPath: HARNESS_REPORT_PATH
3670
3869
  }));
3671
3870
  }
3672
- const timeoutMs = numericEnv("MODA_HARNESS_ANALYST_TIMEOUT_MS", 1200000);
3871
+ const timeoutMs = numericEnv("MODA_HARNESS_ANALYST_TIMEOUT_MS", 1800000);
3673
3872
  try {
3674
3873
  const result = await runExternalAnalystWithRetries(invocation, {
3675
3874
  cwd: options.rootDir,
@@ -3736,7 +3935,9 @@ ${result.stderr}`, 1200);
3736
3935
  elapsed_ms: Date.now() - options.context.startedAt
3737
3936
  });
3738
3937
  analystTui?.setValidation("running");
3739
- const validation = validateHarnessReport(report, options.rootDir);
3938
+ const validation = validateHarnessReport(report, options.rootDir, {
3939
+ deterministicFloor: computeDeterministicFloor(options.rootDir)
3940
+ });
3740
3941
  analystTui?.setValidation(validation.status === "pass" ? "pass" : "fail");
3741
3942
  if (!analystTui)
3742
3943
  writeHumanProgress(options.context, `Moda Analyst: validation ${validation.status}.`);
@@ -3786,7 +3987,7 @@ function buildExternalAnalystEnv(env) {
3786
3987
  return next;
3787
3988
  }
3788
3989
  function readExternalAnalystReport(rootDir, result, adapter) {
3789
- const reportPath = resolve4(rootDir, HARNESS_REPORT_PATH);
3990
+ const reportPath = resolve5(rootDir, HARNESS_REPORT_PATH);
3790
3991
  if (existsSync3(reportPath))
3791
3992
  return repairExternalAnalystReport(readHarnessReport(reportPath), rootDir);
3792
3993
  const output = [
@@ -3799,10 +4000,25 @@ function readExternalAnalystReport(rootDir, result, adapter) {
3799
4000
  const parsed = extractHarnessReportFromText(output, rootDir);
3800
4001
  if (parsed)
3801
4002
  return parsed;
4003
+ let debugHint = "";
4004
+ try {
4005
+ const debugPath = resolve5(rootDir, MODA_DIR, "analyst-debug.log");
4006
+ mkdirSync3(dirname4(debugPath), { recursive: true });
4007
+ writeFileSync3(debugPath, [
4008
+ `# analyst=${adapter} status=${result.status} signal=${result.signal} timedOut=${result.timedOut} maxBufferExceeded=${result.maxBufferExceeded}`,
4009
+ "# ----- stdout -----",
4010
+ result.stdout,
4011
+ "# ----- stderr -----",
4012
+ result.stderr
4013
+ ].join(`
4014
+ `));
4015
+ debugHint = `Raw analyst output saved to ${join2(MODA_DIR, "analyst-debug.log")} for debugging.`;
4016
+ } catch {}
3802
4017
  throw new CliInputError(`Harness analyst ${adapter} did not return a harness report.`, [
3803
4018
  `Expected ${HARNESS_REPORT_PATH} or final JSON between ${REPORT_JSON_BEGIN} and ${REPORT_JSON_END}.`,
4019
+ debugHint,
3804
4020
  "Retry with the selected coding agent CLI fixed, choose another analyst, or run `moda harness analyze --analyst=local-scan --summary` for deterministic fallback."
3805
- ].join(`
4021
+ ].filter(Boolean).join(`
3806
4022
  `));
3807
4023
  }
3808
4024
  function extractHarnessReportFromText(output, rootDir) {
@@ -3846,10 +4062,56 @@ function parseHarnessReportCandidate(candidate) {
3846
4062
  function repairExternalAnalystReport(report, rootDir) {
3847
4063
  const normalized = normalizeHarnessReport(report);
3848
4064
  repairCitationLines(normalized, rootDir);
4065
+ mergeDeterministicArtifacts(normalized.graph, rootDir);
3849
4066
  hydratePromptAndToolBodies(normalized.graph, rootDir);
3850
4067
  repairReportSummaryCounts(normalized);
3851
4068
  return normalized;
3852
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
+ }
3853
4115
  function repairReportSummaryCounts(report) {
3854
4116
  const graph = report.graph;
3855
4117
  report.summary = {
@@ -3873,12 +4135,12 @@ function repairCitationLines(report, rootDir) {
3873
4135
  if (citation.type !== "file" && citation.type !== "generated_doc" || !citation.path || !citation.excerpt)
3874
4136
  continue;
3875
4137
  const excerpt = citation.excerpt;
3876
- const candidatePath = resolve4(rootDir, citation.path);
4138
+ const candidatePath = resolve5(rootDir, citation.path);
3877
4139
  if (!existsSync3(candidatePath))
3878
4140
  continue;
3879
4141
  let text;
3880
4142
  try {
3881
- text = readFileSync3(candidatePath, "utf8");
4143
+ text = readFileSync4(candidatePath, "utf8");
3882
4144
  } catch {
3883
4145
  continue;
3884
4146
  }
@@ -4112,7 +4374,7 @@ function isRetryableExternalAnalystFailure(result) {
4112
4374
  return false;
4113
4375
  const output = `${result.stdout}
4114
4376
  ${result.stderr}`;
4115
- return /\b529\b|overloaded|server-side issue|temporar(?:y|ily)|try again|rate limit|too many requests|\b429\b|service unavailable/i.test(output);
4377
+ return /\b529\b|\b429\b|\b5(?:00|02|03|04)\b|overloaded|server-side issue|temporar(?:y|ily)|try again|rate limit|too many requests|service unavailable|internal server error|connection (?:closed|reset|error)|mid-response|econnreset|econnrefused|etimedout|socket hang ?up|fetch failed|network error|stream (?:closed|disconnected|error)/i.test(output);
4116
4378
  }
4117
4379
  function externalAnalystMaxAttempts(adapter) {
4118
4380
  const defaultRetries = adapter === "claude" ? 2 : 1;
@@ -4352,12 +4614,14 @@ function resolveExternalHarnessAnalystInvocation(options) {
4352
4614
  }
4353
4615
  if (options.adapter === "claude") {
4354
4616
  const extraArgs = externalAnalystExtraArgs(options.adapter);
4355
- const permission = mode === "write" ? ["--permission-mode", "acceptEdits", "--allowedTools", "Read,Grep,Glob,Edit,Write,Bash"] : ["--permission-mode", "plan", "--allowedTools", "Read,Grep,Glob,Bash(pwd),Bash(ls *),Bash(find *),Bash(rg *),Bash(grep *),Bash(cat *),Bash(git status *),Bash(git ls-files *),Bash(git grep *)"];
4617
+ const model = resolveAnalystModel(options.adapter, options.flags);
4618
+ const permission = mode === "write" ? ["--permission-mode", "acceptEdits", "--allowedTools", "Read,Grep,Glob,Edit,Write,Bash"] : ["--allowedTools", `Read,Grep,Glob,Write(${HARNESS_REPORT_PATH}),Bash(pwd),Bash(ls *),Bash(find *),Bash(rg *),Bash(grep *),Bash(cat *),Bash(git status *),Bash(git ls-files *),Bash(git grep *)`];
4356
4619
  return {
4357
4620
  adapter: options.adapter,
4358
4621
  command,
4359
4622
  args: [
4360
4623
  "--print",
4624
+ ...model ? ["--model", model] : [],
4361
4625
  ...extraArgs,
4362
4626
  ...permission,
4363
4627
  ...options.stream ? ["--verbose"] : [],
@@ -4418,7 +4682,7 @@ async function runCodingAgent(options) {
4418
4682
  stream,
4419
4683
  mode: options.mode
4420
4684
  });
4421
- const timeoutMs = options.timeoutMs ?? numericEnv("MODA_HARNESS_ANALYST_TIMEOUT_MS", 1200000);
4685
+ const timeoutMs = options.timeoutMs ?? numericEnv("MODA_HARNESS_ANALYST_TIMEOUT_MS", 1800000);
4422
4686
  const result = await runExternalAnalystWithRetries(invocation, {
4423
4687
  cwd: options.rootDir,
4424
4688
  env: buildExternalAnalystEnv(options.env ?? process.env),
@@ -4447,24 +4711,34 @@ async function runCodingAgent(options) {
4447
4711
  function externalAnalystExtraArgs(adapter) {
4448
4712
  return splitCommandLine(process.env[`MODA_HARNESS_ANALYST_${adapter.toUpperCase()}_ARGS`] ?? "");
4449
4713
  }
4714
+ var DEFAULT_CLAUDE_ANALYST_MODEL = "sonnet";
4715
+ function resolveAnalystModel(adapter, flags) {
4716
+ const flagValue = flags["analyst-model"] && flags["analyst-model"] !== "true" ? flags["analyst-model"] : undefined;
4717
+ const raw = (flagValue ?? process.env.MODA_HARNESS_ANALYST_MODEL)?.trim();
4718
+ if (raw?.toLowerCase() === "default")
4719
+ return;
4720
+ if (raw)
4721
+ return raw;
4722
+ return adapter === "claude" ? DEFAULT_CLAUDE_ANALYST_MODEL : undefined;
4723
+ }
4450
4724
  function renderExternalHarnessAnalystPrompt(options) {
4451
4725
  return [
4452
4726
  "You are Moda Analyst, investigating an agent harness for Moda CLI onboarding.",
4453
4727
  "",
4454
- "Do real repository investigation. Do not call `moda harness scan`, `moda harness analyze`, or rely on deterministic Moda scanner output.",
4728
+ "Do real repository investigation. Do not call `moda harness scan` or `moda harness analyze` yourself. When Moda provides deterministic scanner seeds below, treat them as navigation candidates to verify against the real files — never as evidence or as a complete inventory.",
4455
4729
  "Only inspect the Repository root named below and files/directories under it. Do not inspect parent directories, sibling projects, the Moda CLI implementation, or this monorepo unless the Repository root itself is that directory.",
4456
4730
  "Inspect package manifests, source files, prompt files, tool definitions, skills/rules, evals, deployment config, and README/AGENTS docs as needed.",
4457
4731
  "Do not read secret-looking files such as .env files, private keys, credential files, or token stores.",
4458
4732
  "You are running as a read-only analyst. Do not edit source files, configuration files, git state, dependency files, or project metadata.",
4459
- "Prefer printing the final report JSON for Moda CLI to validate and write. If your runtime unexpectedly allows file writes, only `.moda/harness-report.json` is permitted.",
4733
+ `The ONLY path you may write is ${HARNESS_REPORT_PATH}. Write the final report JSON there with the Write tool as soon as it is complete — the file is the durable deliverable and survives a dropped connection, unlike a long printed message. If (and only if) the write fails, print the report JSON between the sentinel markers instead.`,
4460
4734
  "",
4461
4735
  "Your task:",
4462
4736
  renderExternalAnalystProtocol(),
4463
4737
  "1. Identify the runtime agent harnesses, agent families, channel adapters, identity mappings, model providers, frameworks, deployments, and open questions.",
4464
4738
  "2. Exhaustively enumerate EVERY prompt, tool, guardrail, and eval as its own graph artifact — the complete inventory, not a sample. This report feeds prompt optimization and tool optimization, so a missing tool or prompt is a real defect. Runtime agents may be modeled representatively on framework/example repos, but prompts and tools must be complete.",
4465
4739
  "3. Cite every narrative claim with local file paths, exact line numbers, and excerpts. File citations without line numbers are invalid for agent-authored reports. Enumerated artifacts only need evidence:[{path, reason}], so completeness of the artifact inventory is not limited by the citation cap.",
4466
- `4. After completing investigation, print the report JSON between ${REPORT_JSON_BEGIN} and ${REPORT_JSON_END}. Moda CLI will validate it and write the local artifacts.`,
4467
- "Your final response must contain the sentinel-wrapped JSON report. Do not end with commentary about being read-only, do not summarize without JSON, and do not omit the sentinel markers.",
4740
+ `4. After completing investigation, WRITE the report JSON to ${HARNESS_REPORT_PATH} with the Write tool, then print a one-line confirmation naming the file. Only if that write fails, print the report JSON between ${REPORT_JSON_BEGIN} and ${REPORT_JSON_END}. Moda CLI validates whichever it finds (the file wins).`,
4741
+ "Do not end with commentary about being read-only and do not summarize without delivering the report: either the written file or the sentinel-wrapped JSON must exist before you finish.",
4468
4742
  "If the repository is very large, keep enumerating prompts/tools/guardrails/evals as artifacts (uncapped) and only summarize runtime AGENTS representatively — do not drop tools or prompts to save space.",
4469
4743
  "",
4470
4744
  "Final output format:",
@@ -4491,7 +4765,7 @@ function renderExternalHarnessAnalystPrompt(options) {
4491
4765
  "Each graph item with evidence must use evidence: [{ path, reason }], not citationIds/citations.",
4492
4766
  'Runtime agents must include id, name, kind:"runtime_agent", root, entrypointIds, channelAdapterIds, artifactIds, identityNamespaceIds, deploymentIds, frameworkIds, modelProviderIds, confidence, evidence.',
4493
4767
  "Artifacts must include id, type, name, scope, usedByAgentIds, confidence, evidence, and optional sourcePath/contentHash/ownedByAgentId. Emit one artifact PER tool, PER prompt (including each inline system prompt), PER guardrail, and PER eval — enumerate them all; artifact types are entrypoint, prompt, tool, skill, eval, retrieval_index, memory, guardrail, model_config, unknown.",
4494
- "Also set `body` on each artifact to its actual usable content so it can be consumed downstream WITHOUT re-reading the repo: for `prompt` the COMPLETE prompt text verbatim (full inline `instructions=`/system-prompt string or full prompt file); for `tool` the signature (name + parameters + types) and its docstring/description; for `guardrail` the rule/condition; for `eval` what it asserts; for `model_config` the configuration values. Copy the text in full — do not summarize or truncate `body` (it is the artifact we optimize). Redact any secret literals.",
4768
+ "Do NOT transcribe prompt/tool text into `body` when it exists verbatim in the repo — pin its exact location instead (sourcePath for a dedicated file, or evidence [{ path, line, reason }] at the definition line) and Moda hydrates `body` locally from that location after validation. Set `body` inline ONLY for content that cannot be recovered from one pinned location: dynamically assembled prompts, fragments concatenated at runtime, or values you had to derive. Short bodies for `guardrail` (the rule), `eval` (what it asserts), and `model_config` (the config values) are still welcome. Redact any secret literals.",
4495
4769
  "For every `prompt` and `tool` artifact, at least one evidence entry MUST pin an exact source location so downstream optimization can retrieve the item text: carry a numeric `line` (the definition line, or the first line of the range) — evidence: [{ path, line, reason }]. The only exception is an artifact whose entire dedicated file is the text (e.g. a standalone prompt file); set its sourcePath and cite that same file and no line is required. When a single file holds multiple inline prompts or tool definitions, give each its own distinct line — a bare { path, reason } is a defect there.",
4496
4770
  "Agent families must include id, name, runtimeAgentIds, sharedArtifactIds, confidence, evidence.",
4497
4771
  "Frameworks/providers must include packageNames and runtimeAgentIds arrays; providers also include envVars and modelHints arrays.",
@@ -4502,6 +4776,7 @@ function renderExternalHarnessAnalystPrompt(options) {
4502
4776
  options.tenantId ? `Tenant ID to include in graph.tenant: ${options.tenantId}` : "No tenant ID was provided for this analysis.",
4503
4777
  `Repository root: ${options.rootDir}`,
4504
4778
  renderAnalystPreflightSection(options.rootDir),
4779
+ renderAnalystSeedSection(options.rootDir),
4505
4780
  "",
4506
4781
  "Be conservative. If something is unclear, put it in unknowns instead of inventing an agent."
4507
4782
  ].filter(Boolean).join(`
@@ -4520,7 +4795,7 @@ function renderExternalAnalystProtocol() {
4520
4795
  '- Also emit artifacts for guardrails (type "guardrail"), evals/tests-of-agents (type "eval"), memory/session stores (type "memory"), retrieval indexes (type "retrieval_index"), and model configuration (type "model_config").',
4521
4796
  "- Only runtime AGENTS may be modeled representatively: if the repo is a framework/SDK/examples collection, you need not treat every example as a distinct production agent — but you MUST still enumerate every distinct tool, prompt, guardrail, and eval as its own artifact.",
4522
4797
  "- Each enumerated artifact needs evidence: [{ path, reason }] (file path + short reason); a full line+excerpt citation is required only for the narrative `claims`, NOT for every artifact — so listing hundreds of tools/prompts is cheap and expected.",
4523
- "- Capture the actual usable content in each artifact's `body`: full prompt text for prompts, signature+docstring for tools, the rule for guardrails, the assertion for evals, config values for model configs. This inventory is consumed directly for optimization, so `body` must be the complete text (verbatim, not summarized), with secret literals redacted.",
4798
+ "- Bodies are hydrated locally by Moda from pinned locations — do not copy file text into `body`. Spend your output budget on coverage (every artifact, exact definition locations), not transcription. Inline `body` only for content not recoverable from a single location (e.g. prompts assembled from fragments at runtime), plus short bodies for guardrails/evals/model configs.",
4524
4799
  "- EXCEPTION — `prompt` and `tool` artifacts MUST pin an exact location: put a numeric `line` on at least one evidence entry (evidence: [{ path, line, reason }]) at the definition line. That structured location is what lets downstream optimization retrieve the exact prompt/tool text; when one file has several inline prompts or tools, each artifact gets its own line. The only case that needs no line is a whole dedicated file whose entire content is the artifact (set sourcePath to that file and cite it). Still no excerpt required — just the line.",
4525
4800
  "- For every runtime agent, read and cite at least one concrete entrypoint or orchestration line, and link its prompts/tools via artifactIds.",
4526
4801
  "- Keep going until every prompt, tool, guardrail, and eval in scope is represented as an artifact. Use unknowns only for genuinely ambiguous items — never to skip enumeration. Respect the hard caps (<=250 claims, <=400 citations): artifacts are uncapped, so when there are many items keep enumerating them as artifacts and reserve claims/citations for the most important ones."
@@ -4528,7 +4803,7 @@ function renderExternalAnalystProtocol() {
4528
4803
  `);
4529
4804
  }
4530
4805
  function renderAnalystPreflightSection(rootDir) {
4531
- if (process.env.MODA_HARNESS_ANALYST_PREFLIGHT !== "1")
4806
+ if (process.env.MODA_HARNESS_ANALYST_PREFLIGHT === "0")
4532
4807
  return "";
4533
4808
  const paths = listAnalystPreflightPaths(rootDir);
4534
4809
  if (paths.length === 0)
@@ -4558,6 +4833,91 @@ function renderPreflightGroup(label, paths) {
4558
4833
  return [];
4559
4834
  return [`${label}:`, ...paths.map((path) => `- ${path}`)];
4560
4835
  }
4836
+ var MAX_SEED_ARTIFACT_LINES = 150;
4837
+ function renderAnalystSeedSection(rootDir) {
4838
+ if (process.env.MODA_HARNESS_ANALYST_SEEDS === "0")
4839
+ return "";
4840
+ const sections = [];
4841
+ try {
4842
+ const lines = renderAnalystSeedLines(scanHarness({ rootDir, dryRun: true, pathWasExplicit: true }));
4843
+ if (lines)
4844
+ sections.push(lines);
4845
+ } catch {}
4846
+ try {
4847
+ const lines = renderAnalystCandidateLines(detectHarnessCandidates(rootDir));
4848
+ if (lines)
4849
+ sections.push(lines);
4850
+ } catch {}
4851
+ return sections.join(`
4852
+ `);
4853
+ }
4854
+ var MAX_SEED_CANDIDATES_PER_KIND = 40;
4855
+ var MAX_SEED_READ_PLAN_FILES = 25;
4856
+ var SEED_CANDIDATE_KIND_LABELS = {
4857
+ llm_call_site: "LLM call sites",
4858
+ framework_construct: "Agent-framework constructs",
4859
+ tool_definition: "Tool definitions",
4860
+ tool_schema: "Tool schemas",
4861
+ message_construction: "Message constructions",
4862
+ prompt_file: "Prompt files",
4863
+ prompt_literal: "Inline prompt literals",
4864
+ model_config: "Model configuration"
4865
+ };
4866
+ function renderAnalystCandidateLines(report) {
4867
+ if (report.candidates.length === 0)
4868
+ return "";
4869
+ const byKind = new Map;
4870
+ for (const candidate of report.candidates) {
4871
+ const bucket = byKind.get(candidate.kind);
4872
+ if (bucket)
4873
+ bucket.push(candidate);
4874
+ else
4875
+ byKind.set(candidate.kind, [candidate]);
4876
+ }
4877
+ const lines = [
4878
+ "",
4879
+ `Deterministic candidate inventory (${report.candidates.length} candidates across ${report.stats.filesWithCandidates} files, scanned in ${report.stats.durationMs}ms):`,
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."
4881
+ ];
4882
+ for (const [kind, label] of Object.entries(SEED_CANDIDATE_KIND_LABELS)) {
4883
+ const bucket = byKind.get(kind);
4884
+ if (!bucket || bucket.length === 0)
4885
+ continue;
4886
+ const shown = bucket.slice(0, MAX_SEED_CANDIDATES_PER_KIND);
4887
+ lines.push(`${label} (${bucket.length}${bucket.length > shown.length ? `, showing ${shown.length}` : ""}):`);
4888
+ for (const candidate of shown) {
4889
+ lines.push(`- ${candidate.path}:${candidate.startLine} [${candidate.signal}] ${candidate.excerpt}`);
4890
+ }
4891
+ }
4892
+ const readPlan = report.files.slice(0, MAX_SEED_READ_PLAN_FILES);
4893
+ if (readPlan.length > 0) {
4894
+ lines.push(`Highest-signal files to read first (${readPlan.length} of ${report.files.length}):`, ...readPlan.map((file) => `- ${file.path} (score ${Math.round(file.score)}; ${file.kinds.join(", ")})`));
4895
+ }
4896
+ return lines.join(`
4897
+ `);
4898
+ }
4899
+ function renderAnalystSeedLines(graph) {
4900
+ const providers = graph.modelProviders.map((provider) => `- ${provider.name}${provider.packageNames.length ? ` (packages: ${provider.packageNames.join(", ")})` : ""}`);
4901
+ const frameworks = graph.agentFrameworks.map((framework) => `- ${framework.name}${framework.packageNames.length ? ` (packages: ${framework.packageNames.join(", ")})` : ""}`);
4902
+ const artifacts = graph.artifacts.slice(0, MAX_SEED_ARTIFACT_LINES).map((artifact) => `- [${artifact.type}] ${artifact.sourcePath ?? artifact.name}`);
4903
+ if (providers.length === 0 && frameworks.length === 0 && artifacts.length === 0)
4904
+ return "";
4905
+ const lines = [
4906
+ "",
4907
+ "Deterministic scanner seeds (candidates, not ground truth):",
4908
+ "Moda's static scanner pre-detected the items below from package manifests and filename patterns. Verify each against the real files before including it, correct or drop mislabeled entries, and focus your investigation on what pattern matching cannot see: runtime agents, inline prompts, decorated/registered tool definitions, and the relationships between them. Do not cite this list as evidence."
4909
+ ];
4910
+ if (providers.length > 0)
4911
+ lines.push("Detected model providers:", ...providers);
4912
+ if (frameworks.length > 0) {
4913
+ lines.push("Detected agent frameworks (package-name matches; may be false positives):", ...frameworks);
4914
+ }
4915
+ if (artifacts.length > 0) {
4916
+ lines.push(`Detected artifact candidates (${graph.artifacts.length} total${graph.artifacts.length > MAX_SEED_ARTIFACT_LINES ? `, showing first ${MAX_SEED_ARTIFACT_LINES}` : ""}):`, ...artifacts);
4917
+ }
4918
+ return lines.join(`
4919
+ `);
4920
+ }
4561
4921
  function listAnalystPreflightPaths(rootDir) {
4562
4922
  const gitPaths = readGitTrackedPaths(rootDir);
4563
4923
  const paths = gitPaths.length > 0 ? gitPaths : readDirectoryPreflightPaths(rootDir);
@@ -4669,7 +5029,7 @@ function truncateText(value, maxLength) {
4669
5029
  return compact.length > maxLength ? `${compact.slice(0, maxLength - 1)}...` : compact;
4670
5030
  }
4671
5031
  function scanHarness(options = {}) {
4672
- const rootDir = resolve4(options.rootDir ?? process.cwd());
5032
+ const rootDir = resolve5(options.rootDir ?? process.cwd());
4673
5033
  const skipped = [];
4674
5034
  const files = inventoryFiles(rootDir, skipped);
4675
5035
  const annotations = files.flatMap((file) => parseAnnotations(file));
@@ -4771,7 +5131,7 @@ function collectSnapshotInputs(rootDir, maxFiles, maxTextBytes) {
4771
5131
  let text = "";
4772
5132
  if (isTextFile(path)) {
4773
5133
  try {
4774
- text = readFileSync3(current, "utf8");
5134
+ text = readFileSync4(current, "utf8");
4775
5135
  inventoryTextBytes += Buffer.byteLength(text, "utf8");
4776
5136
  } catch {
4777
5137
  return;
@@ -4784,7 +5144,7 @@ function collectSnapshotInputs(rootDir, maxFiles, maxTextBytes) {
4784
5144
  return inputs;
4785
5145
  }
4786
5146
  function buildSourceSnapshot(rootDir) {
4787
- const root = resolve4(rootDir);
5147
+ const root = resolve5(rootDir);
4788
5148
  const maxFiles = numericEnv("MODA_HARNESS_MAX_FILES", DEFAULT_MAX_INVENTORY_FILES);
4789
5149
  const maxTextBytes = numericEnv("MODA_HARNESS_MAX_TEXT_BYTES", DEFAULT_MAX_INVENTORY_TEXT_BYTES);
4790
5150
  const inputs = collectSnapshotInputs(root, maxFiles, maxTextBytes);
@@ -5011,7 +5371,7 @@ function readHarnessSyncState(rootDir) {
5011
5371
  if (!existsSync3(path))
5012
5372
  return;
5013
5373
  try {
5014
- const parsed = JSON.parse(readFileSync3(path, "utf8"));
5374
+ const parsed = JSON.parse(readFileSync4(path, "utf8"));
5015
5375
  if (!isRecord3(parsed) || parsed.schemaVersion !== "sync_state.v0.1") {
5016
5376
  return;
5017
5377
  }
@@ -5128,7 +5488,7 @@ function findFresherHarnessReports(rootDir) {
5128
5488
  return fresher.sort((a, b) => b.mtimeMs - a.mtimeMs);
5129
5489
  }
5130
5490
  function warnFresherSubdirHarnessReports(context, rootDir, explicitReport) {
5131
- 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;
5132
5492
  if (explicitReportPath && explicitReportPath !== normalizePath3(HARNESS_REPORT_PATH))
5133
5493
  return;
5134
5494
  let fresher;
@@ -5145,7 +5505,7 @@ function warnFresherSubdirHarnessReports(context, rootDir, explicitReport) {
5145
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}.`);
5146
5506
  }
5147
5507
  function readHarnessGraph(manifestPath) {
5148
- return JSON.parse(readFileSync3(manifestPath, "utf8"));
5508
+ return JSON.parse(readFileSync4(manifestPath, "utf8"));
5149
5509
  }
5150
5510
  function writeHarnessArtifacts(graph, rootDir, options = {}) {
5151
5511
  const modaDir = join2(rootDir, MODA_DIR);
@@ -5257,6 +5617,25 @@ function renderFrameworkAndProviderLines(graph) {
5257
5617
  }
5258
5618
  return lines;
5259
5619
  }
5620
+ function assertHarnessGraphSyncQuality(graph, flags) {
5621
+ if (flags["allow-no-agents"] === "true")
5622
+ return;
5623
+ if (graph.runtimeAgents.length > 0)
5624
+ return;
5625
+ const evidence = [];
5626
+ if (graph.modelProviders.length > 0) {
5627
+ evidence.push(`model providers (${graph.modelProviders.map((provider) => provider.name).join(", ")})`);
5628
+ }
5629
+ if (graph.agentFrameworks.length > 0) {
5630
+ evidence.push(`agent frameworks (${graph.agentFrameworks.map((framework) => framework.name).join(", ")})`);
5631
+ }
5632
+ const promptOrToolCount = graph.artifacts.filter((artifact) => artifact.type === "prompt" || artifact.type === "tool").length;
5633
+ if (promptOrToolCount > 0)
5634
+ evidence.push(`${promptOrToolCount} prompt/tool artifacts`);
5635
+ if (evidence.length === 0)
5636
+ return;
5637
+ throw new CliInputError(`Refusing to sync: the harness graph has zero runtime agents but detected ${evidence.join(", ")}.`, "Agent detection likely under-ran (the deterministic scanner only finds agents via @moda annotations). Run `moda harness analyze --analyst=claude` for semantic detection, or pass --allow-no-agents to sync this graph anyway.");
5638
+ }
5260
5639
  function graphForSync(graph) {
5261
5640
  return sortJson2({
5262
5641
  ...graph,
@@ -5303,7 +5682,7 @@ function explainAgent(graph, agent) {
5303
5682
  }
5304
5683
  function inventoryFiles(rootDir, skipped, options = {}) {
5305
5684
  const files = [];
5306
- const rootReal = resolve4(rootDir);
5685
+ const rootReal = resolve5(rootDir);
5307
5686
  const maxFiles = numericEnv("MODA_HARNESS_MAX_FILES", DEFAULT_MAX_INVENTORY_FILES);
5308
5687
  const maxTextBytes = numericEnv("MODA_HARNESS_MAX_TEXT_BYTES", DEFAULT_MAX_INVENTORY_TEXT_BYTES);
5309
5688
  let textBytes = 0;
@@ -5357,7 +5736,7 @@ function inventoryFiles(rootDir, skipped, options = {}) {
5357
5736
  recordSkipped(skipped, { path: rel, reason: "scan text budget exceeded" });
5358
5737
  } else {
5359
5738
  try {
5360
- item.text = readFileSync3(current, "utf8");
5739
+ item.text = readFileSync4(current, "utf8");
5361
5740
  textBytes += Buffer.byteLength(item.text, "utf8");
5362
5741
  } catch {
5363
5742
  recordSkipped(skipped, { path: rel, reason: "text read failed" });
@@ -5664,7 +6043,7 @@ function maybeAddPromptArtifact(graph, file) {
5664
6043
  if (!isPrompt)
5665
6044
  return;
5666
6045
  const id = stableId2("prompt", file.path);
5667
- const text = file.text ?? readFileSync3(file.absPath, "utf8");
6046
+ const text = file.text ?? readFileSync4(file.absPath, "utf8");
5668
6047
  upsertArtifact(graph, {
5669
6048
  id,
5670
6049
  type: "prompt",
@@ -5891,12 +6270,27 @@ function detectToolDefinitions(file) {
5891
6270
  const pyDecorated = /@function_tool[^\n]*\n(?:[ \t]*@[^\n]*\n)*[ \t]*(?:async[ \t]+)?def[ \t]+([A-Za-z_]\w*)/g;
5892
6271
  for (const match of text.matchAll(pyDecorated))
5893
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`);
5894
6279
  const tsFactory = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:[\w.]*\.)?(?:tool|functionTool|createTool|dynamicTool)\s*\(/g;
5895
6280
  for (const match of text.matchAll(tsFactory))
5896
6281
  found.set(match[1], "tool factory assignment");
5897
- 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;
5898
6283
  for (const match of text.matchAll(tsNewTool))
5899
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
+ }
5900
6294
  for (const match of text.matchAll(HOSTED_TOOL_CALL))
5901
6295
  found.set(match[1], `hosted tool ${match[1]}`);
5902
6296
  return [...found.entries()].map(([name, reason]) => ({ name, reason }));
@@ -5912,7 +6306,7 @@ function maybeAddToolArtifact(graph, file) {
5912
6306
  for (const tool of tools) {
5913
6307
  const body = extractToolBody(file, tool);
5914
6308
  upsertArtifact(graph, {
5915
- id: stableId2("tool", `${file.path}:${tool.name}`),
6309
+ id: stableId2("tool", `${file.path.replace(/\.[^./]+$/, "")}:${tool.name}`),
5916
6310
  type: "tool",
5917
6311
  name: titleFromId2(tool.name),
5918
6312
  scope: "agent",
@@ -6020,6 +6414,55 @@ var FRAMEWORK_CATALOG = [
6020
6414
  kind: "agent_framework",
6021
6415
  packageNames: ["pydantic-ai", "pydantic_ai"],
6022
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]
6023
6466
  }
6024
6467
  ];
6025
6468
  var PROVIDER_CATALOG = [
@@ -6070,6 +6513,46 @@ var PROVIDER_CATALOG = [
6070
6513
  packageNames: ["ollama"],
6071
6514
  envVars: ["OLLAMA_HOST"],
6072
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]
6073
6556
  }
6074
6557
  ];
6075
6558
  function detectAgentFrameworks(files) {
@@ -6111,6 +6594,59 @@ function detectModelProviders(files) {
6111
6594
  }
6112
6595
  return providers.sort((a, b) => a.id.localeCompare(b.id));
6113
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
+ }
6114
6650
  function detectionEvidence(files, packageNames, patterns) {
6115
6651
  const evidence = [];
6116
6652
  for (const file of files) {
@@ -6192,7 +6728,7 @@ function pyprojectDependencyNames(text) {
6192
6728
  }
6193
6729
  function modelHintsForProvider(providerId, files) {
6194
6730
  const hints = new Set;
6195
- 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;
6196
6732
  if (!pattern)
6197
6733
  return [];
6198
6734
  for (const file of files) {
@@ -6483,7 +7019,7 @@ function upsertDeployment(graph, next) {
6483
7019
  }
6484
7020
  function contentHashForAnnotatedSource(sourcePath, annotation) {
6485
7021
  const bodyAnnotation = annotation;
6486
- const rootCandidate = resolve4(annotation.rootDir, sourcePath);
7022
+ const rootCandidate = resolve5(annotation.rootDir, sourcePath);
6487
7023
  const candidates = [
6488
7024
  rootCandidate,
6489
7025
  ...annotatedSourceCandidates(sourcePath, bodyAnnotation).filter((c) => c !== rootCandidate)
@@ -6517,7 +7053,7 @@ function readSmallTextFileForHash(absPath, rootDir) {
6517
7053
  if (!isTextFile(rel))
6518
7054
  return;
6519
7055
  try {
6520
- return readFileSync3(absPath, "utf8");
7056
+ return readFileSync4(absPath, "utf8");
6521
7057
  } catch {
6522
7058
  return;
6523
7059
  }
@@ -6637,7 +7173,7 @@ function readRemoteRunState(rootDir) {
6637
7173
  if (!existsSync4(path))
6638
7174
  return;
6639
7175
  try {
6640
- const parsed = JSON.parse(readFileSync4(path, "utf8"));
7176
+ const parsed = JSON.parse(readFileSync5(path, "utf8"));
6641
7177
  if (!parsed || typeof parsed.run_id !== "string" || parsed.run_id.length === 0)
6642
7178
  return;
6643
7179
  return {
@@ -6792,7 +7328,9 @@ function finalizeRemoteAnalyzeResult(options) {
6792
7328
  if (options.snapshotTruncated) {
6793
7329
  report = stampRemoteSnapshotTruncation(report);
6794
7330
  }
6795
- const validation = validateHarnessReport(report, rootDir);
7331
+ const validation = validateHarnessReport(report, rootDir, {
7332
+ deterministicFloor: computeDeterministicFloor(rootDir)
7333
+ });
6796
7334
  context.output.writeEvent({
6797
7335
  event: "progress",
6798
7336
  phase: "validation",
@@ -11,7 +11,7 @@ import {
11
11
  renderStatusHuman,
12
12
  scanHarness,
13
13
  validateHarnessReport
14
- } from "./cli-g931ntph.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-x1a56ws9.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-g931ntph.js";
32
+ } from "./cli-351a5yc4.js";
33
33
  import {
34
34
  authFetch,
35
35
  clearAuthSession,
@@ -5499,6 +5499,11 @@ Init flags:
5499
5499
  --yes Accept all defaults (non-interactive)
5500
5500
  --analyst=auto|claude|codex|cursor|local-scan
5501
5501
  Choose harness analyst adapter
5502
+ --analyst-model=MODEL Model for the claude analyst (default: sonnet;
5503
+ "default" = the claude CLI's own default;
5504
+ env: MODA_HARNESS_ANALYST_MODEL)
5505
+ --allow-no-agents Allow harness sync to push a graph with zero
5506
+ runtime agents despite detected LLM usage
5502
5507
  --remote Run harness analysis server-side (Moda-hosted,
5503
5508
  the default): upload a safe source snapshot; no
5504
5509
  local coding agent required
@@ -6491,7 +6496,7 @@ var commandRegistry = createCommandRegistry([
6491
6496
  resetApiRequestCountBeforeRun: true,
6492
6497
  telemetry: "result",
6493
6498
  handler: async (context) => {
6494
- const { runInit } = await import("./index-g3gt62rm.js");
6499
+ const { runInit } = await import("./index-8ksqhqmr.js");
6495
6500
  if (context.outputMode === "agent-stream") {
6496
6501
  context.output.writeEvent({
6497
6502
  event: "started",
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  buildSourceSnapshot
3
- } from "./cli-g931ntph.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-x1a56ws9.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-g931ntph.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.5",
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-27T02:29:51.059Z",
4
- "cli_version": "1.28.5",
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",