@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.
- package/dist/{cli-g931ntph.js → cli-351a5yc4.js} +583 -45
- package/dist/{cli-x1a56ws9.js → cli-3w6hbpr3.js} +1 -1
- package/dist/cli.js +8 -3
- package/dist/{harness-github-actions-ct8fwzdr.js → harness-github-actions-p8xa3akx.js} +1 -1
- package/dist/{index-g3gt62rm.js → index-8ksqhqmr.js} +2 -2
- package/package.json +1 -1
- package/skills/integration/index.json +2 -2
|
@@ -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
|
|
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
|
|
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
|
|
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|
|
|
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 =
|
|
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-
|
|
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,
|
|
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",
|
|
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 =
|
|
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 =
|
|
4138
|
+
const candidatePath = resolve5(rootDir, citation.path);
|
|
3877
4139
|
if (!existsSync3(candidatePath))
|
|
3878
4140
|
continue;
|
|
3879
4141
|
let text;
|
|
3880
4142
|
try {
|
|
3881
|
-
text =
|
|
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
|
|
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
|
|
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",
|
|
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
|
|
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
|
-
|
|
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
|
|
4467
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"-
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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,
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 ??
|
|
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 =
|
|
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
|
|
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(
|
|
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",
|
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
runPromptsCommand,
|
|
9
9
|
runSkillsCommand,
|
|
10
10
|
runStatusCommand
|
|
11
|
-
} from "./cli-
|
|
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-
|
|
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-
|
|
6499
|
+
const { runInit } = await import("./index-8ksqhqmr.js");
|
|
6495
6500
|
if (context.outputMode === "agent-stream") {
|
|
6496
6501
|
context.output.writeEvent({
|
|
6497
6502
|
event: "started",
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
runPromptSync,
|
|
10
10
|
runSkillSync,
|
|
11
11
|
upsertSkillManifestRecord
|
|
12
|
-
} from "./cli-
|
|
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-
|
|
30
|
+
} from "./cli-351a5yc4.js";
|
|
31
31
|
import {
|
|
32
32
|
selectTenantAndCreateKey
|
|
33
33
|
} from "./cli-pq6rte0w.js";
|
package/package.json
CHANGED