chorus-cli 0.4.5 → 0.4.6

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,49 @@ 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
+ const configDir2 = path.join(os.homedir(), '.config', 'chorus');
58
+ await fs.mkdir(configDir2, { recursive: true });
59
+ await fs.writeFile(path.join(configDir2, 'machine-id'), rawId + '\n');
60
+ }
61
+ }
50
62
 
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;
63
+ return createHash('sha256').update(rawId).digest('hex');
56
64
  }
57
65
 
58
66
  // Run coder.py with real-time stderr streaming so progress is visible
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chorus-cli",
3
- "version": "0.4.5",
3
+ "version": "0.4.6",
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 ────────────────────────────────────────────────────────
@@ -804,18 +825,19 @@ def stream_response(client, messages, system):
804
825
 
805
826
  # ── Headless Prompt Mode ────────────────────────────────────────────────────
806
827
 
807
- def run_prompt(client, prompt, system):
828
+ def run_prompt(client, prompt, plan_system, exec_system):
808
829
  """Run a single prompt non-interactively. Returns a JSON-serializable dict."""
809
-
810
- # PHASE 1: Planning - ask the model to explain its approach first
830
+
831
+ # PHASE 1: Planning - separate API call with planning prompt
811
832
  print(f"\n{C.BOLD}{C.BLUE}📝 PLANNING PHASE{C.RESET}", file=sys.stderr, flush=True)
812
833
  print(f"{C.DIM}Understanding the issue and creating a plan...{C.RESET}\n", file=sys.stderr, flush=True)
813
-
834
+
835
+ plan_text = ""
814
836
  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."}
837
+ {"role": "system", "content": plan_system},
838
+ {"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
839
  ]
818
-
840
+
819
841
  try:
820
842
  plan_response = client.chat.completions.create(
821
843
  model=MODEL,
@@ -823,23 +845,26 @@ def run_prompt(client, prompt, system):
823
845
  messages=plan_messages,
824
846
  )
825
847
  plan_text = plan_response.choices[0].message.content.strip()
826
-
848
+
827
849
  # Print the plan with formatting
828
850
  print(f"{C.CYAN}{'─' * 60}{C.RESET}", file=sys.stderr, flush=True)
829
851
  for line in plan_text.split('\n'):
830
852
  print(f"{C.CYAN} {line}{C.RESET}", file=sys.stderr, flush=True)
831
853
  print(f"{C.CYAN}{'─' * 60}{C.RESET}\n", file=sys.stderr, flush=True)
832
-
854
+
833
855
  except Exception as e:
834
856
  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
857
+
858
+ # PHASE 2: Execution - inject plan into the conversation so the agent can reference it
838
859
  print(f"{C.BOLD}{C.GREEN}🔨 EXECUTING PLAN{C.RESET}\n", file=sys.stderr, flush=True)
839
-
860
+
861
+ exec_user_content = prompt
862
+ if plan_text:
863
+ exec_user_content = f"{prompt}\n\nHere is your plan. Follow it.\n\n{plan_text}"
864
+
840
865
  messages = [
841
- {"role": "system", "content": system},
842
- {"role": "user", "content": prompt}
866
+ {"role": "system", "content": exec_system},
867
+ {"role": "user", "content": exec_user_content}
843
868
  ]
844
869
  files_modified = set()
845
870
  files_created = set()
@@ -982,37 +1007,22 @@ def run_prompt(client, prompt, system):
982
1007
  final_text = msg.content
983
1008
  break
984
1009
 
985
- # Ask LLM for a CodeRabbit-oriented summary (skip if we hit token limit)
1010
+ # Parse PR_TITLE / PR_BODY from the agent's verify output (if present)
986
1011
  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
1012
+ pr_title_match = re.search(r'PR_TITLE:\s*(.+)', final_text)
1013
+ pr_body_match = re.search(r'PR_BODY:\s*([\s\S]+?)$', final_text)
1014
+ if pr_title_match:
1015
+ pr_title = pr_title_match.group(1).strip()
1016
+ pr_body = pr_body_match.group(1).strip() if pr_body_match else ""
1017
+ summary = f"{pr_title}\n\n{pr_body}".strip() if pr_body else pr_title
1018
+ elif not summary:
1019
+ # Fallback: build a minimal summary from file lists
1020
+ parts = []
1021
+ if files_created:
1022
+ parts.append(f"Created: {', '.join(sorted(files_created))}")
1023
+ if files_modified:
1024
+ parts.append(f"Modified: {', '.join(sorted(files_modified))}")
1025
+ summary = ". ".join(parts) if parts else "No changes produced."
1016
1026
 
1017
1027
  result = {
1018
1028
  "completed": len(errors) == 0,
@@ -1050,24 +1060,27 @@ def main():
1050
1060
  if machine_id:
1051
1061
  client_kwargs["default_headers"] = {"X-Machine-Id": machine_id}
1052
1062
  client = OpenAI(**client_kwargs)
1053
- system = SYSTEM_PROMPT.format(cwd=os.getcwd())
1063
+ cwd = os.getcwd()
1054
1064
 
1055
1065
  # Load codebase map if available
1066
+ map_suffix = ""
1056
1067
  map_file = Path.cwd() / ".coder" / "map.md"
1057
1068
  if map_file.exists():
1058
1069
  try:
1059
1070
  map_content = map_file.read_text(encoding="utf-8").strip()
1060
1071
  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}"
1072
+ map_content = map_content[:20000] + "\n\n... (map truncated -- use list_files to explore further)"
1073
+ map_suffix = f"\n\n{map_content}"
1063
1074
  print(f"{C.DIM}Loaded codebase map ({map_content.count(chr(10))} lines){C.RESET}", file=sys.stderr)
1064
1075
  except OSError:
1065
1076
  pass
1066
1077
 
1067
1078
  # ── Headless prompt mode ────────────────────────────────────────────
1068
1079
  if args.prompt:
1080
+ plan_system = PLAN_PROMPT.format(cwd=cwd) + map_suffix
1081
+ exec_system = EXEC_PROMPT.format(cwd=cwd) + map_suffix
1069
1082
  try:
1070
- result = run_prompt(client, args.prompt, system)
1083
+ result = run_prompt(client, args.prompt, plan_system, exec_system)
1071
1084
  print(json.dumps(result, indent=2))
1072
1085
  sys.exit(0 if result["completed"] else 1)
1073
1086
  except Exception as e:
@@ -1092,6 +1105,7 @@ def main():
1092
1105
  sys.exit(130)
1093
1106
 
1094
1107
  # ── Interactive REPL mode ───────────────────────────────────────────
1108
+ system = REPL_PROMPT.format(cwd=cwd) + map_suffix
1095
1109
  messages = []
1096
1110
 
1097
1111
  mode_label = f" {C.YELLOW}(safe mode){C.RESET}" if SAFE_MODE else ""