chorus-cli 0.4.7 → 0.4.9

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/index.js CHANGED
@@ -6,7 +6,6 @@ const _configEnv = _path.join(_configDir, '.env');
6
6
  const _localEnv = _path.join(__dirname, '.env');
7
7
  // Prefer user config dir (works when installed globally), fall back to local .env for dev
8
8
  require('dotenv').config({ path: require('fs').existsSync(_configEnv) ? _configEnv : _localEnv });
9
- const { Octokit } = require('@octokit/rest');
10
9
 
11
10
  const { createProvider } = require('./providers');
12
11
  const OpenAI = require('openai');
@@ -29,16 +28,16 @@ async function getMachineId() {
29
28
  const { stdout } = await execPromise(
30
29
  "ioreg -rd1 -c IOPlatformExpertDevice | awk -F'\"' '/IOPlatformUUID/{print $4}'"
31
30
  );
32
- if (stdout.trim()) rawId = stdout.trim();
31
+ if (stdout.trim()) {rawId = stdout.trim();}
33
32
  } else if (process.platform === 'linux') {
34
33
  const id = (await fs.readFile('/etc/machine-id', 'utf8')).trim();
35
- if (id) rawId = id;
34
+ if (id) {rawId = id;}
36
35
  } else if (process.platform === 'win32') {
37
36
  const { stdout } = await execPromise('wmic csproduct get UUID');
38
37
  const lines = stdout.trim().split('\n');
39
38
  if (lines.length > 1) {
40
39
  const uuid = lines[1].trim();
41
- if (uuid && uuid !== 'FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF') rawId = uuid;
40
+ if (uuid && uuid !== 'FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF') {rawId = uuid;}
42
41
  }
43
42
  }
44
43
  } catch { /* fall through to persistent fallback */ }
@@ -49,7 +48,7 @@ async function getMachineId() {
49
48
  const idPath = path.join(configDir, 'machine-id');
50
49
  try {
51
50
  const existing = await fs.readFile(idPath, 'utf8');
52
- if (existing.trim()) rawId = existing.trim();
51
+ if (existing.trim()) {rawId = existing.trim();}
53
52
  } catch { /* no file yet */ }
54
53
 
55
54
  if (!rawId) {
@@ -77,7 +76,7 @@ function runCoder(prompt) {
77
76
  cwd: process.cwd(),
78
77
  env,
79
78
  stdio: ['ignore', 'pipe', 'pipe'],
80
- timeout: 600000,
79
+ timeout: 600000
81
80
  });
82
81
 
83
82
  let stdout = '';
@@ -110,14 +109,14 @@ function runQAChat(issue, enrichedDetails, qaName, useSuper = false) {
110
109
  issue_number: issue.number,
111
110
  issue_title: issue.title,
112
111
  issue_body: issue.body || '',
113
- enriched_questions: enrichedDetails,
112
+ enriched_questions: enrichedDetails
114
113
  });
115
114
 
116
- const args = ['--messenger', CONFIG.messenger, '--qa', qaName];
115
+ const args = ['--messenger', CONFIG.messenger, '--qa', qaName?.login];
117
116
  if (CONFIG.messenger === 'teams') {
118
117
  args.push('--auth', CONFIG.teams.authPath);
119
118
  }
120
- if (useSuper) args.push('--super');
119
+ if (useSuper) {args.push('--super');}
121
120
 
122
121
  const env = { ...process.env };
123
122
  if (CONFIG.ai.chorusApiKey) {
@@ -134,7 +133,7 @@ function runQAChat(issue, enrichedDetails, qaName, useSuper = false) {
134
133
  cwd: process.cwd(),
135
134
  env,
136
135
  stdio: ['pipe', 'pipe', 'pipe'],
137
- timeout: 1800000, // 30 min
136
+ timeout: 1800000 // 30 min
138
137
  });
139
138
 
140
139
  let stdout = '';
