@@ -6,6 +6,7 @@ import type {
66 AgentTarget ,
77 AgentsMode ,
88 AgentsSection ,
9+ ContextFeedbackConfig ,
910 ContextRegistryConfig ,
1011 EvidencePolicyMode ,
1112 OpenCodePlusplusConfig ,
@@ -41,6 +42,12 @@ export function loadConfig(repoRoot: string, overrides: Partial<OpenCodePlusplus
4142 fileConfig . contextRegistry ?. sources ??
4243 DEFAULT_CONFIG . contextRegistry . sources
4344 } ,
45+ feedback : {
46+ ...DEFAULT_CONFIG . feedback ,
47+ ...fileConfig . feedback ,
48+ ...localConfig . feedback ,
49+ ...overrides . feedback
50+ } ,
4451 llm : {
4552 ...DEFAULT_CONFIG . llm ,
4653 ...fileConfig . llm ,
@@ -104,6 +111,7 @@ function normalizeConfig(input: Record<string, unknown> | null | undefined): Par
104111 typeof input . contextRegistry === "object" && input . contextRegistry
105112 ? normalizeContextRegistryConfig ( input . contextRegistry as Record < string , unknown > )
106113 : undefined ,
114+ feedback : typeof input . feedback === "object" && input . feedback ? normalizeFeedbackConfig ( input . feedback as Record < string , unknown > ) : undefined ,
107115 tokenBudget : typeof input . tokenBudget === "number" ? input . tokenBudget : undefined ,
108116 include : toStringArray ( input . include ) ,
109117 exclude : toStringArray ( input . exclude ) ,
@@ -123,6 +131,16 @@ function normalizeContextRegistryConfig(input: Record<string, unknown>): Partial
123131 } ) ;
124132}
125133
134+ function normalizeFeedbackConfig ( input : Record < string , unknown > ) : Partial < ContextFeedbackConfig > {
135+ return stripUndefined ( {
136+ enabled : typeof input . enabled === "boolean" ? input . enabled : undefined ,
137+ telemetry : typeof input . telemetry === "boolean" ? input . telemetry : undefined ,
138+ network : typeof input . network === "boolean" ? input . network : undefined ,
139+ useLocalQualitySignals : typeof input . useLocalQualitySignals === "boolean" ? input . useLocalQualitySignals : undefined ,
140+ endpoint : typeof input . endpoint === "string" ? input . endpoint : undefined
141+ } ) ;
142+ }
143+
126144function normalizeContextSourceConfig ( input : unknown ) : ContextSourceConfig {
127145 const source = typeof input === "object" && input ? ( input as Record < string , unknown > ) : { } ;
128146 return {
@@ -145,6 +163,7 @@ export function validateConfig(config: OpenCodePlusplusConfig): void {
145163 if ( ! [ "advisory" , "balanced" , "strict" ] . includes ( config . evidencePolicy ) ) {
146164 throw new Error ( `Invalid evidencePolicy "${ config . evidencePolicy } ". Expected one of: advisory, balanced, strict.` ) ;
147165 }
166+ validateFeedbackConfig ( config . feedback ) ;
148167 if ( ! Number . isFinite ( config . tokenBudget ) || config . tokenBudget <= 0 ) {
149168 throw new Error ( "tokenBudget must be a positive number." ) ;
150169 }
@@ -211,6 +230,7 @@ function validateRawConfig(input: Record<string, unknown> | null | undefined, so
211230 if ( new Set ( names ) . size !== names . length ) throw new Error ( "contextRegistry.sources names must be unique." ) ;
212231 }
213232 }
233+ if ( input . feedback !== undefined ) validateRawFeedbackConfig ( input . feedback ) ;
214234 if ( input . tokenBudget !== undefined && ( typeof input . tokenBudget !== "number" || input . tokenBudget <= 0 ) ) {
215235 throw new Error ( "tokenBudget must be a positive number." ) ;
216236 }
@@ -273,6 +293,42 @@ function validateRawConfig(input: Record<string, unknown> | null | undefined, so
273293 void source ;
274294}
275295
296+ function validateFeedbackConfig ( config : ContextFeedbackConfig ) : void {
297+ if (
298+ typeof config . enabled !== "boolean" ||
299+ typeof config . telemetry !== "boolean" ||
300+ typeof config . network !== "boolean" ||
301+ typeof config . useLocalQualitySignals !== "boolean"
302+ ) {
303+ throw new Error ( "feedback switches must be boolean." ) ;
304+ }
305+ if ( config . endpoint !== undefined ) validateFeedbackEndpoint ( config . endpoint ) ;
306+ if ( config . network && ! config . endpoint ) throw new Error ( "feedback.endpoint is required when feedback.network is true." ) ;
307+ }
308+
309+ function validateRawFeedbackConfig ( input : unknown ) : void {
310+ const feedback = objectValue ( input , "feedback" ) ;
311+ for ( const field of [ "enabled" , "telemetry" , "network" , "useLocalQualitySignals" ] ) {
312+ if ( feedback [ field ] !== undefined && typeof feedback [ field ] !== "boolean" ) throw new Error ( "feedback." + field + " must be boolean." ) ;
313+ }
314+ if ( feedback . endpoint !== undefined ) {
315+ if ( typeof feedback . endpoint !== "string" || ! feedback . endpoint . trim ( ) ) throw new Error ( "feedback.endpoint must be a non-empty URL." ) ;
316+ validateFeedbackEndpoint ( feedback . endpoint ) ;
317+ }
318+ const allowed = new Set ( [ "enabled" , "telemetry" , "network" , "useLocalQualitySignals" , "endpoint" ] ) ;
319+ for ( const key of Object . keys ( feedback ) ) if ( ! allowed . has ( key ) ) throw new Error ( "Unknown feedback option: " + key + "." ) ;
320+ if ( feedback . network === true && feedback . endpoint === undefined ) throw new Error ( "feedback.endpoint is required when feedback.network is true." ) ;
321+ }
322+
323+ function validateFeedbackEndpoint ( value : string ) : void {
324+ try {
325+ const url = new URL ( value ) ;
326+ if ( ! [ "http:" , "https:" ] . includes ( url . protocol ) || url . username || url . password ) throw new Error ( "must use an HTTP(S) URL without credentials" ) ;
327+ } catch ( error ) {
328+ throw new Error ( "feedback.endpoint " + ( error instanceof Error ? error . message : "must be a valid URL" ) + "." ) ;
329+ }
330+ }
331+
276332function validateContextSourceConfig ( input : unknown , index : number ) : void {
277333 const source = objectValue ( input , `contextRegistry.sources[${ index } ]` ) ;
278334 for ( const field of [ "name" , "kind" , "location" , "trustLevel" ] ) {
0 commit comments