automata-cli 0.2.2 → 0.3.0-develop.112

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.
Files changed (3) hide show
  1. package/README.md +16 -0
  2. package/dist/index.js +660 -105
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -83,6 +83,22 @@ When no version is given, the latest semver tag on `master` is detected and the
83
83
 
84
84
  ---
85
85
 
86
+ ## `automata implement-next`
87
+
88
+ Find the next open GitHub issue matching the configured filter, claim it, and invoke Claude Code to implement it.
89
+
90
+ ```bash
91
+ automata implement-next # find, claim, and implement
92
+ automata implement-next --query-only # print the issue and exit
93
+ automata implement-next --yolo # skip Claude permission prompts
94
+ automata implement-next --json # JSON output
95
+ automata implement-next --no-claude # claim without launching Claude
96
+ ```
97
+
98
+ See [docs/implement-next.md](docs/implement-next.md) for full details.
99
+
100
+ ---
101
+
86
102
  ## Development
87
103
 
88
104
  ### Prerequisites
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command4 } from "commander";
4
+ import { Command as Command6 } from "commander";
5
5
 
6
6
  // src/version.ts
7
7
  import { readFileSync } from "fs";
@@ -19,16 +19,46 @@ import React2 from "react";
19
19
  // src/config/ConfigWizard.tsx
20
20
  import { useState } from "react";
21
21
  import { Box, Text, useInput, useApp } from "ink";
22
+ import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
23
+ import { join as join2 } from "path";
22
24
 
23
25
  // src/config/configStore.ts
24
26
  import { readFileSync as readFileSync2, writeFileSync, mkdirSync } from "fs";
25
- import { join } from "path";
27
+ import { join, resolve as resolve2, sep } from "path";
28
+ var DEFAULT_CLAUDE_SYSTEM_PROMPT = "You are an expert software engineer. Implement the following issue according to the project's existing conventions and style. Make minimal, targeted changes that satisfy the requirements. Run tests and linting before finishing.";
29
+ var DEFAULT_FIX_COMMENTS_PROMPT = "You are an expert software engineer reviewing a pull request. Below are the open review comments left by reviewers on this PR. Please address each comment by making the appropriate code changes. Focus on the reviewer's concerns and make minimal, targeted changes that resolve each comment without altering unrelated code.";
30
+ var DEFAULT_SONAR_PROMPT = "You are an expert software engineer. You have been given the URL of a SonarCloud analysis for this pull request. If the `sonar-quality-gate` skill is available in this repository, use it. The project is public, so use the SonarCloud REST API directly (no authentication required) rather than scraping the URL. Inspect both the quality gate and the list of issues for this pull request. If the quality gate fails because of duplication or another metric-based condition, use the relevant Sonar APIs to identify the affected files and details instead of relying only on the issues endpoint. Fix all new issues and quality-gate failures reported. Focus on code smells, bugs, vulnerabilities, and blocking quality-gate conditions flagged in this PR. Make targeted, minimal changes that resolve each issue without altering unrelated code.";
26
31
  var CONFIG_DIR = ".automata";
27
32
  var CONFIG_FILE = "config.json";
28
33
  function configPath() {
29
34
  return join(process.cwd(), CONFIG_DIR, CONFIG_FILE);
30
35
  }
