@oisheek_c/codeforge 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +7 -0
  3. package/apps/cli/bin/codeforge.cmd +2 -0
  4. package/apps/cli/src/bootstrap.js +115 -0
  5. package/apps/cli/src/cli.js +189 -0
  6. package/apps/cli/src/dev/git.js +17 -0
  7. package/apps/cli/src/dev/index.js +68 -0
  8. package/apps/cli/src/dev/provider.js +48 -0
  9. package/apps/cli/src/dev/scanner.js +14 -0
  10. package/apps/cli/src/index.js +31 -0
  11. package/bin/codeforge.js +2 -0
  12. package/package.json +63 -0
  13. package/packages/agent/contextBuilder.js +234 -0
  14. package/packages/agent/executor.js +1729 -0
  15. package/packages/agent/fallback.js +129 -0
  16. package/packages/agent/index.js +26 -0
  17. package/packages/agent/intent.js +73 -0
  18. package/packages/agent/modelSelector.js +469 -0
  19. package/packages/agent/planner.js +379 -0
  20. package/packages/agent/rag.js +412 -0
  21. package/packages/agent/ragSelector.js +570 -0
  22. package/packages/agent/router.js +121 -0
  23. package/packages/agent/thinking.js +54 -0
  24. package/packages/agent/toolCalls.js +258 -0
  25. package/packages/config/config.js +391 -0
  26. package/packages/config/defaults.js +72 -0
  27. package/packages/config/index.js +2 -0
  28. package/packages/core/approvalPolicy.js +140 -0
  29. package/packages/core/context.js +0 -0
  30. package/packages/core/message.js +0 -0
  31. package/packages/core/plan.js +0 -0
  32. package/packages/core/reasoning.js +0 -0
  33. package/packages/core/result.js +0 -0
  34. package/packages/core/retrieval.js +0 -0
  35. package/packages/core/tool.js +536 -0
  36. package/packages/git/branch.js +7 -0
  37. package/packages/git/commit.js +16 -0
  38. package/packages/git/diff.js +15 -0
  39. package/packages/git/git.js +35 -0
  40. package/packages/git/index.js +6 -0
  41. package/packages/git/remote.js +9 -0
  42. package/packages/git/status.js +11 -0
  43. package/packages/memory/index.js +0 -0
  44. package/packages/memory/project.js +0 -0
  45. package/packages/memory/session.js +0 -0
  46. package/packages/prompts/builder.js +43 -0
  47. package/packages/prompts/coder.js +38 -0
  48. package/packages/prompts/index.js +5 -0
  49. package/packages/prompts/planner.js +49 -0
  50. package/packages/prompts/reviewer.js +38 -0
  51. package/packages/prompts/system.js +94 -0
  52. package/packages/providers/index.js +31 -0
  53. package/packages/providers/openrouter.js +380 -0
  54. package/packages/reasoning/budget.js +0 -0
  55. package/packages/reasoning/chain.js +0 -0
  56. package/packages/reasoning/coder.js +0 -0
  57. package/packages/reasoning/evaluator.js +0 -0
  58. package/packages/reasoning/index.js +8 -0
  59. package/packages/reasoning/planner.js +0 -0
  60. package/packages/reasoning/reflector.js +0 -0
  61. package/packages/reasoning/reviewer.js +0 -0
  62. package/packages/reasoning/router.js +0 -0
  63. package/packages/retrieval/budget.js +92 -0
  64. package/packages/retrieval/cache.js +84 -0
  65. package/packages/retrieval/embeddings.js +147 -0
  66. package/packages/retrieval/graph.js +151 -0
  67. package/packages/retrieval/imports.js +74 -0
  68. package/packages/retrieval/index.js +9 -0
  69. package/packages/retrieval/parser/extractors/calls.js +43 -0
  70. package/packages/retrieval/parser/extractors/comments.js +49 -0
  71. package/packages/retrieval/parser/extractors/diagnostics.js +21 -0
  72. package/packages/retrieval/parser/extractors/exports.js +71 -0
  73. package/packages/retrieval/parser/extractors/imports.js +40 -0
  74. package/packages/retrieval/parser/extractors/index.js +12 -0
  75. package/packages/retrieval/parser/extractors/metrics.js +78 -0
  76. package/packages/retrieval/parser/extractors/symbols.js +51 -0
  77. package/packages/retrieval/parser/extractors/todos.js +53 -0
  78. package/packages/retrieval/parser/helpers.js +138 -0
  79. package/packages/retrieval/parser/index.js +10 -0
  80. package/packages/retrieval/parser/languages/c.js +14 -0
  81. package/packages/retrieval/parser/languages/cpp.js +21 -0
  82. package/packages/retrieval/parser/languages/csharp.js +14 -0
  83. package/packages/retrieval/parser/languages/go.js +14 -0
  84. package/packages/retrieval/parser/languages/index.js +12 -0
  85. package/packages/retrieval/parser/languages/java.js +14 -0
  86. package/packages/retrieval/parser/languages/javascript.js +14 -0
  87. package/packages/retrieval/parser/languages/php.js +14 -0
  88. package/packages/retrieval/parser/languages/python.js +14 -0
  89. package/packages/retrieval/parser/languages/ruby.js +14 -0
  90. package/packages/retrieval/parser/languages/rust.js +14 -0
  91. package/packages/retrieval/parser/languages/swift.js +14 -0
  92. package/packages/retrieval/parser/languages/typescript.js +14 -0
  93. package/packages/retrieval/parser/languages.js +218 -0
  94. package/packages/retrieval/parser/parseFile.js +79 -0
  95. package/packages/retrieval/parser/parseRepository.js +78 -0
  96. package/packages/retrieval/parser/queries/c.js +148 -0
  97. package/packages/retrieval/parser/queries/cpp.js +198 -0
  98. package/packages/retrieval/parser/queries/csharp.js +151 -0
  99. package/packages/retrieval/parser/queries/go.js +124 -0
  100. package/packages/retrieval/parser/queries/index.js +35 -0
  101. package/packages/retrieval/parser/queries/java.js +128 -0
  102. package/packages/retrieval/parser/queries/javascript.js +181 -0
  103. package/packages/retrieval/parser/queries/php.js +140 -0
  104. package/packages/retrieval/parser/queries/python.js +110 -0
  105. package/packages/retrieval/parser/queries/ruby.js +105 -0
  106. package/packages/retrieval/parser/queries/rust.js +175 -0
  107. package/packages/retrieval/parser/queries/swift.js +134 -0
  108. package/packages/retrieval/parser/queries/typescript.js +202 -0
  109. package/packages/retrieval/parser/registry.js +107 -0
  110. package/packages/retrieval/parser/traverse.js +82 -0
  111. package/packages/retrieval/parser/types.js +239 -0
  112. package/packages/retrieval/parser.js +78 -0
  113. package/packages/retrieval/ranker.js +73 -0
  114. package/packages/retrieval/retriever.js +910 -0
  115. package/packages/retrieval/symbols.js +145 -0
  116. package/packages/scanner/detect.js +69 -0
  117. package/packages/scanner/ignore.js +12 -0
  118. package/packages/scanner/index.js +2 -0
  119. package/packages/scanner/root.js +21 -0
  120. package/packages/scanner/scanner.js +35 -0
  121. package/packages/scanner/tree.js +45 -0
  122. package/packages/terminal/activity.js +514 -0
  123. package/packages/terminal/banner.js +34 -0
  124. package/packages/terminal/colors.js +16 -0
  125. package/packages/terminal/dashboard.js +94 -0
  126. package/packages/terminal/index.js +19 -0
  127. package/packages/terminal/logger.js +43 -0
  128. package/packages/terminal/prompt.js +11 -0
  129. package/packages/terminal/renderer.js +1209 -0
  130. package/packages/terminal/spinner.js +9 -0
  131. package/packages/tools/editFile.js +261 -0
  132. package/packages/tools/filesystem.js +82 -0
  133. package/packages/tools/index.js +24 -0
  134. package/packages/tools/readFile.js +101 -0
  135. package/packages/tools/registry.js +227 -0
  136. package/packages/tools/searchFiles.js +322 -0
  137. package/packages/tools/shell.js +352 -0
  138. package/packages/tools/writeFile.js +212 -0
  139. package/packages/utils/constants.js +0 -0
  140. package/packages/utils/helpers.js +0 -0
  141. package/packages/utils/logger.js +0 -0
