chorus-cli 0.4.5 → 0.4.7
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 +31 -24
- package/package.json +1 -1
- package/tools/__pycache__/coder.cpython-314.pyc +0 -0
- package/tools/__pycache__/mapper.cpython-314.pyc +0 -0
- package/tools/coder.py +128 -116
- package/tools/mapper.py +175 -8
package/index.js
CHANGED
|
@@ -18,41 +18,48 @@ const execPromise = util.promisify(exec);
|
|
|
18
18
|
const execFilePromise = util.promisify(execFile);
|
|
19
19
|
const fs = require('fs').promises;
|
|
20
20
|
|
|
21
|
-
// Returns a
|
|
21
|
+
// Returns a SHA-256 hash of this machine's hardware UUID (or a persistent random fallback).
|
|
22
|
+
// The raw UUID never leaves the device — only the hash is sent to the server.
|
|
22
23
|
async function getMachineId() {
|
|
24
|
+
const { createHash, randomUUID } = require('crypto');
|
|
25
|
+
let rawId;
|
|
26
|
+
|
|
23
27
|
try {
|
|
24
28
|
if (process.platform === 'darwin') {
|
|
25
29
|
const { stdout } = await execPromise(
|
|
26
30
|
"ioreg -rd1 -c IOPlatformExpertDevice | awk -F'\"' '/IOPlatformUUID/{print $4}'"
|
|
27
31
|
);
|
|
28
|
-
if (stdout.trim())
|
|
32
|
+
if (stdout.trim()) rawId = stdout.trim();
|
|
29
33
|
} else if (process.platform === 'linux') {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
if (id) return id;
|
|
34
|
+
const id = (await fs.readFile('/etc/machine-id', 'utf8')).trim();
|
|
35
|
+
if (id) rawId = id;
|
|
33
36
|
} else if (process.platform === 'win32') {
|
|
34
37
|
const { stdout } = await execPromise('wmic csproduct get UUID');
|
|
35
38
|
const lines = stdout.trim().split('\n');
|
|
36
39
|
if (lines.length > 1) {
|
|
37
40
|
const uuid = lines[1].trim();
|
|
38
|
-
if (uuid && uuid !== 'FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF')
|
|
41
|
+
if (uuid && uuid !== 'FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF') rawId = uuid;
|
|
39
42
|
}
|
|
40
43
|
}
|
|
41
44
|
} catch { /* fall through to persistent fallback */ }
|
|
42
45
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
if (!rawId) {
|
|
47
|
+
// Persistent fallback: generate and cache a random UUID
|
|
48
|
+
const configDir = path.join(os.homedir(), '.config', 'chorus');
|
|
49
|
+
const idPath = path.join(configDir, 'machine-id');
|
|
50
|
+
try {
|
|
51
|
+
const existing = await fs.readFile(idPath, 'utf8');
|
|
52
|
+
if (existing.trim()) rawId = existing.trim();
|
|
53
|
+
} catch { /* no file yet */ }
|
|
54
|
+
|
|
55
|
+
if (!rawId) {
|
|
56
|
+
rawId = randomUUID();
|
|
57
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
58
|
+
await fs.writeFile(idPath, rawId + '\n');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
50
61
|
|
|
51
|
-
|
|
52
|
-
const newId = randomUUID();
|
|
53
|
-
await fs.mkdir(configDir, { recursive: true });
|
|
54
|
-
await fs.writeFile(idPath, newId + '\n');
|
|
55
|
-
return newId;
|
|
62
|
+
return createHash('sha256').update(rawId).digest('hex');
|
|
56
63
|
}
|
|
57
64
|
|
|
58
65
|
// Run coder.py with real-time stderr streaming so progress is visible
|
|
@@ -573,7 +580,7 @@ async function processTicket(issueArg, { useSuper = false, skipQA = false, qaNam
|
|
|
573
580
|
efs(CONFIG.ai.venvPython, ['-m', 'pip', 'install', '-r', reqFile], { stdio: 'inherit' });
|
|
574
581
|
}
|
|
575
582
|
|
|
576
|
-
// 0a. Verify no modified tracked files (untracked files like .
|
|
583
|
+
// 0a. Verify no modified tracked files (untracked files like .chorus/ are fine)
|
|
577
584
|
const { stdout: gitStatus } = await execPromise('git status --porcelain --untracked-files=no');
|
|
578
585
|
if (gitStatus.trim()) {
|
|
579
586
|
console.error('⚠️ Working directory has uncommitted changes. Commit or stash first:');
|
|
@@ -899,7 +906,7 @@ async function setupGitHub() {
|
|
|
899
906
|
CONFIG.github.repo = repo;
|
|
900
907
|
if (token) CONFIG.github.token = token;
|
|
901
908
|
|
|
902
|
-
console.log(`\n✅ GitHub config saved
|
|
909
|
+
console.log(`\n✅ GitHub config saved (${owner}/${repo})\n`);
|
|
903
910
|
}
|
|
904
911
|
|
|
905
912
|
async function setupProxyAuth() {
|
|
@@ -995,7 +1002,7 @@ async function setupProxyAuth() {
|
|
|
995
1002
|
CONFIG.ai.chorusApiKey = apiKey;
|
|
996
1003
|
process.env.CHORUS_API_KEY = apiKey;
|
|
997
1004
|
|
|
998
|
-
console.log(`\n✅ Chorus API key saved
|
|
1005
|
+
console.log(`\n✅ Chorus API key saved\n`);
|
|
999
1006
|
}
|
|
1000
1007
|
|
|
1001
1008
|
async function setupTeamsAuth() {
|
|
@@ -1020,7 +1027,7 @@ async function setupTeamsAuth() {
|
|
|
1020
1027
|
await context.storageState({ path: CONFIG.teams.authPath });
|
|
1021
1028
|
await browser.close();
|
|
1022
1029
|
|
|
1023
|
-
console.log(`\n✅
|
|
1030
|
+
console.log(`\n✅ Teams authentication saved`);
|
|
1024
1031
|
}
|
|
1025
1032
|
|
|
1026
1033
|
async function setupSlack() {
|
|
@@ -1068,7 +1075,7 @@ async function setupSlack() {
|
|
|
1068
1075
|
CONFIG.messenger = 'slack';
|
|
1069
1076
|
CONFIG.slack.botToken = token;
|
|
1070
1077
|
|
|
1071
|
-
console.log(`\n✅ Slack config saved
|
|
1078
|
+
console.log(`\n✅ Slack config saved\n`);
|
|
1072
1079
|
}
|
|
1073
1080
|
|
|
1074
1081
|
async function setupAzureDevOps() {
|
|
@@ -1110,7 +1117,7 @@ async function setupAzureDevOps() {
|
|
|
1110
1117
|
CONFIG.azuredevops.repo = adoRepo;
|
|
1111
1118
|
if (adoPat) CONFIG.azuredevops.pat = adoPat;
|
|
1112
1119
|
|
|
1113
|
-
console.log(`\n✅ Azure DevOps config saved
|
|
1120
|
+
console.log(`\n✅ Azure DevOps config saved (${adoOrg}/${adoProject}/${adoRepo})\n`);
|
|
1114
1121
|
}
|
|
1115
1122
|
|
|
1116
1123
|
async function setup() {
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
package/tools/coder.py
CHANGED
|
@@ -49,14 +49,9 @@ def is_token_limit_error(err):
|
|
|
49
49
|
msg = str(err)
|
|
50
50
|
return "token limit exceeded" in msg or "rate_limit_error" in msg
|
|
51
51
|
|
|
52
|
-
SYSTEM_PROMPT = """\
|
|
53
|
-
You are a coding agent. You receive a GitHub/Azure DevOps issue, a codebase map, \
|
|
54
|
-
and optionally a QA conversation with clarified requirements. Your job is to \
|
|
55
|
-
implement the changes and produce a clean, working diff.
|
|
56
|
-
|
|
57
|
-
Working directory: {cwd}
|
|
58
|
-
|
|
59
52
|
|
|
53
|
+
# ── Shared formatting rules (included in all prompts) ─────────────────────
|
|
54
|
+
_FORMAT_RULES = """\
|
|
60
55
|
OUTPUT FORMAT
|
|
61
56
|
|
|
62
57
|
Your output is displayed raw in a terminal. Never use markdown.
|
|
@@ -67,15 +62,20 @@ Use plain numbered lists (1. 2. 3.) when listing things.
|
|
|
67
62
|
Refer to code identifiers by name directly (e.g. myFunction not `myFunction`).
|
|
68
63
|
No greetings, preambles, encouragement, or sign-offs.
|
|
69
64
|
No "Great question!", "Let me", "Sure!", "I'll now", or similar filler.
|
|
70
|
-
State what you are doing, then do it. After completing work, state what changed.
|
|
65
|
+
State what you are doing, then do it. After completing work, state what changed."""
|
|
71
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
72
|
|
|
73
|
-
|
|
73
|
+
Working directory: {cwd}
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
""" + _FORMAT_RULES + """
|
|
76
76
|
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
TASK
|
|
79
79
|
|
|
80
80
|
Read the issue, QA conversation (if provided), and codebase map. Then produce a
|
|
81
81
|
written plan with exactly these sections:
|
|
@@ -101,17 +101,41 @@ APPROACH:
|
|
|
101
101
|
How you will implement this in 2-4 sentences. Reference specific patterns
|
|
102
102
|
from the codebase map.
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
ON AMBIGUITY
|
|
105
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
|
|
106
125
|
|
|
107
|
-
|
|
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.
|
|
108
132
|
|
|
109
|
-
|
|
133
|
+
EXECUTION RULES
|
|
110
134
|
|
|
111
135
|
Reading files:
|
|
112
|
-
Only use read_file on files listed in
|
|
136
|
+
Only use read_file on files listed in the plan. If you discover you need another
|
|
113
137
|
file, note why -- but if you have read more than 12 files total, stop and
|
|
114
|
-
re-evaluate
|
|
138
|
+
re-evaluate. You are exploring, not implementing.
|
|
115
139
|
The one exception to reading a file again: if edit_file fails because old_string
|
|
116
140
|
was not found, re-read that file to get its current content before retrying.
|
|
117
141
|
|
|
@@ -125,77 +149,64 @@ Writing code:
|
|
|
125
149
|
Shell commands (bash tool):
|
|
126
150
|
You may run bash for: installing dependencies, running the project's existing
|
|
127
151
|
linter, running the project's existing tests, checking TypeScript compilation.
|
|
128
|
-
You may not run bash for: exploratory searching beyond what was in
|
|
152
|
+
You may not run bash for: exploratory searching beyond what was in the plan,
|
|
129
153
|
reading files you did not plan to read, testing scripts with improvised piped input.
|
|
130
|
-
Use read_file, list_files, and search_files instead of bash with cat, find, or grep.
|
|
131
154
|
Maximum 10 bash commands total. If you are approaching this limit, you are doing
|
|
132
155
|
too much exploration or too much debugging. Ship what you have.
|
|
133
156
|
|
|
134
|
-
|
|
135
|
-
PHASE 3: VERIFY
|
|
136
|
-
|
|
137
|
-
Before declaring done:
|
|
138
|
-
|
|
139
|
-
1. List every file you created or modified.
|
|
140
|
-
2. For each, confirm it is syntactically valid (ran linter or compiler if available).
|
|
141
|
-
3. If tests exist for the area you changed, run them and confirm they pass.
|
|
142
|
-
4. If you wrote a script/CLI tool, show the --help output or a dry-run invocation.
|
|
143
|
-
5. Write the PR description:
|
|
144
|
-
Title: conventional commit format (feat:, fix:, chore:, etc.)
|
|
145
|
-
Body: what changed, why, any assumptions made, anything the reviewer should
|
|
146
|
-
look at carefully.
|
|
147
|
-
|
|
148
|
-
|
|
149
157
|
BUDGET DISCIPLINE
|
|
150
158
|
|
|
151
|
-
You have a token budget. You do not know exactly how large it is, but you must act
|
|
152
|
-
as though it could run out at any time. This means:
|
|
153
|
-
|
|
154
159
|
Front-load the high-value work. Write the actual implementation code early. File
|
|
155
160
|
exploration is not progress -- committed code is progress.
|
|
156
161
|
|
|
157
162
|
Do not retry the same failing approach. If something fails twice, choose a different
|
|
158
163
|
approach or simplify. Do not iterate more than twice on the same problem.
|
|
159
164
|
|
|
160
|
-
If
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
No yak-shaving. If the issue says "create a screen scaffolding tool," build the tool.
|
|
165
|
-
Do not also create a demo script, a markdown doc, a bash wrapper, and sample outputs.
|
|
166
|
-
Deliver the core ask first. Only add extras if the implementation is solid and you
|
|
167
|
-
have headroom.
|
|
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
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.
|
|
169
171
|
|
|
170
172
|
WHAT NOT TO DO
|
|
171
173
|
|
|
172
|
-
Do not explore the filesystem to "understand the project." The codebase map
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
Do not overuse list_files, search_files, or bash for exploration. This is the single
|
|
176
|
-
most common failure mode. Each call costs tokens and time. If you need more than 3
|
|
177
|
-
exploratory calls, your plan was insufficient -- go back and improve the plan.
|
|
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.
|
|
178
176
|
|
|
179
|
-
Do not
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
Do not create documentation, READMEs, or demo files unless the issue asks for them.
|
|
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.
|
|
183
179
|
|
|
180
|
+
Do not create interactive scripts that require stdin.
|
|
181
|
+
Do not create documentation or READMEs unless the issue asks for them.
|
|
184
182
|
Do not modify package.json, CI configs, or project infrastructure unless the issue
|
|
185
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
186
|
|
|
187
|
-
|
|
188
|
-
the same problem, note the issue, deliver what works, and move on.
|
|
187
|
+
WHEN YOU ARE DONE
|
|
189
188
|
|
|
189
|
+
End with a verify block:
|
|
190
190
|
|
|
191
|
-
|
|
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:
|
|
192
196
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
Do not
|
|
198
|
-
|
|
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 + """
|
|
199
210
|
|
|
200
211
|
|
|
201
212
|
TOOL USAGE
|
|
@@ -205,6 +216,16 @@ TOOL USAGE
|
|
|
205
216
|
get the actual current content before retrying. Never guess at file contents.
|
|
206
217
|
Use edit_file for targeted changes. Use write_file for new files or complete rewrites.
|
|
207
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.
|
|
208
229
|
"""
|
|
209
230
|
|
|
210
231
|
# ── Tool Definitions ────────────────────────────────────────────────────────
|
|
@@ -764,7 +785,6 @@ def stream_response(client, messages, system):
|
|
|
764
785
|
tool_results = []
|
|
765
786
|
for tool_call in message.tool_calls:
|
|
766
787
|
function_name = tool_call.function.name
|
|
767
|
-
import json
|
|
768
788
|
try:
|
|
769
789
|
arguments = json.loads(tool_call.function.arguments)
|
|
770
790
|
except json.JSONDecodeError:
|
|
@@ -804,18 +824,19 @@ def stream_response(client, messages, system):
|
|
|
804
824
|
|
|
805
825
|
# ── Headless Prompt Mode ────────────────────────────────────────────────────
|
|
806
826
|
|
|
807
|
-
def run_prompt(client, prompt,
|
|
827
|
+
def run_prompt(client, prompt, plan_system, exec_system):
|
|
808
828
|
"""Run a single prompt non-interactively. Returns a JSON-serializable dict."""
|
|
809
|
-
|
|
810
|
-
# PHASE 1: Planning -
|
|
829
|
+
|
|
830
|
+
# PHASE 1: Planning - separate API call with planning prompt
|
|
811
831
|
print(f"\n{C.BOLD}{C.BLUE}📝 PLANNING PHASE{C.RESET}", file=sys.stderr, flush=True)
|
|
812
832
|
print(f"{C.DIM}Understanding the issue and creating a plan...{C.RESET}\n", file=sys.stderr, flush=True)
|
|
813
|
-
|
|
833
|
+
|
|
834
|
+
plan_text = ""
|
|
814
835
|
plan_messages = [
|
|
815
|
-
{"role": "system", "content":
|
|
816
|
-
{"role": "user", "content": f"{prompt}\n\
|
|
836
|
+
{"role": "system", "content": plan_system},
|
|
837
|
+
{"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."}
|
|
817
838
|
]
|
|
818
|
-
|
|
839
|
+
|
|
819
840
|
try:
|
|
820
841
|
plan_response = client.chat.completions.create(
|
|
821
842
|
model=MODEL,
|
|
@@ -823,23 +844,26 @@ def run_prompt(client, prompt, system):
|
|
|
823
844
|
messages=plan_messages,
|
|
824
845
|
)
|
|
825
846
|
plan_text = plan_response.choices[0].message.content.strip()
|
|
826
|
-
|
|
847
|
+
|
|
827
848
|
# Print the plan with formatting
|
|
828
849
|
print(f"{C.CYAN}{'─' * 60}{C.RESET}", file=sys.stderr, flush=True)
|
|
829
850
|
for line in plan_text.split('\n'):
|
|
830
851
|
print(f"{C.CYAN} {line}{C.RESET}", file=sys.stderr, flush=True)
|
|
831
852
|
print(f"{C.CYAN}{'─' * 60}{C.RESET}\n", file=sys.stderr, flush=True)
|
|
832
|
-
|
|
853
|
+
|
|
833
854
|
except Exception as e:
|
|
834
855
|
print(f"{C.YELLOW}Could not generate plan: {e}{C.RESET}", file=sys.stderr, flush=True)
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
# PHASE 2: Execution - proceed with the actual coding
|
|
856
|
+
|
|
857
|
+
# PHASE 2: Execution - inject plan into the conversation so the agent can reference it
|
|
838
858
|
print(f"{C.BOLD}{C.GREEN}🔨 EXECUTING PLAN{C.RESET}\n", file=sys.stderr, flush=True)
|
|
839
|
-
|
|
859
|
+
|
|
860
|
+
exec_user_content = prompt
|
|
861
|
+
if plan_text:
|
|
862
|
+
exec_user_content = f"{prompt}\n\nHere is your plan. Follow it.\n\n{plan_text}"
|
|
863
|
+
|
|
840
864
|
messages = [
|
|
841
|
-
{"role": "system", "content":
|
|
842
|
-
{"role": "user", "content":
|
|
865
|
+
{"role": "system", "content": exec_system},
|
|
866
|
+
{"role": "user", "content": exec_user_content}
|
|
843
867
|
]
|
|
844
868
|
files_modified = set()
|
|
845
869
|
files_created = set()
|
|
@@ -890,7 +914,6 @@ def run_prompt(client, prompt, system):
|
|
|
890
914
|
tool_results = []
|
|
891
915
|
for tool_call in message.tool_calls:
|
|
892
916
|
function_name = tool_call.function.name
|
|
893
|
-
import json
|
|
894
917
|
try:
|
|
895
918
|
arguments = json.loads(tool_call.function.arguments)
|
|
896
919
|
except json.JSONDecodeError:
|
|
@@ -982,37 +1005,22 @@ def run_prompt(client, prompt, system):
|
|
|
982
1005
|
final_text = msg.content
|
|
983
1006
|
break
|
|
984
1007
|
|
|
985
|
-
#
|
|
1008
|
+
# Parse PR_TITLE / PR_BODY from the agent's verify output (if present)
|
|
986
1009
|
summary = final_text.strip()
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
model=MODEL,
|
|
1002
|
-
max_tokens=1024,
|
|
1003
|
-
messages=summary_messages,
|
|
1004
|
-
)
|
|
1005
|
-
|
|
1006
|
-
if hasattr(summary_response, "usage") and summary_response.usage:
|
|
1007
|
-
total_input_tokens += summary_response.usage.prompt_tokens
|
|
1008
|
-
total_output_tokens += summary_response.usage.completion_tokens
|
|
1009
|
-
|
|
1010
|
-
summary = summary_response.choices[0].message.content.strip()
|
|
1011
|
-
except Exception as e:
|
|
1012
|
-
if is_token_limit_error(e):
|
|
1013
|
-
errors.append(str(e))
|
|
1014
|
-
else:
|
|
1015
|
-
raise
|
|
1010
|
+
pr_title_match = re.search(r'PR_TITLE:\s*(.+)', final_text)
|
|
1011
|
+
pr_body_match = re.search(r'PR_BODY:\s*([\s\S]+?)$', final_text)
|
|
1012
|
+
if pr_title_match:
|
|
1013
|
+
pr_title = pr_title_match.group(1).strip()
|
|
1014
|
+
pr_body = pr_body_match.group(1).strip() if pr_body_match else ""
|
|
1015
|
+
summary = f"{pr_title}\n\n{pr_body}".strip() if pr_body else pr_title
|
|
1016
|
+
elif not summary:
|
|
1017
|
+
# Fallback: build a minimal summary from file lists
|
|
1018
|
+
parts = []
|
|
1019
|
+
if files_created:
|
|
1020
|
+
parts.append(f"Created: {', '.join(sorted(files_created))}")
|
|
1021
|
+
if files_modified:
|
|
1022
|
+
parts.append(f"Modified: {', '.join(sorted(files_modified))}")
|
|
1023
|
+
summary = ". ".join(parts) if parts else "No changes produced."
|
|
1016
1024
|
|
|
1017
1025
|
result = {
|
|
1018
1026
|
"completed": len(errors) == 0,
|
|
@@ -1050,24 +1058,27 @@ def main():
|
|
|
1050
1058
|
if machine_id:
|
|
1051
1059
|
client_kwargs["default_headers"] = {"X-Machine-Id": machine_id}
|
|
1052
1060
|
client = OpenAI(**client_kwargs)
|
|
1053
|
-
|
|
1061
|
+
cwd = os.getcwd()
|
|
1054
1062
|
|
|
1055
1063
|
# Load codebase map if available
|
|
1056
|
-
|
|
1064
|
+
map_suffix = ""
|
|
1065
|
+
map_file = Path.cwd() / ".chorus" / "map.md"
|
|
1057
1066
|
if map_file.exists():
|
|
1058
1067
|
try:
|
|
1059
1068
|
map_content = map_file.read_text(encoding="utf-8").strip()
|
|
1060
1069
|
if len(map_content) > 20000:
|
|
1061
|
-
map_content = map_content[:20000] + "\n\n... (map truncated
|
|
1062
|
-
|
|
1070
|
+
map_content = map_content[:20000] + "\n\n... (map truncated -- use list_files to explore further)"
|
|
1071
|
+
map_suffix = f"\n\n{map_content}"
|
|
1063
1072
|
print(f"{C.DIM}Loaded codebase map ({map_content.count(chr(10))} lines){C.RESET}", file=sys.stderr)
|
|
1064
1073
|
except OSError:
|
|
1065
1074
|
pass
|
|
1066
1075
|
|
|
1067
1076
|
# ── Headless prompt mode ────────────────────────────────────────────
|
|
1068
1077
|
if args.prompt:
|
|
1078
|
+
plan_system = PLAN_PROMPT.format(cwd=cwd) + map_suffix
|
|
1079
|
+
exec_system = EXEC_PROMPT.format(cwd=cwd) + map_suffix
|
|
1069
1080
|
try:
|
|
1070
|
-
result = run_prompt(client, args.prompt,
|
|
1081
|
+
result = run_prompt(client, args.prompt, plan_system, exec_system)
|
|
1071
1082
|
print(json.dumps(result, indent=2))
|
|
1072
1083
|
sys.exit(0 if result["completed"] else 1)
|
|
1073
1084
|
except Exception as e:
|
|
@@ -1092,6 +1103,7 @@ def main():
|
|
|
1092
1103
|
sys.exit(130)
|
|
1093
1104
|
|
|
1094
1105
|
# ── Interactive REPL mode ───────────────────────────────────────────
|
|
1106
|
+
system = REPL_PROMPT.format(cwd=cwd) + map_suffix
|
|
1095
1107
|
messages = []
|
|
1096
1108
|
|
|
1097
1109
|
mode_label = f" {C.YELLOW}(safe mode){C.RESET}" if SAFE_MODE else ""
|
|
@@ -1101,7 +1113,7 @@ def main():
|
|
|
1101
1113
|
print(f"{C.DIM}Commands: /clear /quit /help{C.RESET}")
|
|
1102
1114
|
print()
|
|
1103
1115
|
|
|
1104
|
-
histfile = os.path.expanduser("~/.
|
|
1116
|
+
histfile = os.path.expanduser("~/.chorus_history")
|
|
1105
1117
|
try:
|
|
1106
1118
|
readline.read_history_file(histfile)
|
|
1107
1119
|
except (FileNotFoundError, OSError, PermissionError):
|
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")
|