@@ -168,13 +167,13 @@ const CONFIG = {
168
167
  github: {
169
168
  owner: process.env.GITHUB_OWNER,
170
169
  repo: process.env.GITHUB_REPO,
171
- token: process.env.GITHUB_TOKEN,
170
+ token: process.env.GITHUB_TOKEN
172
171
  },
173
172
  azuredevops: {
174
173
  org: process.env.AZDO_ORG,
175
174
  project: process.env.AZDO_PROJECT,
176
175
  repo: process.env.AZDO_REPO,
177
- pat: process.env.AZDO_PAT,
176
+ pat: process.env.AZDO_PAT
178
177
  },
179
178
  messenger: process.env.MESSENGER || 'teams',
180
179
  teams: {
@@ -182,11 +181,11 @@ const CONFIG = {
182
181
  pollInterval: 60000 // Check for response every 60 seconds
183
182
  },
184
183
  slack: {
185
- botToken: process.env.SLACK_BOT_TOKEN || '',
184
+ botToken: process.env.SLACK_BOT_TOKEN || ''
186
185
  },
187
186
  ai: {
188
- enrichmentTool: 'claude', // 'claude' or 'kimi'
189
- codingTool: 'coder', // 'coder', 'claude', or 'kimi'
187
+ enrichmentTool: 'claude', // 'claude' or 'kimi'
188
+ codingTool: 'coder', // 'coder', 'claude', or 'kimi'
190
189
  coderPath: path.join(__dirname, 'tools', 'coder.py'),
191
190
  qaPath: path.join(__dirname, 'tools', 'qa.py'),
192
191
  mapperPath: path.join(__dirname, 'tools', 'mapper.py'),
@@ -194,7 +193,7 @@ const CONFIG = {
194
193
  ? path.join(os.homedir(), '.config', 'chorus', '.venv', 'Scripts', 'python.exe')
195
194
  : path.join(os.homedir(), '.config', 'chorus', '.venv', 'bin', 'python'),
196
195
  chorusApiKey: process.env.CHORUS_API_KEY,
197
- chorusApiUrl: process.env.CHORUS_API_URL || 'https://chorus-bad0f.web.app/v1',
196
+ chorusApiUrl: process.env.CHORUS_API_URL || 'https://chorus-bad0f.web.app/v1'
198
197
  }
199
198
  };
200
199
 
@@ -236,9 +235,9 @@ Thanks!
236
235
  IMPORTANT: Output ONLY the message above. Do not include any preamble, thinking process, or explanations. Start directly with "Hi! I need clarification..."`;
237
236
 
238
237
  console.log('🤖 Enriching issue with AI...');
239
-
238
+
240
239
  const tool = CONFIG.ai.enrichmentTool;
241
-
240
+
242
241
  if (tool === 'claude') {
243
242
  // Use Chorus proxy API
244
243
  if (!CONFIG.ai.chorusApiKey) {
@@ -246,15 +245,15 @@ IMPORTANT: Output ONLY the message above. Do not include any preamble, thinking
246
245
  }
247
246
  const openaiOpts = {
248
247
  apiKey: CONFIG.ai.chorusApiKey,
249
- baseURL: CONFIG.ai.chorusApiUrl,
248
+ baseURL: CONFIG.ai.chorusApiUrl
250
249
  };
251
250
  if (CONFIG.ai.machineId) {
252
251
  openaiOpts.defaultHeaders = { 'X-Machine-Id': CONFIG.ai.machineId };
253
252
  }
254
253
  const openai = new OpenAI(openaiOpts);
255
-
254
+
256
255
  const response = await openai.chat.completions.create({
257
- model: 'anthropic/claude-opus-4',
256
+ model: 'chorus-default',
258
257
  max_tokens: 2000,
259
258
  messages: [
260
259
  {
@@ -262,6 +261,8 @@ IMPORTANT: Output ONLY the message above. Do not include any preamble, thinking
262
261
  content: prompt
263
262
  }
264
263
  ]
264
+ }, {
265
+ headers: { 'X-Chorus-Mode': 'enrich' },
265
266
  });
266
267
 
267
268
  if (response.usage) {
@@ -269,12 +270,12 @@ IMPORTANT: Output ONLY the message above. Do not include any preamble, thinking
269
270
  }
270
271
 
271
272
  return response.choices[0].message.content.trim();
272
- } else {
273
- // Use Kimi CLI
274
- const escapedPrompt = prompt.replace(/"/g, '\\"').replace(/\$/g, '\\$');
275
- const { stdout } = await execPromise(`kimi --prompt "${escapedPrompt}"`);
276
- return stdout.trim();
277
273
  }
274
+ // Use Kimi CLI
275
+ const escapedPrompt = prompt.replace(/"/g, '\\"').replace(/\$/g, '\\$');
276
+ const { stdout } = await execPromise(`kimi --prompt "${escapedPrompt}"`);
277
+ return stdout.trim();
278
+
278
279
  }
279
280
 
280
281
  // ===== CODE GENERATION =====
@@ -396,10 +397,10 @@ async function getCodeRabbitReview(solution, issue, provider) {
396
397
  );
397
398
 
398
399
  // 5. Clean up — undo the commit so changes stay in working tree, then go back to main
399
- await execPromise(`git reset --mixed HEAD~1`); // undo commit, keep changes in working tree
400
- await execPromise(`git stash`); // stash working tree changes
400
+ await execPromise(`git reset --mixed HEAD~1`); // undo commit, keep changes in working tree
401
+ await execPromise(`git stash`); // stash working tree changes
401
402
  await execPromise(`git checkout main`);
402
- await execPromise(`git stash pop`); // restore coder's changes on main
403
+ await execPromise(`git stash pop`); // restore coder's changes on main
403
404
  await execPromise(`git branch -D ${branchName}`).catch(() => {});
404
405
 
405
406
  return {
@@ -489,7 +490,7 @@ async function lintAndFix(solution, provider) {
489
490
  console.log(' Auto-fix applied');
490
491
  } catch (e) {
491
492
  // eslint exits non-zero when there are unfixable errors — that's expected
492
- if (e.stdout) console.log(e.stdout);
493
+ if (e.stdout) {console.log(e.stdout);}
493
494
  }
494
495
 
495
496
  // Second pass: check for remaining errors
@@ -525,7 +526,7 @@ ${lintOutput.slice(0, 5000)}`;
525
526
 
526
527
  // Update solution with any new files coder touched
527
528
  for (const f of (fixResult.files_modified || [])) {
528
- if (!solution.files_modified.includes(f)) solution.files_modified.push(f);
529
+ if (!solution.files_modified.includes(f)) {solution.files_modified.push(f);}
529
530
  }
530
531
 
531
532
  console.log(' Lint fix pass complete\n');
@@ -585,12 +586,12 @@ async function processTicket(issueArg, { useSuper = false, skipQA = false, qaNam
585
586
  if (gitStatus.trim()) {
586
587
  console.error('⚠️ Working directory has uncommitted changes. Commit or stash first:');
587
588
  console.error(gitStatus);
588
- //throw new Error('Dirty working directory');
589
+ // throw new Error('Dirty working directory');
589
590
  }
590
591
  const { stdout: currentBranch } = await execPromise('git rev-parse --abbrev-ref HEAD');
591
592
  if (currentBranch.trim() !== 'main') {
592
593
  console.warn(`⚠️ Not on main branch (currently on ${currentBranch.trim()}). Switch to main first.`);
593
- //throw new Error('Not on main branch');
594
+ // throw new Error('Not on main branch');
594
595
  }
595
596
 
596
597
  // 0b. Map the codebase so coder starts with full context
@@ -629,6 +630,7 @@ async function processTicket(issueArg, { useSuper = false, skipQA = false, qaNam
629
630
  console.log('⏭️ Skipping QA conversation (--skip-qa)\n');
630
631
  } else {
631
632
  const qaName = qaNameOverride || await provider.getUserDisplayName(issue.user.login);
633
+ console.log(JSON.stringify(qaName));
632
634
  console.log(`💬 Starting QA conversation with ${qaName?.login}...`);
633
635
  const qaResult = await runQAChat(issue, enrichedDetails, qaName, useSuper);
634
636
  qaResponse = qaResult.requirements;
@@ -642,8 +644,17 @@ async function processTicket(issueArg, { useSuper = false, skipQA = false, qaNam
642
644
  }
643
645
  console.log(`QA complete (${qaResult.conversation_rounds} rounds)\n`);
644
646
  }
645
-
647
+
646
648
  // 4. Generate code (with retry if coder doesn't actually write files)
649
+ // Snapshot current working-tree state so we can detect what the coder *actually* changed
650
+ let baselineDirty = new Set();
651
+ try {
652
+ const { stdout: bDiff } = await execPromise('git diff --name-only');
653
+ const { stdout: bNew } = await execPromise('git ls-files --others --exclude-standard');
654
+ bDiff.trim().split('\n').filter(Boolean).forEach(f => baselineDirty.add(f));
655
+ bNew.trim().split('\n').filter(Boolean).forEach(f => baselineDirty.add(f));
656
+ } catch { /* non-git or error — baseline stays empty */ }
657
+
647
658
  const maxCodeAttempts = 3;
648
659
  let solution;
649
660
 
@@ -683,7 +694,27 @@ CRITICAL: You MUST write code to actual files. Do not just describe changes —
683
694
  throw new Error('Code generation failed');
684
695
  }
685
696
 
686
- const filesChanged = (solution.files_modified || []).length + (solution.files_created || []).length;
697
+ let filesChanged = (solution.files_modified || []).length + (solution.files_created || []).length;
698
+
699
+ // If coder reports 0 files, check git for actual working-tree changes
700
+ // the coder may have written files without reporting them
701
+ if (filesChanged === 0) {
702
+ try {
703
+ const { stdout: gitDiff } = await execPromise('git diff --name-only');
704
+ const { stdout: gitUntracked } = await execPromise('git ls-files --others --exclude-standard');
705
+ const diffFiles = gitDiff.trim().split('\n').filter(Boolean).filter(f => !baselineDirty.has(f));
706
+ const untrackedFiles = gitUntracked.trim().split('\n').filter(Boolean).filter(f => !baselineDirty.has(f));
707
+ const gitChangedFiles = [...diffFiles, ...untrackedFiles];
708
+
709
+ if (gitChangedFiles.length > 0) {
710
+ console.log(` Coder didn't report files, but git detected ${gitChangedFiles.length} changed file(s)`);
711
+ // Backfill the solution so downstream steps (lint, review, PR) work
712
+ solution.files_modified = [...(solution.files_modified || []), ...diffFiles];
713
+ solution.files_created = [...(solution.files_created || []), ...untrackedFiles];
714
+ filesChanged = gitChangedFiles.length;
715
+ }
716
+ } catch { /* git check failed, fall through to existing logic */ }
717
+ }
687
718
 
688
719
  if (filesChanged > 0) {
689
720
  break; // Success — coder actually wrote files
@@ -698,7 +729,7 @@ CRITICAL: You MUST write code to actual files. Do not just describe changes —
698
729
  }
699
730
 
700
731
  console.log('Code generation complete\n');
701
- if (solution.summary) console.log(`Summary: ${solution.summary}\n`);
732
+ if (solution.summary) {console.log(`Summary: ${solution.summary}\n`);}
702
733
 
703
734
  // 5. Lint files the coder touched
704
735
  await lintAndFix(solution, provider);
@@ -754,11 +785,11 @@ CRITICAL: You MUST write code to actual files. Do not just describe changes —
754
785
  while (userIteration < maxUserIterations) {
755
786
  const allFiles = [...new Set([
756
787
  ...(solution.files_modified || []),
757
- ...(solution.files_created || []),
788
+ ...(solution.files_created || [])
758
789
  ])];
759
790
  console.log('FILES CHANGED:');
760
791
  allFiles.forEach(f => console.log(` ${f}`));
761
- if (solution.summary) console.log(`\nSUMMARY: ${solution.summary}`);
792
+ if (solution.summary) {console.log(`\nSUMMARY: ${solution.summary}`);}
762
793
  console.log();
763
794
 
764
795
  const answer = await prompt(rl, '✅ Happy with the changes? (y/n): ');
@@ -856,7 +887,7 @@ function prompt(rl, question, hidden = false) {
856
887
  } else if (ch === '\u0003') {
857
888
  process.exit();
858
889
  } else if (ch === '\u007F' || ch === '\b') {
859
- if (input.length > 0) input = input.slice(0, -1);
890
+ if (input.length > 0) {input = input.slice(0, -1);}
860
891
  } else {
861
892
  input += ch;
862
893
  }
@@ -888,7 +919,7 @@ async function setupGitHub() {
888
919
  } catch { /* no .env yet */ }
889
920
 
890
921
  const updates = { GITHUB_OWNER: owner, GITHUB_REPO: repo };
891
- if (token) updates.GITHUB_TOKEN = token;
922
+ if (token) {updates.GITHUB_TOKEN = token;}
892
923
 
893
924
  for (const [key, value] of Object.entries(updates)) {
894
925
  const regex = new RegExp(`^${key}=.*$`, 'm');
@@ -904,7 +935,7 @@ async function setupGitHub() {
904
935
  // Update in-memory config
905
936
  CONFIG.github.owner = owner;
906
937
  CONFIG.github.repo = repo;
907
- if (token) CONFIG.github.token = token;
938
+ if (token) {CONFIG.github.token = token;}
908
939
 
909
940
  console.log(`\n✅ GitHub config saved (${owner}/${repo})\n`);
910
941
  }
@@ -940,7 +971,7 @@ async function setupProxyAuth() {
940
971
  const mod = parsed.protocol === 'https:' ? https : http;
941
972
  const req = mod.request(parsed, { method: 'POST', headers: { 'Content-Type': 'application/json' } }, (res) => {
942
973
  let data = '';
943
- res.on('data', (chunk) => data += chunk);
974
+ res.on('data', (chunk) => { data += chunk; });
944
975
  res.on('end', () => {
945
976
  try {
946
977
  resolve({ status: res.statusCode, body: JSON.parse(data) });
@@ -1099,7 +1130,7 @@ async function setupAzureDevOps() {
1099
1130
  } catch { /* no .env yet */ }
1100
1131
 
1101
1132
  const updates = { AZDO_ORG: adoOrg, AZDO_PROJECT: adoProject, AZDO_REPO: adoRepo, PROVIDER: 'azuredevops' };
1102
- if (adoPat) updates.AZDO_PAT = adoPat;
1133
+ if (adoPat) {updates.AZDO_PAT = adoPat;}
1103
1134
 
1104
1135
  for (const [key, value] of Object.entries(updates)) {
1105
1136
  const regex = new RegExp(`^${key}=.*$`, 'm');
@@ -1115,7 +1146,7 @@ async function setupAzureDevOps() {
1115
1146
  CONFIG.azuredevops.org = adoOrg;
1116
1147
  CONFIG.azuredevops.project = adoProject;
1117
1148
  CONFIG.azuredevops.repo = adoRepo;
1118
- if (adoPat) CONFIG.azuredevops.pat = adoPat;
1149
+ if (adoPat) {CONFIG.azuredevops.pat = adoPat;}
1119
1150
 
1120
1151
  console.log(`\n✅ Azure DevOps config saved (${adoOrg}/${adoProject}/${adoRepo})\n`);
1121
1152
  }
@@ -1154,6 +1185,70 @@ async function setup() {
1154
1185
  console.log('\n✅ Setup complete. You can now run: chorus run\n');
1155
1186
  }
1156
1187
 
1188
+ // === EASTER EGG ====
1189
+
1190
+ function printZEP() {
1191
+ const w = 'zep';
1192
+ const size = 9;
1193
+ const thick = 2;
1194
+ const blank = ' '.repeat(w.length);
1195
+
1196
+ // Colors
1197
+ const red = '\x1b[31m';
1198
+ const yellow = '\x1b[33m';
1199
+ const cyan = '\x1b[36m';
1200
+ const bold = '\x1b[1m';
1201
+ const reset = '\x1b[0m';
1202
+
1203
+ // Z
1204
+ const Z = [];
1205
+ for (let i = 0; i < size; i++) {
1206
+ if (i < thick || i >= size - thick) {
1207
+ Z.push(w.repeat(size));
1208
+ } else {
1209
+ const pos = size - 1 - i;
1210
+ Z.push(blank.repeat(pos) + w.repeat(thick) + blank.repeat(Math.max(0, size - pos - thick)));
1211
+ }
1212
+ }
1213
+
1214
+ // E
1215
+ const E = [];
1216
+ for (let i = 0; i < size; i++) {
1217
+ if (i < thick || i >= size - thick || (i >= Math.floor(size / 2) - Math.floor(thick / 2) && i < Math.floor(size / 2) + thick - Math.floor(thick / 2))) {
1218
+ E.push(w.repeat(size));
1219
+ } else {
1220
+ E.push(w.repeat(thick) + blank.repeat(size - thick));
1221
+ }
1222
+ }
1223
+
1224
+ // P
1225
+ const P = [];
1226
+ for (let i = 0; i < size; i++) {
1227
+ if (i < thick || (i >= Math.floor(size / 2) - Math.floor(thick / 2) && i < Math.floor(size / 2) + thick - Math.floor(thick / 2))) {
1228
+ P.push(w.repeat(size));
1229
+ } else if (i < Math.floor(size / 2)) {
1230
+ P.push(w.repeat(thick) + blank.repeat(size - thick * 2) + w.repeat(thick));
1231
+ } else {
1232
+ P.push(w.repeat(thick) + blank.repeat(size - thick));
1233
+ }
1234
+ }
1235
+
1236
+ const gap = ' ';
1237
+
1238
+ console.log('\n\n');
1239
+ for (let i = 0; i < size; i++) {
1240
+ console.log(
1241
+ bold + red + Z[i] + reset +
1242
+ gap +
1243
+ bold + yellow + E[i] + reset +
1244
+ gap +
1245
+ bold + cyan + P[i] + reset
1246
+ );
1247
+ }
1248
+ console.log('\n\n');
1249
+ }
1250
+
1251
+
1157
1252
  // ===== CLI =====
1158
1253
  const command = process.argv[2];
1159
1254
  const _envExists = require('fs').existsSync(path.join(os.homedir(), '.config', 'chorus', '.env'));
@@ -1190,6 +1285,8 @@ if (command === 'setup') {
1190
1285
  const { issueArg, ...opts } = parseRunArgs();
1191
1286
  processTicket(issueArg, opts).catch(console.error);
1192
1287
  }
1288
+ } else if (command === 'zep') {
1289
+ printZEP()
1193
1290
  } else {
1194
1291
  if (!_envExists) {
1195
1292
  console.log(`
@@ -1210,12 +1307,12 @@ Usage:
1210
1307
  chorus run 4464 - Process specific issue by number
1211
1308
  chorus run <url> - Process issue from full URL (auto-detects provider)
1212
1309
  chorus run 4464 --super - Use Opus 4.6 for QA evaluation
1213
- chorus run 4464 --qa 'John Doe' - Specify QA contact name for chat
1310
+ chorus run 4464 --qa 'John Doe' - Specify QA contact name for chat
1214
1311
  chorus run 4464 --skip-qa - Skip QA conversation, go straight to coding
1215
1312
 
1216
1313
  GitHub examples:
1217
1314
  chorus run 4464
1218
- chorus run https://github.com/shesha-io/shesha-framework/issues/4464
1315
+ chorus run https://github.com/org/repository/issues/4464
1219
1316
 
1220
1317
  Azure DevOps examples:
1221
1318
  chorus run 456
@@ -1238,4 +1335,4 @@ Configuration is stored in ~/.config/chorus/.env
1238
1335
  Run "chorus setup" to configure credentials and messenger auth.
1239
1336
  `);
1240
1337
  }
1241
- }
1338
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chorus-cli",
3
- "version": "0.4.7",
3
+ "version": "0.4.9",
4
4
  "description": "Automated ticket resolution with AI, Teams, and Slack integration",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -15,7 +15,9 @@
15
15
  "scripts": {
16
16
  "postinstall": "node scripts/postinstall.js",
17
17
  "setup": "node index.js setup",
18
- "start": "node index.js run"
18
+ "start": "node index.js run",
19
+ "lint": "eslint .",
20
+ "lint:fix": "eslint . --fix"
19
21
  },
20
22
  "dependencies": {
21
23
  "@octokit/rest": "^20.0.2",
@@ -23,6 +25,9 @@
23
25
  "openai": "^4.0.0",
24
26
  "playwright": "^1.40.0"
25
27
  },
28
+ "devDependencies": {
29
+ "eslint": "^8.56.0"
30
+ },
26
31
  "engines": {
27
32
  "node": ">=18.0.0"
28
33
  }
@@ -22,8 +22,8 @@ function normalize(workItem) {
22
22
  user: {
23
23
  login: fields['System.CreatedBy']?.displayName
24
24
  || fields['System.CreatedBy']?.uniqueName
25
- || 'unknown',
26
- },
25
+ || 'unknown'
26
+ }
27
27
  };
28
28
  }
29
29
 
@@ -37,14 +37,14 @@ function createAzureDevOpsProvider(config) {
37
37
  const encoded = Buffer.from(`:${pat}`).toString('base64');
38
38
  return {
39
39
  Authorization: `Basic ${encoded}`,
40
- 'Content-Type': 'application/json',
40
+ 'Content-Type': 'application/json'
41
41
  };
42
42
  }
43
43
 
44
44
  async function adoFetch(url, options = {}) {
45
45
  const res = await fetch(url, {
46
46
  ...options,
47
- headers: { ...authHeaders(), ...options.headers },
47
+ headers: { ...authHeaders(), ...options.headers }
48
48
  });
49
49
  if (!res.ok) {
50
50
  const text = await res.text();
@@ -59,7 +59,7 @@ function createAzureDevOpsProvider(config) {
59
59
  WHERE [System.AssignedTo] = @me
60
60
  AND [System.State] <> 'Closed'
61
61
  AND [System.State] <> 'Removed'
62
- ORDER BY [System.CreatedDate] DESC`,
62
+ ORDER BY [System.CreatedDate] DESC`
63
63
  };
64
64
 
65
65
  const result = await adoFetch(
@@ -88,7 +88,7 @@ function createAzureDevOpsProvider(config) {
88
88
  }
89
89
 
90
90
  function parseIssueArg(arg) {
91
- if (!arg) return null;
91
+ if (!arg) {return null;}
92
92
 
93
93
  // Full ADO URL: https://dev.azure.com/{org}/{project}/_workitems/edit/{id}
94
94
  const urlMatch = arg.match(
@@ -98,7 +98,7 @@ function createAzureDevOpsProvider(config) {
98
98
  return {
99
99
  org: urlMatch[1],
100
100
  project: urlMatch[2],
101
- number: parseInt(urlMatch[3], 10),
101
+ number: parseInt(urlMatch[3], 10)
102
102
  };
103
103
  }
104
104
 
@@ -142,7 +142,7 @@ function createAzureDevOpsProvider(config) {
142
142
  const cleanPath = filepath.trim();
143
143
  if (cleanPath.includes('/') || cleanPath.includes('.')) {
144
144
  const dir = cleanPath.substring(0, cleanPath.lastIndexOf('/'));
145
- if (dir) await execPromise(`mkdir -p ${dir}`).catch(() => {});
145
+ if (dir) {await execPromise(`mkdir -p ${dir}`).catch(() => {});}
146
146
  await fs.writeFile(cleanPath, content.trim());
147
147
  }
148
148
  }
@@ -176,7 +176,7 @@ function createAzureDevOpsProvider(config) {
176
176
  sourceRefName: `refs/heads/${branchName}`,
177
177
  targetRefName: 'refs/heads/main',
178
178
  title: `Fix: ${issue.title}`,
179
- description: `Resolves AB#${issue.number}\n\n## Summary\n${summary}${filesChanged ? `\n\n## Files Changed\n${filesChanged}` : ''}`,
179
+ description: `Resolves AB#${issue.number}\n\n## Summary\n${summary}${filesChanged ? `\n\n## Files Changed\n${filesChanged}` : ''}`
180
180
  };
181
181
 
182
182
  const prResult = await adoFetch(
@@ -195,7 +195,7 @@ function createAzureDevOpsProvider(config) {
195
195
  parseIssueArg,
196
196
  createPR,
197
197
  getSolutionFiles,
198
- gitAddSolutionFiles,
198
+ gitAddSolutionFiles
199
199
  };
200
200
  }
201
201
 
@@ -22,7 +22,7 @@ function createGitHubProvider(config) {
22
22
  assignee: 'james-baloyi',
23
23
  sort: 'created',
24
24
  direction: 'desc',
25
- per_page: 10,
25
+ per_page: 10
26
26
  });
27
27
 
28
28
  if (issues.length === 0) {
@@ -37,7 +37,7 @@ function createGitHubProvider(config) {
37
37
  const { data: issue } = await octokit.issues.get({
38
38
  owner: issueOwner || owner,
39
39
  repo: issueRepo || repo,
40
- issue_number: number,
40
+ issue_number: number
41
41
  });
42
42
  return issue;
43
43
  }
@@ -49,7 +49,7 @@ function createGitHubProvider(config) {
49
49
  }
50
50
 
51
51
  function parseIssueArg(arg) {
52
- if (!arg) return null;
52
+ if (!arg) {return null;}
53
53
 
54
54
  // Full GitHub URL: https://github.com/owner/repo/issues/123
55
55
  const urlMatch = arg.match(/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/);
@@ -86,7 +86,19 @@ function createGitHubProvider(config) {
86
86
  console.log('🌿 Creating PR...');
87
87
 
88
88
  if (!isCoder) {
89
- await writeSolutionToFiles(solution._raw || solution);
89
+ // Legacy: parse code blocks and write files
90
+ const fileRegex = /```(.+?)\n([\s\S]+?)```/g;
91
+ const raw = solution._raw || solution;
92
+ let match;
93
+ while ((match = fileRegex.exec(raw)) !== null) {
94
+ const [, filepath, content] = match;
95
+ const cleanPath = filepath.trim();
96
+ if (cleanPath.includes('/') || cleanPath.includes('.')) {
97
+ const dir = cleanPath.substring(0, cleanPath.lastIndexOf('/'));
98
+ if (dir) {await execPromise(`mkdir -p ${dir}`).catch(() => {});}
99
+ await fs.writeFile(cleanPath, content.trim());
100
+ }
101
+ }
90
102
  }
91
103
 
92
104
  // Clean up stale branch from previous runs
@@ -137,7 +149,7 @@ ${filesChanged ? `\n## Files Changed\n${filesChanged}` : ''}`;
137
149
  parseIssueArg,
138
150
  createPR,
139
151
  getSolutionFiles,
140
- gitAddSolutionFiles,
152
+ gitAddSolutionFiles
141
153
  };
142
154
  }
143
155
 
@@ -8,9 +8,9 @@ const { createAzureDevOpsProvider } = require('./azuredevops');
8
8
  * Returns 'github', 'azuredevops', or null if undetectable.
9
9
  */
10
10
  function detectProviderFromArg(issueArg) {
11
- if (!issueArg || typeof issueArg !== 'string') return null;
12
- if (issueArg.includes('github.com')) return 'github';
13
- if (issueArg.includes('dev.azure.com')) return 'azuredevops';
11
+ if (!issueArg || typeof issueArg !== 'string') {return null;}
12
+ if (issueArg.includes('github.com')) {return 'github';}
13
+ if (issueArg.includes('dev.azure.com')) {return 'azuredevops';}
14
14
  return null;
15
15
  }
16
16
 
@@ -35,7 +35,7 @@ function createProvider(config, issueArg) {
35
35
  }
36
36
  return createAzureDevOpsProvider({
37
37
  ...config.azuredevops,
38
- codingTool: config.ai.codingTool,
38
+ codingTool: config.ai.codingTool
39
39
  });
40
40
  }
41
41
 
@@ -44,7 +44,7 @@ function createProvider(config, issueArg) {
44
44
  owner: config.github.owner,
45
45
  repo: config.github.repo,
46
46
  token: config.github.token,
47
- codingTool: config.ai.codingTool,
47
+ codingTool: config.ai.codingTool
48
48
  });
49
49
  }
50
50
 
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
- const { execFileSync, execSync } = require('child_process');
4
+ const { execFileSync } = require('child_process');
5
5
  const path = require('path');
6
6
  const fs = require('fs');
7
7
 
@@ -25,7 +25,7 @@ function run(cmd, args, opts) {
25
25
  return execFileSync(cmd, args, { stdio: 'inherit', ...opts });
26
26
  } catch (e) {
27
27
  console.error(` Command failed: ${cmd} ${args.join(' ')}`);
28
- if (e.message) console.error(` ${e.message}`);
28
+ if (e.message) {console.error(` ${e.message}`);}
29
29
  return null;
30
30
  }
31
31
  }
@@ -35,7 +35,7 @@ function findPython() {
35
35
  try {
36
36
  const version = execFileSync(candidate, ['--version'], {
37
37
  encoding: 'utf8',
38
- stdio: ['ignore', 'pipe', 'pipe'],
38
+ stdio: ['ignore', 'pipe', 'pipe']
39
39
  }).trim(); // e.g. "Python 3.11.4"
40
40
 
41
41
  const match = version.match(/Python (\d+)\.(\d+)/);
package/tools/coder.py CHANGED
@@ -9,7 +9,6 @@ Usage:
9
9
  Environment variables:
10
10
  CHORUS_API_KEY — Required. Your Chorus API key.
11
11
  CHORUS_API_URL — Optional. Chorus proxy base URL (default: https://chorus-bad0f.web.app/v1)
12
- CODER_MODEL — Model to use (default: anthropic/claude-sonnet-4)
13
12
  CODER_MAX_TOKENS — Max response tokens (default: 16384)
14
13
  CODER_SAFE_MODE — Set to 1 to require approval for writes/edits/bash
15
14
  """
@@ -41,7 +40,9 @@ class C:
41
40
 
42
41
  # ── Config ──────────────────────────────────────────────────────────────────
43
42
 
44
- MODEL = os.environ.get("CODER_MODEL", "anthropic/claude-sonnet-4")
43
+ # Model is resolved server-side by the proxy based on chorus_mode.
44
+ # This placeholder is sent in the request but overridden by the proxy.
45
+ MODEL = "chorus-default"
45
46
  MAX_TOKENS = int(os.environ.get("CODER_MAX_TOKENS", "16384"))
46
47
  SAFE_MODE = os.environ.get("CODER_SAFE_MODE", "").lower() in ("1", "true", "yes")
47
48
 
@@ -50,183 +51,8 @@ def is_token_limit_error(err):
50
51
  return "token limit exceeded" in msg or "rate_limit_error" in msg
51
52
 
52
53
 
53
- # ── Shared formatting rules (included in all prompts) ─────────────────────
54
- _FORMAT_RULES = """\
55
- OUTPUT FORMAT
56
-
57
- Your output is displayed raw in a terminal. Never use markdown.
58
- No headings with #, no **bold**, no *italic*, no `backticks`, no [links](url),
59
- no bullet characters like - or *.
60
- Use blank lines and indentation for structure. Use CAPS for section labels.
61
- Use plain numbered lists (1. 2. 3.) when listing things.
62
- Refer to code identifiers by name directly (e.g. myFunction not `myFunction`).
63
- No greetings, preambles, encouragement, or sign-offs.
64
- No "Great question!", "Let me", "Sure!", "I'll now", or similar filler.
65
- State what you are doing, then do it. After completing work, state what changed."""
66
-
67
- # ── Phase 1: Planning prompt (used once at the start of headless mode) ────
68
- PLAN_PROMPT = """\
69
- You are a coding agent. You receive a GitHub/Azure DevOps issue, a codebase map, \
70
- and optionally a QA conversation with clarified requirements. Your job is to \
71
- plan the implementation.
72
-
73
- Working directory: {cwd}
74
-
75
- """ + _FORMAT_RULES + """
76
-
77
-
78
- TASK
79
-
80
- Read the issue, QA conversation (if provided), and codebase map. Then produce a
81
- written plan with exactly these sections:
82
-
83
- UNDERSTANDING:
84
- What the issue is asking for in one sentence.
85
-
86
- QUESTIONS STILL OPEN:
87
- Anything unresolved from QA. If --skip-qa was used, list assumptions you are making
88
- and flag them clearly. Prefer the conservative/conventional choice for each.
89
-
90
- FILES TO READ:
91
- The minimum set of existing files you need to examine to understand the patterns,
92
- interfaces, and conventions. Maximum 8 files. Justify each.
93
-
94
- FILES TO CREATE:
95
- New files with their paths and a one-line description of contents.
96
-
97
- FILES TO MODIFY:
98
- Existing files with a one-line description of what changes.
99
-
100
- APPROACH:
101
- How you will implement this in 2-4 sentences. Reference specific patterns
102
- from the codebase map.
103
-
104
- ON AMBIGUITY
105
-
106
- When requirements are unclear and no QA conversation was provided:
107
- 1. State your assumption explicitly.
108
- 2. Choose the most conventional/standard approach.
109
- 3. Flag it so the reviewer can catch disagreements early.
110
- Do not guess ambitiously. Guess conservatively. A correct simple implementation beats
111
- a broken ambitious one.
112
- """
113
-
114
- # ── Phase 2: Execution prompt (used on every turn of the tool loop) ───────
115
- EXEC_PROMPT = """\
116
- You are a coding agent executing a plan. Implement the changes described in the
117
- plan below. Do not re-plan or re-analyze. Execute.
118
-
119
- Working directory: {cwd}
120
-
121
- """ + _FORMAT_RULES + """
122
-
123
-
124
- TOOL USAGE
125
-
126
- Always use read_file before editing a file.
127
- If edit_file fails with "old_string not found", re-read the file with read_file to
128
- get the actual current content before retrying. Never guess at file contents.
129
- Use edit_file for targeted changes. Use write_file for new files or complete rewrites.
130
- Prefer editing existing files over creating new ones.
131
- Use read_file, list_files, and search_files instead of bash with cat, find, or grep.
132
-
133
- EXECUTION RULES
134
-
135
- Reading files:
136
- Only use read_file on files listed in the plan. If you discover you need another
137
- file, note why -- but if you have read more than 12 files total, stop and
138
- re-evaluate. You are exploring, not implementing.
139
- The one exception to reading a file again: if edit_file fails because old_string
140
- was not found, re-read that file to get its current content before retrying.
141
-
142
- Writing code:
143
- Follow existing patterns exactly. Match the conventions you observed for: naming,
144
- exports, file structure, import ordering, indentation, and comment style.
145
- Write complete files. Do not leave TODOs, placeholders, or "implement this" comments.
146
- If the issue asks for a tool/script, it must work non-interactively (accept arguments
147
- or config, not interactive prompts) unless the issue explicitly requires interactivity.
148
-
149
- Shell commands (bash tool):
150
- You may run bash for: installing dependencies, running the project's existing
151
- linter, running the project's existing tests, checking TypeScript compilation.
152
- You may not run bash for: exploratory searching beyond what was in the plan,
153
- reading files you did not plan to read, testing scripts with improvised piped input.
154
- Maximum 10 bash commands total. If you are approaching this limit, you are doing
155
- too much exploration or too much debugging. Ship what you have.
156
-
157
- BUDGET DISCIPLINE
158
-
159
- Front-load the high-value work. Write the actual implementation code early. File
160
- exploration is not progress -- committed code is progress.
161
-
162
- Do not retry the same failing approach. If something fails twice, choose a different
163
- approach or simplify. Do not iterate more than twice on the same problem.
164
-
165
- If something fundamental is broken, stop. Produce what you have, note what is
166
- incomplete, and let the human finish. A partial, clean implementation is more
167
- valuable than a complete, broken one.
168
-
169
- No yak-shaving. Deliver the core ask. Do not create demo scripts, markdown docs,
170
- bash wrappers, or sample outputs unless the issue asks for them.
171
-
172
- WHAT NOT TO DO
173
-
174
- Do not explore the filesystem to "understand the project." The codebase map and your
175
- plan already cover that. Read specific files for specific reasons.
176
-
177
- Do not overuse list_files, search_files, or bash for exploration. If you need more
178
- than 3 exploratory calls, your plan was insufficient.
179
-
180
- Do not create interactive scripts that require stdin.
181
- Do not create documentation or READMEs unless the issue asks for them.
182
- Do not modify package.json, CI configs, or project infrastructure unless the issue
183
- specifically requires it.
184
- Do not keep going when stuck. After 2 failed attempts at the same problem, note the
185
- issue, deliver what works, and move on.
186
-
187
- WHEN YOU ARE DONE
188
-
189
- End with a verify block:
190
-
191
- 1. List every file you created or modified.
192
- 2. For each, confirm it is syntactically valid (ran linter or compiler if available).
193
- 3. If tests exist for the area you changed, run them and confirm they pass.
194
- 4. If you wrote a script/CLI tool, show the --help output or a dry-run invocation.
195
- 5. Write a summary in this exact format:
196
-
197
- PR_TITLE: conventional commit format (feat:, fix:, chore:, etc.)
198
- PR_BODY: what changed, why, any assumptions made, anything the reviewer should
199
- look at carefully.
200
-
201
- Do not output anything after the summary.
202
- """
203
-
204
- # ── Interactive REPL prompt (conversational, no phased workflow) ──────────
205
- REPL_PROMPT = """\
206
- You are a coding agent running in an interactive terminal session.
207
- Working directory: {cwd}
208
-
209
- """ + _FORMAT_RULES + """
210
-
211
-
212
- TOOL USAGE
213
-
214
- Always use read_file before editing a file.
215
- If edit_file fails with "old_string not found", re-read the file with read_file to
216
- get the actual current content before retrying. Never guess at file contents.
217
- Use edit_file for targeted changes. Use write_file for new files or complete rewrites.
218
- Prefer editing existing files over creating new ones.
219
- Use read_file, list_files, and search_files instead of bash with cat, find, or grep.
220
-
221
- GUIDELINES
222
-
223
- Be direct and concise. State what you will do, then do it.
224
- Always use your tools. Never guess when you can look.
225
- Do not write new unit tests unless the project already has substantive test coverage.
226
- Do not attempt to build or compile the project unless asked.
227
- Do not add unnecessary comments, docstrings, or type annotations.
228
- For bash commands, prefer non-interactive commands.
229
- """
54
+ # System prompts are served by the Chorus proxy (chorus_mode field).
55
+ # The client sends mode + context; the server injects the full prompt.
230
56
 
231
57
  # ── Tool Definitions ────────────────────────────────────────────────────────
232
58
 
@@ -754,21 +580,22 @@ def prune_context(messages, token_budget=None):
754
580
 
755
581
  # ── Streaming Response Handler ──────────────────────────────────────────────
756
582
 
757
- def stream_response(client, messages, system):
583
+ def stream_response(client, messages, chorus_mode, chorus_context):
758
584
  """Stream LLM response via Chorus proxy, handling tool-use loops until done."""
759
- openai_messages = [{"role": "system", "content": system}]
760
- # Convert existing messages to OpenAI format
761
- for msg in messages:
762
- openai_messages.append(msg)
763
-
585
+ openai_messages = list(messages)
586
+
764
587
  while True:
765
588
  printed_text = False
766
-
589
+
767
590
  response = client.chat.completions.create(
768
591
  model=MODEL,
769
592
  max_tokens=MAX_TOKENS,
770
593
  messages=openai_messages,
771
594
  tools=TOOLS,
595
+ extra_body={
596
+ "chorus_mode": chorus_mode,
597
+ "chorus_context": chorus_context,
598
+ },
772
599
  )
773
600
 
774
601
  message = response.choices[0].message
@@ -824,7 +651,7 @@ def stream_response(client, messages, system):
824
651
 
825
652
  # ── Headless Prompt Mode ────────────────────────────────────────────────────
826
653
 
827
- def run_prompt(client, prompt, plan_system, exec_system):
654
+ def run_prompt(client, prompt, chorus_context):
828
655
  """Run a single prompt non-interactively. Returns a JSON-serializable dict."""
829
656
 
830
657
  # PHASE 1: Planning - separate API call with planning prompt
@@ -833,7 +660,6 @@ def run_prompt(client, prompt, plan_system, exec_system):
833
660
 
834
661
  plan_text = ""
835
662
  plan_messages = [
836
- {"role": "system", "content": plan_system},
837
663
  {"role": "user", "content": f"{prompt}\n\nProduce the plan using the exact sections: UNDERSTANDING, QUESTIONS STILL OPEN, FILES TO READ, FILES TO CREATE, FILES TO MODIFY, APPROACH. Do NOT write any code yet."}
838
664
  ]
839
665
 
@@ -842,6 +668,10 @@ def run_prompt(client, prompt, plan_system, exec_system):
842
668
  model=MODEL,
843
669
  max_tokens=MAX_TOKENS,
844
670
  messages=plan_messages,
671
+ extra_body={
672
+ "chorus_mode": "plan",
673
+ "chorus_context": chorus_context,
674
+ },
845
675
  )
846
676
  plan_text = plan_response.choices[0].message.content.strip()
847
677
 
@@ -862,7 +692,6 @@ def run_prompt(client, prompt, plan_system, exec_system):
862
692
  exec_user_content = f"{prompt}\n\nHere is your plan. Follow it.\n\n{plan_text}"
863
693
 
864
694
  messages = [
865
- {"role": "system", "content": exec_system},
866
695
  {"role": "user", "content": exec_user_content}
867
696
  ]
868
697
  files_modified = set()
@@ -886,6 +715,10 @@ def run_prompt(client, prompt, plan_system, exec_system):
886
715
  max_tokens=MAX_TOKENS,
887
716
  messages=messages,
888
717
  tools=TOOLS,
718
+ extra_body={
719
+ "chorus_mode": "exec",
720
+ "chorus_context": chorus_context,
721
+ },
889
722
  )
890
723
  except Exception as e:
891
724
  if is_token_limit_error(e):
@@ -1073,12 +906,15 @@ def main():
1073
906
  except OSError:
1074
907
  pass
1075
908
 
909
+ # Build context for server-side prompt injection
910
+ chorus_context = {"cwd": cwd}
911
+ if map_suffix:
912
+ chorus_context["map"] = map_suffix.strip()
913
+
1076
914
  # ── Headless prompt mode ────────────────────────────────────────────
1077
915
  if args.prompt:
1078
- plan_system = PLAN_PROMPT.format(cwd=cwd) + map_suffix
1079
- exec_system = EXEC_PROMPT.format(cwd=cwd) + map_suffix
1080
916
  try:
1081
- result = run_prompt(client, args.prompt, plan_system, exec_system)
917
+ result = run_prompt(client, args.prompt, chorus_context)
1082
918
  print(json.dumps(result, indent=2))
1083
919
  sys.exit(0 if result["completed"] else 1)
1084
920
  except Exception as e:
@@ -1103,7 +939,6 @@ def main():
1103
939
  sys.exit(130)
1104
940
 
1105
941
  # ── Interactive REPL mode ───────────────────────────────────────────
1106
- system = REPL_PROMPT.format(cwd=cwd) + map_suffix
1107
942
  messages = []
1108
943
 
1109
944
  mode_label = f" {C.YELLOW}(safe mode){C.RESET}" if SAFE_MODE else ""
@@ -1175,7 +1010,7 @@ def main():
1175
1010
 
1176
1011
  try:
1177
1012
  print()
1178
- stream_response(client, messages, system)
1013
+ stream_response(client, messages, "repl", chorus_context)
1179
1014
  print()
1180
1015
  except KeyboardInterrupt:
1181
1016
  del messages[snapshot:]
package/tools/qa.py CHANGED
@@ -27,7 +27,9 @@ from abc import ABC, abstractmethod
27
27
 
28
28
  # ── Config ──────────────────────────────────────────────────────────────────
29
29
 
30
- MODEL = os.environ.get("QA_MODEL", "anthropic/claude-sonnet-4")
30
+ # Model is resolved server-side by the proxy based on chorus_mode.
31
+ MODEL = "chorus-default"
32
+ QA_CHORUS_MODE = "qa"
31
33
  MAX_ROUNDS = int(os.environ.get("QA_MAX_ROUNDS", "5"))
32
34
  POLL_INTERVAL = int(os.environ.get("QA_POLL_INTERVAL", "60")) # seconds
33
35
  POLL_TIMEOUT = int(os.environ.get("QA_POLL_TIMEOUT", "1800")) # 30 min
@@ -339,6 +341,7 @@ If NO: set sufficient=false and write a short, friendly follow-up message asking
339
341
  max_tokens=1024,
340
342
  messages=messages,
341
343
  response_format={"type": "json_object"},
344
+ extra_body={"chorus_mode": QA_CHORUS_MODE},
342
345
  )
343
346
 
344
347
  if hasattr(response, "usage") and response.usage:
@@ -391,6 +394,7 @@ Write a clear numbered list of requirements. Each requirement should be specific
391
394
  model=MODEL,
392
395
  max_tokens=2048,
393
396
  messages=messages,
397
+ extra_body={"chorus_mode": QA_CHORUS_MODE},
394
398
  )
395
399
 
396
400
  if hasattr(response, "usage") and response.usage:
@@ -481,10 +485,13 @@ def main():
481
485
  parser.add_argument("--super", action="store_true", help="Use Opus 4.6 instead of Sonnet")
482
486
  args = parser.parse_args()
483
487
 
488
+ # chorus_mode tells the proxy which model to use
489
+ global QA_CHORUS_MODE
484
490
  if args.super:
485
- global MODEL
486
- MODEL = "anthropic/claude-opus-4"
487
- log(f"Super mode: using {MODEL}")
491
+ QA_CHORUS_MODE = "qa_super"
492
+ log("Super mode: proxy will use upgraded model")
493
+ else:
494
+ QA_CHORUS_MODE = "qa"
488
495
 
489
496
  if not os.environ.get("CHORUS_API_KEY"):
490
497
  log("Error: CHORUS_API_KEY not set. Run 'chorus setup' to configure.")