gbos 1.4.1 → 1.4.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gbos",
3
- "version": "1.4.1",
3
+ "version": "1.4.5",
4
4
  "description": "GBOS - Command line interface for GBOS services",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -146,6 +146,9 @@ program
146
146
  .option('-c, --continuous', 'Continuously process tasks')
147
147
  .option('-n, --max-tasks <number>', 'Maximum tasks to process', '1')
148
148
  .option('--show-prompt', 'Show the generated prompt')
149
+ .option('--skip-verification', 'Skip post-processing and test verification')
150
+ .option('--skip-git', 'Skip git commit and push')
151
+ .option('--task-id <id>', 'Run a specific task by ID')
149
152
  .action(startCommand);
150
153
 
151
154
  program
@@ -1,15 +1,21 @@
1
1
  /**
2
2
  * Orchestrator Commands
3
3
  * CLI commands for start, resume, and stop
4
+ * Beautiful terminal output with real-time agent streaming
4
5
  */
5
6
 
6
7
  const config = require('../lib/config');
8
+ const api = require('../lib/api');
7
9
  const { displayMessageBox, fg, LOGO_PURPLE, RESET, BOLD, DIM, getTerminalWidth } = require('../lib/display');
8
10
  const Orchestrator = require('../orchestrator/orchestrator');
9
11
  const { StateMachine, STATES, RUNS_DIR } = require('../orchestrator/state-machine');
10
12
  const { checkInstalledAdapters } = require('../orchestrator/adapters');
11
13
  const fs = require('fs');
12
14
  const path = require('path');
15
+ const readline = require('readline');
16
+ const { exec } = require('child_process');
17
+ const { promisify } = require('util');
18
+ const execAsync = promisify(exec);
13
19
 
14
20
  // Colors
15
21
  const GREEN = '\x1b[32m';
@@ -17,6 +23,134 @@ const YELLOW = '\x1b[33m';
17
23
  const RED = '\x1b[31m';
18
24
  const CYAN = '\x1b[36m';
19
25
 
