@link-assistant/hive-mind 1.59.7 → 1.60.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/src/task.mjs CHANGED
@@ -1,13 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // Early exit paths - handle these before loading all modules to speed up testing
3
+ import crypto from 'crypto';
4
+ import path from 'path';
5
+ import { spawn } from 'child_process';
6
+ import { promises as fs } from 'fs';
7
+ import { buildStartAgentArgs, resolveStartAgentCommand } from './task.agent-command.lib.mjs';
8
+ import { getDefaultTaskModel, parseTaskArguments } from './task.config.lib.mjs';
9
+ import { validateModelName } from './models/index.mjs';
10
+ import { appendOrReplaceParentSplitSection, buildAddSubIssueApiArgs, buildIssueRestIdApiArgs, buildTaskSplitPrompt, buildTaskSplitSystemPrompt, extractTaskSplitJson, formatChildIssueBody, normalizeSplitTasks, parseCreatedIssueUrl, parseTaskIssueUrl } from './task.split.lib.mjs';
11
+
4
12
  const earlyArgs = process.argv.slice(2);
5
13
 
6
14
  if (earlyArgs.includes('--version')) {
7
15
  const { getVersion } = await import('./version.lib.mjs');
8
16
  try {
9
- const version = await getVersion();
10
- console.log(version);
17
+ console.log(await getVersion());
11
18
  } catch {
12
19
  console.error('Error: Unable to determine version');
13
20
  process.exit(1);
@@ -15,8 +22,7 @@ if (earlyArgs.includes('--version')) {
15
22
  process.exit(0);
16
23
  }
17
24
 
18
- if (earlyArgs.includes('--help') || earlyArgs.includes('-h')) {
19
- // Show help and exit
25
+ if (earlyArgs.length === 0 || earlyArgs.includes('--help') || earlyArgs.includes('-h')) {
20
26
  console.log('Usage: task.mjs <task-description> [options]');
21
27
  console.log('\nOptions:');
22
28
  console.log(' --version Show version number');
@@ -25,283 +31,262 @@ if (earlyArgs.includes('--help') || earlyArgs.includes('-h')) {
25
31
  console.log(' --decompose Enable decomposition mode [default: true]');
26
32
  console.log(' --only-clarify Only run clarification mode');
27
33
  console.log(' --only-decompose Only run decomposition mode');
28
- console.log(' --model, -m Model to use (opus, sonnet, or full model ID) [default: sonnet]');
34
+ console.log(' --split Split a GitHub issue into smaller issues');
35
+ console.log(' --split-count Number of issues to split into [default: 2]');
36
+ console.log(' --tool AI tool for agent-commander read-only mode (claude, codex, opencode, agent) [default: claude]');
37
+ console.log(' --model, -m Model to use');
38
+ console.log(' --isolation agent-commander isolation mode [default: screen]');
39
+ console.log(' --dry-run Print split output without creating GitHub issues');
29
40
  console.log(' --verbose, -v Enable verbose logging');
30
41
  console.log(' --output-format Output format (text or json) [default: text]');
31
- process.exit(0);
42
+ process.exit(earlyArgs.length === 0 ? 1 : 0);
32
43
  }
33
44
 
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
- import { claudeModels, defaultModels, buildModelOptionDescription } from './models/index.mjs';
45
-
46
- // Global log file reference
47
- let logFile = null;
48
-
49
- // Helper function to log to both console and file
50
- const log = async (message, options = {}) => {
51
- const { level = 'info', verbose = false } = options;
52
-
53
- // Skip verbose logs unless --verbose is enabled
54
- if (verbose && !global.verboseMode) {
55
- return;
56
- }
57
-
58
- // Write to file if log file is set
59
- if (logFile) {
60
- const logMessage = `[${new Date().toISOString()}] [${level.toUpperCase()}] ${message}`;
61
- await fs.appendFile(logFile, logMessage + '\n').catch(() => {});
62
- }
63
-
64
- // Write to console based on level
65
- switch (level) {
66
- case 'error':
67
- console.error(message);
68
- break;
69
- case 'warning':
70
- case 'warn':
71
- console.warn(message);
72
- break;
73
- case 'info':
74
- default:
75
- console.log(message);
76
- break;
77
- }
78
- };
79
-
80
- // Configure command line arguments - task description as positional argument
81
- // Use yargs().parse(args) instead of yargs(args).argv to ensure .strict() mode works
82
- const argv = yargs()
83
- .usage('Usage: $0 <task-description> [options]')
84
- .positional('task-description', {
85
- type: 'string',
86
- description: 'The task to clarify and decompose',
87
- })
88
- .option('clarify', {
89
- type: 'boolean',
90
- description: 'Enable clarification mode (asks clarifying questions about the task)',
91
- default: true,
92
- })
93
- .option('decompose', {
94
- type: 'boolean',
95
- description: 'Enable decomposition mode (breaks down the task into subtasks)',
96
- default: true,
97
- })
98
- .option('only-clarify', {
99
- type: 'boolean',
100
- description: 'Only run clarification mode, skip decomposition',
101
- default: false,
102
- })
103
- .option('only-decompose', {
104
- type: 'boolean',
105
- description: 'Only run decomposition mode, skip clarification',
106
- default: false,
107
- })
108
- .option('model', {
109
- type: 'string',
110
- description: buildModelOptionDescription(),
111
- alias: 'm',
112
- default: defaultModels.claude,
113
- choices: Object.keys(claudeModels),
114
- })
115
- .option('verbose', {
116
- type: 'boolean',
117
- description: 'Enable verbose logging for debugging',
118
- alias: 'v',
119
- default: false,
120
- })
121
- .option('output-format', {
122
- type: 'string',
123
- description: 'Output format (text or json)',
124
- alias: 'o',
125
- default: 'text',
126
- choices: ['text', 'json'],
127
- })
128
- .option('execute-tool-with-bun', {
129
- type: 'boolean',
130
- description: 'Execute the AI tool using bunx (experimental, may improve speed and memory usage)',
131
- default: false,
132
- })
133
- .check(argv => {
134
- if (!argv['task-description'] && !argv._[0]) {
135
- throw new Error('Please provide a task description');
136
- }
137
-
138
- // Handle mutual exclusivity of only-clarify and only-decompose
139
- if (argv['only-clarify'] && argv['only-decompose']) {
140
- throw new Error('Cannot use both --only-clarify and --only-decompose at the same time');
141
- }
142
-
143
- // If only-clarify is set, disable decompose
144
- if (argv['only-clarify']) {
145
- argv.decompose = false;
146
- }
147
-
148
- // If only-decompose is set, disable clarify
149
- if (argv['only-decompose']) {
150
- argv.clarify = false;
151
- }
152
-
153
- return true;
154
- })
155
- .parserConfiguration({
156
- 'boolean-negation': true,
157
- })
158
- .help()
159
- .alias('h', 'help')
160
- // Use yargs built-in strict mode to reject unrecognized options
161
- // This prevents issues like #453 and #482 where unknown options are silently ignored
162
- .strict()
163
- .parse(process.argv.slice(2));
164
-
165
- const taskDescription = argv['task-description'] || argv._[0];
45
+ let argv;
46
+ try {
47
+ argv = parseTaskArguments(process.argv);
48
+ } catch (error) {
49
+ console.error(error.message || String(error));
50
+ process.exit(1);
51
+ }
166
52
 
167
- // Set global verbose mode for log function
168
- global.verboseMode = argv.verbose;
53
+ const taskInput = argv['task-input'] || argv.taskInput || argv._[0];
54
+ const selectedModel = argv.model || getDefaultTaskModel(argv.tool);
55
+ const modelValidation = validateModelName(selectedModel, argv.tool);
56
+ if (!modelValidation.valid) {
57
+ console.error(modelValidation.message);
58
+ process.exit(1);
59
+ }
169
60
 
170
- // Create permanent log file immediately with timestamp
171
61
  const scriptDir = path.dirname(process.argv[1]);
172
62
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
173
- logFile = path.join(scriptDir, `task-${timestamp}.log`);
63
+ const logFile = path.join(scriptDir, `task-${timestamp}.log`);
174
64
 
175
- // Create the log file immediately
176
- await fs.writeFile(logFile, `# Task.mjs Log - ${new Date().toISOString()}\n\n`);
177
- await log(`šŸ“ Log file: ${logFile}`);
178
- await log(' (All output will be logged here)');
65
+ async function log(message, options = {}) {
66
+ const { level = 'info', verbose = false } = options;
67
+ if (verbose && !argv.verbose) return;
68
+ await fs.appendFile(logFile, `[${new Date().toISOString()}] [${level.toUpperCase()}] ${message}\n`).catch(() => {});
69
+ if (level === 'error') console.error(message);
70
+ else if (level === 'warning' || level === 'warn') console.warn(message);
71
+ else console.log(message);
72
+ }
179
73
 
180
- // Helper function to format aligned console output
181
- const formatAligned = (icon, label, value, indent = 0) => {
74
+ function formatAligned(icon, label, value, indent = 0) {
182
75
  const spaces = ' '.repeat(indent);
183
76
  const labelWidth = 25 - indent;
184
- const paddedLabel = label.padEnd(labelWidth, ' ');
185
- return `${spaces}${icon} ${paddedLabel} ${value || ''}`;
186
- };
187
-
188
- await log('\nšŸŽÆ Task Processing Started');
189
- await log(formatAligned('šŸ“', 'Task description:', taskDescription));
190
- await log(formatAligned('šŸ¤–', 'Model:', argv.model));
191
- await log(formatAligned('šŸ’”', 'Clarify mode:', argv.clarify ? 'enabled' : 'disabled'));
192
- await log(formatAligned('šŸ”', 'Decompose mode:', argv.decompose ? 'enabled' : 'disabled'));
193
- await log(formatAligned('šŸ“„', 'Output format:', argv.outputFormat));
194
-
195
- const claudePath = argv.executeToolWithBun ? 'bunx claude' : process.env.CLAUDE_PATH || 'claude';
196
-
197
- // Helper function to execute Claude command
198
- const executeClaude = (prompt, model) => {
199
- return new Promise((resolve, reject) => {
200
- // Map model alias to full ID
201
- const mappedModel = mapModelToId(model);
202
-
203
- const args = ['-p', prompt, '--output-format', 'text', '--dangerously-skip-permissions', '--model', mappedModel];
77
+ return `${spaces}${icon} ${label.padEnd(labelWidth)} ${value || ''}`;
78
+ }
204
79
 
205
- const child = spawn(claudePath, args, {
80
+ function runCommand(command, args, options = {}) {
81
+ return new Promise(resolve => {
82
+ const child = spawn(command, args, {
206
83
  stdio: ['ignore', 'pipe', 'pipe'],
207
84
  env: process.env,
85
+ ...options,
208
86
  });
209
-
210
87
  let stdout = '';
211
88
  let stderr = '';
212
-
213
89
  child.stdout.on('data', data => {
214
90
  stdout += data.toString();
215
91
  });
216
-
217
92
  child.stderr.on('data', data => {
218
93
  stderr += data.toString();
219
94
  });
220
-
95
+ child.on('error', error => {
96
+ resolve({ code: 1, stdout, stderr: stderr || error.message });
97
+ });
221
98
  child.on('close', code => {
222
- if (code === 0) {
223
- resolve(stdout.trim());
224
- } else {
225
- reject(new Error(`Claude exited with code ${code}: ${stderr}`));
226
- }
99
+ resolve({ code, stdout, stderr });
227
100
  });
101
+ });
102
+ }
228
103
 
229
- child.on('error', error => {
230
- reject(error);
231
- });
104
+ async function commandOutput(command, args, options = {}) {
105
+ const result = await runCommand(command, args, options);
106
+ if (result.code !== 0) {
107
+ const output = `${result.stderr || ''}${result.stdout || ''}`.trim();
108
+ throw new Error(output || `${command} exited with code ${result.code}`);
109
+ }
110
+ return result.stdout.trim();
111
+ }
112
+
113
+ function buildScreenName(issue) {
114
+ const base = issue ? `task-split-${issue.owner}-${issue.repo}-${issue.number}` : 'task-agent';
115
+ return `${base}-${crypto.randomUUID().slice(0, 8)}`.replace(/[^A-Za-z0-9_.-]/g, '-');
116
+ }
117
+
118
+ async function runAgentPrompt(prompt, systemPrompt, issue = null) {
119
+ const startAgent = await resolveStartAgentCommand({ runCommand });
120
+ if (!startAgent) {
121
+ throw new Error('agent-commander start-agent binary not found. Run npm install so the agent-commander dependency is available, or install start-agent on PATH.');
122
+ }
123
+
124
+ const args = buildStartAgentArgs({
125
+ tool: argv.tool,
126
+ workingDirectory: process.cwd(),
127
+ prompt,
128
+ systemPrompt,
129
+ model: selectedModel,
130
+ isolation: argv.isolation,
131
+ screenName: argv.isolation === 'screen' ? argv.screenName || buildScreenName(issue) : null,
132
+ verbose: argv.verbose,
232
133
  });
233
- };
234
134
 
235
- try {
236
- const results = {
237
- task: taskDescription,
238
- timestamp: new Date().toISOString(),
239
- clarification: null,
240
- decomposition: null,
135
+ await log(`Running agent-commander with tool=${argv.tool}, model=${selectedModel}, isolation=${argv.isolation}, readOnly=true`, { verbose: true });
136
+ const result = await runCommand(startAgent, args);
137
+ const output = `${result.stdout || ''}${result.stderr ? `\n${result.stderr}` : ''}`.trim();
138
+ if (result.code !== 0) {
139
+ throw new Error(output || `start-agent exited with code ${result.code}`);
140
+ }
141
+ return output;
142
+ }
143
+
144
+ async function fetchIssue(issue) {
145
+ const issueJson = await commandOutput('gh', ['issue', 'view', String(issue.number), '--repo', `${issue.owner}/${issue.repo}`, '--json', 'title,body,number,url,labels']);
146
+ const data = JSON.parse(issueJson);
147
+ return {
148
+ owner: issue.owner,
149
+ repo: issue.repo,
150
+ number: data.number,
151
+ url: data.url,
152
+ title: data.title,
153
+ body: data.body || '',
154
+ labels: Array.isArray(data.labels) ? data.labels.map(label => label.name).filter(Boolean) : [],
241
155
  };
156
+ }
242
157
 
243
- // Phase 1: Clarification
244
- if (argv.clarify) {
245
- await log('\nšŸ¤” Phase 1: Task Clarification');
246
- await log(' Analyzing task and generating clarifying questions...');
158
+ async function fetchIssueRestId(issue) {
159
+ const id = Number(await commandOutput('gh', buildIssueRestIdApiArgs(issue)));
160
+ if (!Number.isInteger(id) || id <= 0) {
161
+ throw new Error(`Could not resolve REST id for issue #${issue.number}`);
162
+ }
163
+ return id;
164
+ }
247
165
 
248
- const clarifyPrompt = `Task: "${taskDescription}"
166
+ async function createChildIssue(parentIssue, task, index, splitCount) {
167
+ const args = ['issue', 'create', '--repo', `${parentIssue.owner}/${parentIssue.repo}`, '--title', task.title, '--body', formatChildIssueBody({ parentIssue, task, index, splitCount })];
168
+ if (parentIssue.labels.length > 0) {
169
+ args.push('--label', parentIssue.labels.join(','));
170
+ }
249
171
 
250
- Please help clarify this task by:
251
- 1. Identifying any ambiguous aspects of the task
252
- 2. Asking 3-5 specific clarifying questions that would help someone implement this task more effectively
253
- 3. Suggesting potential assumptions that could be made if these questions aren't answered
254
- 4. Identifying any missing context or requirements
172
+ const url = await commandOutput('gh', args);
173
+ const parsed = parseCreatedIssueUrl(url);
174
+ const restId = await fetchIssueRestId(parsed);
175
+ return {
176
+ owner: parsed.owner,
177
+ repo: parsed.repo,
178
+ number: parsed.number,
179
+ restId,
180
+ url,
181
+ title: task.title,
182
+ };
183
+ }
255
184
 
256
- Provide your response in a clear, structured format that helps refine the task understanding.`;
185
+ async function linkChildIssue(parentIssue, childIssue) {
186
+ await commandOutput('gh', buildAddSubIssueApiArgs({ parentIssue, subIssueId: childIssue.restId }));
187
+ }
257
188
 
258
- const clarificationOutput = await executeClaude(clarifyPrompt, argv.model);
259
- if (!argv.verbose) {
260
- console.log('\nšŸ“ Clarification Results:');
261
- console.log(clarificationOutput);
262
- }
189
+ async function updateParentIssue(parentIssue, childIssues) {
190
+ const body = appendOrReplaceParentSplitSection(parentIssue.body, childIssues);
191
+ await commandOutput('gh', ['issue', 'edit', String(parentIssue.number), '--repo', `${parentIssue.owner}/${parentIssue.repo}`, '--body', body]);
192
+ const childList = childIssues.map(issue => `- #${issue.number} ${issue.title}`).join('\n');
193
+ await commandOutput('gh', ['issue', 'comment', String(parentIssue.number), '--repo', `${parentIssue.owner}/${parentIssue.repo}`, '--body', `Split into ${childIssues.length} tasks:\n\n${childList}`]);
194
+ }
263
195
 
264
- results.clarification = clarificationOutput;
265
- await log('\nāœ… Clarification phase completed');
196
+ async function runSplitMode() {
197
+ const parsedIssue = parseTaskIssueUrl(taskInput);
198
+ if (!parsedIssue.valid) {
199
+ throw new Error(parsedIssue.error || 'Invalid GitHub issue URL');
266
200
  }
267
201
 
268
- // Phase 2: Decomposition
269
- if (argv.decompose) {
270
- await log('\nšŸ” Phase 2: Task Decomposition');
271
- await log(' Breaking down task into manageable subtasks...');
202
+ const parentIssue = await fetchIssue(parsedIssue);
203
+ const prompt = buildTaskSplitPrompt({ issue: parentIssue, splitCount: argv.splitCount });
204
+ const output = await runAgentPrompt(prompt, buildTaskSplitSystemPrompt(), parentIssue);
205
+ const tasks = normalizeSplitTasks(extractTaskSplitJson(output), argv.splitCount);
206
+
207
+ if (argv.dryRun) {
208
+ return {
209
+ parentIssue,
210
+ tasks,
211
+ createdIssues: [],
212
+ dryRun: true,
213
+ };
214
+ }
272
215
 
273
- let decomposePrompt = `Task: "${taskDescription}"`;
216
+ const createdIssues = [];
217
+ for (let i = 0; i < tasks.length; i++) {
218
+ createdIssues.push(await createChildIssue(parentIssue, tasks[i], i, tasks.length));
219
+ }
220
+ for (const childIssue of createdIssues) {
221
+ await linkChildIssue(parentIssue, childIssue);
222
+ }
223
+ await updateParentIssue(parentIssue, createdIssues);
274
224
 
275
- if (results.clarification) {
276
- decomposePrompt += `\n\nClarification analysis:\n${results.clarification}`;
277
- }
225
+ return {
226
+ parentIssue,
227
+ tasks,
228
+ createdIssues,
229
+ dryRun: false,
230
+ };
231
+ }
278
232
 
279
- decomposePrompt += `\n\nPlease decompose this task by:
280
- 1. Breaking it down into 3-8 specific, actionable subtasks
281
- 2. Ordering the subtasks logically (dependencies and sequence)
282
- 3. Estimating relative complexity/effort for each subtask (simple/medium/complex)
283
- 4. Identifying any potential risks or challenges for each subtask
284
- 5. Suggesting success criteria for each subtask
233
+ async function runClarifyOrDecomposeMode() {
234
+ const results = {
235
+ task: taskInput,
236
+ timestamp: new Date().toISOString(),
237
+ clarification: null,
238
+ decomposition: null,
239
+ };
285
240
 
286
- Provide your response as a structured breakdown that someone could use as a implementation roadmap.`;
241
+ if (argv.clarify) {
242
+ const prompt = `Task: "${taskInput}"
287
243
 
288
- const decompositionOutput = await executeClaude(decomposePrompt, argv.model);
289
- if (!argv.verbose) {
290
- console.log('\nšŸ” Decomposition Results:');
291
- console.log(decompositionOutput);
292
- }
244
+ Please help clarify this task by:
245
+ 1. Identifying ambiguous aspects of the task
246
+ 2. Asking 3-5 specific clarifying questions
247
+ 3. Suggesting assumptions that could be made if these questions are not answered
248
+ 4. Identifying missing context or requirements`;
249
+ results.clarification = await runAgentPrompt(prompt, 'Return a concise clarification analysis. Do not modify files or run commands.');
250
+ }
293
251
 
294
- results.decomposition = decompositionOutput;
295
- await log('\nāœ… Decomposition phase completed');
252
+ if (argv.decompose) {
253
+ const prompt = `Task: "${taskInput}"
254
+
255
+ ${results.clarification ? `Clarification analysis:\n${results.clarification}\n\n` : ''}Please decompose this task into actionable subtasks with dependencies, complexity, risks, and success criteria.`;
256
+ results.decomposition = await runAgentPrompt(prompt, 'Return a concise decomposition. Do not modify files or run commands.');
296
257
  }
297
258
 
298
- // Output results
259
+ return results;
260
+ }
261
+
262
+ try {
263
+ await fs.writeFile(logFile, `# Task Log - ${new Date().toISOString()}\n\n`);
264
+ await log(`šŸ“ Log file: ${logFile}`);
265
+ await log('\nšŸŽÆ Task Processing Started');
266
+ await log(formatAligned('šŸ“', 'Task input:', taskInput));
267
+ await log(formatAligned('šŸ› ', 'Tool:', argv.tool));
268
+ await log(formatAligned('šŸ¤–', 'Model:', selectedModel));
269
+ await log(formatAligned('šŸ”’', 'Isolation:', argv.isolation));
270
+ await log(formatAligned('āœ‚ļø', 'Split mode:', argv.split ? `enabled (count: ${argv.splitCount})` : 'disabled'));
271
+
272
+ const result = argv.split ? await runSplitMode() : await runClarifyOrDecomposeMode();
273
+
299
274
  if (argv.outputFormat === 'json') {
300
- console.log('\n' + JSON.stringify(results, null, 2));
275
+ console.log(JSON.stringify(result, null, 2));
276
+ } else if (argv.split) {
277
+ if (result.dryRun) {
278
+ console.log('\nPlanned split issues:');
279
+ result.tasks.forEach((task, index) => console.log(`${index + 1}. ${task.title}`));
280
+ } else {
281
+ console.log('\nCreated split issues:');
282
+ result.createdIssues.forEach(issue => console.log(`- #${issue.number} ${issue.url}`));
283
+ }
284
+ } else {
285
+ if (result.clarification) console.log(`\nClarification Results:\n${result.clarification}`);
286
+ if (result.decomposition) console.log(`\nDecomposition Results:\n${result.decomposition}`);
301
287
  }
302
288
 
303
289
  await log('\nšŸŽ‰ Task processing completed successfully');
304
- await log(`šŸ’” Review the session log for details: ${logFile}`);
305
290
  } catch (error) {
306
291
  await log(`āŒ Error processing task: ${error.message}`, { level: 'error' });
307
292
  process.exit(1);