chorus-cli 0.4.4 → 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.4",
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,47 +49,183 @@ 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 running inside a CLI tool called Chorus.
54
- Your output goes straight to a terminal. There is no browser, no rich renderer.
52
+
53
+ # ── Shared formatting rules (included in all prompts) ─────────────────────
54
+ _FORMAT_RULES = """\
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
+
55
119
  Working directory: {cwd}
56
120
 
57
- You help with software engineering tasks: writing code, debugging, refactoring, \
58
- explaining code, running commands, and managing files.
59
-
60
- Formatting:
61
- - Plain text only. Never use markdown.
62
- - No ## headers, **bold**, *italic*, `backticks`, [links](url), or bullet symbols like -.
63
- - Use blank lines, indentation, and CAPS for emphasis or section labels.
64
- - Use plain numbered lists (1. 2. 3.) when listing things.
65
- - Refer to code identifiers by name directly (e.g. myFunction, not `myFunction`).
66
-
67
- Communication style:
68
- - Be terse. Say what you are doing and why in one or two lines, then do it.
69
- - No greetings, preambles, encouragement, or sign-offs.
70
- - No "Great question!", "Let me", "Sure!", "I'll now", or similar filler.
71
- - When explaining your plan, use short declarative sentences. Skip obvious reasoning.
72
- - After completing work, state what changed and nothing else.
73
-
74
- {approach}Guidelines:
75
- - Always use your tools. If a question can be answered by running a command (git, ls, etc.), use the bash tool. Never guess.
76
- - Always read a file before editing it.
77
- - If edit_file fails with "old_string not found", re-read the file to get the actual current content before retrying. Never guess at file contents.
78
- - Use edit_file for targeted changes. Use write_file for new files or complete rewrites.
79
- - Prefer editing existing files over creating new ones.
80
- - Do not write new unit tests unless the project already has substantive test coverage.
81
- - Do not attempt to build or compile the project.
82
- - Don't add unnecessary comments, docstrings, or type annotations.
83
- - For bash commands, prefer non-interactive commands.
84
- - When asked about the codebase, use list_files and search_files to explore it.
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.
85
202
  """
86
203
 
87
- APPROACH_BLOCK = """\
88
- Approach:
89
- - Before making changes, read the relevant files and briefly state your approach.
90
- - For multi-file changes, outline which files you'll modify and in what order.
91
- - Do not start editing until you understand the existing code.
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.
92
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.
93
229
  """
94
230
 
95
231
  # ── Tool Definitions ────────────────────────────────────────────────────────
@@ -689,18 +825,19 @@ def stream_response(client, messages, system):
689
825
 
690
826
  # ── Headless Prompt Mode ────────────────────────────────────────────────────
691
827
 
692
- def run_prompt(client, prompt, system):
828
+ def run_prompt(client, prompt, plan_system, exec_system):
693
829
  """Run a single prompt non-interactively. Returns a JSON-serializable dict."""
694
-
695
- # PHASE 1: Planning - ask the model to explain its approach first
830
+
831
+ # PHASE 1: Planning - separate API call with planning prompt
696
832
  print(f"\n{C.BOLD}{C.BLUE}📝 PLANNING PHASE{C.RESET}", file=sys.stderr, flush=True)
697
833
  print(f"{C.DIM}Understanding the issue and creating a plan...{C.RESET}\n", file=sys.stderr, flush=True)
698
-
834
+
835
+ plan_text = ""
699
836
  plan_messages = [
700
- {"role": "system", "content": system},
701
- {"role": "user", "content": f"{prompt}\n\nBefore making any code changes, briefly state:\n1. The goal\n2. Files to examine\n3. Files to modify and how\n\nKeep it short. 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."}
702
839
  ]
703
-
840
+
704
841
  try:
705
842
  plan_response = client.chat.completions.create(
706
843
  model=MODEL,
@@ -708,23 +845,26 @@ def run_prompt(client, prompt, system):
708
845
  messages=plan_messages,
709
846
  )
710
847
  plan_text = plan_response.choices[0].message.content.strip()
711
-
848
+
712
849
  # Print the plan with formatting
713
850
  print(f"{C.CYAN}{'─' * 60}{C.RESET}", file=sys.stderr, flush=True)
714
851
  for line in plan_text.split('\n'):
715
852
  print(f"{C.CYAN} {line}{C.RESET}", file=sys.stderr, flush=True)
716
853
  print(f"{C.CYAN}{'─' * 60}{C.RESET}\n", file=sys.stderr, flush=True)
717
-
854
+
718
855
  except Exception as e:
