@link-assistant/hive-mind 0.39.0
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/CHANGELOG.md +20 -0
- package/LICENSE +24 -0
- package/README.md +769 -0
- package/package.json +58 -0
- package/src/agent.lib.mjs +705 -0
- package/src/agent.prompts.lib.mjs +196 -0
- package/src/buildUserMention.lib.mjs +71 -0
- package/src/claude-limits.lib.mjs +389 -0
- package/src/claude.lib.mjs +1445 -0
- package/src/claude.prompts.lib.mjs +203 -0
- package/src/codex.lib.mjs +552 -0
- package/src/codex.prompts.lib.mjs +194 -0
- package/src/config.lib.mjs +207 -0
- package/src/contributing-guidelines.lib.mjs +268 -0
- package/src/exit-handler.lib.mjs +205 -0
- package/src/git.lib.mjs +145 -0
- package/src/github-issue-creator.lib.mjs +246 -0
- package/src/github-linking.lib.mjs +152 -0
- package/src/github.batch.lib.mjs +272 -0
- package/src/github.graphql.lib.mjs +258 -0
- package/src/github.lib.mjs +1479 -0
- package/src/hive.config.lib.mjs +254 -0
- package/src/hive.mjs +1500 -0
- package/src/instrument.mjs +191 -0
- package/src/interactive-mode.lib.mjs +1000 -0
- package/src/lenv-reader.lib.mjs +206 -0
- package/src/lib.mjs +490 -0
- package/src/lino.lib.mjs +176 -0
- package/src/local-ci-checks.lib.mjs +324 -0
- package/src/memory-check.mjs +419 -0
- package/src/model-mapping.lib.mjs +145 -0
- package/src/model-validation.lib.mjs +278 -0
- package/src/opencode.lib.mjs +479 -0
- package/src/opencode.prompts.lib.mjs +194 -0
- package/src/protect-branch.mjs +159 -0
- package/src/review.mjs +433 -0
- package/src/reviewers-hive.mjs +643 -0
- package/src/sentry.lib.mjs +284 -0
- package/src/solve.auto-continue.lib.mjs +568 -0
- package/src/solve.auto-pr.lib.mjs +1374 -0
- package/src/solve.branch-errors.lib.mjs +341 -0
- package/src/solve.branch.lib.mjs +230 -0
- package/src/solve.config.lib.mjs +342 -0
- package/src/solve.error-handlers.lib.mjs +256 -0
- package/src/solve.execution.lib.mjs +291 -0
- package/src/solve.feedback.lib.mjs +436 -0
- package/src/solve.mjs +1128 -0
- package/src/solve.preparation.lib.mjs +210 -0
- package/src/solve.repo-setup.lib.mjs +114 -0
- package/src/solve.repository.lib.mjs +961 -0
- package/src/solve.results.lib.mjs +558 -0
- package/src/solve.session.lib.mjs +135 -0
- package/src/solve.validation.lib.mjs +325 -0
- package/src/solve.watch.lib.mjs +572 -0
- package/src/start-screen.mjs +324 -0
- package/src/task.mjs +308 -0
- package/src/telegram-bot.mjs +1481 -0
- package/src/telegram-markdown.lib.mjs +64 -0
- package/src/usage-limit.lib.mjs +218 -0
- package/src/version.lib.mjs +41 -0
- package/src/youtrack/solve.youtrack.lib.mjs +116 -0
- package/src/youtrack/youtrack-sync.mjs +219 -0
- package/src/youtrack/youtrack.lib.mjs +425 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// start-screen.mjs - Launch solve or hive commands in GNU screen sessions
|
|
3
|
+
|
|
4
|
+
import { exec } from 'child_process';
|
|
5
|
+
import { promisify } from 'util';
|
|
6
|
+
|
|
7
|
+
const execAsync = promisify(exec);
|
|
8
|
+
|
|
9
|
+
// Import the shared parseGitHubUrl function from github.lib.mjs
|
|
10
|
+
// This ensures consistent URL validation across all commands (hive, solve, start-screen)
|
|
11
|
+
const { parseGitHubUrl } = await import('./github.lib.mjs');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Generate a screen session name based on the command and GitHub URL
|
|
15
|
+
* @param {string} command - Either 'solve' or 'hive'
|
|
16
|
+
* @param {string} githubUrl - GitHub repository or issue URL
|
|
17
|
+
* @returns {string} The generated screen session name
|
|
18
|
+
*/
|
|
19
|
+
function generateScreenName(command, githubUrl) {
|
|
20
|
+
const parsed = parseGitHubUrl(githubUrl);
|
|
21
|
+
|
|
22
|
+
if (!parsed.valid) {
|
|
23
|
+
// Fallback to simple naming if parsing fails
|
|
24
|
+
const sanitized = githubUrl.replace(/[^a-zA-Z0-9-]/g, '-').substring(0, 30);
|
|
25
|
+
return `${command}-${sanitized}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Build name parts
|
|
29
|
+
const parts = [command];
|
|
30
|
+
|
|
31
|
+
if (parsed.owner) {
|
|
32
|
+
parts.push(parsed.owner);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (parsed.repo) {
|
|
36
|
+
parts.push(parsed.repo);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (parsed.number) {
|
|
40
|
+
parts.push(parsed.number);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return parts.join('-');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Check if a screen session exists
|
|
48
|
+
* @param {string} sessionName - The name of the screen session
|
|
49
|
+
* @returns {Promise<boolean>} Whether the session exists
|
|
50
|
+
*/
|
|
51
|
+
async function screenSessionExists(sessionName) {
|
|
52
|
+
try {
|
|
53
|
+
const { stdout } = await execAsync('screen -ls');
|
|
54
|
+
return stdout.includes(sessionName);
|
|
55
|
+
} catch {
|
|
56
|
+
// screen -ls returns non-zero exit code when no sessions exist
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Wait for a screen session to be ready to accept commands
|
|
63
|
+
* A session is considered ready when it can execute a test command
|
|
64
|
+
* @param {string} sessionName - The name of the screen session
|
|
65
|
+
* @param {number} maxWaitSeconds - Maximum time to wait in seconds (default: 5)
|
|
66
|
+
* @returns {Promise<boolean>} Whether the session became ready
|
|
67
|
+
*/
|
|
68
|
+
async function waitForSessionReady(sessionName, maxWaitSeconds = 5) {
|
|
69
|
+
const startTime = Date.now();
|
|
70
|
+
const maxWaitMs = maxWaitSeconds * 1000;
|
|
71
|
+
|
|
72
|
+
// Use a unique marker file for this check to avoid conflicts
|
|
73
|
+
const markerFile = `/tmp/screen-ready-${sessionName}-${Date.now()}.marker`;
|
|
74
|
+
|
|
75
|
+
while (Date.now() - startTime < maxWaitMs) {
|
|
76
|
+
try {
|
|
77
|
+
// Send a test command that creates a marker file
|
|
78
|
+
// This command will only execute when the session is actually ready at a prompt
|
|
79
|
+
await execAsync(`screen -S ${sessionName} -X stuff "touch ${markerFile} 2>/dev/null\n"`);
|
|
80
|
+
|
|
81
|
+
// Wait for the marker file to appear
|
|
82
|
+
const checkStartTime = Date.now();
|
|
83
|
+
const checkTimeout = 1000; // 1 second to check if marker appears
|
|
84
|
+
|
|
85
|
+
while (Date.now() - checkStartTime < checkTimeout) {
|
|
86
|
+
try {
|
|
87
|
+
const { code } = await execAsync(`test -f ${markerFile}`);
|
|
88
|
+
if (code === 0) {
|
|
89
|
+
// Marker file exists, session is ready!
|
|
90
|
+
// Clean up the marker file
|
|
91
|
+
await execAsync(`rm -f ${markerFile}`).catch(() => { });
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
// Marker file doesn't exist yet
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Wait before checking again
|
|
99
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Marker file didn't appear, session is still busy
|
|
103
|
+
// Clean up any leftover marker file from the queued command
|
|
104
|
+
await execAsync(`rm -f ${markerFile}`).catch(() => { });
|
|
105
|
+
} catch {
|
|
106
|
+
// Error sending test command or checking marker
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Wait before trying again
|
|
110
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Timeout reached, session is not ready
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Create or enter a screen session with the given command
|
|
119
|
+
* @param {string} sessionName - The name of the screen session
|
|
120
|
+
* @param {string} command - The command to run ('solve' or 'hive')
|
|
121
|
+
* @param {string[]} args - Arguments to pass to the command
|
|
122
|
+
* @param {boolean} autoTerminate - If true, session terminates after command completes
|
|
123
|
+
*/
|
|
124
|
+
async function createOrEnterScreen(sessionName, command, args, autoTerminate = false) {
|
|
125
|
+
const sessionExists = await screenSessionExists(sessionName);
|
|
126
|
+
|
|
127
|
+
if (sessionExists) {
|
|
128
|
+
console.log(`Screen session '${sessionName}' already exists.`);
|
|
129
|
+
console.log('Checking if session is ready to accept commands...');
|
|
130
|
+
|
|
131
|
+
// Wait for the session to be ready (at a prompt)
|
|
132
|
+
const isReady = await waitForSessionReady(sessionName);
|
|
133
|
+
|
|
134
|
+
if (isReady) {
|
|
135
|
+
console.log('Session is ready.');
|
|
136
|
+
} else {
|
|
137
|
+
console.log('Session might still be running a command. Will attempt to send command anyway.');
|
|
138
|
+
console.log('Note: The command will execute once the current operation completes.');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
console.log('Sending command to existing session...');
|
|
142
|
+
|
|
143
|
+
// Build the full command to send to the existing session
|
|
144
|
+
const quotedArgs = args.map(arg => {
|
|
145
|
+
// If arg contains spaces or special chars, wrap in single quotes
|
|
146
|
+
if (arg.includes(' ') || arg.includes('&') || arg.includes('|') ||
|
|
147
|
+
arg.includes(';') || arg.includes('$') || arg.includes('*') ||
|
|
148
|
+
arg.includes('?') || arg.includes('(') || arg.includes(')')) {
|
|
149
|
+
// Escape single quotes within the argument
|
|
150
|
+
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
151
|
+
}
|
|
152
|
+
return arg;
|
|
153
|
+
}).join(' ');
|
|
154
|
+
|
|
155
|
+
const fullCommand = `${command} ${quotedArgs}`;
|
|
156
|
+
|
|
157
|
+
// Escape the command for screen's stuff command
|
|
158
|
+
// We need to escape special characters for the shell
|
|
159
|
+
const escapedCommand = fullCommand.replace(/'/g, "'\\''");
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
// Send the command to the existing screen session
|
|
163
|
+
// The \n at the end simulates pressing Enter
|
|
164
|
+
await execAsync(`screen -S ${sessionName} -X stuff '${escapedCommand}\n'`);
|
|
165
|
+
console.log(`Command sent to session '${sessionName}' successfully.`);
|
|
166
|
+
console.log(`To attach and view the session, run: screen -r ${sessionName}`);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.error('Failed to send command to existing screen session:', error.message);
|
|
169
|
+
console.error('You may need to terminate the old session and try again.');
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
console.log(`Creating screen session: ${sessionName}`);
|
|
176
|
+
|
|
177
|
+
// Create a detached session with the command
|
|
178
|
+
// Quote arguments properly to preserve spaces and special characters
|
|
179
|
+
const quotedArgs = args.map(arg => {
|
|
180
|
+
// If arg contains spaces or special chars, wrap in single quotes
|
|
181
|
+
if (arg.includes(' ') || arg.includes('&') || arg.includes('|') ||
|
|
182
|
+
arg.includes(';') || arg.includes('$') || arg.includes('*') ||
|
|
183
|
+
arg.includes('?') || arg.includes('(') || arg.includes(')')) {
|
|
184
|
+
// Escape single quotes within the argument
|
|
185
|
+
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
186
|
+
}
|
|
187
|
+
return arg;
|
|
188
|
+
}).join(' ');
|
|
189
|
+
|
|
190
|
+
let screenCommand;
|
|
191
|
+
if (autoTerminate) {
|
|
192
|
+
// Old behavior: session terminates after command completes
|
|
193
|
+
const fullCommand = `${command} ${quotedArgs}`;
|
|
194
|
+
screenCommand = `screen -dmS ${sessionName} ${fullCommand}`;
|
|
195
|
+
} else {
|
|
196
|
+
// New behavior: wrap the command in a bash shell that will stay alive after the command finishes
|
|
197
|
+
// This allows the user to reattach to the screen session after the command completes
|
|
198
|
+
const fullCommand = `${command} ${quotedArgs}`;
|
|
199
|
+
const escapedCommand = fullCommand.replace(/'/g, "'\\''");
|
|
200
|
+
screenCommand = `screen -dmS ${sessionName} bash -c '${escapedCommand}; exec bash'`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
await execAsync(screenCommand);
|
|
205
|
+
console.log(`Started ${command} in detached screen session: ${sessionName}`);
|
|
206
|
+
if (autoTerminate) {
|
|
207
|
+
console.log('Note: Session will terminate after command completes (--auto-terminate mode)');
|
|
208
|
+
} else {
|
|
209
|
+
console.log('Session will remain active after command completes');
|
|
210
|
+
}
|
|
211
|
+
console.log(`To attach to this session, run: screen -r ${sessionName}`);
|
|
212
|
+
} catch (error) {
|
|
213
|
+
console.error('Failed to create screen session:', error.message);
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Main function
|
|
220
|
+
*/
|
|
221
|
+
async function main() {
|
|
222
|
+
const args = process.argv.slice(2);
|
|
223
|
+
|
|
224
|
+
if (args.length < 2) {
|
|
225
|
+
console.error('Usage: start-screen [--auto-terminate] <solve|hive> <github-url> [additional-args...]');
|
|
226
|
+
console.error('');
|
|
227
|
+
console.error('Options:');
|
|
228
|
+
console.error(' --auto-terminate Session terminates after command completes (old behavior)');
|
|
229
|
+
console.error(' By default, session stays alive for review and reattachment');
|
|
230
|
+
console.error('');
|
|
231
|
+
console.error('Examples:');
|
|
232
|
+
console.error(' start-screen solve https://github.com/user/repo/issues/123 --dry-run');
|
|
233
|
+
console.error(' start-screen --auto-terminate solve https://github.com/user/repo/issues/456');
|
|
234
|
+
console.error(' start-screen hive https://github.com/user/repo --flag value');
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Check for --auto-terminate flag at the beginning
|
|
239
|
+
// Also validate that first arg is not an unrecognized option with em-dash or other invalid dash
|
|
240
|
+
let autoTerminate = false;
|
|
241
|
+
let argsOffset = 0;
|
|
242
|
+
|
|
243
|
+
// Check for various dash characters in first argument (em-dash \u2014, en-dash \u2013, etc.)
|
|
244
|
+
if (args[0] && /^[\u2010\u2011\u2012\u2013\u2014]/.test(args[0])) {
|
|
245
|
+
console.error(`Unknown option: ${args[0]}`);
|
|
246
|
+
console.error('Usage: start-screen [--auto-terminate] <solve|hive> <github-url> [additional-args...]');
|
|
247
|
+
console.error('Note: Use regular hyphens (--) not em-dashes or en-dashes.');
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (args[0] === '--auto-terminate') {
|
|
252
|
+
autoTerminate = true;
|
|
253
|
+
argsOffset = 1;
|
|
254
|
+
|
|
255
|
+
if (args.length < 3) {
|
|
256
|
+
console.error('Error: --auto-terminate requires a command and GitHub URL');
|
|
257
|
+
console.error('Usage: start-screen [--auto-terminate] <solve|hive> <github-url> [additional-args...]');
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
} else if (args[0] && args[0].startsWith('-') && args[0] !== '--help' && args[0] !== '-h') {
|
|
261
|
+
// First arg is an unrecognized option
|
|
262
|
+
console.error(`Unknown option: ${args[0]}`);
|
|
263
|
+
console.error('Usage: start-screen [--auto-terminate] <solve|hive> <github-url> [additional-args...]');
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Check if the next arg (after --auto-terminate if present) looks like an unrecognized option
|
|
268
|
+
if (args[argsOffset] && args[argsOffset].startsWith('-')) {
|
|
269
|
+
// Check for various dash characters (em-dash, en-dash, etc.)
|
|
270
|
+
const firstArg = args[argsOffset];
|
|
271
|
+
const hasInvalidDash = /^[\u2010\u2011\u2012\u2013\u2014]/.test(firstArg);
|
|
272
|
+
if (hasInvalidDash || (firstArg.startsWith('-') && firstArg !== '--help' && firstArg !== '-h')) {
|
|
273
|
+
console.error(`Unknown option: ${firstArg}`);
|
|
274
|
+
console.error('Usage: start-screen [--auto-terminate] <solve|hive> <github-url> [additional-args...]');
|
|
275
|
+
console.error('Expected command to be "solve" or "hive", not an option.');
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const command = args[argsOffset];
|
|
281
|
+
const githubUrl = args[argsOffset + 1];
|
|
282
|
+
const commandArgs = args.slice(argsOffset + 2);
|
|
283
|
+
|
|
284
|
+
// Validate command
|
|
285
|
+
if (command !== 'solve' && command !== 'hive') {
|
|
286
|
+
console.error(`Error: Invalid command '${command}'. Must be 'solve' or 'hive'.`);
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Validate GitHub URL
|
|
291
|
+
const parsed = parseGitHubUrl(githubUrl);
|
|
292
|
+
if (!parsed.valid) {
|
|
293
|
+
console.error(`Error: Invalid GitHub URL. ${parsed.error}`);
|
|
294
|
+
console.error('Please provide a valid GitHub repository or issue URL.');
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Generate screen session name
|
|
299
|
+
const sessionName = generateScreenName(command, githubUrl);
|
|
300
|
+
|
|
301
|
+
// Check for screen availability
|
|
302
|
+
try {
|
|
303
|
+
await execAsync('which screen');
|
|
304
|
+
} catch {
|
|
305
|
+
console.error('Error: GNU Screen is not installed or not in PATH.');
|
|
306
|
+
console.error('Please install it using your package manager:');
|
|
307
|
+
console.error(' Ubuntu/Debian: sudo apt-get install screen');
|
|
308
|
+
console.error(' macOS: brew install screen');
|
|
309
|
+
console.error(' RHEL/CentOS: sudo yum install screen');
|
|
310
|
+
process.exit(1);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Prepare full argument list for the command
|
|
314
|
+
const fullArgs = [githubUrl, ...commandArgs];
|
|
315
|
+
|
|
316
|
+
// Create or enter the screen session
|
|
317
|
+
await createOrEnterScreen(sessionName, command, fullArgs, autoTerminate);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Run the main function
|
|
321
|
+
main().catch((error) => {
|
|
322
|
+
console.error('Unexpected error:', error);
|
|
323
|
+
process.exit(1);
|
|
324
|
+
});
|
package/src/task.mjs
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Early exit paths - handle these before loading all modules to speed up testing
|
|
4
|
+
const earlyArgs = process.argv.slice(2);
|
|
5
|
+
|
|
6
|
+
if (earlyArgs.includes('--version')) {
|
|
7
|
+
const { getVersion } = await import('./version.lib.mjs');
|
|
8
|
+
try {
|
|
9
|
+
const version = await getVersion();
|
|
10
|
+
console.log(version);
|
|
11
|
+
} catch {
|
|
12
|
+
console.error('Error: Unable to determine version');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (earlyArgs.includes('--help') || earlyArgs.includes('-h')) {
|
|
19
|
+
// Show help and exit
|
|
20
|
+
console.log('Usage: task.mjs <task-description> [options]');
|
|
21
|
+
console.log('\nOptions:');
|
|
22
|
+
console.log(' --version Show version number');
|
|
23
|
+
console.log(' --help, -h Show help');
|
|
24
|
+
console.log(' --clarify Enable clarification mode [default: true]');
|
|
25
|
+
console.log(' --decompose Enable decomposition mode [default: true]');
|
|
26
|
+
console.log(' --only-clarify Only run clarification mode');
|
|
27
|
+
console.log(' --only-decompose Only run decomposition mode');
|
|
28
|
+
console.log(' --model, -m Model to use (opus, sonnet, or full model ID) [default: sonnet]');
|
|
29
|
+
console.log(' --verbose, -v Enable verbose logging');
|
|
30
|
+
console.log(' --output-format Output format (text or json) [default: text]');
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Use use-m to dynamically import modules for cross-runtime compatibility
|
|
35
|
+
const { use } = eval(await (await fetch('https://unpkg.com/use-m/use.js')).text());
|
|
36
|
+
|
|
37
|
+
const yargs = (await use('yargs@latest')).default;
|
|
38
|
+
const path = (await use('path')).default;
|
|
39
|
+
const fs = (await use('fs')).promises;
|
|
40
|
+
const { spawn } = (await use('child_process')).default;
|
|
41
|
+
|
|
42
|
+
// Import Claude execution functions
|
|
43
|
+
import { mapModelToId } from './claude.lib.mjs';
|
|
44
|
+
|
|
45
|
+
// Global log file reference
|
|
46
|
+
let logFile = null;
|
|
47
|
+
|
|
48
|
+
// Helper function to log to both console and file
|
|
49
|
+
const log = async (message, options = {}) => {
|
|
50
|
+
const { level = 'info', verbose = false } = options;
|
|
51
|
+
|
|
52
|
+
// Skip verbose logs unless --verbose is enabled
|
|
53
|
+
if (verbose && !global.verboseMode) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Write to file if log file is set
|
|
58
|
+
if (logFile) {
|
|
59
|
+
const logMessage = `[${new Date().toISOString()}] [${level.toUpperCase()}] ${message}`;
|
|
60
|
+
await fs.appendFile(logFile, logMessage + '\n').catch(() => {});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Write to console based on level
|
|
64
|
+
switch (level) {
|
|
65
|
+
case 'error':
|
|
66
|
+
console.error(message);
|
|
67
|
+
break;
|
|
68
|
+
case 'warning':
|
|
69
|
+
case 'warn':
|
|
70
|
+
console.warn(message);
|
|
71
|
+
break;
|
|
72
|
+
case 'info':
|
|
73
|
+
default:
|
|
74
|
+
console.log(message);
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Configure command line arguments - task description as positional argument
|
|
80
|
+
// Use yargs().parse(args) instead of yargs(args).argv to ensure .strict() mode works
|
|
81
|
+
const argv = yargs()
|
|
82
|
+
.usage('Usage: $0 <task-description> [options]')
|
|
83
|
+
.positional('task-description', {
|
|
84
|
+
type: 'string',
|
|
85
|
+
description: 'The task to clarify and decompose'
|
|
86
|
+
})
|
|
87
|
+
.option('clarify', {
|
|
88
|
+
type: 'boolean',
|
|
89
|
+
description: 'Enable clarification mode (asks clarifying questions about the task)',
|
|
90
|
+
default: true
|
|
91
|
+
})
|
|
92
|
+
.option('decompose', {
|
|
93
|
+
type: 'boolean',
|
|
94
|
+
description: 'Enable decomposition mode (breaks down the task into subtasks)',
|
|
95
|
+
default: true
|
|
96
|
+
})
|
|
97
|
+
.option('only-clarify', {
|
|
98
|
+
type: 'boolean',
|
|
99
|
+
description: 'Only run clarification mode, skip decomposition',
|
|
100
|
+
default: false
|
|
101
|
+
})
|
|
102
|
+
.option('only-decompose', {
|
|
103
|
+
type: 'boolean',
|
|
104
|
+
description: 'Only run decomposition mode, skip clarification',
|
|
105
|
+
default: false
|
|
106
|
+
})
|
|
107
|
+
.option('model', {
|
|
108
|
+
type: 'string',
|
|
109
|
+
description: 'Model to use (opus, sonnet, or full model ID like claude-sonnet-4-5-20250929)',
|
|
110
|
+
alias: 'm',
|
|
111
|
+
default: 'sonnet',
|
|
112
|
+
choices: ['opus', 'sonnet', 'claude-sonnet-4-5-20250929', 'claude-opus-4-5-20251101']
|
|
113
|
+
})
|
|
114
|
+
.option('verbose', {
|
|
115
|
+
type: 'boolean',
|
|
116
|
+
description: 'Enable verbose logging for debugging',
|
|
117
|
+
alias: 'v',
|
|
118
|
+
default: false
|
|
119
|
+
})
|
|
120
|
+
.option('output-format', {
|
|
121
|
+
type: 'string',
|
|
122
|
+
description: 'Output format (text or json)',
|
|
123
|
+
alias: 'o',
|
|
124
|
+
default: 'text',
|
|
125
|
+
choices: ['text', 'json']
|
|
126
|
+
})
|
|
127
|
+
.check((argv) => {
|
|
128
|
+
if (!argv['task-description'] && !argv._[0]) {
|
|
129
|
+
throw new Error('Please provide a task description');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Handle mutual exclusivity of only-clarify and only-decompose
|
|
133
|
+
if (argv['only-clarify'] && argv['only-decompose']) {
|
|
134
|
+
throw new Error('Cannot use both --only-clarify and --only-decompose at the same time');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// If only-clarify is set, disable decompose
|
|
138
|
+
if (argv['only-clarify']) {
|
|
139
|
+
argv.decompose = false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// If only-decompose is set, disable clarify
|
|
143
|
+
if (argv['only-decompose']) {
|
|
144
|
+
argv.clarify = false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return true;
|
|
148
|
+
})
|
|
149
|
+
.parserConfiguration({
|
|
150
|
+
'boolean-negation': true
|
|
151
|
+
})
|
|
152
|
+
.help()
|
|
153
|
+
.alias('h', 'help')
|
|
154
|
+
// Use yargs built-in strict mode to reject unrecognized options
|
|
155
|
+
// This prevents issues like #453 and #482 where unknown options are silently ignored
|
|
156
|
+
.strict()
|
|
157
|
+
.parse(process.argv.slice(2));
|
|
158
|
+
|
|
159
|
+
const taskDescription = argv['task-description'] || argv._[0];
|
|
160
|
+
|
|
161
|
+
// Set global verbose mode for log function
|
|
162
|
+
global.verboseMode = argv.verbose;
|
|
163
|
+
|
|
164
|
+
// Create permanent log file immediately with timestamp
|
|
165
|
+
const scriptDir = path.dirname(process.argv[1]);
|
|
166
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
167
|
+
logFile = path.join(scriptDir, `task-${timestamp}.log`);
|
|
168
|
+
|
|
169
|
+
// Create the log file immediately
|
|
170
|
+
await fs.writeFile(logFile, `# Task.mjs Log - ${new Date().toISOString()}\n\n`);
|
|
171
|
+
await log(`š Log file: ${logFile}`);
|
|
172
|
+
await log(' (All output will be logged here)');
|
|
173
|
+
|
|
174
|
+
// Helper function to format aligned console output
|
|
175
|
+
const formatAligned = (icon, label, value, indent = 0) => {
|
|
176
|
+
const spaces = ' '.repeat(indent);
|
|
177
|
+
const labelWidth = 25 - indent;
|
|
178
|
+
const paddedLabel = label.padEnd(labelWidth, ' ');
|
|
179
|
+
return `${spaces}${icon} ${paddedLabel} ${value || ''}`;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
await log('\nšÆ Task Processing Started');
|
|
183
|
+
await log(formatAligned('š', 'Task description:', taskDescription));
|
|
184
|
+
await log(formatAligned('š¤', 'Model:', argv.model));
|
|
185
|
+
await log(formatAligned('š”', 'Clarify mode:', argv.clarify ? 'enabled' : 'disabled'));
|
|
186
|
+
await log(formatAligned('š', 'Decompose mode:', argv.decompose ? 'enabled' : 'disabled'));
|
|
187
|
+
await log(formatAligned('š', 'Output format:', argv.outputFormat));
|
|
188
|
+
|
|
189
|
+
const claudePath = process.env.CLAUDE_PATH || 'claude';
|
|
190
|
+
|
|
191
|
+
// Helper function to execute Claude command
|
|
192
|
+
const executeClaude = (prompt, model) => {
|
|
193
|
+
return new Promise((resolve, reject) => {
|
|
194
|
+
// Map model alias to full ID
|
|
195
|
+
const mappedModel = mapModelToId(model);
|
|
196
|
+
|
|
197
|
+
const args = [
|
|
198
|
+
'-p', prompt,
|
|
199
|
+
'--output-format', 'text',
|
|
200
|
+
'--dangerously-skip-permissions',
|
|
201
|
+
'--model', mappedModel
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
const child = spawn(claudePath, args, {
|
|
205
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
206
|
+
env: process.env
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
let stdout = '';
|
|
210
|
+
let stderr = '';
|
|
211
|
+
|
|
212
|
+
child.stdout.on('data', (data) => {
|
|
213
|
+
stdout += data.toString();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
child.stderr.on('data', (data) => {
|
|
217
|
+
stderr += data.toString();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
child.on('close', (code) => {
|
|
221
|
+
if (code === 0) {
|
|
222
|
+
resolve(stdout.trim());
|
|
223
|
+
} else {
|
|
224
|
+
reject(new Error(`Claude exited with code ${code}: ${stderr}`));
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
child.on('error', (error) => {
|
|
229
|
+
reject(error);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
const results = {
|
|
236
|
+
task: taskDescription,
|
|
237
|
+
timestamp: new Date().toISOString(),
|
|
238
|
+
clarification: null,
|
|
239
|
+
decomposition: null
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// Phase 1: Clarification
|
|
243
|
+
if (argv.clarify) {
|
|
244
|
+
await log('\nš¤ Phase 1: Task Clarification');
|
|
245
|
+
await log(' Analyzing task and generating clarifying questions...');
|
|
246
|
+
|
|
247
|
+
const clarifyPrompt = `Task: "${taskDescription}"
|
|
248
|
+
|
|
249
|
+
Please help clarify this task by:
|
|
250
|
+
1. Identifying any ambiguous aspects of the task
|
|
251
|
+
2. Asking 3-5 specific clarifying questions that would help someone implement this task more effectively
|
|
252
|
+
3. Suggesting potential assumptions that could be made if these questions aren't answered
|
|
253
|
+
4. Identifying any missing context or requirements
|
|
254
|
+
|
|
255
|
+
Provide your response in a clear, structured format that helps refine the task understanding.`;
|
|
256
|
+
|
|
257
|
+
const clarificationOutput = await executeClaude(clarifyPrompt, argv.model);
|
|
258
|
+
if (!argv.verbose) {
|
|
259
|
+
console.log('\nš Clarification Results:');
|
|
260
|
+
console.log(clarificationOutput);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
results.clarification = clarificationOutput;
|
|
264
|
+
await log('\nā
Clarification phase completed');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Phase 2: Decomposition
|
|
268
|
+
if (argv.decompose) {
|
|
269
|
+
await log('\nš Phase 2: Task Decomposition');
|
|
270
|
+
await log(' Breaking down task into manageable subtasks...');
|
|
271
|
+
|
|
272
|
+
let decomposePrompt = `Task: "${taskDescription}"`;
|
|
273
|
+
|
|
274
|
+
if (results.clarification) {
|
|
275
|
+
decomposePrompt += `\n\nClarification analysis:\n${results.clarification}`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
decomposePrompt += `\n\nPlease decompose this task by:
|
|
279
|
+
1. Breaking it down into 3-8 specific, actionable subtasks
|
|
280
|
+
2. Ordering the subtasks logically (dependencies and sequence)
|
|
281
|
+
3. Estimating relative complexity/effort for each subtask (simple/medium/complex)
|
|
282
|
+
4. Identifying any potential risks or challenges for each subtask
|
|
283
|
+
5. Suggesting success criteria for each subtask
|
|
284
|
+
|
|
285
|
+
Provide your response as a structured breakdown that someone could use as a implementation roadmap.`;
|
|
286
|
+
|
|
287
|
+
const decompositionOutput = await executeClaude(decomposePrompt, argv.model);
|
|
288
|
+
if (!argv.verbose) {
|
|
289
|
+
console.log('\nš Decomposition Results:');
|
|
290
|
+
console.log(decompositionOutput);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
results.decomposition = decompositionOutput;
|
|
294
|
+
await log('\nā
Decomposition phase completed');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Output results
|
|
298
|
+
if (argv.outputFormat === 'json') {
|
|
299
|
+
console.log('\n' + JSON.stringify(results, null, 2));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
await log('\nš Task processing completed successfully');
|
|
303
|
+
await log(`š” Review the session log for details: ${logFile}`);
|
|
304
|
+
|
|
305
|
+
} catch (error) {
|
|
306
|
+
await log(`ā Error processing task: ${error.message}`, { level: 'error' });
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|