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 +144 -47
- package/package.json +7 -2
- package/providers/azuredevops.js +10 -10
- package/providers/github.js +17 -5
- package/providers/index.js +5 -5
- package/scripts/postinstall.js +3 -3
- package/tools/coder.py +29 -194
- package/tools/qa.py +11 -4
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
|
|
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',
|
|
189
|
-
codingTool: 'coder',
|
|
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: '
|
|
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`);
|
|
400
|
-
await execPromise(`git stash`);
|
|
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`);
|
|
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
|
-
|
|
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'
|
|
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/
|
|
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.
|
|
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
|
}
|
package/providers/azuredevops.js
CHANGED
|
@@ -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
|
|
package/providers/github.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
package/providers/index.js
CHANGED
|
@@ -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
|
|
package/scripts/postinstall.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
const { execFileSync
|
|
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
|
-
|
|
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
|
-
#
|
|
54
|
-
|
|
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,
|
|
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 =
|
|
760
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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.")
|