31
- function readConfig() {
36
+ function automataDir() {
37
+ return join(process.cwd(), CONFIG_DIR);
38
+ }
39
+ function resolvePromptRef(value, dir) {
40
+ if (!value.endsWith(".md")) return value;
41
+ if (value.includes("/") || value.includes("\\")) {
42
+ throw new Error(`Prompt file "${value}" must be a plain filename with no subdirectories`);
43
+ }
44
+ const fullPath = resolve2(dir, value);
45
+ const safeBase = resolve2(dir) + sep;
46
+ if (!fullPath.startsWith(safeBase)) {
47
+ throw new Error(`Prompt file "${value}" resolves outside .automata/`);
48
+ }
49
+ try {
50
+ return readFileSync2(fullPath, "utf8");
51
+ } catch (err) {
52
+ if (err?.code === "ENOENT") {
53
+ throw new Error(
54
+ `Prompt file "${value}" not found in "${dir}". Expected path: ${fullPath}`,
55
+ { cause: err }
56
+ );
57
+ }
58
+ throw err;
59
+ }
60
+ }
61
+ function readRawConfig() {
32
62
  try {
33
63
  const raw = readFileSync2(configPath(), "utf8");
34
64
  return JSON.parse(raw);
@@ -36,6 +66,26 @@ function readConfig() {
36
66
  return {};
37
67
  }
38
68
  }
69
+ function readConfig() {
70
+ let config;
71
+ try {
72
+ const raw = readFileSync2(configPath(), "utf8");
73
+ config = JSON.parse(raw);
74
+ } catch {
75
+ return {};
76
+ }
77
+ const dir = automataDir();
78
+ if (config.claudeSystemPrompt) {
79
+ config.claudeSystemPrompt = resolvePromptRef(config.claudeSystemPrompt, dir);
80
+ }
81
+ if (config.prompts?.sonar) {
82
+ config.prompts.sonar = resolvePromptRef(config.prompts.sonar, dir);
83
+ }
84
+ if (config.prompts?.fixComments) {
85
+ config.prompts.fixComments = resolvePromptRef(config.prompts.fixComments, dir);
86
+ }
87
+ return config;
88
+ }
39
89
  function writeConfig(config) {
40
90
  const dir = join(process.cwd(), CONFIG_DIR);
41
91
  mkdirSync(dir, { recursive: true });
@@ -44,6 +94,11 @@ function writeConfig(config) {
44
94
 
45
95
  // src/config/ConfigWizard.tsx
46
96
  import { jsx, jsxs } from "react/jsx-runtime";
97
+ function writePromptFile(filename, content) {
98
+ const dir = join2(process.cwd(), ".automata");
99
+ mkdirSync2(dir, { recursive: true });
100
+ writeFileSync2(join2(dir, filename), content, "utf8");
101
+ }
47
102
  var REMOTE_OPTIONS = [
48
103
  { label: "GitHub", value: "gh" },
49
104
  { label: "Azure DevOps", value: "azdo" }
@@ -53,22 +108,48 @@ var TECHNIQUE_OPTIONS = [
53
108
  { label: "By Assignee", value: "assignee" },
54
109
  { label: "By Title Contains", value: "title-contains" }
55
110
  ];
111
+ var MAIN_MENU_OPTIONS = ["Remote / Mode", "Implement-Next", "Prompts"];
112
+ var PROMPTS_MENU_OPTIONS = ["Sonar", "Fix-Comments"];
56
113
  function ConfigWizard() {
57
114
  const existing = readConfig();
115
+ const rawExisting = readRawConfig();
58
116
  const initialRemoteIndex = REMOTE_OPTIONS.findIndex((o) => o.value === existing.remoteType);
59
117
  const initialTechIndex = TECHNIQUE_OPTIONS.findIndex((o) => o.value === existing.issueDiscoveryTechnique);
60
- const [screen, setScreen] = useState("remote");
118
+ const [screen, setScreen] = useState("main");
119
+ const [mainMenuIndex, setMainMenuIndex] = useState(0);
61
120
  const [selectedRemoteIndex, setSelectedRemoteIndex] = useState(initialRemoteIndex >= 0 ? initialRemoteIndex : 0);
62
121
  const [selectedTechIndex, setSelectedTechIndex] = useState(initialTechIndex >= 0 ? initialTechIndex : 0);
63
122
  const [discoveryValue, setDiscoveryValue] = useState(existing.issueDiscoveryValue ?? "");
64
123
  const [systemPrompt, setSystemPrompt] = useState(existing.claudeSystemPrompt ?? "");
124
+ const [promptsMenuIndex, setPromptsMenuIndex] = useState(0);
125
+ const [sonarPrompt, setSonarPrompt] = useState(existing.prompts?.sonar ?? DEFAULT_SONAR_PROMPT);
126
+ const [fixCommentsPrompt, setFixCommentsPrompt] = useState(
127
+ existing.prompts?.fixComments ?? DEFAULT_FIX_COMMENTS_PROMPT
128
+ );
65
129
  const [pendingRemote, setPendingRemote] = useState(existing.remoteType ?? "gh");
66
130
  const [pendingTechnique, setPendingTechnique] = useState(
67
131
  existing.issueDiscoveryTechnique ?? "label"
68
132
  );
69
133
  const { exit } = useApp();
70
134
  useInput((input, key) => {
71
- if (screen === "remote") {
135
+ if (screen === "main") {
136
+ if (key.upArrow) {
137
+ setMainMenuIndex((i) => i > 0 ? i - 1 : MAIN_MENU_OPTIONS.length - 1);
138
+ } else if (key.downArrow) {
139
+ setMainMenuIndex((i) => i < MAIN_MENU_OPTIONS.length - 1 ? i + 1 : 0);
140
+ } else if (key.return) {
141
+ const chosen = MAIN_MENU_OPTIONS[mainMenuIndex];
142
+ if (chosen === "Remote / Mode") {
143
+ setScreen("remote");
144
+ } else if (chosen === "Implement-Next") {
145
+ setScreen("technique");
146
+ } else {
147
+ setScreen("prompts-menu");
148
+ }
149
+ } else if (key.escape || key.ctrl && input === "c") {
150
+ exit();
151
+ }
152
+ } else if (screen === "remote") {
72
153
  if (key.upArrow) {
73
154
  setSelectedRemoteIndex((i) => i > 0 ? i - 1 : REMOTE_OPTIONS.length - 1);
74
155
  } else if (key.downArrow) {
@@ -79,10 +160,12 @@ function ConfigWizard() {
79
160
  if (chosen.value === "gh") {
80
161
  setScreen("technique");
81
162
  } else {
82
- writeConfig({ ...existing, remoteType: chosen.value });
163
+ writeConfig({ ...rawExisting, remoteType: chosen.value });
83
164
  exit();
84
165
  }
85
- } else if (key.escape || key.ctrl && input === "c") {
166
+ } else if (key.escape) {
167
+ setScreen("main");
168
+ } else if (key.ctrl && input === "c") {
86
169
  exit();
87
170
  }
88
171
  } else if (screen === "technique") {
@@ -94,7 +177,9 @@ function ConfigWizard() {
94
177
  const chosen = TECHNIQUE_OPTIONS[selectedTechIndex];
95
178
  setPendingTechnique(chosen.value);
96
179
  setScreen("value");
97
- } else if (key.escape || key.ctrl && input === "c") {
180
+ } else if (key.escape) {
181
+ setScreen("main");
182
+ } else if (key.ctrl && input === "c") {
98
183
  exit();
99
184
  }
100
185
  } else if (screen === "value") {
@@ -102,46 +187,128 @@ function ConfigWizard() {
102
187
  setScreen("system-prompt");
103
188
  } else if (key.backspace || key.delete) {
104
189
  setDiscoveryValue((v) => v.slice(0, -1));
105
- } else if (key.escape || key.ctrl && input === "c") {
190
+ } else if (key.escape) {
191
+ setScreen("main");
192
+ } else if (key.ctrl && input === "c") {
106
193
  exit();
107
194
  } else if (input && !key.ctrl && !key.meta) {
108
195
  setDiscoveryValue((v) => v + input);
109
196
  }
110
197
  } else if (screen === "system-prompt") {
111
198
  if (key.return) {
199
+ let claudeSystemPromptValue;
200
+ if (systemPrompt) {
201
+ writePromptFile("claude-system-prompt.md", systemPrompt);
202
+ claudeSystemPromptValue = "claude-system-prompt.md";
203
+ }
112
204
  writeConfig({
113
- ...existing,
205
+ ...rawExisting,
114
206
  remoteType: pendingRemote,
115
207
  issueDiscoveryTechnique: pendingTechnique,
116
208
  issueDiscoveryValue: discoveryValue || void 0,
117
- claudeSystemPrompt: systemPrompt || void 0
209
+ claudeSystemPrompt: claudeSystemPromptValue
118
210
  });
119
211
  exit();
120
212
  } else if (key.backspace || key.delete) {
121
213
  setSystemPrompt((v) => v.slice(0, -1));
122
- } else if (key.escape || key.ctrl && input === "c") {
214
+ } else if (key.escape) {
215
+ setScreen("main");
216
+ } else if (key.ctrl && input === "c") {
123
217
  exit();
124
218
  } else if (input && !key.ctrl && !key.meta) {
125
219
  setSystemPrompt((v) => v + input);
126
220
  }
221
+ } else if (screen === "prompts-menu") {
222
+ if (key.upArrow) {
223
+ setPromptsMenuIndex((i) => i > 0 ? i - 1 : PROMPTS_MENU_OPTIONS.length - 1);
224
+ } else if (key.downArrow) {
225
+ setPromptsMenuIndex((i) => i < PROMPTS_MENU_OPTIONS.length - 1 ? i + 1 : 0);
226
+ } else if (key.return) {
227
+ const chosen = PROMPTS_MENU_OPTIONS[promptsMenuIndex];
228
+ if (chosen === "Sonar") {
229
+ setScreen("sonar-prompt");
230
+ } else {
231
+ setScreen("fix-comments-prompt");
232
+ }
233
+ } else if (key.escape) {
234
+ setScreen("main");
235
+ } else if (key.ctrl && input === "c") {
236
+ exit();
237
+ }
238
+ } else if (screen === "sonar-prompt") {
239
+ if (key.return) {
240
+ let sonarValue;
241
+ if (sonarPrompt) {
242
+ writePromptFile("sonar-prompt.md", sonarPrompt);
243
+ sonarValue = "sonar-prompt.md";
244
+ }
245
+ const current = readRawConfig();
246
+ writeConfig({
247
+ ...current,
248
+ prompts: { ...current.prompts, sonar: sonarValue }
249
+ });
250
+ setScreen("prompts-menu");
251
+ } else if (key.backspace || key.delete) {
252
+ setSonarPrompt((v) => v.slice(0, -1));
253
+ } else if (key.escape) {
254
+ setScreen("prompts-menu");
255
+ } else if (key.ctrl && input === "c") {
256
+ exit();
257
+ } else if (input && !key.ctrl && !key.meta) {
258
+ setSonarPrompt((v) => v + input);
259
+ }
260
+ } else if (screen === "fix-comments-prompt") {
261
+ if (key.return) {
262
+ let fixCommentsValue;
263
+ if (fixCommentsPrompt) {
264
+ writePromptFile("fix-comments-prompt.md", fixCommentsPrompt);
265
+ fixCommentsValue = "fix-comments-prompt.md";
266
+ }
267
+ const current = readRawConfig();
268
+ writeConfig({
269
+ ...current,
270
+ prompts: { ...current.prompts, fixComments: fixCommentsValue }
271
+ });
272
+ setScreen("prompts-menu");
273
+ } else if (key.backspace || key.delete) {
274
+ setFixCommentsPrompt((v) => v.slice(0, -1));
275
+ } else if (key.escape) {
276
+ setScreen("prompts-menu");
277
+ } else if (key.ctrl && input === "c") {
278
+ exit();
279
+ } else if (input && !key.ctrl && !key.meta) {
280
+ setFixCommentsPrompt((v) => v + input);
281
+ }
127
282
  }
128
283
  });
129
- if (screen === "remote") {
284
+ if (screen === "main") {
130
285
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginY: 1, children: [
131
286
  /* @__PURE__ */ jsx(Text, { bold: true, children: "Configure Automata" }),
132
287
  /* @__PURE__ */ jsx(Text, { children: " " }),
288
+ MAIN_MENU_OPTIONS.map((option, index) => /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { color: index === mainMenuIndex ? "cyan" : void 0, children: [
289
+ index === mainMenuIndex ? "\u276F " : " ",
290
+ option
291
+ ] }) }, option)),
292
+ /* @__PURE__ */ jsx(Text, { children: " " }),
293
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191/\u2193 to move \xB7 Enter to select \xB7 Ctrl+C to cancel" })
294
+ ] });
295
+ }
296
+ if (screen === "remote") {
297
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginY: 1, children: [
298
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Remote / Mode" }),
299
+ /* @__PURE__ */ jsx(Text, { children: " " }),
133
300
  /* @__PURE__ */ jsx(Text, { children: "Remote environment type:" }),
134
301
  REMOTE_OPTIONS.map((option, index) => /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { color: index === selectedRemoteIndex ? "cyan" : void 0, children: [
135
302
  index === selectedRemoteIndex ? "\u276F " : " ",
136
303
  option.label
137
304
  ] }) }, option.value)),
138
305
  /* @__PURE__ */ jsx(Text, { children: " " }),
139
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191/\u2193 to move \xB7 Enter to confirm \xB7 Ctrl+C to cancel" })
306
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191/\u2193 to move \xB7 Enter to confirm \xB7 Esc to go back \xB7 Ctrl+C to cancel" })
140
307
  ] });
