chorus-cli 0.4.6 → 0.4.8
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 +149 -55
- 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/__pycache__/coder.cpython-314.pyc +0 -0
- package/tools/__pycache__/mapper.cpython-314.pyc +0 -0
- package/tools/coder.py +28 -196
- package/tools/mapper.py +175 -8
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,14 +48,13 @@ 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) {
|
|
56
55
|
rawId = randomUUID();
|
|
57
|
-
|
|
58
|
-
await fs.
|
|
59
|
-
await fs.writeFile(path.join(configDir2, 'machine-id'), rawId + '\n');
|
|
56
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
57
|
+
await fs.writeFile(idPath, rawId + '\n');
|
|
60
58
|
}
|
|
61
59
|
}
|
|
62
60
|
|
|
@@ -78,7 +76,7 @@ function runCoder(prompt) {
|
|
|
78
76
|
cwd: process.cwd(),
|
|
79
77
|
env,
|
|
80
78
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
81
|
-
timeout: 600000
|
|
79
|
+
timeout: 600000
|
|
82
80
|
});
|
|
83
81
|
|
|
84
82
|
let stdout = '';
|
|
@@ -111,14 +109,14 @@ function runQAChat(issue, enrichedDetails, qaName, useSuper = false) {
|
|
|
111
109
|
issue_number: issue.number,
|
|
112
110
|
issue_title: issue.title,
|
|
113
111
|
issue_body: issue.body || '',
|
|
114
|
-
enriched_questions: enrichedDetails
|
|
112
|
+
enriched_questions: enrichedDetails
|
|
115
113
|
});
|
|
116
114
|
|
|
117
|
-
const args = ['--messenger', CONFIG.messenger, '--qa', qaName];
|
|
115
|
+
const args = ['--messenger', CONFIG.messenger, '--qa', qaName?.login];
|
|
118
116
|
if (CONFIG.messenger === 'teams') {
|
|
119
117
|
args.push('--auth', CONFIG.teams.authPath);
|
|
120
118
|
}
|
|
121
|
-
if (useSuper) args.push('--super');
|
|
119
|
+
if (useSuper) {args.push('--super');}
|
|
122
120
|
|
|
123
121
|
const env = { ...process.env };
|
|
124
122
|
if (CONFIG.ai.chorusApiKey) {
|
|
@@ -135,7 +133,7 @@ function runQAChat(issue, enrichedDetails, qaName, useSuper = false) {
|
|
|
135
133
|
cwd: process.cwd(),
|
|
136
134
|
env,
|
|
137
135
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
138
|
-
timeout: 1800000
|
|
136
|
+
timeout: 1800000 // 30 min
|
|
139
137
|
});
|
|
140
138
|
|
|
141
139
|
let stdout = '';
|
|
@@ -169,13 +167,13 @@ const CONFIG = {
|
|
|
169
167
|
github: {
|
|
170
168
|
owner: process.env.GITHUB_OWNER,
|
|
171
169
|
repo: process.env.GITHUB_REPO,
|
|
172
|
-
token: process.env.GITHUB_TOKEN
|
|
170
|
+
token: process.env.GITHUB_TOKEN
|
|
173
171
|
},
|
|
174
172
|
azuredevops: {
|
|
175
173
|
org: process.env.AZDO_ORG,
|
|
176
174
|
project: process.env.AZDO_PROJECT,
|
|
177
175
|
repo: process.env.AZDO_REPO,
|
|
178
|
-
pat: process.env.AZDO_PAT
|
|
176
|
+
pat: process.env.AZDO_PAT
|
|
179
177
|
},
|
|
180
178
|
messenger: process.env.MESSENGER || 'teams',
|
|
181
179
|
teams: {
|
|
@@ -183,11 +181,11 @@ const CONFIG = {
|
|
|
183
181
|
pollInterval: 60000 // Check for response every 60 seconds
|
|
184
182
|
},
|
|
185
183
|
slack: {
|
|
186
|
-
botToken: process.env.SLACK_BOT_TOKEN || ''
|
|
184
|
+
botToken: process.env.SLACK_BOT_TOKEN || ''
|
|
187
185
|
},
|
|
188
186
|
ai: {
|
|
189
|
-
enrichmentTool: 'claude',
|
|
190
|
-
codingTool: 'coder',
|
|
187
|
+
enrichmentTool: 'claude', // 'claude' or 'kimi'
|
|
188
|
+
codingTool: 'coder', // 'coder', 'claude', or 'kimi'
|
|
191
189
|
coderPath: path.join(__dirname, 'tools', 'coder.py'),
|
|
192
190
|
qaPath: path.join(__dirname, 'tools', 'qa.py'),
|
|
193
191
|
mapperPath: path.join(__dirname, 'tools', 'mapper.py'),
|
|
@@ -195,7 +193,7 @@ const CONFIG = {
|
|
|
195
193
|
? path.join(os.homedir(), '.config', 'chorus', '.venv', 'Scripts', 'python.exe')
|
|
196
194
|
: path.join(os.homedir(), '.config', 'chorus', '.venv', 'bin', 'python'),
|
|
197
195
|
chorusApiKey: process.env.CHORUS_API_KEY,
|
|
198
|
-
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'
|
|
199
197
|
}
|
|
200
198
|
};
|
|
201
199
|
|
|
@@ -237,9 +235,9 @@ Thanks!
|
|
|
237
235
|
IMPORTANT: Output ONLY the message above. Do not include any preamble, thinking process, or explanations. Start directly with "Hi! I need clarification..."`;
|
|
238
236
|
|
|
239
237
|
console.log('🤖 Enriching issue with AI...');
|
|
240
|
-
|
|
238
|
+
|
|
241
239
|
const tool = CONFIG.ai.enrichmentTool;
|
|
242
|
-
|
|
240
|
+
|
|
243
241
|
if (tool === 'claude') {
|
|
244
242
|
// Use Chorus proxy API
|
|
245
243
|
if (!CONFIG.ai.chorusApiKey) {
|
|
@@ -247,13 +245,13 @@ IMPORTANT: Output ONLY the message above. Do not include any preamble, thinking
|
|
|
247
245
|
}
|
|
248
246
|
const openaiOpts = {
|
|
249
247
|
apiKey: CONFIG.ai.chorusApiKey,
|
|
250
|
-
baseURL: CONFIG.ai.chorusApiUrl
|
|
248
|
+
baseURL: CONFIG.ai.chorusApiUrl
|
|
251
249
|
};
|
|
252
250
|
if (CONFIG.ai.machineId) {
|
|
253
251
|
openaiOpts.defaultHeaders = { 'X-Machine-Id': CONFIG.ai.machineId };
|
|
254
252
|
}
|
|
255
253
|
const openai = new OpenAI(openaiOpts);
|
|
256
|
-
|
|
254
|
+
|
|
257
255
|
const response = await openai.chat.completions.create({
|
|
258
256
|
model: 'anthropic/claude-opus-4',
|
|
259
257
|
max_tokens: 2000,
|
|
@@ -270,12 +268,12 @@ IMPORTANT: Output ONLY the message above. Do not include any preamble, thinking
|
|
|
270
268
|
}
|
|
271
269
|
|
|
272
270
|
return response.choices[0].message.content.trim();
|
|
273
|
-
} else {
|
|
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
271
|
}
|
|
272
|
+
// Use Kimi CLI
|
|
273
|
+
const escapedPrompt = prompt.replace(/"/g, '\\"').replace(/\$/g, '\\$');
|
|
274
|
+
const { stdout } = await execPromise(`kimi --prompt "${escapedPrompt}"`);
|
|
275
|
+
return stdout.trim();
|
|
276
|
+
|
|
279
277
|
}
|
|
280
278
|
|
|
281
279
|
// ===== CODE GENERATION =====
|
|
@@ -397,10 +395,10 @@ async function getCodeRabbitReview(solution, issue, provider) {
|
|
|
397
395
|
);
|
|
398
396
|
|
|
399
397
|
// 5. Clean up — undo the commit so changes stay in working tree, then go back to main
|
|
400
|
-
await execPromise(`git reset --mixed HEAD~1`);
|
|
401
|
-
await execPromise(`git stash`);
|
|
398
|
+
await execPromise(`git reset --mixed HEAD~1`); // undo commit, keep changes in working tree
|
|
399
|
+
await execPromise(`git stash`); // stash working tree changes
|
|
402
400
|
await execPromise(`git checkout main`);
|
|
403
|
-
await execPromise(`git stash pop`);
|
|
401
|
+
await execPromise(`git stash pop`); // restore coder's changes on main
|
|
404
402
|
await execPromise(`git branch -D ${branchName}`).catch(() => {});
|
|
405
403
|
|
|
406
404
|
return {
|
|
@@ -490,7 +488,7 @@ async function lintAndFix(solution, provider) {
|
|
|
490
488
|
console.log(' Auto-fix applied');
|
|
491
489
|
} catch (e) {
|
|
492
490
|
// eslint exits non-zero when there are unfixable errors — that's expected
|
|
493
|
-
if (e.stdout) console.log(e.stdout);
|
|
491
|
+
if (e.stdout) {console.log(e.stdout);}
|
|
494
492
|
}
|
|
495
493
|
|
|
496
494
|
// Second pass: check for remaining errors
|
|
@@ -526,7 +524,7 @@ ${lintOutput.slice(0, 5000)}`;
|
|
|
526
524
|
|
|
527
525
|
// Update solution with any new files coder touched
|
|
528
526
|
for (const f of (fixResult.files_modified || [])) {
|
|
529
|
-
if (!solution.files_modified.includes(f)) solution.files_modified.push(f);
|
|
527
|
+
if (!solution.files_modified.includes(f)) {solution.files_modified.push(f);}
|
|
530
528
|
}
|
|
531
529
|
|
|
532
530
|
console.log(' Lint fix pass complete\n');
|
|
@@ -581,17 +579,17 @@ async function processTicket(issueArg, { useSuper = false, skipQA = false, qaNam
|
|
|
581
579
|
efs(CONFIG.ai.venvPython, ['-m', 'pip', 'install', '-r', reqFile], { stdio: 'inherit' });
|
|
582
580
|
}
|
|
583
581
|
|
|
584
|
-
// 0a. Verify no modified tracked files (untracked files like .
|
|
582
|
+
// 0a. Verify no modified tracked files (untracked files like .chorus/ are fine)
|
|
585
583
|
const { stdout: gitStatus } = await execPromise('git status --porcelain --untracked-files=no');
|
|
586
584
|
if (gitStatus.trim()) {
|
|
587
585
|
console.error('⚠️ Working directory has uncommitted changes. Commit or stash first:');
|
|
588
586
|
console.error(gitStatus);
|
|
589
|
-
//throw new Error('Dirty working directory');
|
|
587
|
+
// throw new Error('Dirty working directory');
|
|
590
588
|
}
|
|
591
589
|
const { stdout: currentBranch } = await execPromise('git rev-parse --abbrev-ref HEAD');
|
|
592
590
|
if (currentBranch.trim() !== 'main') {
|
|
593
591
|
console.warn(`⚠️ Not on main branch (currently on ${currentBranch.trim()}). Switch to main first.`);
|
|
594
|
-
//throw new Error('Not on main branch');
|
|
592
|
+
// throw new Error('Not on main branch');
|
|
595
593
|
}
|
|
596
594
|
|
|
597
595
|
// 0b. Map the codebase so coder starts with full context
|
|
@@ -630,6 +628,7 @@ async function processTicket(issueArg, { useSuper = false, skipQA = false, qaNam
|
|
|
630
628
|
console.log('⏭️ Skipping QA conversation (--skip-qa)\n');
|
|
631
629
|
} else {
|
|
632
630
|
const qaName = qaNameOverride || await provider.getUserDisplayName(issue.user.login);
|
|
631
|
+
console.log(JSON.stringify(qaName));
|
|
633
632
|
console.log(`💬 Starting QA conversation with ${qaName?.login}...`);
|
|
634
633
|
const qaResult = await runQAChat(issue, enrichedDetails, qaName, useSuper);
|
|
635
634
|
qaResponse = qaResult.requirements;
|
|
@@ -643,8 +642,17 @@ async function processTicket(issueArg, { useSuper = false, skipQA = false, qaNam
|
|
|
643
642
|
}
|
|
644
643
|
console.log(`QA complete (${qaResult.conversation_rounds} rounds)\n`);
|
|
645
644
|
}
|
|
646
|
-
|
|
645
|
+
|
|
647
646
|
// 4. Generate code (with retry if coder doesn't actually write files)
|
|
647
|
+
// Snapshot current working-tree state so we can detect what the coder *actually* changed
|
|
648
|
+
let baselineDirty = new Set();
|
|
649
|
+
try {
|
|
650
|
+
const { stdout: bDiff } = await execPromise('git diff --name-only');
|
|
651
|
+
const { stdout: bNew } = await execPromise('git ls-files --others --exclude-standard');
|
|
652
|
+
bDiff.trim().split('\n').filter(Boolean).forEach(f => baselineDirty.add(f));
|
|
653
|
+
bNew.trim().split('\n').filter(Boolean).forEach(f => baselineDirty.add(f));
|
|
654
|
+
} catch { /* non-git or error — baseline stays empty */ }
|
|
655
|
+
|
|
648
656
|
const maxCodeAttempts = 3;
|
|
649
657
|
let solution;
|
|
650
658
|
|
|
@@ -684,7 +692,27 @@ CRITICAL: You MUST write code to actual files. Do not just describe changes —
|
|
|
684
692
|
throw new Error('Code generation failed');
|
|
685
693
|
}
|
|
686
694
|
|
|
687
|
-
|
|
695
|
+
let filesChanged = (solution.files_modified || []).length + (solution.files_created || []).length;
|
|
696
|
+
|
|
697
|
+
// If coder reports 0 files, check git for actual working-tree changes
|
|
698
|
+
// the coder may have written files without reporting them
|
|
699
|
+
if (filesChanged === 0) {
|
|
700
|
+
try {
|
|
701
|
+
const { stdout: gitDiff } = await execPromise('git diff --name-only');
|
|
702
|
+
const { stdout: gitUntracked } = await execPromise('git ls-files --others --exclude-standard');
|
|
703
|
+
const diffFiles = gitDiff.trim().split('\n').filter(Boolean).filter(f => !baselineDirty.has(f));
|
|
704
|
+
const untrackedFiles = gitUntracked.trim().split('\n').filter(Boolean).filter(f => !baselineDirty.has(f));
|
|
705
|
+
const gitChangedFiles = [...diffFiles, ...untrackedFiles];
|
|
706
|
+
|
|
707
|
+
if (gitChangedFiles.length > 0) {
|
|
708
|
+
console.log(` Coder didn't report files, but git detected ${gitChangedFiles.length} changed file(s)`);
|
|
709
|
+
// Backfill the solution so downstream steps (lint, review, PR) work
|
|
710
|
+
solution.files_modified = [...(solution.files_modified || []), ...diffFiles];
|
|
711
|
+
solution.files_created = [...(solution.files_created || []), ...untrackedFiles];
|
|
712
|
+
filesChanged = gitChangedFiles.length;
|
|
713
|
+
}
|
|
714
|
+
} catch { /* git check failed, fall through to existing logic */ }
|
|
715
|
+
}
|
|
688
716
|
|
|
689
717
|
if (filesChanged > 0) {
|
|
690
718
|
break; // Success — coder actually wrote files
|
|
@@ -699,7 +727,7 @@ CRITICAL: You MUST write code to actual files. Do not just describe changes —
|
|
|
699
727
|
}
|
|
700
728
|
|
|
701
729
|
console.log('Code generation complete\n');
|
|
702
|
-
if (solution.summary) console.log(`Summary: ${solution.summary}\n`);
|
|
730
|
+
if (solution.summary) {console.log(`Summary: ${solution.summary}\n`);}
|
|
703
731
|
|
|
704
732
|
// 5. Lint files the coder touched
|
|
705
733
|
await lintAndFix(solution, provider);
|
|
@@ -755,11 +783,11 @@ CRITICAL: You MUST write code to actual files. Do not just describe changes —
|
|
|
755
783
|
while (userIteration < maxUserIterations) {
|
|
756
784
|
const allFiles = [...new Set([
|
|
757
785
|
...(solution.files_modified || []),
|
|
758
|
-
...(solution.files_created || [])
|
|
786
|
+
...(solution.files_created || [])
|
|
759
787
|
])];
|
|
760
788
|
console.log('FILES CHANGED:');
|
|
761
789
|
allFiles.forEach(f => console.log(` ${f}`));
|
|
762
|
-
if (solution.summary) console.log(`\nSUMMARY: ${solution.summary}`);
|
|
790
|
+
if (solution.summary) {console.log(`\nSUMMARY: ${solution.summary}`);}
|
|
763
791
|
console.log();
|
|
764
792
|
|
|
765
793
|
const answer = await prompt(rl, '✅ Happy with the changes? (y/n): ');
|
|
@@ -857,7 +885,7 @@ function prompt(rl, question, hidden = false) {
|
|
|
857
885
|
} else if (ch === '\u0003') {
|
|
858
886
|
process.exit();
|
|
859
887
|
} else if (ch === '\u007F' || ch === '\b') {
|
|
860
|
-
if (input.length > 0) input = input.slice(0, -1);
|
|
888
|
+
if (input.length > 0) {input = input.slice(0, -1);}
|
|
861
889
|
} else {
|
|
862
890
|
input += ch;
|
|
863
891
|
}
|
|
@@ -889,7 +917,7 @@ async function setupGitHub() {
|
|
|
889
917
|
} catch { /* no .env yet */ }
|
|
890
918
|
|
|
891
919
|
const updates = { GITHUB_OWNER: owner, GITHUB_REPO: repo };
|
|
892
|
-
if (token) updates.GITHUB_TOKEN = token;
|
|
920
|
+
if (token) {updates.GITHUB_TOKEN = token;}
|
|
893
921
|
|
|
894
922
|
for (const [key, value] of Object.entries(updates)) {
|
|
895
923
|
const regex = new RegExp(`^${key}=.*$`, 'm');
|
|
@@ -905,9 +933,9 @@ async function setupGitHub() {
|
|
|
905
933
|
// Update in-memory config
|
|
906
934
|
CONFIG.github.owner = owner;
|
|
907
935
|
CONFIG.github.repo = repo;
|
|
908
|
-
if (token) CONFIG.github.token = token;
|
|
936
|
+
if (token) {CONFIG.github.token = token;}
|
|
909
937
|
|
|
910
|
-
console.log(`\n✅ GitHub config saved
|
|
938
|
+
console.log(`\n✅ GitHub config saved (${owner}/${repo})\n`);
|
|
911
939
|
}
|
|
912
940
|
|
|
913
941
|
async function setupProxyAuth() {
|
|
@@ -941,7 +969,7 @@ async function setupProxyAuth() {
|
|
|
941
969
|
const mod = parsed.protocol === 'https:' ? https : http;
|
|
942
970
|
const req = mod.request(parsed, { method: 'POST', headers: { 'Content-Type': 'application/json' } }, (res) => {
|
|
943
971
|
let data = '';
|
|
944
|
-
res.on('data', (chunk) => data += chunk);
|
|
972
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
945
973
|
res.on('end', () => {
|
|
946
974
|
try {
|
|
947
975
|
resolve({ status: res.statusCode, body: JSON.parse(data) });
|
|
@@ -1003,7 +1031,7 @@ async function setupProxyAuth() {
|
|
|
1003
1031
|
CONFIG.ai.chorusApiKey = apiKey;
|
|
1004
1032
|
process.env.CHORUS_API_KEY = apiKey;
|
|
1005
1033
|
|
|
1006
|
-
console.log(`\n✅ Chorus API key saved
|
|
1034
|
+
console.log(`\n✅ Chorus API key saved\n`);
|
|
1007
1035
|
}
|
|
1008
1036
|
|
|
1009
1037
|
async function setupTeamsAuth() {
|
|
@@ -1028,7 +1056,7 @@ async function setupTeamsAuth() {
|
|
|
1028
1056
|
await context.storageState({ path: CONFIG.teams.authPath });
|
|
1029
1057
|
await browser.close();
|
|
1030
1058
|
|
|
1031
|
-
console.log(`\n✅
|
|
1059
|
+
console.log(`\n✅ Teams authentication saved`);
|
|
1032
1060
|
}
|
|
1033
1061
|
|
|
1034
1062
|
async function setupSlack() {
|
|
@@ -1076,7 +1104,7 @@ async function setupSlack() {
|
|
|
1076
1104
|
CONFIG.messenger = 'slack';
|
|
1077
1105
|
CONFIG.slack.botToken = token;
|
|
1078
1106
|
|
|
1079
|
-
console.log(`\n✅ Slack config saved
|
|
1107
|
+
console.log(`\n✅ Slack config saved\n`);
|
|
1080
1108
|
}
|
|
1081
1109
|
|
|
1082
1110
|
async function setupAzureDevOps() {
|
|
@@ -1100,7 +1128,7 @@ async function setupAzureDevOps() {
|
|
|
1100
1128
|
} catch { /* no .env yet */ }
|
|
1101
1129
|
|
|
1102
1130
|
const updates = { AZDO_ORG: adoOrg, AZDO_PROJECT: adoProject, AZDO_REPO: adoRepo, PROVIDER: 'azuredevops' };
|
|
1103
|
-
if (adoPat) updates.AZDO_PAT = adoPat;
|
|
1131
|
+
if (adoPat) {updates.AZDO_PAT = adoPat;}
|
|
1104
1132
|
|
|
1105
1133
|
for (const [key, value] of Object.entries(updates)) {
|
|
1106
1134
|
const regex = new RegExp(`^${key}=.*$`, 'm');
|
|
@@ -1116,9 +1144,9 @@ async function setupAzureDevOps() {
|
|
|
1116
1144
|
CONFIG.azuredevops.org = adoOrg;
|
|
1117
1145
|
CONFIG.azuredevops.project = adoProject;
|
|
1118
1146
|
CONFIG.azuredevops.repo = adoRepo;
|
|
1119
|
-
if (adoPat) CONFIG.azuredevops.pat = adoPat;
|
|
1147
|
+
if (adoPat) {CONFIG.azuredevops.pat = adoPat;}
|
|
1120
1148
|
|
|
1121
|
-
console.log(`\n✅ Azure DevOps config saved
|
|
1149
|
+
console.log(`\n✅ Azure DevOps config saved (${adoOrg}/${adoProject}/${adoRepo})\n`);
|
|
1122
1150
|
}
|
|
1123
1151
|
|
|
1124
1152
|
async function setup() {
|
|
@@ -1155,6 +1183,70 @@ async function setup() {
|
|
|
1155
1183
|
console.log('\n✅ Setup complete. You can now run: chorus run\n');
|
|
1156
1184
|
}
|
|
1157
1185
|
|
|
1186
|
+
// === EASTER EGG ====
|
|
1187
|
+
|
|
1188
|
+
function printZEP() {
|
|
1189
|
+
const w = 'zep';
|
|
1190
|
+
const size = 9;
|
|
1191
|
+
const thick = 2;
|
|
1192
|
+
const blank = ' '.repeat(w.length);
|
|
1193
|
+
|
|
1194
|
+
// Colors
|
|
1195
|
+
const red = '\x1b[31m';
|
|
1196
|
+
const yellow = '\x1b[33m';
|
|
1197
|
+
const cyan = '\x1b[36m';
|
|
1198
|
+
const bold = '\x1b[1m';
|
|
1199
|
+
const reset = '\x1b[0m';
|
|
1200
|
+
|
|
1201
|
+
// Z
|
|
1202
|
+
const Z = [];
|
|
1203
|
+
for (let i = 0; i < size; i++) {
|
|
1204
|
+
if (i < thick || i >= size - thick) {
|
|
1205
|
+
Z.push(w.repeat(size));
|
|
1206
|
+
} else {
|
|
1207
|
+
const pos = size - 1 - i;
|
|
1208
|
+
Z.push(blank.repeat(pos) + w.repeat(thick) + blank.repeat(Math.max(0, size - pos - thick)));
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// E
|
|
1213
|
+
const E = [];
|
|
1214
|
+
for (let i = 0; i < size; i++) {
|
|
1215
|
+
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))) {
|
|
1216
|
+
E.push(w.repeat(size));
|
|
1217
|
+
} else {
|
|
1218
|
+
E.push(w.repeat(thick) + blank.repeat(size - thick));
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// P
|
|
1223
|
+
const P = [];
|
|
1224
|
+
for (let i = 0; i < size; i++) {
|
|
1225
|
+
if (i < thick || (i >= Math.floor(size / 2) - Math.floor(thick / 2) && i < Math.floor(size / 2) + thick - Math.floor(thick / 2))) {
|
|
1226
|
+
P.push(w.repeat(size));
|
|
1227
|
+
} else if (i < Math.floor(size / 2)) {
|
|
1228
|
+
P.push(w.repeat(thick) + blank.repeat(size - thick * 2) + w.repeat(thick));
|
|
1229
|
+
} else {
|
|
1230
|
+
P.push(w.repeat(thick) + blank.repeat(size - thick));
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
const gap = ' ';
|
|
1235
|
+
|
|
1236
|
+
console.log('\n\n');
|
|
1237
|
+
for (let i = 0; i < size; i++) {
|
|
1238
|
+
console.log(
|
|
1239
|
+
bold + red + Z[i] + reset +
|
|
1240
|
+
gap +
|
|
1241
|
+
bold + yellow + E[i] + reset +
|
|
1242
|
+
gap +
|
|
1243
|
+
bold + cyan + P[i] + reset
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
1246
|
+
console.log('\n\n');
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
|
|
1158
1250
|
// ===== CLI =====
|
|
1159
1251
|
const command = process.argv[2];
|
|
1160
1252
|
const _envExists = require('fs').existsSync(path.join(os.homedir(), '.config', 'chorus', '.env'));
|
|
@@ -1191,6 +1283,8 @@ if (command === 'setup') {
|
|
|
1191
1283
|
const { issueArg, ...opts } = parseRunArgs();
|
|
1192
1284
|
processTicket(issueArg, opts).catch(console.error);
|
|
1193
1285
|
}
|
|
1286
|
+
} else if (command === 'zep') {
|
|
1287
|
+
printZEP()
|
|
1194
1288
|
} else {
|
|
1195
1289
|
if (!_envExists) {
|
|
1196
1290
|
console.log(`
|
|
@@ -1211,12 +1305,12 @@ Usage:
|
|
|
1211
1305
|
chorus run 4464 - Process specific issue by number
|
|
1212
1306
|
chorus run <url> - Process issue from full URL (auto-detects provider)
|
|
1213
1307
|
chorus run 4464 --super - Use Opus 4.6 for QA evaluation
|
|
1214
|
-
chorus run 4464 --qa 'John Doe'
|
|
1308
|
+
chorus run 4464 --qa 'John Doe' - Specify QA contact name for chat
|
|
1215
1309
|
chorus run 4464 --skip-qa - Skip QA conversation, go straight to coding
|
|
1216
1310
|
|
|
1217
1311
|
GitHub examples:
|
|
1218
1312
|
chorus run 4464
|
|
1219
|
-
chorus run https://github.com/
|
|
1313
|
+
chorus run https://github.com/org/repository/issues/4464
|
|
1220
1314
|
|
|
1221
1315
|
Azure DevOps examples:
|
|
1222
1316
|
chorus run 456
|
|
@@ -1239,4 +1333,4 @@ Configuration is stored in ~/.config/chorus/.env
|
|
|
1239
1333
|
Run "chorus setup" to configure credentials and messenger auth.
|
|
1240
1334
|
`);
|
|
1241
1335
|
}
|
|
1242
|
-
}
|
|
1336
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chorus-cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.8",
|
|
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+)/);
|
|
Binary file
|
|
Binary file
|
package/tools/coder.py
CHANGED
|
@@ -50,183 +50,8 @@ def is_token_limit_error(err):
|
|
|
50
50
|
return "token limit exceeded" in msg or "rate_limit_error" in msg
|
|
51
51
|
|
|
52
52
|
|
|
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
|
-
"""
|
|
53
|
+
# System prompts are served by the Chorus proxy (chorus_mode field).
|
|
54
|
+
# The client sends mode + context; the server injects the full prompt.
|
|
230
55
|
|
|
231
56
|
# ── Tool Definitions ────────────────────────────────────────────────────────
|
|
232
57
|
|
|
@@ -754,21 +579,22 @@ def prune_context(messages, token_budget=None):
|
|
|
754
579
|
|
|
755
580
|
# ── Streaming Response Handler ──────────────────────────────────────────────
|
|
756
581
|
|
|
757
|
-
def stream_response(client, messages,
|
|
582
|
+
def stream_response(client, messages, chorus_mode, chorus_context):
|
|
758
583
|
"""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
|
-
|
|
584
|
+
openai_messages = list(messages)
|
|
585
|
+
|
|
764
586
|
while True:
|
|
765
587
|
printed_text = False
|
|
766
|
-
|
|
588
|
+
|
|
767
589
|
response = client.chat.completions.create(
|
|
768
590
|
model=MODEL,
|
|
769
591
|
max_tokens=MAX_TOKENS,
|
|
770
592
|
messages=openai_messages,
|
|
771
593
|
tools=TOOLS,
|
|
594
|
+
extra_body={
|
|
595
|
+
"chorus_mode": chorus_mode,
|
|
596
|
+
"chorus_context": chorus_context,
|
|
597
|
+
},
|
|
772
598
|
)
|
|
773
599
|
|
|
774
600
|
message = response.choices[0].message
|
|
@@ -785,7 +611,6 @@ def stream_response(client, messages, system):
|
|
|
785
611
|
tool_results = []
|
|
786
612
|
for tool_call in message.tool_calls:
|
|
787
613
|
function_name = tool_call.function.name
|
|
788
|
-
import json
|
|
789
614
|
try:
|
|
790
615
|
arguments = json.loads(tool_call.function.arguments)
|
|
791
616
|
except json.JSONDecodeError:
|
|
@@ -825,7 +650,7 @@ def stream_response(client, messages, system):
|
|
|
825
650
|
|
|
826
651
|
# ── Headless Prompt Mode ────────────────────────────────────────────────────
|
|
827
652
|
|
|
828
|
-
def run_prompt(client, prompt,
|
|
653
|
+
def run_prompt(client, prompt, chorus_context):
|
|
829
654
|
"""Run a single prompt non-interactively. Returns a JSON-serializable dict."""
|
|
830
655
|
|
|
831
656
|
# PHASE 1: Planning - separate API call with planning prompt
|
|
@@ -834,7 +659,6 @@ def run_prompt(client, prompt, plan_system, exec_system):
|
|
|
834
659
|
|
|
835
660
|
plan_text = ""
|
|
836
661
|
plan_messages = [
|
|
837
|
-
{"role": "system", "content": plan_system},
|
|
838
662
|
{"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."}
|
|
839
663
|
]
|
|
840
664
|
|
|
@@ -843,6 +667,10 @@ def run_prompt(client, prompt, plan_system, exec_system):
|
|
|
843
667
|
model=MODEL,
|
|
844
668
|
max_tokens=MAX_TOKENS,
|
|
845
669
|
messages=plan_messages,
|
|
670
|
+
extra_body={
|
|
671
|
+
"chorus_mode": "plan",
|
|
672
|
+
"chorus_context": chorus_context,
|
|
673
|
+
},
|
|
846
674
|
)
|
|
847
675
|
plan_text = plan_response.choices[0].message.content.strip()
|
|
848
676
|
|
|
@@ -863,7 +691,6 @@ def run_prompt(client, prompt, plan_system, exec_system):
|
|
|
863
691
|
exec_user_content = f"{prompt}\n\nHere is your plan. Follow it.\n\n{plan_text}"
|
|
864
692
|
|
|
865
693
|
messages = [
|
|
866
|
-
{"role": "system", "content": exec_system},
|
|
867
694
|
{"role": "user", "content": exec_user_content}
|
|
868
695
|
]
|
|
869
696
|
files_modified = set()
|
|
@@ -887,6 +714,10 @@ def run_prompt(client, prompt, plan_system, exec_system):
|
|
|
887
714
|
max_tokens=MAX_TOKENS,
|
|
888
715
|
messages=messages,
|
|
889
716
|
tools=TOOLS,
|
|
717
|
+
extra_body={
|
|
718
|
+
"chorus_mode": "exec",
|
|
719
|
+
"chorus_context": chorus_context,
|
|
720
|
+
},
|
|
890
721
|
)
|
|
891
722
|
except Exception as e:
|
|
892
723
|
if is_token_limit_error(e):
|
|
@@ -915,7 +746,6 @@ def run_prompt(client, prompt, plan_system, exec_system):
|
|
|
915
746
|
tool_results = []
|
|
916
747
|
for tool_call in message.tool_calls:
|
|
917
748
|
function_name = tool_call.function.name
|
|
918
|
-
import json
|
|
919
749
|
try:
|
|
920
750
|
arguments = json.loads(tool_call.function.arguments)
|
|
921
751
|
except json.JSONDecodeError:
|
|
@@ -1064,7 +894,7 @@ def main():
|
|
|
1064
894
|
|
|
1065
895
|
# Load codebase map if available
|
|
1066
896
|
map_suffix = ""
|
|
1067
|
-
map_file = Path.cwd() / ".
|
|
897
|
+
map_file = Path.cwd() / ".chorus" / "map.md"
|
|
1068
898
|
if map_file.exists():
|
|
1069
899
|
try:
|
|
1070
900
|
map_content = map_file.read_text(encoding="utf-8").strip()
|
|
@@ -1075,12 +905,15 @@ def main():
|
|
|
1075
905
|
except OSError:
|
|
1076
906
|
pass
|
|
1077
907
|
|
|
908
|
+
# Build context for server-side prompt injection
|
|
909
|
+
chorus_context = {"cwd": cwd}
|
|
910
|
+
if map_suffix:
|
|
911
|
+
chorus_context["map"] = map_suffix.strip()
|
|
912
|
+
|
|
1078
913
|
# ── Headless prompt mode ────────────────────────────────────────────
|
|
1079
914
|
if args.prompt:
|
|
1080
|
-
plan_system = PLAN_PROMPT.format(cwd=cwd) + map_suffix
|
|
1081
|
-
exec_system = EXEC_PROMPT.format(cwd=cwd) + map_suffix
|
|
1082
915
|
try:
|
|
1083
|
-
result = run_prompt(client, args.prompt,
|
|
916
|
+
result = run_prompt(client, args.prompt, chorus_context)
|
|
1084
917
|
print(json.dumps(result, indent=2))
|
|
1085
918
|
sys.exit(0 if result["completed"] else 1)
|
|
1086
919
|
except Exception as e:
|
|
@@ -1105,7 +938,6 @@ def main():
|
|
|
1105
938
|
sys.exit(130)
|
|
1106
939
|
|
|
1107
940
|
# ── Interactive REPL mode ───────────────────────────────────────────
|
|
1108
|
-
system = REPL_PROMPT.format(cwd=cwd) + map_suffix
|
|
1109
941
|
messages = []
|
|
1110
942
|
|
|
1111
943
|
mode_label = f" {C.YELLOW}(safe mode){C.RESET}" if SAFE_MODE else ""
|
|
@@ -1115,7 +947,7 @@ def main():
|
|
|
1115
947
|
print(f"{C.DIM}Commands: /clear /quit /help{C.RESET}")
|
|
1116
948
|
print()
|
|
1117
949
|
|
|
1118
|
-
histfile = os.path.expanduser("~/.
|
|
950
|
+
histfile = os.path.expanduser("~/.chorus_history")
|
|
1119
951
|
try:
|
|
1120
952
|
readline.read_history_file(histfile)
|
|
1121
953
|
except (FileNotFoundError, OSError, PermissionError):
|
|
@@ -1177,7 +1009,7 @@ def main():
|
|
|
1177
1009
|
|
|
1178
1010
|
try:
|
|
1179
1011
|
print()
|
|
1180
|
-
stream_response(client, messages,
|
|
1012
|
+
stream_response(client, messages, "repl", chorus_context)
|
|
1181
1013
|
print()
|
|
1182
1014
|
except KeyboardInterrupt:
|
|
1183
1015
|
del messages[snapshot:]
|
package/tools/mapper.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"""
|
|
3
3
|
mapper.py — Generate a codebase map for the coder agent.
|
|
4
4
|
|
|
5
|
-
Scans a project directory and produces a compact .
|
|
5
|
+
Scans a project directory and produces a compact .chorus/map.md that coder
|
|
6
6
|
loads into its system prompt, so it starts every session already knowing
|
|
7
7
|
the file structure, key modules, exports, dependencies, and test status.
|
|
8
8
|
|
|
@@ -275,6 +275,157 @@ def detect_project(root):
|
|
|
275
275
|
return info
|
|
276
276
|
|
|
277
277
|
|
|
278
|
+
def _parse_jsonc(text):
|
|
279
|
+
"""Parse JSON with trailing commas and // comments (tsconfig, eslint, etc.)."""
|
|
280
|
+
# Strip single-line comments (but not inside strings)
|
|
281
|
+
lines = []
|
|
282
|
+
for line in text.splitlines():
|
|
283
|
+
stripped = line.lstrip()
|
|
284
|
+
if stripped.startswith("//"):
|
|
285
|
+
continue
|
|
286
|
+
# Remove inline // comments (naive but covers common cases)
|
|
287
|
+
in_str = False
|
|
288
|
+
result = []
|
|
289
|
+
i = 0
|
|
290
|
+
while i < len(line):
|
|
291
|
+
ch = line[i]
|
|
292
|
+
if ch == '"' and (i == 0 or line[i - 1] != '\\'):
|
|
293
|
+
in_str = not in_str
|
|
294
|
+
elif ch == '/' and i + 1 < len(line) and line[i + 1] == '/' and not in_str:
|
|
295
|
+
break
|
|
296
|
+
result.append(ch)
|
|
297
|
+
i += 1
|
|
298
|
+
lines.append(''.join(result))
|
|
299
|
+
cleaned = '\n'.join(lines)
|
|
300
|
+
# Strip trailing commas before } or ]
|
|
301
|
+
cleaned = re.sub(r',\s*([}\]])', r'\1', cleaned)
|
|
302
|
+
return json.loads(cleaned)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def detect_project_rules(root):
|
|
306
|
+
"""Extract project configuration and rules that the coder should follow."""
|
|
307
|
+
rules = {}
|
|
308
|
+
|
|
309
|
+
# TypeScript config — resolve extends chain to find compilerOptions
|
|
310
|
+
ts_candidates = ["tsconfig.json", "tsconfig.app.json", "tsconfig.build.json"]
|
|
311
|
+
ts_checked = set()
|
|
312
|
+
ts_queue = [name for name in ts_candidates if (root / name).exists()]
|
|
313
|
+
ts_merged = {}
|
|
314
|
+
|
|
315
|
+
while ts_queue:
|
|
316
|
+
name = ts_queue.pop(0)
|
|
317
|
+
if name in ts_checked:
|
|
318
|
+
continue
|
|
319
|
+
ts_checked.add(name)
|
|
320
|
+
ts_path = root / name
|
|
321
|
+
if not ts_path.exists():
|
|
322
|
+
continue
|
|
323
|
+
try:
|
|
324
|
+
data = _parse_jsonc(ts_path.read_text())
|
|
325
|
+
# Follow extends
|
|
326
|
+
extends = data.get("extends")
|
|
327
|
+
if extends and isinstance(extends, str):
|
|
328
|
+
ext_name = extends.lstrip("./")
|
|
329
|
+
if ext_name not in ts_checked:
|
|
330
|
+
ts_queue.append(ext_name)
|
|
331
|
+
# Merge compilerOptions (later files override earlier)
|
|
332
|
+
compiler = data.get("compilerOptions", {})
|
|
333
|
+
for key in ("strict", "target", "module", "moduleResolution",
|
|
334
|
+
"jsx", "baseUrl", "paths", "esModuleInterop",
|
|
335
|
+
"noImplicitAny", "strictNullChecks", "skipLibCheck"):
|
|
336
|
+
if key in compiler:
|
|
337
|
+
ts_merged[key] = compiler[key]
|
|
338
|
+
if data.get("include"):
|
|
339
|
+
ts_merged["include"] = data["include"]
|
|
340
|
+
if data.get("exclude"):
|
|
341
|
+
ts_merged["exclude"] = data["exclude"]
|
|
342
|
+
except (json.JSONDecodeError, OSError, ValueError):
|
|
343
|
+
pass
|
|
344
|
+
|
|
345
|
+
if ts_merged:
|
|
346
|
+
rules["tsconfig.json"] = ts_merged
|
|
347
|
+
|
|
348
|
+
# ESLint config
|
|
349
|
+
eslint_files = [
|
|
350
|
+
".eslintrc.json", ".eslintrc.js", ".eslintrc.cjs", ".eslintrc.yml",
|
|
351
|
+
".eslintrc.yaml", ".eslintrc", "eslint.config.js", "eslint.config.mjs",
|
|
352
|
+
"eslint.config.cjs",
|
|
353
|
+
]
|
|
354
|
+
for name in eslint_files:
|
|
355
|
+
eslint_path = root / name
|
|
356
|
+
if eslint_path.exists():
|
|
357
|
+
try:
|
|
358
|
+
content = eslint_path.read_text(encoding="utf-8", errors="replace")
|
|
359
|
+
# For JSON configs, parse and extract key fields
|
|
360
|
+
if name.endswith(".json") or name == ".eslintrc":
|
|
361
|
+
try:
|
|
362
|
+
data = _parse_jsonc(content)
|
|
363
|
+
summary = {}
|
|
364
|
+
if data.get("extends"):
|
|
365
|
+
summary["extends"] = data["extends"]
|
|
366
|
+
if data.get("parser"):
|
|
367
|
+
summary["parser"] = data["parser"]
|
|
368
|
+
if data.get("rules"):
|
|
369
|
+
summary["rules"] = data["rules"]
|
|
370
|
+
rules[name] = summary
|
|
371
|
+
except json.JSONDecodeError:
|
|
372
|
+
# Treat as raw text
|
|
373
|
+
if len(content) <= 3000:
|
|
374
|
+
rules[name] = content
|
|
375
|
+
else:
|
|
376
|
+
# JS/YAML configs — include raw (truncated)
|
|
377
|
+
if len(content) <= 3000:
|
|
378
|
+
rules[name] = content
|
|
379
|
+
else:
|
|
380
|
+
rules[name] = content[:3000] + "\n... (truncated)"
|
|
381
|
+
except OSError:
|
|
382
|
+
pass
|
|
383
|
+
break # Only include the first eslint config found
|
|
384
|
+
|
|
385
|
+
# Prettier config
|
|
386
|
+
prettier_files = [
|
|
387
|
+
".prettierrc", ".prettierrc.json", ".prettierrc.js", ".prettierrc.cjs",
|
|
388
|
+
".prettierrc.yml", ".prettierrc.yaml", "prettier.config.js",
|
|
389
|
+
"prettier.config.cjs",
|
|
390
|
+
]
|
|
391
|
+
for name in prettier_files:
|
|
392
|
+
p_path = root / name
|
|
393
|
+
if p_path.exists():
|
|
394
|
+
try:
|
|
395
|
+
content = p_path.read_text(encoding="utf-8", errors="replace")
|
|
396
|
+
if len(content) <= 1000:
|
|
397
|
+
rules[name] = content
|
|
398
|
+
except OSError:
|
|
399
|
+
pass
|
|
400
|
+
break
|
|
401
|
+
|
|
402
|
+
# .editorconfig
|
|
403
|
+
ec_path = root / ".editorconfig"
|
|
404
|
+
if ec_path.exists():
|
|
405
|
+
try:
|
|
406
|
+
content = ec_path.read_text(encoding="utf-8", errors="replace")
|
|
407
|
+
if len(content) <= 1000:
|
|
408
|
+
rules[".editorconfig"] = content
|
|
409
|
+
except OSError:
|
|
410
|
+
pass
|
|
411
|
+
|
|
412
|
+
# AI agent instructions (CLAUDE.md, CONTRIBUTING.md, .cursorrules)
|
|
413
|
+
for name in ("CLAUDE.md", ".claude", "CONTRIBUTING.md", ".cursorrules",
|
|
414
|
+
".github/CONTRIBUTING.md"):
|
|
415
|
+
ai_path = root / name
|
|
416
|
+
if ai_path.exists() and ai_path.is_file():
|
|
417
|
+
try:
|
|
418
|
+
content = ai_path.read_text(encoding="utf-8", errors="replace")
|
|
419
|
+
if len(content) <= 5000:
|
|
420
|
+
rules[name] = content
|
|
421
|
+
else:
|
|
422
|
+
rules[name] = content[:5000] + "\n... (truncated)"
|
|
423
|
+
except OSError:
|
|
424
|
+
pass
|
|
425
|
+
|
|
426
|
+
return rules
|
|
427
|
+
|
|
428
|
+
|
|
278
429
|
def detect_tests(root):
|
|
279
430
|
"""Check if the project has substantive test coverage."""
|
|
280
431
|
test_dir_names = {"test", "tests", "__tests__", "spec", "specs", "test_suite"}
|
|
@@ -364,6 +515,22 @@ def generate_map(root):
|
|
|
364
515
|
lines.append(f" Dev dependencies: {dev_str}")
|
|
365
516
|
lines.append("")
|
|
366
517
|
|
|
518
|
+
# Project rules and configs
|
|
519
|
+
rules = detect_project_rules(root)
|
|
520
|
+
if rules:
|
|
521
|
+
lines.append("PROJECT RULES")
|
|
522
|
+
for name, value in rules.items():
|
|
523
|
+
lines.append(f" {name}:")
|
|
524
|
+
if isinstance(value, dict):
|
|
525
|
+
for k, v in value.items():
|
|
526
|
+
lines.append(f" {k}: {json.dumps(v) if not isinstance(v, str) else v}")
|
|
527
|
+
elif isinstance(value, str):
|
|
528
|
+
for vline in value.splitlines():
|
|
529
|
+
lines.append(f" {vline}")
|
|
530
|
+
else:
|
|
531
|
+
lines.append(f" {value}")
|
|
532
|
+
lines.append("")
|
|
533
|
+
|
|
367
534
|
# Tests
|
|
368
535
|
test_status, test_files = detect_tests(root)
|
|
369
536
|
lines.append("TESTS")
|
|
@@ -434,22 +601,22 @@ def main():
|
|
|
434
601
|
|
|
435
602
|
map_content = generate_map(root)
|
|
436
603
|
|
|
437
|
-
out_dir = root / ".
|
|
604
|
+
out_dir = root / ".chorus"
|
|
438
605
|
out_dir.mkdir(exist_ok=True)
|
|
439
606
|
out_file = out_dir / "map.md"
|
|
440
607
|
out_file.write_text(map_content, encoding="utf-8")
|
|
441
608
|
|
|
442
|
-
# Add .
|
|
609
|
+
# Add .chorus to .gitignore if not already there
|
|
443
610
|
gitignore = root / ".gitignore"
|
|
444
611
|
if gitignore.exists():
|
|
445
612
|
gi_text = gitignore.read_text()
|
|
446
|
-
if ".
|
|
613
|
+
if ".chorus" not in gi_text and ".chorus/" not in gi_text:
|
|
447
614
|
with open(gitignore, "a") as f:
|
|
448
|
-
f.write("\n.
|
|
449
|
-
print(f"Added .
|
|
615
|
+
f.write("\n.chorus/\n")
|
|
616
|
+
print(f"Added .chorus/ to .gitignore", file=sys.stderr)
|
|
450
617
|
elif (root / ".git").is_dir():
|
|
451
|
-
gitignore.write_text(".
|
|
452
|
-
print(f"Created .gitignore with .
|
|
618
|
+
gitignore.write_text(".chorus/\n")
|
|
619
|
+
print(f"Created .gitignore with .chorus/", file=sys.stderr)
|
|
453
620
|
|
|
454
621
|
# Stats
|
|
455
622
|
line_count = map_content.count("\n")
|