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 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 stable hardware UUID for this machine, with a persistent fallback
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()) return stdout.trim();
32
+ if (stdout.trim()) rawId = stdout.trim();
29
33
  } else if (process.platform === 'linux') {
30
- const fsp = require('fs').promises;
31
- const id = (await fsp.readFile('/etc/machine-id', 'utf8')).trim();
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') return uuid;
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
- // Persistent fallback: generate and cache a random UUID
44
- const configDir = path.join(os.homedir(), '.config', 'chorus');
45
- const idPath = path.join(configDir, 'machine-id');
46
- try {
47
- const existing = await fs.readFile(idPath, 'utf8');
48
- if (existing.trim()) return existing.trim();
49
- } catch { /* no file yet */ }
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
- const { randomUUID } = require('crypto');
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 .coder/ are fine)
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 to ${envPath} (${owner}/${repo})\n`);
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 to ${envPath}\n`);
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✅ Authentication state saved to ${CONFIG.teams.authPath}`);
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 to ${envPath}\n`);
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 to ${envPath} (${adoOrg}/${adoProject}/${adoRepo})\n`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chorus-cli",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "description": "Automated ticket resolution with AI, Teams, and Slack integration",
5
5
  "main": "index.js",
6
6
  "bin": {
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
- HOW YOU WORK
73
+ Working directory: {cwd}
74
74
 
75
- You operate in three strict phases: Plan, Execute, Verify. Do not blend them.
75
+ """ + _FORMAT_RULES + """
76
76
 
77
77
 
78
- PHASE 1: PLAN (before writing any code)
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
- Do not proceed to Phase 2 until your plan is complete.
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
- PHASE 2: EXECUTE
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
- Implement the plan. Follow these rules:
133
+ EXECUTION RULES
110
134
 
111
135
  Reading files:
112
- Only use read_file on files listed in your plan. If you discover you need another
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 your plan. You are exploring, not implementing.
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 your plan,
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 you are 60% through your work and something fundamental is broken, stop. Produce
161
- what you have, note what is incomplete, and let the human finish. A partial, clean
162
- implementation is more valuable than a complete, broken one.
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 already
173
- gives you the structure. Read specific files for specific reasons.
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 create interactive scripts that require stdin. They are untestable in this
180
- environment and will waste your budget on failed pipe attempts.
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
- Do not keep going when you are stuck. If you have spent more than 2 attempts debugging
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
- ON AMBIGUITY
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
- When requirements are unclear and no QA conversation was provided:
194
- 1. State your assumption explicitly in the plan.
195
- 2. Choose the most conventional/standard approach.
196
- 3. Note the assumption in the PR description so the reviewer can catch disagreements early.
197
- Do not guess ambitiously. Guess conservatively. A correct simple implementation beats
198
- a broken ambitious one.
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, system):
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 - ask the model to explain its approach first
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": system},
816
- {"role": "user", "content": f"{prompt}\n\nExecute Phase 1 (Plan) now. Produce the plan using the exact sections from your instructions: UNDERSTANDING, QUESTIONS STILL OPEN, FILES TO READ, FILES TO CREATE, FILES TO MODIFY, APPROACH. Do NOT write any code yet."}
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
- plan_text = ""
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": system},
842
- {"role": "user", "content": prompt}
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
- # Ask LLM for a CodeRabbit-oriented summary (skip if we hit token limit)
1008
+ # Parse PR_TITLE / PR_BODY from the agent's verify output (if present)
986
1009
  summary = final_text.strip()
987
- if not any(is_token_limit_error(e) for e in errors):
988
- summary_messages = [
989
- {"role": "system", "content": "You are a helpful assistant that summarizes code changes."},
990
- {"role": "user", "content": (
991
- f"Summarize these code changes in 2-3 sentences for a code review tool.\n\n"
992
- f"Files modified: {', '.join(sorted(files_modified)) or 'none'}\n"
993
- f"Files created: {', '.join(sorted(files_created)) or 'none'}\n\n"
994
- f"Agent's final notes:\n{final_text[:2000]}\n\n"
995
- f"Focus on what changed, what was added/fixed, and why. Be specific. No preamble."
996
- )},
997
- ]
998
-
999
- try:
1000
- summary_response = client.chat.completions.create(
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
- system = SYSTEM_PROMPT.format(cwd=os.getcwd())
1061
+ cwd = os.getcwd()
1054
1062
 
1055
1063
  # Load codebase map if available
1056
- map_file = Path.cwd() / ".coder" / "map.md"
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 use list_files to explore further)"
1062
- system += f"\n\n{map_content}"
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, system)
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("~/.coder_history")
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 .coder/map.md that coder
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 / ".coder"
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 .coder to .gitignore if not already there
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 ".coder" not in gi_text and ".coder/" not in gi_text:
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.coder/\n")
449
- print(f"Added .coder/ to .gitignore", file=sys.stderr)
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(".coder/\n")
452
- print(f"Created .gitignore with .coder/", file=sys.stderr)
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")