141
308
  }
142
309
  if (screen === "technique") {
143
310
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginY: 1, children: [
144
- /* @__PURE__ */ jsx(Text, { bold: true, children: "Configure Issue Discovery Technique" }),
311
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Implement-Next \u2014 Issue Discovery Technique" }),
145
312
  /* @__PURE__ */ jsx(Text, { children: " " }),
146
313
  /* @__PURE__ */ jsx(Text, { children: "How to find the next issue to work on:" }),
147
314
  TECHNIQUE_OPTIONS.map((option, index) => /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { color: index === selectedTechIndex ? "cyan" : void 0, children: [
@@ -149,13 +316,13 @@ function ConfigWizard() {
149
316
  option.label
150
317
  ] }) }, option.value)),
151
318
  /* @__PURE__ */ jsx(Text, { children: " " }),
152
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191/\u2193 to move \xB7 Enter to confirm \xB7 Ctrl+C to cancel" })
319
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191/\u2193 to move \xB7 Enter to confirm \xB7 Esc to go back \xB7 Ctrl+C to cancel" })
153
320
  ] });
154
321
  }
155
322
  if (screen === "value") {
156
323
  const techLabel = TECHNIQUE_OPTIONS.find((t) => t.value === pendingTechnique)?.label ?? pendingTechnique;
157
324
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginY: 1, children: [
158
- /* @__PURE__ */ jsx(Text, { bold: true, children: "Configure Issue Discovery Value" }),
325
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Implement-Next \u2014 Issue Discovery Value" }),
159
326
  /* @__PURE__ */ jsx(Text, { children: " " }),
160
327
  /* @__PURE__ */ jsxs(Text, { children: [
161
328
  techLabel,
@@ -167,22 +334,66 @@ function ConfigWizard() {
167
334
  ] })
168
335
  ] }),
169
336
  /* @__PURE__ */ jsx(Text, { children: " " }),
170
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Type value \xB7 Enter to confirm \xB7 Ctrl+C to cancel" })
337
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Type value \xB7 Enter to continue \xB7 Esc to go back \xB7 Ctrl+C to cancel" })
338
+ ] });
339
+ }
340
+ if (screen === "system-prompt") {
341
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginY: 1, children: [
342
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Implement-Next \u2014 Claude System Prompt" }),
343
+ /* @__PURE__ */ jsx(Text, { children: " " }),
344
+ /* @__PURE__ */ jsxs(Text, { children: [
345
+ "System prompt (optional):",
346
+ " ",
347
+ /* @__PURE__ */ jsxs(Text, { color: "cyan", children: [
348
+ systemPrompt,
349
+ /* @__PURE__ */ jsx(Text, { children: "_" })
350
+ ] })
351
+ ] }),
352
+ /* @__PURE__ */ jsx(Text, { children: " " }),
353
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Type prompt \xB7 Enter to save and exit \xB7 Esc to go back \xB7 Ctrl+C to cancel" })
354
+ ] });
355
+ }
356
+ if (screen === "prompts-menu") {
357
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginY: 1, children: [
358
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Prompts" }),
359
+ /* @__PURE__ */ jsx(Text, { children: " " }),
360
+ PROMPTS_MENU_OPTIONS.map((option, index) => /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { color: index === promptsMenuIndex ? "cyan" : void 0, children: [
361
+ index === promptsMenuIndex ? "\u276F " : " ",
362
+ option
363
+ ] }) }, option)),
364
+ /* @__PURE__ */ jsx(Text, { children: " " }),
365
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191/\u2193 to move \xB7 Enter to edit \xB7 Esc to go back \xB7 Ctrl+C to cancel" })
366
+ ] });
367
+ }
368
+ if (screen === "sonar-prompt") {
369
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginY: 1, children: [
370
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Prompts \u2014 Sonar" }),
371
+ /* @__PURE__ */ jsx(Text, { children: " " }),
372
+ /* @__PURE__ */ jsxs(Text, { children: [
373
+ "Sonar prompt:",
374
+ " ",
375
+ /* @__PURE__ */ jsxs(Text, { color: "cyan", children: [
376
+ sonarPrompt,
377
+ /* @__PURE__ */ jsx(Text, { children: "_" })
378
+ ] })
379
+ ] }),
380
+ /* @__PURE__ */ jsx(Text, { children: " " }),
381
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Type prompt \xB7 Enter to save \xB7 Esc to go back \xB7 Ctrl+C to cancel" })
171
382
  ] });
172
383
  }