@@ -0,0 +1,129 @@
1
+ const DEFAULT_OPTIONS = Object.freeze({
2
+ maxAttempts: 3,
3
+
4
+ retryableErrors: [
5
+ "rate_limit",
6
+ "timeout",
7
+ "overloaded",
8
+ "network",
9
+ "provider_error",
10
+ ],
11
+ });
12
+
13
+ function isRetryable(
14
+ error,
15
+ options
16
+ ) {
17
+ if (!error) {
18
+ return false;
19
+ }
20
+
21
+ return options.retryableErrors.includes(
22
+ error.code
23
+ );
24
+ }
25
+
26
+ function findNextModel(
27
+ route,
28
+ models = {}
29
+ ) {
30
+ const candidates = [
31
+ {
32
+ role: "fallback",
33
+ model: models.fallback,
34
+ },
35
+ {
36
+ role: "emergency",
37
+ model: models.emergency,
38
+ },
39
+ ];
40
+
41
+ return (
42
+ candidates.find(
43
+ (candidate) =>
44
+ typeof candidate.model ===
45
+ "string" &&
46
+ candidate.model.length > 0 &&
47
+ candidate.model !== route.model
48
+ ) ?? null
49
+ );
50
+ }
51
+
52
+ export function getFallbackRoute({
53
+ error,
54
+ route,
55
+ providers = [],
56
+ models = {},
57
+ options = {},
58
+ }) {
59
+ const config = {
60
+ ...DEFAULT_OPTIONS,
61
+ ...options,
62
+ };
63
+
64
+ if (!isRetryable(error, config)) {
65
+ return null;
66
+ }
67
+
68
+ const attempt =
69
+ route.attempt ?? 1;
70
+
71
+ if (attempt >= config.maxAttempts) {
72
+ return null;
73
+ }
74
+
75
+ const nextModel =
76
+ findNextModel(
77
+ route,
78
+ models
79
+ );
80
+
81
+ if (nextModel) {
82
+ return {
83
+ ...route,
84
+
85
+ modelRole:
86
+ nextModel.role,
87
+
88
+ model:
89
+ nextModel.model,
90
+
91
+ attempt:
92
+ attempt + 1,
93
+ };
94
+ }
95
+
96
+ const currentIndex =
97
+ providers.findIndex(
98
+ (provider) =>
99
+ provider.name ===
100
+ route.provider
101
+ );
102
+
103
+ if (currentIndex === -1) {
104
+ return null;
105
+ }
106
+
107
+ const nextProvider =
108
+ providers[currentIndex + 1];
109
+
110
+ if (!nextProvider) {
111
+ return null;
112
+ }
113
+
114
+ return {
115
+ ...route,
116
+
117
+ provider:
118
+ nextProvider.name,
119
+
120
+ modelRole: "fallback",
121
+
122
+ model:
123
+ nextProvider.defaultModel ??
124
+ null,
125
+
126
+ attempt:
127
+ attempt + 1,
128
+ };
129
+ }
@@ -0,0 +1,26 @@
1
+ export { Intent, detectIntent } from "./intent.js";
2
+
3
+ export { createExecutionPlan } from "./planner.js";
4
+
5
+ export { retrieveContext } from "./rag.js";
6
+
7
+ export { buildContext } from "./contextBuilder.js";
8
+
9
+ export { routeRequest } from "./router.js";
10
+
11
+ export { configureThinking } from "./thinking.js";
12
+
13
+ export { getFallbackRoute } from "./fallback.js";
14
+
15
+ export { execute } from "./executor.js";
16
+
17
+ export {
18
+ createModelSelector,
19
+ MODEL_ROLES,
20
+ COMPLEXITIES,
21
+ } from "./modelSelector.js";
22
+
23
+ export {
24
+ createRagSelector,
25
+ RAG_SCOPES,
26
+ } from "./ragSelector.js";
@@ -0,0 +1,73 @@
1
+ export const Intent = Object.freeze({
2
+ CHAT: "chat",
3
+ EXPLAIN: "explain",
4
+ PLAN: "plan",
5
+ CODE: "code",
6
+ DEBUG: "debug",
7
+ REVIEW: "review",
8
+ REFACTOR: "refactor",
9
+ TEST: "test",
10
+ SEARCH: "search",
11
+ });
12
+
13
+ const PATTERNS = [
14
+ {
15
+ intent: Intent.REVIEW,
16
+ regex:
17
+ /\b(review|audit|inspect|evaluate|analyze|analyse|check|assess)\b/i,
18
+ },
19
+ {
20
+ intent: Intent.DEBUG,
21
+ regex:
22
+ /\b(debug|fix|bug|error|exception|crash|issue|failure|trace|stack)\b/i,
23
+ },
24
+ {
25
+ intent: Intent.REFACTOR,
26
+ regex:
27
+ /\b(refactor|cleanup|clean up|optimize|optimise|rewrite|rename|restructure|improve)\b/i,
28
+ },
29
+ {
30
+ intent: Intent.TEST,
31
+ regex:
32
+ /\b(test|testing|unit test|integration test|e2e|end[- ]to[- ]end|spec)\b/i,
33
+ },
34
+ {
35
+ intent: Intent.PLAN,
36
+ regex:
37
+ /\b(plan|design|architecture|architect|roadmap|strategy|approach)\b/i,
38
+ },
39
+ {
40
+ intent: Intent.SEARCH,
41
+ regex:
42
+ /\b(search|find|locate|lookup|look up|grep|where is|show me)\b/i,
43
+ },
44
+ {
45
+ intent: Intent.EXPLAIN,
46
+ regex:
47
+ /\b(explain|describe|what is|why|how does|understand|clarify)\b/i,
48
+ },
49
+ {
50
+ intent: Intent.CODE,
51
+ regex:
52
+ /\b(create|build|implement|generate|write|add|develop|make)\b/i,
53
+ },
54
+ ];
55
+
56
+ function normalize(text) {
57
+ return text
58
+ .trim()
59
+ .replace(/\s+/g, " ")
60
+ .toLowerCase();
61
+ }
62
+
63
+ export function detectIntent(input = "") {
64
+ const text = normalize(input);
65
+
66
+ for (const { intent, regex } of PATTERNS) {
67
+ if (regex.test(text)) {
68
+ return intent;
69
+ }
70
+ }
71
+
72
+ return Intent.CHAT;
73
+ }
@@ -0,0 +1,469 @@
1
+ const MODEL_ROLES = Object.freeze([
2
+ "fast",
3
+ "general",
4
+ "coding",
5
+ "planner",
6
+ "heavyCoding",
7
+ "subagent",
8
+ ]);
9
+
10
+ const COMPLEXITIES = Object.freeze([
11
+ "low",
12
+ "medium",
13
+ "high",
14
+ ]);
15
+
16
+ const DEFAULT_SELECTION = Object.freeze({
17
+ role: "general",
18
+ confidence: 0,
19
+ complexity: "medium",
20
+ reasoningRequired: false,
21
+ toolRequired: false,
22
+ });
23
+
24
+ function clampConfidence(value) {
25
+ const number = Number(value);
26
+
27
+ if (!Number.isFinite(number)) {
28
+ return 0;
29
+ }
30
+
31
+ return Math.max(
32
+ 0,
33
+ Math.min(1, number)
34
+ );
35
+ }
36
+
37
+ function normalizeRole(
38
+ role,
39
+ availableRoles
40
+ ) {
41
+ if (
42
+ typeof role !== "string"
43
+ ) {
44
+ return null;
45
+ }
46
+
47
+ const normalized =
48
+ role.trim().toLowerCase();
49
+
50
+ const canonicalRole =
51
+ MODEL_ROLES.find(
52
+ (candidate) =>
53
+ candidate.toLowerCase() ===
54
+ normalized
55
+ );
56
+
57
+ if (!canonicalRole) {
58
+ return null;
59
+ }
60
+
61
+ if (
62
+ Array.isArray(availableRoles) &&
63
+ availableRoles.length > 0
64
+ ) {
65
+ const availableRole =
66
+ availableRoles.find(
67
+ (candidate) =>
68
+ typeof candidate === "string" &&
69
+ candidate.toLowerCase() ===
70
+ normalized
71
+ );
72
+
73
+ if (!availableRole) {
74
+ return null;
75
+ }
76
+
77
+ return availableRole;
78
+ }
79
+
80
+ return canonicalRole;
81
+ }
82
+
83
+ function normalizeComplexity(
84
+ complexity
85
+ ) {
86
+ if (
87
+ typeof complexity !== "string"
88
+ ) {
89
+ return "medium";
90
+ }
91
+
92
+ const normalized =
93
+ complexity
94
+ .trim()
95
+ .toLowerCase();
96
+
97
+ return COMPLEXITIES.includes(
98
+ normalized
99
+ )
100
+ ? normalized
101
+ : "medium";
102
+ }
103
+
104
+ function extractJson(text) {
105
+ if (
106
+ typeof text !== "string"
107
+ ) {
108
+ return null;
109
+ }
110
+
111
+ const trimmed = text.trim();
112
+
113
+ /*
114
+ * Normal JSON response.
115
+ */
116
+ try {
117
+ return JSON.parse(trimmed);
118
+ } catch {
119
+ // Continue with fenced / embedded JSON.
120
+ }
121
+
122
+ /*
123
+ * ```json
124
+ * {...}
125
+ * ```
126
+ */
127
+ const fenced =
128
+ trimmed.match(
129
+ /```(?:json)?\s*([\s\S]*?)\s*```/i
130
+ );
131
+
132
+ if (fenced) {
133
+ try {
134
+ return JSON.parse(
135
+ fenced[1]
136
+ );
137
+ } catch {
138
+ // Continue.
139
+ }
140
+ }
141
+
142
+ /*
143
+ * Some models add a short sentence before
144
+ * the JSON object. Extract the outer object.
145
+ */
146
+ const start =
147
+ trimmed.indexOf("{");
148
+
149
+ const end =
150
+ trimmed.lastIndexOf("}");
151
+
152
+ if (
153
+ start >= 0 &&
154
+ end > start
155
+ ) {
156
+ try {
157
+ return JSON.parse(
158
+ trimmed.slice(
159
+ start,
160
+ end + 1
161
+ )
162
+ );
163
+ } catch {
164
+ return null;
165
+ }
166
+ }
167
+
168
+ return null;
169
+ }
170
+
171
+ function normalizeSelection(
172
+ value,
173
+ {
174
+ availableRoles = [],
175
+ fallback = DEFAULT_SELECTION,
176
+ } = {}
177
+ ) {
178
+ if (
179
+ !value ||
180
+ typeof value !== "object"
181
+ ) {
182
+ return {
183
+ ...fallback,
184
+ };
185
+ }
186
+
187
+ const role =
188
+ normalizeRole(
189
+ value.role,
190
+ availableRoles
191
+ );
192
+
193
+ if (!role) {
194
+ return {
195
+ ...fallback,
196
+ };
197
+ }
198
+
199
+ return {
200
+ role,
201
+
202
+ confidence:
203
+ clampConfidence(
204
+ value.confidence
205
+ ),
206
+
207
+ complexity:
208
+ normalizeComplexity(
209
+ value.complexity
210
+ ),
211
+
212
+ reasoningRequired:
213
+ Boolean(
214
+ value.reasoningRequired
215
+ ),
216
+
217
+ toolRequired:
218
+ Boolean(
219
+ value.toolRequired
220
+ ),
221
+ };
222
+ }
223
+
224
+ function buildDeterministicFallback(
225
+ {
226
+ plan = {},
227
+ intent = "",
228
+ availableRoles = [],
229
+ } = {}
230
+ ) {
231
+ let role = "general";
232
+
233
+ if (
234
+ plan.requiresThinking
235
+ ) {
236
+ role = "planner";
237
+ } else if (
238
+ [
239
+ "code",
240
+ "debug",
241
+ "review",
242
+ "refactor",
243
+ "test",
244
+ ].includes(intent)
245
+ ) {
246
+ role = "coding";
247
+ } else if (
248
+ intent === "chat"
249
+ ) {
250
+ role = "fast";
251
+ }
252
+
253
+ if (
254
+ Array.isArray(
255
+ availableRoles
256
+ ) &&
257
+ availableRoles.length > 0 &&
258
+ !availableRoles.includes(role)
259
+ ) {
260
+ role =
261
+ availableRoles.includes(
262
+ "general"
263
+ )
264
+ ? "general"
265
+ : availableRoles[0];
266
+ }
267
+
268
+ return {
269
+ role,
270
+
271
+ confidence: 0,
272
+
273
+ complexity:
274
+ plan.requiresThinking
275
+ ? "high"
276
+ : "medium",
277
+
278
+ reasoningRequired:
279
+ Boolean(
280
+ plan.requiresThinking
281
+ ),
282
+
283
+ toolRequired:
284
+ Boolean(
285
+ plan.requiresTools
286
+ ),
287
+ };
288
+ }
289
+
290
+ function buildSelectorPrompt({
291
+ prompt,
292
+ intent,
293
+ plan,
294
+ ragDecision,
295
+ availableRoles,
296
+ }) {
297
+ return [
298
+ "You are CodeForge's model selection agent.",
299
+ "Your only task is to select the best model role for the user's task.",
300
+ "",
301
+ "Do not solve the user's task.",
302
+ "Do not provide an explanation.",
303
+ "Return ONLY valid JSON.",
304
+ "",
305
+ "Allowed roles:",
306
+ ...availableRoles.map(
307
+ (role) => `- ${role}`
308
+ ),
309
+ "",
310
+ "Return exactly this shape:",
311
+ JSON.stringify(
312
+ {
313
+ role: "general",
314
+ confidence: 0.0,
315
+ complexity: "medium",
316
+ reasoningRequired: false,
317
+ toolRequired: false,
318
+ },
319
+ null,
320
+ 2
321
+ ),
322
+ "",
323
+ `User request: ${prompt}`,
324
+ `Detected intent: ${intent}`,
325
+ `RAG required: ${Boolean(
326
+ ragDecision?.required
327
+ )}`,
328
+ `RAG scope: ${ragDecision?.scope ?? "none"}`,
329
+ `Requires thinking: ${Boolean(
330
+ plan?.requiresThinking
331
+ )}`,
332
+ `Requires tools: ${Boolean(
333
+ plan?.requiresTools
334
+ )}`,
335
+ `Requires write: ${Boolean(
336
+ plan?.requiresWrite
337
+ )}`,
338
+ ].join("\n");
339
+ }
340
+
341
+ /**
342
+ * Create the v1 model-selection agent.
343
+ *
344
+ * The selector model makes a small structured decision.
345
+ * It does not execute tools and does not receive repository
346
+ * contents.
347
+ */
348
+ export function createModelSelector({
349
+ provider,
350
+ model,
351
+ maxTokens = 256,
352
+ } = {}) {
353
+ async function select({
354
+ prompt = "",
355
+ intent = "chat",
356
+ plan = {},
357
+ ragDecision = null,
358
+ availableRoles = MODEL_ROLES,
359
+ } = {}) {
360
+ const fallback =
361
+ buildDeterministicFallback({
362
+ plan,
363
+ intent,
364
+ availableRoles,
365
+ });
366
+
367
+ /*
368
+ * Selector is optional. If no selector model has been
369
+ * configured, preserve deterministic v1 routing behavior.
370
+ */
371
+ if (
372
+ !provider ||
373
+ !model
374
+ ) {
375
+ return {
376
+ ...fallback,
377
+ source: "fallback",
378
+ };
379
+ }
380
+
381
+ const selectorPrompt =
382
+ buildSelectorPrompt({
383
+ prompt,
384
+ intent,
385
+ plan,
386
+ ragDecision,
387
+ availableRoles,
388
+ });
389
+
390
+ try {
391
+ const response =
392
+ await provider.generate({
393
+ model,
394
+ temperature: 0,
395
+ maxTokens,
396
+ messages: [
397
+ {
398
+ role: "system",
399
+ content:
400
+ "You are a routing classifier. Output JSON only.",
401
+ },
402
+ {
403
+ role: "user",
404
+ content:
405
+ selectorPrompt,
406
+ },
407
+ ],
408
+ });
409
+
410
+ const content =
411
+ response?.message?.content;
412
+
413
+ const parsed =
414
+ extractJson(content);
415
+
416
+ const selection =
417
+ normalizeSelection(
418
+ parsed,
419
+ {
420
+ availableRoles,
421
+ fallback,
422
+ }
423
+ );
424
+
425
+ /*
426
+ * A confidence of zero combined with an invalid
427
+ * selector response means we should preserve the
428
+ * deterministic routing decision.
429
+ */
430
+ if (
431
+ !parsed ||
432
+ !normalizeRole(
433
+ parsed.role,
434
+ availableRoles
435
+ )
436
+ ) {
437
+ return {
438
+ ...fallback,
439
+ source: "fallback",
440
+ };
441
+ }
442
+
443
+ return {
444
+ ...selection,
445
+ source: "model",
446
+ };
447
+ } catch {
448
+ /*
449
+ * Model selection must never make CodeForge unusable.
450
+ * If the selector fails, existing deterministic
451
+ * routing remains authoritative.
452
+ */
453
+ return {
454
+ ...fallback,
455
+ source: "fallback",
456
+ };
457
+ }
458
+ }
459
+
460
+ return {
461
+ select,
462
+ };
463
+ }
464
+
465
+ export {
466
+ MODEL_ROLES,
467
+ COMPLEXITIES,
468
+ normalizeSelection,
469
+ };