719
856
  print(f"{C.YELLOW}Could not generate plan: {e}{C.RESET}", file=sys.stderr, flush=True)
720
- plan_text = ""
721
-
722
- # PHASE 2: Execution - proceed with the actual coding
857
+
858
+ # PHASE 2: Execution - inject plan into the conversation so the agent can reference it
723
859
  print(f"{C.BOLD}{C.GREEN}🔨 EXECUTING PLAN{C.RESET}\n", file=sys.stderr, flush=True)
724
-
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
+
725
865
  messages = [
726
- {"role": "system", "content": system},
727
- {"role": "user", "content": prompt}
866
+ {"role": "system", "content": exec_system},
867
+ {"role": "user", "content": exec_user_content}
728
868
  ]
729
869
  files_modified = set()
730
870
  files_created = set()
@@ -867,37 +1007,22 @@ def run_prompt(client, prompt, system):
867
1007
  final_text = msg.content
868
1008
  break
869
1009
 
870
- # 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)
871
1011
  summary = final_text.strip()
872
- if not any(is_token_limit_error(e) for e in errors):
873
- summary_messages = [
874
- {"role": "system", "content": "You are a helpful assistant that summarizes code changes."},
875
- {"role": "user", "content": (
876
- f"Summarize these code changes in 2-3 sentences for a code review tool.\n\n"
877
- f"Files modified: {', '.join(sorted(files_modified)) or 'none'}\n"
878
- f"Files created: {', '.join(sorted(files_created)) or 'none'}\n\n"
879
- f"Agent's final notes:\n{final_text[:2000]}\n\n"
880
- f"Focus on what changed, what was added/fixed, and why. Be specific. No preamble."
881
- )},
882
- ]
883
-
884
- try:
885
- summary_response = client.chat.completions.create(
886
- model=MODEL,
887
- max_tokens=1024,
888
- messages=summary_messages,
889
- )
890
-
891
- if hasattr(summary_response, "usage") and summary_response.usage:
892
- total_input_tokens += summary_response.usage.prompt_tokens
893
- total_output_tokens += summary_response.usage.completion_tokens
894
-
895
- summary = summary_response.choices[0].message.content.strip()
896
- except Exception as e:
897
- if is_token_limit_error(e):
898
- errors.append(str(e))
899
- else:
900
- 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."
901
1026
 
902
1027
  result = {
903
1028
  "completed": len(errors) == 0,
@@ -935,24 +1060,27 @@ def main():
935
1060
  if machine_id:
936
1061
  client_kwargs["default_headers"] = {"X-Machine-Id": machine_id}
937
1062
  client = OpenAI(**client_kwargs)
938
- system = SYSTEM_PROMPT.format(cwd=os.getcwd(), approach=APPROACH_BLOCK)
1063
+ cwd = os.getcwd()
939
1064
 
940
1065
  # Load codebase map if available
1066
+ map_suffix = ""
941
1067
  map_file = Path.cwd() / ".coder" / "map.md"
942
1068
  if map_file.exists():
943
1069
  try:
944
1070
  map_content = map_file.read_text(encoding="utf-8").strip()
945
1071
  if len(map_content) > 20000:
946
- map_content = map_content[:20000] + "\n\n... (map truncated use list_files to explore further)"
947
- 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}"
948
1074
  print(f"{C.DIM}Loaded codebase map ({map_content.count(chr(10))} lines){C.RESET}", file=sys.stderr)
949
1075
  except OSError:
950
1076
  pass
951
1077
 
952
1078
  # ── Headless prompt mode ────────────────────────────────────────────
953
1079
  if args.prompt:
1080
+ plan_system = PLAN_PROMPT.format(cwd=cwd) + map_suffix
1081
+ exec_system = EXEC_PROMPT.format(cwd=cwd) + map_suffix
954
1082
  try:
955
- result = run_prompt(client, args.prompt, system)
1083
+ result = run_prompt(client, args.prompt, plan_system, exec_system)
956
1084
  print(json.dumps(result, indent=2))
957
1085
  sys.exit(0 if result["completed"] else 1)
958
1086
  except Exception as e:
@@ -977,6 +1105,7 @@ def main():
977
1105
  sys.exit(130)
978
1106
 
979
1107
  # ── Interactive REPL mode ───────────────────────────────────────────
1108
+ system = REPL_PROMPT.format(cwd=cwd) + map_suffix
980
1109
  messages = []
981
1110
 
982
1111
  mode_label = f" {C.YELLOW}(safe mode){C.RESET}" if SAFE_MODE else ""