173
384
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginY: 1, children: [
174
- /* @__PURE__ */ jsx(Text, { bold: true, children: "Configure Claude System Prompt" }),
385
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Prompts \u2014 Fix-Comments" }),
175
386
  /* @__PURE__ */ jsx(Text, { children: " " }),
176
387
  /* @__PURE__ */ jsxs(Text, { children: [
177
- "System prompt (optional):",
388
+ "Fix-Comments prompt:",
178
389
  " ",
179
390
  /* @__PURE__ */ jsxs(Text, { color: "cyan", children: [
180
- systemPrompt,
391
+ fixCommentsPrompt,
181
392
  /* @__PURE__ */ jsx(Text, { children: "_" })
182
393
  ] })
183
394
  ] }),
184
395
  /* @__PURE__ */ jsx(Text, { children: " " }),
185
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Type prompt \xB7 Enter to save and exit \xB7 Ctrl+C to cancel" })
396
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Type prompt \xB7 Enter to save \xB7 Esc to go back \xB7 Ctrl+C to cancel" })
186
397
  ] });
187
398
  }
188
399
 
@@ -195,7 +406,7 @@ var configSetType = new Command("type").description("Set the remote environment
195
406
  `);
196
407
  process.exit(1);
197
408
  }
198
- const current = readConfig();
409
+ const current = readRawConfig();
199
410
  writeConfig({ ...current, remoteType: value });
200
411
  process.stdout.write(`Remote type set to: ${value}
201
412
  `);
@@ -206,19 +417,19 @@ var configSetIssueDiscoveryTechnique = new Command("issue-discovery-technique").
206
417
  `);
207
418
  process.exit(1);
208
419
  }
209
- const current = readConfig();
420
+ const current = readRawConfig();
210
421
  writeConfig({ ...current, issueDiscoveryTechnique: value });
211
422
  process.stdout.write(`Issue discovery technique set to: ${value}
212
423
  `);
213
424
  });
214
425
  var configSetIssueDiscoveryValue = new Command("issue-discovery-value").description("Set the value for the issue discovery technique (label name, username, or search string)").argument("<value>", "The filter value").action((value) => {
215
- const current = readConfig();
426
+ const current = readRawConfig();
216
427
  writeConfig({ ...current, issueDiscoveryValue: value });
217
428
  process.stdout.write(`Issue discovery value set to: ${value}
218
429
  `);
219
430
  });
