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 +1 -1
- package/src/cli.js +3 -0
- package/src/commands/orchestrator.js +207 -6
- package/src/orchestrator/adapters/claude-adapter.js +28 -122
- package/src/orchestrator/managers/git-manager.js +24 -0
- package/src/orchestrator/managers/workspace-manager.js +102 -24
- package/src/orchestrator/orchestrator.js +61 -19
- package/src/orchestrator/runners/session-runner.js +38 -213
- package/src/orchestrator/state-machine.js +5 -5
package/package.json
CHANGED
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
42
|
-
if (options.
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
83
|
-
lines.push(
|
|
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
|
-
|
|
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
|
-
|
|
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('```
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
*/
|