26
+ // Spinner characters
27
+ const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
28
+
29
+ function createSpinner(text) {
30
+ let i = 0;
31
+ const interval = setInterval(() => {
32
+ process.stdout.write(`\r ${CYAN}${SPINNER[i % SPINNER.length]}${RESET} ${text}`);
33
+ i++;
34
+ }, 80);
35
+ return {
36
+ stop: (finalText) => {
37
+ clearInterval(interval);
38
+ process.stdout.write(`\r ${GREEN}✓${RESET} ${finalText || text}\n`);
39
+ },
40
+ fail: (finalText) => {
41
+ clearInterval(interval);
42
+ process.stdout.write(`\r ${RED}✗${RESET} ${finalText || text}\n`);
43
+ },
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Prompt user for yes/no confirmation
49
+ */
50
+ function promptConfirm(question) {
51
+ const rl = readline.createInterface({
52
+ input: process.stdin,
53
+ output: process.stdout,
54
+ });
55
+ return new Promise((resolve) => {
56
+ rl.question(` ${CYAN}?${RESET} ${question} ${DIM}(y/N)${RESET}: `, (answer) => {
57
+ rl.close();
58
+ const lower = answer.trim().toLowerCase();
59
+ resolve(lower === 'y' || lower === 'yes');
60
+ });
61
+ });
62
+ }
63
+
64
+ /**
65
+ * Check if directory is a git repo with a matching remote
66
+ */
67
+ async function isMatchingGitRepo(dir, repoUrl) {
68
+ try {
69
+ const { stdout } = await execAsync('git remote get-url origin', { cwd: dir });
70
+ const currentUrl = stdout.trim();
71
+ const normalize = (url) => url
72
+ .replace(/\.git$/, '')
73
+ .replace(/^git@([^:]+):/, 'https://$1/')
74
+ .replace(/^https?:\/\//, '')
75
+ .toLowerCase();
76
+ return normalize(currentUrl) === normalize(repoUrl);
77
+ } catch (e) {
78
+ return false;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Check if a directory is empty (ignoring hidden files like .DS_Store)
84
+ */
85
+ function isDirEmpty(dir) {
86
+ if (!fs.existsSync(dir)) return true;
87
+ const entries = fs.readdirSync(dir).filter(f => f !== '.DS_Store');
88
+ return entries.length === 0;
89
+ }
90
+
91
+ /**
92
+ * Pre-check workspace: ensure CWD is the app's repo or offer to initialize
93
+ * Returns the working directory to use, or null to abort
94
+ */
95
+ async function preCheckWorkspace(connection) {
96
+ const currentDir = process.cwd();
97
+
98
+ // Get application details to find the repo URL
99
+ let app = connection.application;
100
+ if (app?.id) {
101
+ try {
102
+ const response = await api.getApplication(app.id);
103
+ app = response.data || response;
104
+ } catch (e) {
105
+ // Use connection's app data
106
+ }
107
+ }
108
+
109
+ const repoUrl = app?.gitlab_repo_url || app?.repo_url || app?.repository_url;
110
+
111
+ // If no repo URL configured, use CWD as local-only workspace
112
+ if (!repoUrl) {
113
+ console.log(` ${DIM}No repository configured - using local workspace${RESET}`);
114
+ return currentDir;
115
+ }
116
+
117
+ // Check if CWD is already the matching repo
118
+ const isMatching = await isMatchingGitRepo(currentDir, repoUrl);
119
+ if (isMatching) {
120
+ console.log(` ${GREEN}✓${RESET} Current directory matches app repository`);
121
+ return currentDir;
122
+ }
123
+
124
+ // CWD doesn't match - check if it's empty
125
+ const empty = isDirEmpty(currentDir);
126
+
127
+ if (!empty) {
128
+ // Directory is not empty and not the repo
129
+ console.log(` ${RED}✗${RESET} Current directory is not the app's GitLab repository`);
130
+ console.log(` ${DIM}Expected repo: ${repoUrl}${RESET}`);
131
+ console.log(` ${DIM}Current dir: ${currentDir}${RESET}\n`);
132
+ console.log(` ${YELLOW}!${RESET} To use this directory, it must either be:`);
133
+ console.log(` 1. The root of the GitLab repository (clone it first)`);
134
+ console.log(` 2. An empty folder (GBOS will initialize the repo for you)\n`);
135
+ return null;
136
+ }
137
+
138
+ // Directory is empty - offer to initialize
139
+ console.log(` ${YELLOW}!${RESET} Current directory is empty and not a git repository`);
140
+ console.log(` ${DIM}App repo: ${repoUrl}${RESET}`);
141
+ console.log(` ${DIM}Current dir: ${currentDir}${RESET}\n`);
142
+
143
+ const confirmed = await promptConfirm(`Initialize GitLab repository for "${app.name || 'this app'}" in this directory?`);
144
+
145
+ if (!confirmed) {
146
+ console.log(`\n ${DIM}Aborted. Navigate to the correct repo directory or use an empty folder.${RESET}\n`);
147
+ return null;
148
+ }
149
+
150
+ console.log('');
151
+ return currentDir;
152
+ }
153
+
20
154
  /**
21
155
  * gbos start - Start the orchestrator
22
156
  */
@@ -50,8 +184,16 @@ async function startCommand(options) {
50
184
  return;
51
185
  }
52
186
 
187
+ // Pre-check workspace (unless --dir is explicitly provided)
188
+ let workingDir = options.dir ? path.resolve(options.dir) : null;
189
+ if (!workingDir) {
190
+ workingDir = await preCheckWorkspace(connection);
191
+ if (workingDir === null) {
192
+ process.exit(1);
193
+ }
194
+ }
195
+
53
196
  // Check agent availability
54
- console.log(` ${DIM}Checking agent availability...${RESET}`);
55
197
  const adapters = await checkInstalledAdapters();
56
198
  const agentName = options.agent || 'claude-code';
57
199
  const agentInfo = adapters[agentName] || adapters['claude-code'];
@@ -69,28 +211,82 @@ async function startCommand(options) {
69
211
  // Create orchestrator
70
212
  const orchestrator = new Orchestrator({
71
213
  agent: agentName,
72
- autoApprove: options.autoApprove || false,
214
+ autoApprove: options.autoApprove !== false,
73
215
  createMR: options.mr !== false,
74
216
  continuous: options.continuous || false,
75
217
  maxTasks: options.maxTasks ? parseInt(options.maxTasks) : 1,
76
- workingDir: options.dir ? path.resolve(options.dir) : null,
218
+ workingDir: workingDir,
219
+ skipVerification: options.skipVerification || false,
220
+ skipGit: options.skipGit || false,
221
+ taskId: options.taskId || null,
77
222
  });
78
223
 
224
+ // Track active spinner
225
+ let activeSpinner = null;
226
+
79
227
  // Set up event handlers
80
228
  orchestrator.on('started', ({ runId }) => {
81
- console.log(` ${GREEN}✓${RESET} Run started: ${runId}\n`);
229
+ console.log(` ${GREEN}✓${RESET} Run started: ${DIM}${runId}${RESET}\n`);
82
230
  });
83
231
 
84
232
  orchestrator.on('stage', ({ stage }) => {
85
- console.log(` ${CYAN}▸${RESET} ${stage.replace(/_/g, ' ')}`);
233
+ // Stop any active spinner
234
+ if (activeSpinner) {
235
+ activeSpinner.stop();
236
+ activeSpinner = null;
237
+ }
238
+
239
+ const stageLabels = {
240
+ auth_config: 'Authenticating & configuring',
241
+ workspace_ready: 'Preparing workspace',
242
+ fetch_task: 'Fetching next task',
243
+ generate_prompt: 'Generating agent prompt',
244
+ run_agent: null, // handled by agent_start
245
+ post_process: 'Post-processing',
246
+ run_tests: 'Running tests',
247
+ commit_push: 'Committing & pushing',
248
+ report_status: 'Reporting status to GBOS',
249
+ };
250
+
251
+ const label = stageLabels[stage];
252
+ if (label) {
253
+ activeSpinner = createSpinner(label);
254
+ }
255
+ });
256
+
257
+ orchestrator.on('agent_start', ({ agent }) => {
258
+ if (activeSpinner) {
259
+ activeSpinner.stop();
260
+ activeSpinner = null;
261
+ }
262
+ console.log(`\n${fg(...LOGO_PURPLE)}${'─'.repeat(tableWidth)}${RESET}`);
263
+ console.log(`${BOLD} Agent Output${RESET} ${DIM}(${agent})${RESET}`);
264
+ console.log(`${fg(...LOGO_PURPLE)}${'─'.repeat(tableWidth)}${RESET}\n`);
265
+ });
266
+
267
+ orchestrator.on('agent_done', ({ exitCode }) => {
268
+ console.log(`\n${fg(...LOGO_PURPLE)}${'─'.repeat(tableWidth)}${RESET}`);
269
+ if (exitCode === 0) {
270
+ console.log(` ${GREEN}✓${RESET} Agent completed successfully`);
271
+ } else {
272
+ console.log(` ${YELLOW}!${RESET} Agent exited with code ${exitCode}`);
273
+ }
274
+ console.log(`${fg(...LOGO_PURPLE)}${'─'.repeat(tableWidth)}${RESET}\n`);
86
275
  });
87
276
 
88
277
  orchestrator.on('log', ({ message }) => {
89
- console.log(` ${DIM}${message}${RESET}`);
278
+ // Only show important logs, not during spinner stages
279
+ if (!activeSpinner) {
280
+ // Don't print "Stage: X" logs since we handle those with spinners
281
+ if (!message.startsWith('Stage:')) {
282
+ console.log(` ${DIM}${message}${RESET}`);
283
+ }
284
+ }
90
285
  });
91
286
 
92
287
  orchestrator.on('prompt', ({ prompt }) => {
93
288
  if (options.showPrompt) {
289
+ if (activeSpinner) { activeSpinner.stop(); activeSpinner = null; }
94
290
  console.log(`\n${fg(...LOGO_PURPLE)}${'─'.repeat(tableWidth)}${RESET}`);
95
291
  console.log(`${BOLD} Task Prompt${RESET}`);
96
292
  console.log(`${fg(...LOGO_PURPLE)}${'─'.repeat(tableWidth)}${RESET}\n`);
@@ -100,6 +296,7 @@ async function startCommand(options) {
100
296
  });
101
297
 
102
298
  orchestrator.on('committed', (result) => {
299
+ if (activeSpinner) { activeSpinner.stop(); activeSpinner = null; }
103
300
  if (result.commit) {
104
301
  console.log(` ${GREEN}✓${RESET} Committed: ${result.commit.shortHash}`);
105
302
  }
@@ -109,6 +306,7 @@ async function startCommand(options) {
109
306
  });
110
307
 
111
308
  orchestrator.on('completed', ({ tasksCompleted }) => {
309
+ if (activeSpinner) { activeSpinner.stop(); activeSpinner = null; }
112
310
  console.log(`\n${fg(...LOGO_PURPLE)}${'─'.repeat(tableWidth)}${RESET}`);
113
311
  console.log(`${GREEN}✓${RESET} ${BOLD}Orchestrator completed${RESET}`);
114
312
  console.log(` ${DIM}Tasks completed: ${tasksCompleted}${RESET}`);
@@ -116,12 +314,14 @@ async function startCommand(options) {
116
314
  });
117
315
 
118
316
  orchestrator.on('failed', ({ error }) => {
317
+ if (activeSpinner) { activeSpinner.fail(); activeSpinner = null; }
119
318
  console.log(`\n${RED}✗${RESET} ${BOLD}Orchestrator failed${RESET}`);
120
319
  console.log(` ${DIM}Error: ${error.message}${RESET}\n`);
121
320
  });
122
321
 
123
322
  // Handle interrupts
124
323
  process.on('SIGINT', async () => {
324
+ if (activeSpinner) { activeSpinner.fail('Interrupted'); activeSpinner = null; }
125
325
  console.log(`\n\n ${YELLOW}!${RESET} Stopping orchestrator...`);
126
326
  await orchestrator.stop();
127
327
  console.log(` ${DIM}Run paused. Use "gbos resume" to continue.${RESET}\n`);
@@ -132,6 +332,7 @@ async function startCommand(options) {
132
332
  try {
133
333
  await orchestrator.start();
134
334
  } catch (error) {
335
+ if (activeSpinner) { activeSpinner.fail(); activeSpinner = null; }
135
336
  console.log(`\n${RED}✗${RESET} ${error.message}\n`);
136
337
  process.exit(1);
137
338
  }
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * Claude Code Agent Adapter
3
- * Adapter for Anthropic's Claude Code CLI
3
+ * Uses `claude -p` (print mode) which reads prompt from stdin,
4
+ * runs autonomously, streams output, and exits when done.
4
5
  */
5
6
 
6
7
  const BaseAdapter = require('./base-adapter');
7
8
  const { exec } = require('child_process');
8
9
  const { promisify } = require('util');
9
-
10
10
  const execAsync = promisify(exec);
11
11
 
12
12
  class ClaudeAdapter extends BaseAdapter {
@@ -18,178 +18,84 @@ class ClaudeAdapter extends BaseAdapter {
18
18
  }
19
19
 
20
20
  async isAvailable() {
21
- try {
22
- await execAsync('which claude');
23
- return true;
24
- } catch (e) {
25
- return false;
26
- }
21
+ try { await execAsync('which claude'); return true; } catch (e) { return false; }
27
22
  }
28
23
 
29
24
  async getVersion() {
30
- try {
31
- const { stdout } = await execAsync('claude --version');
32
- return stdout.trim();
33
- } catch (e) {
34
- return 'unknown';
35
- }
25
+ try { const { stdout } = await execAsync('claude --version'); return stdout.trim(); } catch (e) { return 'unknown'; }
36
26
  }
37
27
 
38
28
  getCommand(options = {}) {
39
- const args = [];
40
-
41
- // Use print mode for non-interactive (single prompt)
42
- if (options.nonInteractive && options.prompt) {
43
- args.push('--print');
44
- args.push('--dangerously-skip-permissions');
45
- }
46
-
47
- // Add model if specified
48
- if (options.model) {
49
- args.push('--model', options.model);
50
- }
51
-
52
- // Add max turns if specified
53
- if (options.maxTurns) {
54
- args.push('--max-turns', options.maxTurns.toString());
55
- }
29
+ const args = ['-p']; // Print mode: reads from stdin, outputs result, exits
30
+ if (options.autoApprove) args.push('--dangerously-skip-permissions');
31
+ if (options.model) args.push('--model', options.model);
32
+ if (options.maxTurns) args.push('--max-turns', options.maxTurns.toString());
33
+ if (options.verbose) args.push('--verbose');
56
34
 
57
35
  return {
58
36
  command: 'claude',
59
37
  args,
60
- env: {
61
- ...process.env,
62
- ANTHROPIC_API_KEY: options.apiKey || process.env.ANTHROPIC_API_KEY,
63
- },
38
+ env: { ...process.env, ANTHROPIC_API_KEY: options.apiKey || process.env.ANTHROPIC_API_KEY },
39
+ closeStdinOnWrite: true,
64
40
  };
65
41
  }
66
42
 
67
43
  formatPrompt(task, context = {}) {
68
44
  const lines = [];
69
-
70
- // Task header
71
- lines.push(`# GBOS Task: ${task.title || task.name || 'Task'}`);
72
- lines.push('');
73
-
74
- // Task metadata
45
+ lines.push(`# GBOS Task: ${task.title || task.name || 'Task'}`, '');
75
46
  lines.push('## Task Information');
76
47
  lines.push(`- **Task ID:** ${task.id}`);
77
48
  if (task.task_key) lines.push(`- **Task Key:** ${task.task_key}`);
78
49
  if (task.priority) lines.push(`- **Priority:** ${task.priority}`);
79
50
  if (task.task_type) lines.push(`- **Type:** ${task.task_type}`);
80
51
  lines.push('');
81
-
82
- // Main instructions
83
- lines.push('## Instructions');
84
- lines.push('');
85
- if (task.agent_prompt) {
86
- lines.push(task.agent_prompt);
87
- } else if (task.prompt) {
88
- lines.push(task.prompt);
89
- } else if (task.description) {
90
- lines.push(task.description);
91
- }
52
+ lines.push('## Instructions', '');
53
+ if (task.agent_prompt) lines.push(task.agent_prompt);
54
+ else if (task.prompt) lines.push(task.prompt);
55
+ else if (task.description) lines.push(task.description);
92
56
  lines.push('');
93
57
 
94
- // Acceptance criteria
95
- if (task.acceptance_criteria && task.acceptance_criteria.length > 0) {
58
+ if (task.acceptance_criteria?.length > 0) {
96
59
  lines.push('## Acceptance Criteria');
97
- task.acceptance_criteria.forEach((c, i) => {
98
- lines.push(`${i + 1}. ${c}`);
99
- });
60
+ task.acceptance_criteria.forEach((c, i) => lines.push(`${i + 1}. ${c}`));
100
61
  lines.push('');
101
62
  }
102
-
103
- // Target files
104
- if (task.target_files && task.target_files.length > 0) {
63
+ if (task.target_files?.length > 0) {
105
64
  lines.push('## Target Files');
106
65
  task.target_files.forEach(f => lines.push(`- ${f}`));
107
66
  lines.push('');
108
67
  }
109
68
 
110
- // Testing instructions
111
- lines.push('## Testing Requirements');
112
- lines.push('');
113
- lines.push('After implementing the changes, you MUST test your work:');
114
- lines.push('');
115
-
69
+ lines.push('## Testing Requirements', '', 'After implementing the changes, you MUST test your work:', '');
116
70
  if (context.cloudRunUrl) {
117
71
  lines.push(`1. The application is deployed at: ${context.cloudRunUrl}`);
118
72
  lines.push('2. Write and run Playwright tests to verify the changes');
119
- lines.push('3. Use the following test approach:');
120
- lines.push('');
121
- lines.push('```bash');
122
- lines.push('# Install Playwright if not present');
123
- lines.push('npm install -D @playwright/test');
124
- lines.push('npx playwright install');
125
- lines.push('');
126
- lines.push('# Run tests against the deployed app');
127
- lines.push(`npx playwright test --project=chromium`);
128
- lines.push('```');
129
- lines.push('');
130
- lines.push('Example Playwright test:');
131
- lines.push('```typescript');
132
- lines.push("import { test, expect } from '@playwright/test';");
133
- lines.push('');
134
- lines.push("test('verify changes', async ({ page }) => {");
135
- lines.push(` await page.goto('${context.cloudRunUrl}');`);
136
- lines.push(' // Add your test assertions here');
137
- lines.push('});');
138
- lines.push('```');
73
+ lines.push('3. Use the following test approach:', '');
74
+ lines.push('```bash', '# Install Playwright if not present', 'npm install -D @playwright/test', 'npx playwright install', '', 'npx playwright test --project=chromium', '```', '');
75
+ lines.push('Example Playwright test:', '```typescript', "import { test, expect } from '@playwright/test';", '', "test('verify changes', async ({ page }) => {", ` await page.goto('${context.cloudRunUrl}');`, ' // Add your test assertions here', '});', '```');
139
76
  } else {
140
77
  lines.push('1. Run the existing test suite to ensure no regressions');
141
78
  lines.push('2. Add new tests for the implemented functionality');
142
79
  lines.push('3. Verify all tests pass before completing');
143
80
  }
144
81
  lines.push('');
82
+ lines.push('## Workspace', '', 'You are working inside the `codebase/` directory of the repository. All files you create or modify should be in this directory (your current working directory). Do NOT create files outside of this directory.', '');
145
83
 
146
- // Completion instructions
147
- lines.push('## Completion');
148
- lines.push('');
149
- lines.push('When you have completed the task:');
150
- lines.push('1. Ensure all tests pass');
151
- lines.push('2. Review your changes for quality');
152
- lines.push('3. The system will automatically commit and push your changes');
153
- lines.push('');
154
- lines.push('**Important:** Do not run `git commit` or `git push` yourself - the GBOS orchestrator will handle this.');
155
- lines.push('');
84
+ lines.push('## Completion', '', 'When you have completed the task:', '1. Ensure all tests pass', '2. Review your changes for quality', '3. The system will automatically commit and push your changes', '', '**Important:** Do not run `git commit` or `git push` yourself - the GBOS orchestrator will handle this.', '');
156
85
 
157
- // Context
158
86
  if (context.repoUrl) {
159
- lines.push('## Repository');
160
- lines.push(`- **URL:** ${context.repoUrl}`);
161
- lines.push(`- **Branch:** ${context.branch || 'main'}`);
162
- lines.push('');
87
+ lines.push('## Repository', `- **URL:** ${context.repoUrl}`, `- **Branch:** ${context.branch || 'main'}`, '');
163
88
  }
164
-
165
89
  return lines.join('\n');
166
90
  }
167
91
 
168
92
  detectCompletion(output) {
169
- // Claude Code specific completion patterns
170
- const patterns = [
171
- /I've completed/i,
172
- /changes have been made/i,
173
- /implementation is complete/i,
174
- /task is done/i,
175
- /all tests pass/i,
176
- /ready for review/i,
177
- /I've finished/i,
178
- /I have completed/i,
179
- ];
93
+ const patterns = [/I've completed/i, /changes have been made/i, /implementation is complete/i, /task is done/i, /all tests pass/i, /ready for review/i, /I've finished/i, /I have completed/i];
180
94
  return patterns.some(p => p.test(output)) || super.detectCompletion(output);
181
95
  }
182
96
 
183
97
  detectWaitingForInput(output) {
184
- // Claude Code specific wait patterns
185
- const patterns = [
186
- /Do you want me to/i,
187
- /Should I/i,
188
- /Would you like me to/i,
189
- /Shall I/i,
190
- /May I/i,
191
- /Can I proceed/i,
192
- ];
98
+ const patterns = [/Do you want me to/i, /Should I/i, /Would you like me to/i, /Shall I/i, /May I/i, /Can I proceed/i];
193
99
  return patterns.some(p => p.test(output)) || super.detectWaitingForInput(output);
194
100
  }
195
101
  }
@@ -256,6 +256,30 @@ class GitManager {
256
256
  }
257
257
  }
258
258
 
259
+ /**
260
+ * Commit only (no push) - for local-only workspaces
261
+ */
262
+ async commitOnly(message, task = null) {
263
+ const status = await this.stageAll();
264
+
265
+ if (!status.hasChanges) {
266
+ return {
267
+ committed: false,
268
+ pushed: false,
269
+ message: 'No changes to commit',
270
+ };
271
+ }
272
+
273
+ const commit = await this.commit(message, task);
274
+
275
+ return {
276
+ committed: true,
277
+ pushed: false,
278
+ commit,
279
+ message: `Committed locally: ${commit.shortHash}`,
280
+ };
281
+ }
282
+
259
283
  /**
260
284
  * Full commit and push workflow
261
285
  */