220
431
  var configSetClaudeSystemPrompt = new Command("claude-system-prompt").description("Set the system prompt used when invoking Claude Code").argument("<value>", "System prompt text").action((value) => {
221
- const current = readConfig();
432
+ const current = readRawConfig();
222
433
  writeConfig({ ...current, claudeSystemPrompt: value });
223
434
  process.stdout.write(`Claude system prompt set.
224
435
  `);
@@ -335,7 +546,31 @@ function fetchCheckRunOutputs(ownerRepo, sha) {
335
546
  }
336
547
  return map;
337
548
  }
338
- function getPrInfoGh(branch) {
549
+ function extractSonarProjectKey(url) {
550
+ try {
551
+ const parsed = new URL(url);
552
+ const id = parsed.searchParams.get("id");
553
+ return id ?? null;
554
+ } catch {
555
+ return null;
556
+ }
557
+ }
558
+ async function fetchSonarNewIssues(projectKey, prNumber) {
559
+ const apiUrl = `https://sonarcloud.io/api/issues/search?componentKeys=${encodeURIComponent(projectKey)}&pullRequest=${String(prNumber)}&resolved=false&ps=1`;
560
+ const controller = new AbortController();
561
+ const timeoutId = setTimeout(() => controller.abort(), 5e3);
562
+ try {
563
+ const response = await fetch(apiUrl, { signal: controller.signal });
564
+ if (!response.ok) return null;
565
+ const data = await response.json();
566
+ return data.paging?.total ?? null;
567
+ } catch {
568
+ return null;
569
+ } finally {
570
+ clearTimeout(timeoutId);
571
+ }
572
+ }
573
+ async function getPrInfoGh(branch) {
339
574
  const { stdout, stderr, status } = run2("gh", [
340
575
  "pr",
341
576
  "view",
@@ -365,9 +600,35 @@ function getPrInfoGh(branch) {
365
600
  detailsUrl: enriched?.detailsUrl || c.detailsUrl || ""
366
601
  };
367
602
  });
368
- return { number: raw.number, title: raw.title, state: raw.state, url: raw.url, checks };
603
+ const sonarCheck = checks.find((c) => {
604
+ try {
605
+ const hostname = new URL(c.detailsUrl).hostname;
606
+ return hostname === "sonarcloud.io" || hostname.endsWith(".sonarcloud.io");
607
+ } catch {
608
+ return false;
609
+ }
610
+ });
611
+ let sonarcloudUrl;
612
+ let sonarNewIssues;
613
+ if (sonarCheck) {
614
+ sonarcloudUrl = sonarCheck.detailsUrl;
615
+ const projectKey = extractSonarProjectKey(sonarCheck.detailsUrl);
616
+ if (projectKey) {
617
+ sonarNewIssues = await fetchSonarNewIssues(projectKey, raw.number);
618
+ } else {
619
+ sonarNewIssues = null;
620
+ }
621
+ }
622
+ return {
623
+ number: raw.number,
624
+ title: raw.title,
625
+ state: raw.state,
626
+ url: raw.url,
627
+ checks,
628
+ ...sonarcloudUrl === void 0 ? {} : { sonarcloudUrl, sonarNewIssues }
629
+ };
369
630
  }
370
- function getPrInfo2(branch) {
631
+ async function getPrInfo2(branch) {
371
632
  const config = readConfig();
372
633
  if (config.remoteType === "azdo") {
373
634
  return getPrInfo();
@@ -471,6 +732,23 @@ function getPrComments(branch) {
471
732
  }
472
733
  return getPrCommentsGh(branch);
473
734
  }
735
+ function resolveCurrentBranchComments() {
736
+ let branch;
737
+ try {
738
+ branch = getCurrentBranch();
739
+ } catch (err) {
740
+ return { ok: false, kind: "error", message: err.message };
741
+ }
742
+ let raw;
743
+ try {
744
+ raw = getPrComments(branch);
745
+ } catch (err) {
746
+ return { ok: false, kind: "error", message: err.message };
747
+ }
748
+ if (raw === "unsupported") return { ok: false, kind: "unsupported" };
749
+ if (raw === null) return { ok: false, kind: "no-pr", branch };
750
+ return { ok: true, branch, comments: raw };
751
+ }
474
752
  var SEMVER_RE = /^v?(\d+)\.(\d+)\.(\d+)$/;
475
753
  function getLatestTagOnMaster() {
476
754
  const { stdout, status } = run2("git", [
@@ -553,33 +831,34 @@ function formatChecks(checks) {
553
831
  const sym = checkSymbol(check);
554
832
  const pending = check.status !== "COMPLETED" ? " (pending)" : "";
555
833
  lines.push(` ${sym} ${check.name}${pending}`);
556
- if (check.conclusion !== null && FAIL_CONCLUSIONS.has(check.conclusion)) {
557
- const desc = check.description.trim();
558
- const url = check.detailsUrl.trim();
559
- if (desc) lines.push(` Details: ${desc}`);
560
- if (url) lines.push(` URL: ${url}`);
561
- if (!desc && !url) lines.push(` Details: (no details available)`);
562
- }
563
834
  }
564
835
  return lines.join("\n") + "\n";
565
836
  }
566
837
  function sleep(ms) {
567
- return new Promise((resolve2) => setTimeout(resolve2, ms));
838
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
839
+ }
840
+ function isSonarCheck(check) {
841
+ try {
842
+ const hostname = new URL(check.detailsUrl).hostname;
843
+ return hostname === "sonarcloud.io" || hostname.endsWith(".sonarcloud.io");
844
+ } catch {
845
+ return false;
846
+ }
568
847
  }
569
848
  function formatFailedChecks(failed) {
570
- const lines = [];
849
+ const lines = ["FailedChecks:"];
571
850
  for (const check of failed) {
572
851
  lines.push(` \u2717 ${check.name}`);
573
852
  const desc = check.description.trim();
574
853
  const url = check.detailsUrl.trim();
575
854
  if (desc) lines.push(` Details: ${desc}`);
576
- if (url) lines.push(` URL: ${url}`);
855
+ if (url && (isSonarCheck(check) || !desc)) lines.push(` URL: ${url}`);
577
856
  if (!desc && !url) lines.push(` Details: (no details available)`);
578
857
  }
579
858
  return lines.join("\n") + "\n";
580
859
  }
581
860
  var POLL_INTERVAL_MS = 1e4;
582
- var getPrInfoCmd = new Command2("get-pr-info").description("Show pull request info for the current branch").option("--json", "Output as JSON").option("--wait-finish-checks", "Poll until all checks complete, then report pass/fail (exit 1 on failure)").addHelpText(
861
+ var getPrInfoCmd = new Command2("get-pr-info").description("Show pull request info for the current branch").option("--json", "Output as JSON").option("--wait-finish-checks", "Poll until all checks complete, then print the normal get-pr-info output").addHelpText(
583
862
  "after",
584
863
  `
585
864
  Check status symbols:
@@ -588,10 +867,11 @@ Check status symbols:
588
867
  \u25CF Pending (status: QUEUED or IN_PROGRESS)
589
868
  \u25CB Skipped (conclusion: SKIPPED or NEUTRAL)
590
869
 
591
- Failure details are printed beneath each \u2717 check.
870
+ When checks fail, details are printed in a trailing FailedChecks section.
592
871
  See docs/git.md for full output reference.`
593
872
  ).action(async (options) => {
594
873
  let branch;
874
+ let pr = null;
595
875
  try {
596
876
  branch = getCurrentBranch();
597
877
  } catch (err) {
@@ -600,54 +880,31 @@ See docs/git.md for full output reference.`
600
880
  process.exit(1);
601
881
  }
602
882
  if (options.waitFinishChecks) {
603
- let pr2;
604
883
  while (true) {
605
884
  try {
606
- pr2 = getPrInfo2(branch);
885
+ pr = await getPrInfo2(branch);
607
886
  } catch (err) {
608
887
  process.stderr.write(`Error: ${err.message}
609
888
  `);
610
889
  process.exit(1);
611
890
  }
612
- if (pr2 === null) {
613
- process.stderr.write(`Error: No pull request found for branch: ${branch}
614
- `);
615
- process.exit(1);
891
+ if (pr === null) {
892
+ break;
616
893
  }
617
- const running = pr2.checks.filter((c) => c.status !== "COMPLETED");
894
+ const running = pr.checks.filter((c) => c.status !== "COMPLETED");
618
895
  if (running.length === 0) break;
619
896
  process.stdout.write(`Waiting for ${running.length} check(s) to complete...
620
897
  `);
621
898
  await sleep(POLL_INTERVAL_MS);
622
899
  }
623
- const failed = pr2.checks.filter((c) => c.conclusion !== null && FAIL_CONCLUSIONS.has(c.conclusion));
624
- if (failed.length === 0) {
625
- if (options.json) {
626
- process.stdout.write(JSON.stringify({ result: "passed" }, null, 2) + "\n");
627
- } else {
628
- process.stdout.write(`All checks passed. \u2713
629
- `);
630
- }
631
- process.exit(0);
632
- } else {
633
- if (options.json) {
634
- process.stdout.write(JSON.stringify({ result: "failed", failed }, null, 2) + "\n");
635
- } else {
636
- process.stdout.write(`${failed.length} check(s) failed:
900
+ } else {
901
+ try {
902
+ pr = await getPrInfo2(branch);
903
+ } catch (err) {
904
+ process.stderr.write(`Error: ${err.message}
637
905
  `);
638
- process.stdout.write(formatFailedChecks(failed));
639
- }
640
906
  process.exit(1);
641
907
  }
642
- return;
643
- }
644
- let pr;
645
- try {
646
- pr = getPrInfo2(branch);
647
- } catch (err) {
648
- process.stderr.write(`Error: ${err.message}
649
- `);
650
- process.exit(1);
651
908
  }
652
909
  if (pr === null) {
653
910
  process.stdout.write(`No pull request found for branch: ${branch}
@@ -657,13 +914,24 @@ See docs/git.md for full output reference.`
657
914
  if (options.json) {
658
915
  process.stdout.write(JSON.stringify(pr, null, 2) + "\n");
659
916
  } else {
917
+ const failed = pr.checks.filter((c) => c.conclusion !== null && FAIL_CONCLUSIONS.has(c.conclusion));
660
918
  process.stdout.write(`PR: #${pr.number}
661
919
  Title: ${pr.title}
662
920
  State: ${pr.state}
663
921
  URL: ${pr.url}
664
922
  `);
923
+ if (pr.sonarcloudUrl !== void 0) {
924
+ process.stdout.write(`Sonar: ${pr.sonarcloudUrl}
925
+ `);
926
+ const issueStr = pr.sonarNewIssues === null || pr.sonarNewIssues === void 0 ? "unavailable" : String(pr.sonarNewIssues);
927
+ process.stdout.write(`Sonar New Issues: ${issueStr}
928
+ `);
929
+ }
665
930
  process.stdout.write(formatCheckSummary(pr.checks));
666
931
  process.stdout.write(formatChecks(pr.checks));
932
+ if (failed.length > 0) {
933
+ process.stdout.write(formatFailedChecks(failed));
934
+ }
667
935
  }
668
936
  });
669
937
  var ANSI_ESCAPE_RE = new RegExp("\x1B(?:[@-Z\\\\-_]|\\[[0-?]*[ -/]*[@-~])", "g");
@@ -724,7 +992,7 @@ ${safeBody}`);
724
992
  }
725
993
  process.stdout.write(lines.join("\n\n") + "\n");
726
994
  });
727
- var finishFeatureCmd = new Command2("finish-feature").description("Clean up a merged feature branch: checkout develop, pull, and delete local branch").action(() => {
995
+ var finishFeatureCmd = new Command2("finish-feature").description("Clean up a merged feature branch: checkout develop, pull, and delete local branch").action(async () => {
728
996
  let branch;
729
997
  try {
730
998
  branch = getCurrentBranch();
@@ -745,7 +1013,7 @@ var finishFeatureCmd = new Command2("finish-feature").description("Clean up a me
745
1013
  }
746
1014
  let pr;
747
1015
  try {
748
- pr = getPrInfo2(branch);
1016
+ pr = await getPrInfo2(branch);
749
1017
  } catch (err) {
750
1018
  process.stderr.write(`Error: ${err.message}
751
1019
  `);
@@ -876,9 +1144,6 @@ var gitCommand = new Command2("git").description("Git workflow commands (some re
876
1144
 
877
1145
  // src/commands/getReady.ts
878
1146
  import { Command as Command3 } from "commander";
879
- import { spawnSync as spawnSync4 } from "child_process";
880
- import { existsSync } from "fs";
881
- import { delimiter, join as join2 } from "path";
882
1147
 
883
1148
  // src/config/githubService.ts
884
1149
  import { spawnSync as spawnSync3 } from "child_process";
@@ -903,10 +1168,6 @@ function listIssues(technique, value) {
903
1168
  "list",
904
1169
  "--state",
905
1170
  "open",
906
- "--sort",
907
- "created",
908
- "--order",
909
- "asc",
910
1171
  "--limit",
911
1172
  "1",
912
1173
  "--json",
@@ -941,42 +1202,195 @@ function postComment(issueNumber, body) {
941
1202
  }
942
1203
  }
943
1204
 
944
- // src/commands/getReady.ts
1205
+ // src/claude/claudeService.ts
1206
+ import { spawn, spawnSync as spawnSync4 } from "child_process";
1207
+ import { createInterface } from "readline";
1208
+ import { existsSync } from "fs";
1209
+ import { delimiter, join as join3 } from "path";
1210
+
1211
+ // src/cli/spawnUtils.ts
1212
+ function truncate(str, max) {
1213
+ return str.length > max ? str.slice(0, max) + "..." : str;
1214
+ }
1215
+ function handleSpawnError(error, toolName) {
1216
+ if (!error) return;
1217
+ const err = error;
1218
+ if (err.code === "ENOENT") {
1219
+ process.stderr.write(`Error: \`${toolName}\` CLI is not installed or not on PATH.
1220
+ `);
1221
+ process.exit(1);
1222
+ }
1223
+ process.stderr.write(`Error: ${err.message}
1224
+ `);
1225
+ process.exit(1);
1226
+ }
1227
+ function handleExitCode(status, toolName) {
1228
+ if (status === null) {
1229
+ process.stderr.write(`Error: ${toolName} terminated abnormally (exit code is null, likely due to a signal).
1230
+ `);
1231
+ process.exit(1);
1232
+ }
1233
+ if (status !== 0) {
1234
+ process.stderr.write(`Error: ${toolName} exited with code ${status}.
1235
+ `);
1236
+ process.exit(status);
1237
+ }
1238
+ }
1239
+
1240
+ // src/claude/claudeService.ts
945
1241
  function resolveCommand(name) {
946
1242
  const pathDirs = (process.env["PATH"] ?? "").split(delimiter);
947
1243
  for (const dir of pathDirs) {
948
- const candidate = join2(dir, name);
1244
+ const candidate = join3(dir, name);
949
1245
  if (existsSync(candidate)) return candidate;
950
1246
  }
951
1247
  return name;
952
1248
  }
953
- function invokeClaudeCode(issue, systemPrompt) {
954
- const prompt = systemPrompt ? `${systemPrompt}
955
-
956
- ${issue.body}` : issue.body;
957
- const claudeBin = resolveCommand("claude");
958
- const result = spawnSync4(claudeBin, ["-p", prompt], { encoding: "utf8", stdio: "inherit" });
959
- if (result.error) {
960
- const err = result.error;
961
- if (err.code === "ENOENT") {
962
- process.stderr.write("Error: `claude` CLI is not installed or not on PATH.\n");
963
- process.exit(1);
964
- }
965
- process.stderr.write(`Error: ${err.message}
1249
+ var MODEL_IDS = {
1250
+ opus: "claude-opus-4-6",
1251
+ sonnet: "claude-sonnet-4-6",
1252
+ haiku: "claude-haiku-4-5-20251001"
1253
+ };
1254
+ function resolveModelOption(opts) {
1255
+ const selected = ["opus", "sonnet", "haiku"].filter((m) => opts[m]);
1256
+ if (selected.length > 1) {
1257
+ process.stderr.write(`Error: --${selected[0]} and --${selected[1]} are mutually exclusive.
966
1258
  `);
967
1259
  process.exit(1);
968
1260
  }
969
- if (result.status !== 0) {
970
- process.stderr.write(`Error: Claude Code exited with code ${result.status ?? "unknown"}.
1261
+ return selected.length === 1 ? MODEL_IDS[selected[0]] : void 0;
1262
+ }
1263
+ function invokeClaudeCode(prompt, options = {}) {
1264
+ if (options.verbose) {
1265
+ return invokeClaudeCodeVerbose(prompt, options.yolo ?? false, options.model);
1266
+ }
1267
+ invokeClaudeCodeSync(prompt, options.yolo ?? false, options.model);
1268
+ }
1269
+ function invokeClaudeCodeSync(prompt, yolo, model) {
1270
+ const claudeBin = resolveCommand("claude");
1271
+ const args = [];
1272
+ if (yolo) args.push("--dangerously-skip-permissions");
1273
+ if (model) args.push("--model", model);
1274
+ args.push("-p", prompt);
1275
+ const result = spawnSync4(claudeBin, args, { encoding: "utf8", stdio: "inherit" });
1276
+ handleSpawnError(result.error, "claude");
1277
+ handleExitCode(result.status, "Claude Code");
1278
+ }
1279
+ function invokeClaudeCodeVerbose(prompt, yolo, model) {
1280
+ return new Promise((resolve3) => {
1281
+ const claudeBin = resolveCommand("claude");
1282
+ const args = [];
1283
+ if (yolo) args.push("--dangerously-skip-permissions");
1284
+ if (model) args.push("--model", model);
1285
+ args.push("--verbose", "--output-format", "stream-json", "-p", prompt);
1286
+ const child = spawn(claudeBin, args, { stdio: ["inherit", "pipe", "inherit"] });
1287
+ const rl = createInterface({ input: child.stdout });
1288
+ let turnCount = 0;
1289
+ child.on("error", (err) => {
1290
+ handleSpawnError(err, "claude");
1291
+ });
1292
+ rl.on("line", (line) => {
1293
+ try {
1294
+ const event = JSON.parse(line);
1295
+ formatEvent(event, turnCount);
1296
+ if (event["type"] === "assistant") turnCount++;
1297
+ } catch {
1298
+ }
1299
+ });
1300
+ child.on("close", (code) => {
1301
+ handleExitCode(code, "Claude Code");
1302
+ resolve3();
1303
+ });
1304
+ });
1305
+ }
1306
+ function formatEvent(event, turnCount) {
1307
+ const type = event["type"];
1308
+ if (type === "assistant") {
1309
+ const message = event["message"];
1310
+ const content = message?.["content"];
1311
+ if (!content) return;
1312
+ for (const block of content) {
1313
+ if (block["type"] === "tool_use") {
1314
+ const toolName = block["name"];
1315
+ const input = block["input"];
1316
+ const summary = summarizeTool(toolName, input);
1317
+ process.stderr.write(` [step ${turnCount + 1}] ${summary}
1318
+ `);
1319
+ } else if (block["type"] === "text") {
1320
+ const text = block["text"] ?? "";
1321
+ if (text.length > 0) {
1322
+ const preview = text.length > 120 ? text.slice(0, 120) + "..." : text;
1323
+ const firstLine = preview.split("\n")[0];
1324
+ process.stderr.write(` [step ${turnCount + 1}] ${firstLine}
1325
+ `);
1326
+ }
1327
+ }
1328
+ }
1329
+ } else if (type === "result") {
1330
+ const result = event["result"];
1331
+ const cost = event["cost_usd"];
1332
+ const duration = event["duration_ms"];
1333
+ const turns = event["num_turns"];
1334
+ process.stderr.write("\n--- Result ---\n");
1335
+ if (cost !== void 0 || duration !== void 0 || turns !== void 0) {
1336
+ const parts = [];
1337
+ if (turns !== void 0) parts.push(`${turns} turns`);
1338
+ if (duration !== void 0) parts.push(`${(duration / 1e3).toFixed(1)}s`);
1339
+ if (cost !== void 0) parts.push(`$${cost.toFixed(4)}`);
1340
+ process.stderr.write(` [info] ${parts.join(" | ")}
971
1341
  `);
972
- process.exit(result.status ?? 1);
1342
+ }
1343
+ if (result) {
1344
+ process.stdout.write(result + "\n");
1345
+ }
973
1346
  }
974
1347
  }
975
- var getReadyCommand = new Command3("get-ready").description("Find the next open GitHub issue matching the configured filter, claim it, and invoke Claude Code").option("--json", "Output issue details as JSON").option("--no-claude", "Skip Claude Code invocation after claiming the issue").action((options) => {
1348
+ function summarizeTool(name, input) {
1349
+ if (!input) return `tool: ${name}`;
1350
+ switch (name) {
1351
+ case "Read":
1352
+ return `reading ${input["file_path"] ?? "file"}`;
1353
+ case "Write":
1354
+ return `writing ${input["file_path"] ?? "file"}`;
1355
+ case "Edit":
1356
+ return `editing ${input["file_path"] ?? "file"}`;
1357
+ case "Bash":
1358
+ return `running: ${truncate(String(input["command"] ?? ""), 80)}`;
1359
+ case "Glob":
1360
+ return `searching files: ${input["pattern"] ?? ""}`;
1361
+ case "Grep":
1362
+ return `searching content: ${truncate(String(input["pattern"] ?? ""), 60)}`;
1363
+ case "Agent":
1364
+ return `spawning agent: ${input["description"] ?? name}`;
1365
+ default:
1366
+ return `tool: ${name}`;
1367
+ }
1368
+ }
1369
+
1370
+ // src/codex/codexService.ts
1371
+ import { spawnSync as spawnSync5 } from "child_process";
1372
+ function invokeCodexCode(prompt, options = {}) {
1373
+ if (options.verbose) {
1374
+ process.stderr.write("Warning: --verbose is not supported for Codex and will be ignored.\n");
1375
+ }
1376
+ invokeCodexCodeSync(prompt, options.yolo ?? false);
1377
+ }
1378
+ function invokeCodexCodeSync(prompt, yolo) {
1379
+ const codexBin = resolveCommand("codex");
1380
+ const args = ["exec"];
1381
+ if (yolo) args.push("--dangerously-bypass-approvals-and-sandbox");
1382
+ args.push(prompt);
1383
+ const result = spawnSync5(codexBin, args, { encoding: "utf8", stdio: "inherit" });
1384
+ handleSpawnError(result.error, "codex");
1385
+ handleExitCode(result.status, "Codex");
1386
+ }
1387
+
1388
+ // src/commands/getReady.ts
1389
+ var implementNextCommand = new Command3("implement-next").description("Find the next open GitHub issue matching the configured filter, claim it, and invoke the AI code assistant (Claude or Codex)").option("--json", "Output issue details as JSON").option("--no-claude", "Skip all AI invocation (Claude or Codex) after claiming the issue").option("--codex", "Use Codex CLI instead of Claude Code").option("--query-only", "Print issue content and exit without claiming or invoking any AI tools").option("--yolo", "Launch with --dangerously-skip-permissions (Claude) or --dangerously-bypass-approvals-and-sandbox (Codex)").option("--verbose", "Show step-by-step progress summary and final result").option("--opus", "Use claude-opus-4-6").option("--sonnet", "Use claude-sonnet-4-6").option("--haiku", "Use claude-haiku-4-5-20251001").action(async (options) => {
976
1390
  const config = readConfig();
977
1391
  if (config.remoteType !== "gh") {
978
1392
  process.stderr.write(
979
- "Error: get-ready is not supported in Azure DevOps mode. Work item discovery is not available in azdo-cli. See docs/azdo-gap.md for details.\n"
1393
+ "Error: implement-next is not supported in Azure DevOps mode. Work item discovery is not available in azdo-cli. See docs/azdo-gap.md for details.\n"
980
1394
  );
981
1395
  process.exit(1);
982
1396
  }
@@ -1014,6 +1428,9 @@ URL: ${issue.url}
1014
1428
  ${issue.body}
1015
1429
  `);
1016
1430
  }
1431
+ if (options.queryOnly) {
1432
+ process.exit(0);
1433
+ }
1017
1434
  try {
1018
1435
  postComment(issue.number, "working");
1019
1436
  } catch (err) {
@@ -1022,16 +1439,154 @@ ${issue.body}
1022
1439
  process.exit(1);
1023
1440
  }
1024
1441
  if (options.claude !== false) {
1025
- invokeClaudeCode(issue, config.claudeSystemPrompt);
1442
+ const systemPrompt = config.claudeSystemPrompt ?? DEFAULT_CLAUDE_SYSTEM_PROMPT;
1443
+ const prompt = `${systemPrompt}
1444
+
1445
+ ${issue.body}`;
1446
+ if (options.codex) {
1447
+ await invokeCodexCode(prompt, { yolo: options.yolo, verbose: options.verbose });
1448
+ } else {
1449
+ const model = resolveModelOption(options);
1450
+ await invokeClaudeCode(prompt, { yolo: options.yolo, verbose: options.verbose, model });
1451
+ }
1452
+ }
1453
+ });
1454
+
1455
+ // src/commands/test.ts
1456
+ import { Command as Command4 } from "commander";
1457
+ var testClaudeCmd = new Command4("claude").description("Test Claude Code invocation with a user-supplied prompt").requiredOption("--prompt <string>", "Prompt to send to Claude Code").option("--yolo", "Launch Claude Code with --dangerously-skip-permissions").option("--verbose", "Show step-by-step progress summary and final result").option("--opus", "Use claude-opus-4-6").option("--sonnet", "Use claude-sonnet-4-6").option("--haiku", "Use claude-haiku-4-5-20251001").action(async (options) => {
1458
+ const model = resolveModelOption(options);
1459
+ await invokeClaudeCode(options.prompt, { yolo: options.yolo, verbose: options.verbose, model });
1460
+ });
1461
+ var testCodexCmd = new Command4("codex").description("Test Codex CLI invocation with a user-supplied prompt").requiredOption("--prompt <string>", "Prompt to send to Codex CLI").option("--yolo", "Launch Codex with --dangerously-bypass-approvals-and-sandbox").option("--verbose", "Not supported for Codex; prints a warning and is otherwise ignored").action((options) => {
1462
+ invokeCodexCode(options.prompt, { yolo: options.yolo, verbose: options.verbose });
1463
+ });
1464
+ var testCommand = new Command4("test").description("Test commands for verifying automata integrations").addCommand(testClaudeCmd).addCommand(testCodexCmd);
1465
+
1466
+ // src/commands/executePrompt.ts
1467
+ import { Command as Command5 } from "commander";
1468
+ var PUSH_INSTRUCTION = "Once all changes are complete, stage every modified file, create a single commit with a clear and descriptive commit message that summarises what was fixed, and push the branch to the remote.";
1469
+ function withPush(prompt, push) {
1470
+ return push ? `${prompt}
1471
+
1472
+ ${PUSH_INSTRUCTION}` : prompt;
1473
+ }
1474
+ function addAiOptions(cmd) {
1475
+ return cmd.option("--codex", "Use Codex CLI instead of Claude Code").option("--verbose", "Show step-by-step progress (Claude only; ignored for Codex)").option("--push", "Append instruction to commit and push changes after the AI finishes").option("--opus", "Use claude-opus-4-6 (Claude only)").option("--sonnet", "Use claude-sonnet-4-6 (Claude only)").option("--haiku", "Use claude-haiku-4-5-20251001 (Claude only)");
1476
+ }
1477
+ var executeSonarCmd = addAiOptions(
1478
+ new Command5("sonar").description(
1479
+ "Check the current branch for a SonarCloud analysis and invoke the AI with the Sonar prompt and analysis URL"
1480
+ )
1481
+ ).action(async (options) => {
1482
+ let branch;
1483
+ try {
1484
+ branch = getCurrentBranch();
1485
+ } catch (err) {
1486
+ process.stderr.write(`Error: ${err.message}
1487
+ `);
1488
+ process.exit(1);
1489
+ }
1490
+ let pr;
1491
+ try {
1492
+ pr = await getPrInfo2(branch);
1493
+ } catch (err) {
1494
+ process.stderr.write(`Error: ${err.message}
1495
+ `);
1496
+ process.exit(1);
1497
+ }
1498
+ if (pr === null) {
1499
+ process.stderr.write(`Error: No pull request found for branch: ${branch}
1500
+ `);
1501
+ process.exit(1);
1502
+ }
1503
+ if (!pr.sonarcloudUrl) {
1504
+ process.stderr.write(
1505
+ `Error: No SonarCloud analysis found for PR #${pr.number}. Ensure a SonarCloud check is configured on this repository.
1506
+ `
1507
+ );
1508
+ process.exit(1);
1509
+ }
1510
+ const config = readConfig();
1511
+ const sonarPromptText = config.prompts?.sonar ?? DEFAULT_SONAR_PROMPT;
1512
+ const fullPrompt = withPush(
1513
+ `${sonarPromptText}
1514
+
1515
+ SonarCloud analysis URL: ${pr.sonarcloudUrl}`,
1516
+ options.push
1517
+ );
1518
+ if (options.codex) {
1519
+ invokeCodexCode(fullPrompt, { yolo: true });
1520
+ } else {
1521
+ const model = resolveModelOption(options);
1522
+ await invokeClaudeCode(fullPrompt, { yolo: true, verbose: options.verbose, model });
1523
+ }
1524
+ });
1525
+ function formatComments(comments) {
1526
+ return comments.map((c) => {
1527
+ const loc = c.line === null ? `${c.path}:(file)` : `${c.path}:${String(c.line)}`;
1528
+ return `[${c.author}] on ${loc}
1529
+ ${c.body}`;
1530
+ }).join("\n\n");
1531
+ }
1532
+ var executeFixCommentsCmd = addAiOptions(
1533
+ new Command5("fix-comments").description(
1534
+ "Fetch open review comments on the current PR and invoke the AI with the Fix-Comments prompt"
1535
+ )
1536
+ ).action(async (options) => {
1537
+ const result = resolveCurrentBranchComments();
1538
+ if (!result.ok) {
1539
+ if (result.kind === "error") {
1540
+ process.stderr.write(`Error: ${result.message}
1541
+ `);
1542
+ process.exit(1);
1543
+ }
1544
+ if (result.kind === "unsupported") {
1545
+ process.stderr.write(
1546
+ `Error: fix-comments is not supported for Azure DevOps. See docs/azdo-gap.md for details.
1547
+ `
1548
+ );
1549
+ process.exit(1);
1550
+ }
1551
+ process.stderr.write(`Error: No pull request found for branch: ${result.branch}
1552
+ `);
1553
+ process.exit(1);
1554
+ }
1555
+ const { comments } = result;
1556
+ if (comments.length === 0) {
1557
+ process.stderr.write(`Error: No open review comments found on the pull request.
1558
+ `);
1559
+ process.exit(1);
1560
+ }
1561
+ process.stdout.write(`Found ${String(comments.length)} open review comment${comments.length === 1 ? "" : "s"} on PR. Invoking AI\u2026
1562
+ `);
1563
+ const config = readConfig();
1564
+ const promptText = config.prompts?.fixComments ?? DEFAULT_FIX_COMMENTS_PROMPT;
1565
+ const fullPrompt = withPush(
1566
+ `${promptText}
1567
+
1568
+ Open review comments:
1569
+
1570
+ ${formatComments(comments)}`,
1571
+ options.push
1572
+ );
1573
+ if (options.codex) {
1574
+ invokeCodexCode(fullPrompt, { yolo: true });
1575
+ } else {
1576
+ const model = resolveModelOption(options);
1577
+ await invokeClaudeCode(fullPrompt, { yolo: true, verbose: options.verbose, model });
1026
1578
  }
1027
1579
  });
1580
+ var executePromptCommand = new Command5("execute-prompt").description("Execute a configured custom prompt using an AI assistant").addCommand(executeSonarCmd).addCommand(executeFixCommentsCmd);
1028
1581
 
1029
1582
  // src/index.ts
1030
- var program = new Command4();
1583
+ var program = new Command6();
1031
1584
  program.name("automata").description("Automata CLI tool").version(version, "-v, --version");
1032
1585
  program.addCommand(configCommand);
1033
1586
  program.addCommand(gitCommand);
1034
- program.addCommand(getReadyCommand);
1587
+ program.addCommand(implementNextCommand);
1588
+ program.addCommand(testCommand);
1589
+ program.addCommand(executePromptCommand);
1035
1590
  program.showHelpAfterError();
1036
1591
  program.parse();
1037
1592
  if (process.argv.length <= 2) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "automata-cli",
3
- "version": "0.2.2",
3
+ "version": "0.3.0-develop.112",
4
4
  "description": "Automata CLI tool",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,6 +33,7 @@
33
33
  "@types/node": "^25.5.0",
34
34
  "@types/react": "^19.2.14",
35
35
  "eslint": "^10.1.0",
36
+ "ink-testing-library": "^4.0.0",
36
37
  "prettier": "^3.8.1",
37
38
  "tsup": "^8.5.1",
38
39
  "typescript": "^5.9.3",