@link-assistant/hive-mind 1.0.4 → 1.1.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 +39 -0
- package/package.json +3 -2
- package/src/agent.lib.mjs +3 -1
- package/src/agent.prompts.lib.mjs +43 -5
- package/src/claude.lib.mjs +3 -1
- package/src/claude.prompts.lib.mjs +44 -5
- package/src/codex.lib.mjs +3 -1
- package/src/codex.prompts.lib.mjs +43 -5
- package/src/opencode.lib.mjs +3 -1
- package/src/opencode.prompts.lib.mjs +44 -4
- package/src/solve.config.lib.mjs +5 -0
- package/src/solve.mjs +13 -2
- package/src/solve.repository.lib.mjs +47 -2
- package/src/telegram-solve-queue.lib.mjs +68 -19
- package/src/usage-limit.lib.mjs +105 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 4c46685: Add --enable-workspaces option for separate workspace directories
|
|
8
|
+
|
|
9
|
+
This feature adds support for creating separate workspace directories for all AI tools (claude, opencode, codex, agent). When enabled with `--enable-workspaces`, the tool creates a structured workspace:
|
|
10
|
+
- `/tmp/hive-mind-solve-gh-{owner}/{repo}-issue-{issueNumber}-workspace-{timestamp}/repository` - for the cloned repo
|
|
11
|
+
- `/tmp/hive-mind-solve-gh-{owner}/{repo}-issue-{issueNumber}-workspace-{timestamp}/tmp` - for temp files, logs, downloads
|
|
12
|
+
|
|
13
|
+
The workspace tmp directory is passed to all tool prompts, with explicit examples for saving CI logs, diffs, and command outputs.
|
|
14
|
+
|
|
15
|
+
- Add relative time display for usage limit reset messages in GitHub comments
|
|
16
|
+
|
|
17
|
+
When the AI tool hits its usage limit, GitHub comments now show the reset time in a more user-friendly format:
|
|
18
|
+
- Before: `11:00 PM`
|
|
19
|
+
- After: `in 1h 23m (11:00 PM UTC)`
|
|
20
|
+
|
|
21
|
+
This helps users in different timezones understand when the limit will reset more quickly.
|
|
22
|
+
|
|
23
|
+
## 1.0.5
|
|
24
|
+
|
|
25
|
+
### Patch Changes
|
|
26
|
+
|
|
27
|
+
- a68a9f2: fix(queue): simplify queue logic based on PR feedback
|
|
28
|
+
- **Use 5-minute load average for CPU**: Uses `loadAvg5` instead of instantaneous CPU usage,
|
|
29
|
+
providing a more stable metric not affected by transient spikes during claude startup.
|
|
30
|
+
Cache TTL is 2 minutes.
|
|
31
|
+
- **Keep RAM threshold with caching**: RAM_THRESHOLD (50%) is still checked but uses cached
|
|
32
|
+
values only (no uncached rechecks) to simplify the logic.
|
|
33
|
+
- **Increase MIN_START_INTERVAL_MS to 2 minutes**: Allows enough time for solve command to
|
|
34
|
+
start actual claude process, ensuring running processes are counted when API limits are checked.
|
|
35
|
+
- **Increase CONSUMER_POLL_INTERVAL_MS to 1 minute**: Reduces unnecessary system checks.
|
|
36
|
+
One-minute polling is sufficient for queue management.
|
|
37
|
+
- **Running processes not a blocking limit**: Commands can run in parallel as long as actual
|
|
38
|
+
limits (CPU, API, etc.) are not exceeded. Claude process info is only supplementary.
|
|
39
|
+
|
|
40
|
+
Fixes #1078
|
|
41
|
+
|
|
3
42
|
## 1.0.4
|
|
4
43
|
|
|
5
44
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@link-assistant/hive-mind",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "AI-powered issue solver and hive mind for collaborative problem solving",
|
|
5
5
|
"main": "src/hive.mjs",
|
|
6
6
|
"type": "module",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"hive-telegram-bot": "./src/telegram-bot.mjs"
|
|
14
14
|
},
|
|
15
15
|
"scripts": {
|
|
16
|
-
"test": "
|
|
16
|
+
"test": "node tests/solve-queue.test.mjs",
|
|
17
|
+
"test:queue": "node tests/solve-queue.test.mjs",
|
|
17
18
|
"lint": "eslint 'src/**/*.{js,mjs,cjs}'",
|
|
18
19
|
"lint:fix": "eslint 'src/**/*.{js,mjs,cjs}' --fix",
|
|
19
20
|
"format": "prettier --write \"**/*.{js,mjs,json,md}\"",
|
package/src/agent.lib.mjs
CHANGED
|
@@ -245,7 +245,7 @@ export const handleAgentRuntimeSwitch = async () => {
|
|
|
245
245
|
|
|
246
246
|
// Main function to execute Agent with prompts and settings
|
|
247
247
|
export const executeAgent = async params => {
|
|
248
|
-
const { issueUrl, issueNumber, prNumber, prUrl, branchName, tempDir, isContinueMode, mergeStateStatus, forkedRepo, feedbackLines, forkActionsUrl, owner, repo, argv, log, formatAligned, getResourceSnapshot, agentPath = 'agent', $ } = params;
|
|
248
|
+
const { issueUrl, issueNumber, prNumber, prUrl, branchName, tempDir, workspaceTmpDir, isContinueMode, mergeStateStatus, forkedRepo, feedbackLines, forkActionsUrl, owner, repo, argv, log, formatAligned, getResourceSnapshot, agentPath = 'agent', $ } = params;
|
|
249
249
|
|
|
250
250
|
// Import prompt building functions from agent.prompts.lib.mjs
|
|
251
251
|
const { buildUserPrompt, buildSystemPrompt } = await import('./agent.prompts.lib.mjs');
|
|
@@ -258,6 +258,7 @@ export const executeAgent = async params => {
|
|
|
258
258
|
prUrl,
|
|
259
259
|
branchName,
|
|
260
260
|
tempDir,
|
|
261
|
+
workspaceTmpDir,
|
|
261
262
|
isContinueMode,
|
|
262
263
|
mergeStateStatus,
|
|
263
264
|
forkedRepo,
|
|
@@ -276,6 +277,7 @@ export const executeAgent = async params => {
|
|
|
276
277
|
prNumber,
|
|
277
278
|
branchName,
|
|
278
279
|
tempDir,
|
|
280
|
+
workspaceTmpDir,
|
|
279
281
|
isContinueMode,
|
|
280
282
|
forkedRepo,
|
|
281
283
|
argv,
|
|
@@ -11,7 +11,7 @@ import { getArchitectureCareSubPrompt } from './architecture-care.prompts.lib.mj
|
|
|
11
11
|
* @returns {string} The formatted user prompt
|
|
12
12
|
*/
|
|
13
13
|
export const buildUserPrompt = params => {
|
|
14
|
-
const { issueUrl, issueNumber, prNumber, prUrl, branchName, tempDir, isContinueMode, forkedRepo, feedbackLines, forkActionsUrl, owner, repo, argv } = params;
|
|
14
|
+
const { issueUrl, issueNumber, prNumber, prUrl, branchName, tempDir, workspaceTmpDir, isContinueMode, forkedRepo, feedbackLines, forkActionsUrl, owner, repo, argv } = params;
|
|
15
15
|
|
|
16
16
|
const promptLines = [];
|
|
17
17
|
|
|
@@ -26,6 +26,11 @@ export const buildUserPrompt = params => {
|
|
|
26
26
|
promptLines.push(`Your prepared branch: ${branchName}`);
|
|
27
27
|
promptLines.push(`Your prepared working directory: ${tempDir}`);
|
|
28
28
|
|
|
29
|
+
// Workspace tmp directory for logs and temp files (when --enable-workspaces is used)
|
|
30
|
+
if (workspaceTmpDir) {
|
|
31
|
+
promptLines.push(`Your prepared tmp directory for logs and downloads: ${workspaceTmpDir}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
29
34
|
// PR info if available
|
|
30
35
|
if (prUrl) {
|
|
31
36
|
promptLines.push(`Your prepared Pull Request: ${prUrl}`);
|
|
@@ -76,7 +81,7 @@ export const buildUserPrompt = params => {
|
|
|
76
81
|
* @returns {string} The formatted system prompt
|
|
77
82
|
*/
|
|
78
83
|
export const buildSystemPrompt = params => {
|
|
79
|
-
const { owner, repo, issueNumber, prNumber, branchName, argv } = params;
|
|
84
|
+
const { owner, repo, issueNumber, prNumber, branchName, workspaceTmpDir, argv } = params;
|
|
80
85
|
|
|
81
86
|
// Build thinking instruction based on --think level
|
|
82
87
|
let thinkLine = '';
|
|
@@ -90,9 +95,42 @@ export const buildSystemPrompt = params => {
|
|
|
90
95
|
thinkLine = `\n${thinkMessages[argv.think]}\n`;
|
|
91
96
|
}
|
|
92
97
|
|
|
93
|
-
|
|
98
|
+
// Build workspace-specific instructions and examples
|
|
99
|
+
let workspaceInstructions = '';
|
|
100
|
+
if (workspaceTmpDir) {
|
|
101
|
+
workspaceInstructions = `
|
|
102
|
+
Workspace tmp directory.
|
|
103
|
+
- Use ${workspaceTmpDir} for all temporary files, logs, and downloads.
|
|
104
|
+
- When saving command output to files, save to ${workspaceTmpDir}/command-output.log.
|
|
105
|
+
- When downloading CI logs, save to ${workspaceTmpDir}/ci-logs/.
|
|
106
|
+
- When saving diffs for review, save to ${workspaceTmpDir}/diffs/.
|
|
107
|
+
- When creating debug files, save to ${workspaceTmpDir}/debug/.
|
|
108
|
+
|
|
109
|
+
`;
|
|
110
|
+
}
|
|
94
111
|
|
|
95
|
-
|
|
112
|
+
// Build CI command examples with workspace tmp paths
|
|
113
|
+
let ciExamples = '';
|
|
114
|
+
if (workspaceTmpDir) {
|
|
115
|
+
ciExamples = `
|
|
116
|
+
CI investigation with workspace tmp directory.
|
|
117
|
+
- When downloading CI run logs:
|
|
118
|
+
gh run view RUN_ID --repo ${owner}/${repo} --log > ${workspaceTmpDir}/ci-logs/run-RUN_ID.log
|
|
119
|
+
- When downloading failed job logs:
|
|
120
|
+
gh run view RUN_ID --repo ${owner}/${repo} --log-failed > ${workspaceTmpDir}/ci-logs/run-RUN_ID-failed.log
|
|
121
|
+
- When listing CI runs with details:
|
|
122
|
+
gh run list --repo ${owner}/${repo} --branch ${branchName} --limit 5 --json databaseId,conclusion,createdAt,headSha > ${workspaceTmpDir}/ci-logs/recent-runs.json
|
|
123
|
+
- When saving PR diff for review:
|
|
124
|
+
gh pr diff ${prNumber} --repo ${owner}/${repo} > ${workspaceTmpDir}/diffs/pr-${prNumber}.diff
|
|
125
|
+
- When saving command output with stderr:
|
|
126
|
+
npm test 2>&1 | tee ${workspaceTmpDir}/test-output.log
|
|
127
|
+
- When investigating issue details:
|
|
128
|
+
gh issue view ${issueNumber} --repo ${owner}/${repo} --json body,comments > ${workspaceTmpDir}/issue-${issueNumber}.json
|
|
129
|
+
`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return `You are AI issue solver using @link-assistant/agent.${thinkLine}
|
|
133
|
+
${workspaceInstructions}General guidelines.
|
|
96
134
|
- When you execute commands, always save their logs to files for easier reading if the output becomes large.
|
|
97
135
|
- When running commands, do not set a timeout yourself — let them run as long as needed.
|
|
98
136
|
- When running sudo commands (especially package installations), always run them in the background to avoid timeout issues.
|
|
@@ -185,7 +223,7 @@ GitHub CLI command patterns.
|
|
|
185
223
|
- When adding PR comment, use gh pr comment NUMBER --body "text" --repo OWNER/REPO.
|
|
186
224
|
- When adding issue comment, use gh issue comment NUMBER --body "text" --repo OWNER/REPO.
|
|
187
225
|
- When viewing PR details, use gh pr view NUMBER --repo OWNER/REPO.
|
|
188
|
-
- When filtering with jq, use gh api repos/${owner}/${repo}/pulls/${prNumber}/comments --paginate --jq 'reverse | .[0:5]'.${getArchitectureCareSubPrompt(argv)}`;
|
|
226
|
+
- When filtering with jq, use gh api repos/${owner}/${repo}/pulls/${prNumber}/comments --paginate --jq 'reverse | .[0:5]'.${ciExamples}${getArchitectureCareSubPrompt(argv)}`;
|
|
189
227
|
};
|
|
190
228
|
|
|
191
229
|
// Export all functions as default object too
|
package/src/claude.lib.mjs
CHANGED
|
@@ -413,7 +413,7 @@ export const checkPlaywrightMcpAvailability = async () => {
|
|
|
413
413
|
* @returns {Object} Result of the execution including success status and session info
|
|
414
414
|
*/
|
|
415
415
|
export const executeClaude = async params => {
|
|
416
|
-
const { issueUrl, issueNumber, prNumber, prUrl, branchName, tempDir, isContinueMode, mergeStateStatus, forkedRepo, feedbackLines, forkActionsUrl, owner, repo, argv, log, setLogFile, getLogFile, formatAligned, getResourceSnapshot, claudePath, $ } = params;
|
|
416
|
+
const { issueUrl, issueNumber, prNumber, prUrl, branchName, tempDir, workspaceTmpDir, isContinueMode, mergeStateStatus, forkedRepo, feedbackLines, forkActionsUrl, owner, repo, argv, log, setLogFile, getLogFile, formatAligned, getResourceSnapshot, claudePath, $ } = params;
|
|
417
417
|
// Import prompt building functions from claude.prompts.lib.mjs
|
|
418
418
|
const { buildUserPrompt, buildSystemPrompt } = await import('./claude.prompts.lib.mjs');
|
|
419
419
|
// Build the user prompt
|
|
@@ -424,6 +424,7 @@ export const executeClaude = async params => {
|
|
|
424
424
|
prUrl,
|
|
425
425
|
branchName,
|
|
426
426
|
tempDir,
|
|
427
|
+
workspaceTmpDir,
|
|
427
428
|
isContinueMode,
|
|
428
429
|
mergeStateStatus,
|
|
429
430
|
forkedRepo,
|
|
@@ -443,6 +444,7 @@ export const executeClaude = async params => {
|
|
|
443
444
|
prUrl,
|
|
444
445
|
branchName,
|
|
445
446
|
tempDir,
|
|
447
|
+
workspaceTmpDir,
|
|
446
448
|
isContinueMode,
|
|
447
449
|
forkedRepo,
|
|
448
450
|
argv,
|
|
@@ -11,7 +11,7 @@ import { getArchitectureCareSubPrompt } from './architecture-care.prompts.lib.mj
|
|
|
11
11
|
* @returns {string} The formatted user prompt
|
|
12
12
|
*/
|
|
13
13
|
export const buildUserPrompt = params => {
|
|
14
|
-
const { issueUrl, issueNumber, prNumber, prUrl, branchName, tempDir, isContinueMode, forkedRepo, feedbackLines, owner, repo, argv, contributingGuidelines } = params;
|
|
14
|
+
const { issueUrl, issueNumber, prNumber, prUrl, branchName, tempDir, workspaceTmpDir, isContinueMode, forkedRepo, feedbackLines, owner, repo, argv, contributingGuidelines } = params;
|
|
15
15
|
|
|
16
16
|
const promptLines = [];
|
|
17
17
|
|
|
@@ -26,6 +26,11 @@ export const buildUserPrompt = params => {
|
|
|
26
26
|
promptLines.push(`Your prepared branch: ${branchName}`);
|
|
27
27
|
promptLines.push(`Your prepared working directory: ${tempDir}`);
|
|
28
28
|
|
|
29
|
+
// Workspace tmp directory for logs and temp files (when --enable-workspaces is used)
|
|
30
|
+
if (workspaceTmpDir) {
|
|
31
|
+
promptLines.push(`Your prepared tmp directory for logs and downloads: ${workspaceTmpDir}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
29
34
|
// PR info if available
|
|
30
35
|
if (prUrl) {
|
|
31
36
|
promptLines.push(`Your prepared Pull Request: ${prUrl}`);
|
|
@@ -82,7 +87,7 @@ export const buildUserPrompt = params => {
|
|
|
82
87
|
* @returns {string} The formatted system prompt
|
|
83
88
|
*/
|
|
84
89
|
export const buildSystemPrompt = params => {
|
|
85
|
-
const { owner, repo, issueNumber, prNumber, branchName, argv } = params;
|
|
90
|
+
const { owner, repo, issueNumber, prNumber, branchName, workspaceTmpDir, argv } = params;
|
|
86
91
|
|
|
87
92
|
// Build thinking instruction based on --think level
|
|
88
93
|
let thinkLine = '';
|
|
@@ -96,10 +101,44 @@ export const buildSystemPrompt = params => {
|
|
|
96
101
|
thinkLine = `\n${thinkMessages[argv.think]}\n`;
|
|
97
102
|
}
|
|
98
103
|
|
|
104
|
+
// Build workspace-specific instructions and examples
|
|
105
|
+
let workspaceInstructions = '';
|
|
106
|
+
if (workspaceTmpDir) {
|
|
107
|
+
workspaceInstructions = `
|
|
108
|
+
Workspace tmp directory.
|
|
109
|
+
- Use ${workspaceTmpDir} for all temporary files, logs, and downloads.
|
|
110
|
+
- When saving command output to files, save to ${workspaceTmpDir}/command-output.log.
|
|
111
|
+
- When downloading CI logs, save to ${workspaceTmpDir}/ci-logs/.
|
|
112
|
+
- When saving diffs for review, save to ${workspaceTmpDir}/diffs/.
|
|
113
|
+
- When creating debug files, save to ${workspaceTmpDir}/debug/.
|
|
114
|
+
|
|
115
|
+
`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Build CI command examples with workspace tmp paths
|
|
119
|
+
let ciExamples = '';
|
|
120
|
+
if (workspaceTmpDir) {
|
|
121
|
+
ciExamples = `
|
|
122
|
+
CI investigation with workspace tmp directory.
|
|
123
|
+
- When downloading CI run logs:
|
|
124
|
+
gh run view RUN_ID --repo ${owner}/${repo} --log > ${workspaceTmpDir}/ci-logs/run-RUN_ID.log
|
|
125
|
+
- When downloading failed job logs:
|
|
126
|
+
gh run view RUN_ID --repo ${owner}/${repo} --log-failed > ${workspaceTmpDir}/ci-logs/run-RUN_ID-failed.log
|
|
127
|
+
- When listing CI runs with details:
|
|
128
|
+
gh run list --repo ${owner}/${repo} --branch ${branchName} --limit 5 --json databaseId,conclusion,createdAt,headSha > ${workspaceTmpDir}/ci-logs/recent-runs.json
|
|
129
|
+
- When saving PR diff for review:
|
|
130
|
+
gh pr diff ${prNumber} --repo ${owner}/${repo} > ${workspaceTmpDir}/diffs/pr-${prNumber}.diff
|
|
131
|
+
- When saving command output with stderr:
|
|
132
|
+
npm test 2>&1 | tee ${workspaceTmpDir}/test-output.log
|
|
133
|
+
- When investigating issue details:
|
|
134
|
+
gh issue view ${issueNumber} --repo ${owner}/${repo} --json body,comments > ${workspaceTmpDir}/issue-${issueNumber}.json
|
|
135
|
+
|
|
136
|
+
`;
|
|
137
|
+
}
|
|
138
|
+
|
|
99
139
|
// Use backticks for jq commands to avoid quote escaping issues
|
|
100
140
|
return `You are an AI issue solver. You prefer to find the root cause of each and every issue. When you talk, you prefer to speak with facts which you have double-checked yourself or cite sources that provide evidence, like quote actual code or give references to documents or pages found on the internet. You are polite and patient, and prefer to assume good intent, trying your best to be helpful. If you are unsure or have assumptions, you prefer to test them yourself or ask questions to clarify requirements.${thinkLine}
|
|
101
|
-
|
|
102
|
-
General guidelines.
|
|
141
|
+
${workspaceInstructions}General guidelines.
|
|
103
142
|
- When you execute commands, always save their logs to files for easier reading if the output becomes large.
|
|
104
143
|
- When running commands, do not set a timeout yourself — let them run as long as needed (default timeout - 2 minutes is more than enough), and once they finish, review the logs in the file.
|
|
105
144
|
- When running sudo commands (especially package installations like apt-get, yum, npm install, etc.), always run them in the background to avoid timeout issues and permission errors when the process needs to be killed. Use the run_in_background parameter or append & to the command.${
|
|
@@ -243,7 +282,7 @@ Plan sub-agent usage.
|
|
|
243
282
|
- When using the Plan sub-agent, you can add it as the first item in your todo list.
|
|
244
283
|
- When you delegate planning, use the Task tool with subagent_type="Plan" before starting implementation work.`
|
|
245
284
|
: ''
|
|
246
|
-
}${getArchitectureCareSubPrompt(argv)}`;
|
|
285
|
+
}${ciExamples}${getArchitectureCareSubPrompt(argv)}`;
|
|
247
286
|
};
|
|
248
287
|
|
|
249
288
|
// Export all functions as default object too
|
package/src/codex.lib.mjs
CHANGED
|
@@ -115,7 +115,7 @@ export const handleCodexRuntimeSwitch = async () => {
|
|
|
115
115
|
|
|
116
116
|
// Main function to execute Codex with prompts and settings
|
|
117
117
|
export const executeCodex = async params => {
|
|
118
|
-
const { issueUrl, issueNumber, prNumber, prUrl, branchName, tempDir, isContinueMode, mergeStateStatus, forkedRepo, feedbackLines, forkActionsUrl, owner, repo, argv, log, formatAligned, getResourceSnapshot, codexPath = 'codex', $ } = params;
|
|
118
|
+
const { issueUrl, issueNumber, prNumber, prUrl, branchName, tempDir, workspaceTmpDir, isContinueMode, mergeStateStatus, forkedRepo, feedbackLines, forkActionsUrl, owner, repo, argv, log, formatAligned, getResourceSnapshot, codexPath = 'codex', $ } = params;
|
|
119
119
|
|
|
120
120
|
// Import prompt building functions from codex.prompts.lib.mjs
|
|
121
121
|
const { buildUserPrompt, buildSystemPrompt } = await import('./codex.prompts.lib.mjs');
|
|
@@ -128,6 +128,7 @@ export const executeCodex = async params => {
|
|
|
128
128
|
prUrl,
|
|
129
129
|
branchName,
|
|
130
130
|
tempDir,
|
|
131
|
+
workspaceTmpDir,
|
|
131
132
|
isContinueMode,
|
|
132
133
|
mergeStateStatus,
|
|
133
134
|
forkedRepo,
|
|
@@ -146,6 +147,7 @@ export const executeCodex = async params => {
|
|
|
146
147
|
prNumber,
|
|
147
148
|
branchName,
|
|
148
149
|
tempDir,
|
|
150
|
+
workspaceTmpDir,
|
|
149
151
|
isContinueMode,
|
|
150
152
|
forkedRepo,
|
|
151
153
|
argv,
|
|
@@ -11,7 +11,7 @@ import { getArchitectureCareSubPrompt } from './architecture-care.prompts.lib.mj
|
|
|
11
11
|
* @returns {string} The formatted user prompt
|
|
12
12
|
*/
|
|
13
13
|
export const buildUserPrompt = params => {
|
|
14
|
-
const { issueUrl, issueNumber, prNumber, prUrl, branchName, tempDir, isContinueMode, forkedRepo, feedbackLines, forkActionsUrl, owner, repo, argv } = params;
|
|
14
|
+
const { issueUrl, issueNumber, prNumber, prUrl, branchName, tempDir, workspaceTmpDir, isContinueMode, forkedRepo, feedbackLines, forkActionsUrl, owner, repo, argv } = params;
|
|
15
15
|
|
|
16
16
|
const promptLines = [];
|
|
17
17
|
|
|
@@ -26,6 +26,11 @@ export const buildUserPrompt = params => {
|
|
|
26
26
|
promptLines.push(`Your prepared branch: ${branchName}`);
|
|
27
27
|
promptLines.push(`Your prepared working directory: ${tempDir}`);
|
|
28
28
|
|
|
29
|
+
// Workspace tmp directory for logs and temp files (when --enable-workspaces is used)
|
|
30
|
+
if (workspaceTmpDir) {
|
|
31
|
+
promptLines.push(`Your prepared tmp directory for logs and downloads: ${workspaceTmpDir}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
29
34
|
// PR info if available
|
|
30
35
|
if (prUrl) {
|
|
31
36
|
promptLines.push(`Your prepared Pull Request: ${prUrl}`);
|
|
@@ -76,7 +81,7 @@ export const buildUserPrompt = params => {
|
|
|
76
81
|
* @returns {string} The formatted system prompt
|
|
77
82
|
*/
|
|
78
83
|
export const buildSystemPrompt = params => {
|
|
79
|
-
const { owner, repo, issueNumber, prNumber, branchName, argv } = params;
|
|
84
|
+
const { owner, repo, issueNumber, prNumber, branchName, workspaceTmpDir, argv } = params;
|
|
80
85
|
|
|
81
86
|
// Build thinking instruction based on --think level
|
|
82
87
|
let thinkLine = '';
|
|
@@ -90,9 +95,42 @@ export const buildSystemPrompt = params => {
|
|
|
90
95
|
thinkLine = `\n${thinkMessages[argv.think]}\n`;
|
|
91
96
|
}
|
|
92
97
|
|
|
93
|
-
|
|
98
|
+
// Build workspace-specific instructions and examples
|
|
99
|
+
let workspaceInstructions = '';
|
|
100
|
+
if (workspaceTmpDir) {
|
|
101
|
+
workspaceInstructions = `
|
|
102
|
+
Workspace tmp directory.
|
|
103
|
+
- Use ${workspaceTmpDir} for all temporary files, logs, and downloads.
|
|
104
|
+
- When saving command output to files, save to ${workspaceTmpDir}/command-output.log.
|
|
105
|
+
- When downloading CI logs, save to ${workspaceTmpDir}/ci-logs/.
|
|
106
|
+
- When saving diffs for review, save to ${workspaceTmpDir}/diffs/.
|
|
107
|
+
- When creating debug files, save to ${workspaceTmpDir}/debug/.
|
|
108
|
+
|
|
109
|
+
`;
|
|
110
|
+
}
|
|
94
111
|
|
|
95
|
-
|
|
112
|
+
// Build CI command examples with workspace tmp paths
|
|
113
|
+
let ciExamples = '';
|
|
114
|
+
if (workspaceTmpDir) {
|
|
115
|
+
ciExamples = `
|
|
116
|
+
CI investigation with workspace tmp directory.
|
|
117
|
+
- When downloading CI run logs:
|
|
118
|
+
gh run view RUN_ID --repo ${owner}/${repo} --log > ${workspaceTmpDir}/ci-logs/run-RUN_ID.log
|
|
119
|
+
- When downloading failed job logs:
|
|
120
|
+
gh run view RUN_ID --repo ${owner}/${repo} --log-failed > ${workspaceTmpDir}/ci-logs/run-RUN_ID-failed.log
|
|
121
|
+
- When listing CI runs with details:
|
|
122
|
+
gh run list --repo ${owner}/${repo} --branch ${branchName} --limit 5 --json databaseId,conclusion,createdAt,headSha > ${workspaceTmpDir}/ci-logs/recent-runs.json
|
|
123
|
+
- When saving PR diff for review:
|
|
124
|
+
gh pr diff ${prNumber} --repo ${owner}/${repo} > ${workspaceTmpDir}/diffs/pr-${prNumber}.diff
|
|
125
|
+
- When saving command output with stderr:
|
|
126
|
+
npm test 2>&1 | tee ${workspaceTmpDir}/test-output.log
|
|
127
|
+
- When investigating issue details:
|
|
128
|
+
gh issue view ${issueNumber} --repo ${owner}/${repo} --json body,comments > ${workspaceTmpDir}/issue-${issueNumber}.json
|
|
129
|
+
`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return `You are AI issue solver using OpenAI Codex.${thinkLine}
|
|
133
|
+
${workspaceInstructions}General guidelines.
|
|
96
134
|
- When you execute commands, always save their logs to files for easier reading if the output becomes large.
|
|
97
135
|
- When running commands, do not set a timeout yourself — let them run as long as needed (default timeout - 2 minutes is more than enough), and once they finish, review the logs in the file.
|
|
98
136
|
- When running sudo commands (especially package installations like apt-get, yum, npm install, etc.), always run them in the background to avoid timeout issues and permission errors when the process needs to be killed. Use the run_in_background parameter or append & to the command.
|
|
@@ -193,7 +231,7 @@ GitHub CLI command patterns.
|
|
|
193
231
|
- When adding PR comment, use gh pr comment NUMBER --body "text" --repo OWNER/REPO.
|
|
194
232
|
- When adding issue comment, use gh issue comment NUMBER --body "text" --repo OWNER/REPO.
|
|
195
233
|
- When viewing PR details, use gh pr view NUMBER --repo OWNER/REPO.
|
|
196
|
-
- When filtering with jq, use gh api repos/\${owner}/\${repo}/pulls/\${prNumber}/comments --paginate --jq 'reverse | .[0:5]'.${getArchitectureCareSubPrompt(argv)}`;
|
|
234
|
+
- When filtering with jq, use gh api repos/\${owner}/\${repo}/pulls/\${prNumber}/comments --paginate --jq 'reverse | .[0:5]'.${ciExamples}${getArchitectureCareSubPrompt(argv)}`;
|
|
197
235
|
};
|
|
198
236
|
|
|
199
237
|
// Export all functions as default object too
|
package/src/opencode.lib.mjs
CHANGED
|
@@ -109,7 +109,7 @@ export const handleOpenCodeRuntimeSwitch = async () => {
|
|
|
109
109
|
|
|
110
110
|
// Main function to execute OpenCode with prompts and settings
|
|
111
111
|
export const executeOpenCode = async params => {
|
|
112
|
-
const { issueUrl, issueNumber, prNumber, prUrl, branchName, tempDir, isContinueMode, mergeStateStatus, forkedRepo, feedbackLines, forkActionsUrl, owner, repo, argv, log, formatAligned, getResourceSnapshot, opencodePath = 'opencode', $ } = params;
|
|
112
|
+
const { issueUrl, issueNumber, prNumber, prUrl, branchName, tempDir, workspaceTmpDir, isContinueMode, mergeStateStatus, forkedRepo, feedbackLines, forkActionsUrl, owner, repo, argv, log, formatAligned, getResourceSnapshot, opencodePath = 'opencode', $ } = params;
|
|
113
113
|
|
|
114
114
|
// Import prompt building functions from opencode.prompts.lib.mjs
|
|
115
115
|
const { buildUserPrompt, buildSystemPrompt } = await import('./opencode.prompts.lib.mjs');
|
|
@@ -122,6 +122,7 @@ export const executeOpenCode = async params => {
|
|
|
122
122
|
prUrl,
|
|
123
123
|
branchName,
|
|
124
124
|
tempDir,
|
|
125
|
+
workspaceTmpDir,
|
|
125
126
|
isContinueMode,
|
|
126
127
|
mergeStateStatus,
|
|
127
128
|
forkedRepo,
|
|
@@ -140,6 +141,7 @@ export const executeOpenCode = async params => {
|
|
|
140
141
|
prNumber,
|
|
141
142
|
branchName,
|
|
142
143
|
tempDir,
|
|
144
|
+
workspaceTmpDir,
|
|
143
145
|
isContinueMode,
|
|
144
146
|
forkedRepo,
|
|
145
147
|
argv,
|
|
@@ -11,7 +11,7 @@ import { getArchitectureCareSubPrompt } from './architecture-care.prompts.lib.mj
|
|
|
11
11
|
* @returns {string} The formatted user prompt
|
|
12
12
|
*/
|
|
13
13
|
export const buildUserPrompt = params => {
|
|
14
|
-
const { issueUrl, issueNumber, prNumber, prUrl, branchName, tempDir, isContinueMode, forkedRepo, feedbackLines, forkActionsUrl, owner, repo, argv } = params;
|
|
14
|
+
const { issueUrl, issueNumber, prNumber, prUrl, branchName, tempDir, workspaceTmpDir, isContinueMode, forkedRepo, feedbackLines, forkActionsUrl, owner, repo, argv } = params;
|
|
15
15
|
|
|
16
16
|
const promptLines = [];
|
|
17
17
|
|
|
@@ -26,6 +26,11 @@ export const buildUserPrompt = params => {
|
|
|
26
26
|
promptLines.push(`Your prepared branch: ${branchName}`);
|
|
27
27
|
promptLines.push(`Your prepared working directory: ${tempDir}`);
|
|
28
28
|
|
|
29
|
+
// Workspace tmp directory for logs and temp files (when --enable-workspaces is used)
|
|
30
|
+
if (workspaceTmpDir) {
|
|
31
|
+
promptLines.push(`Your prepared tmp directory for logs and downloads: ${workspaceTmpDir}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
29
34
|
// PR info if available
|
|
30
35
|
if (prUrl) {
|
|
31
36
|
promptLines.push(`Your prepared Pull Request: ${prUrl}`);
|
|
@@ -76,7 +81,7 @@ export const buildUserPrompt = params => {
|
|
|
76
81
|
* @returns {string} The formatted system prompt
|
|
77
82
|
*/
|
|
78
83
|
export const buildSystemPrompt = params => {
|
|
79
|
-
const { owner, repo, issueNumber, prNumber, branchName, argv } = params;
|
|
84
|
+
const { owner, repo, issueNumber, prNumber, branchName, workspaceTmpDir, argv } = params;
|
|
80
85
|
|
|
81
86
|
// Build thinking instruction based on --think level
|
|
82
87
|
let thinkLine = '';
|
|
@@ -90,6 +95,41 @@ export const buildSystemPrompt = params => {
|
|
|
90
95
|
thinkLine = `\n${thinkMessages[argv.think]}\n`;
|
|
91
96
|
}
|
|
92
97
|
|
|
98
|
+
// Build workspace-specific instructions and examples
|
|
99
|
+
let workspaceInstructions = '';
|
|
100
|
+
if (workspaceTmpDir) {
|
|
101
|
+
workspaceInstructions = `
|
|
102
|
+
Workspace tmp directory.
|
|
103
|
+
- Use ${workspaceTmpDir} for all temporary files, logs, and downloads.
|
|
104
|
+
- When saving command output to files, save to ${workspaceTmpDir}/command-output.log.
|
|
105
|
+
- When downloading CI logs, save to ${workspaceTmpDir}/ci-logs/.
|
|
106
|
+
- When saving diffs for review, save to ${workspaceTmpDir}/diffs/.
|
|
107
|
+
- When creating debug files, save to ${workspaceTmpDir}/debug/.
|
|
108
|
+
|
|
109
|
+
`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Build CI command examples with workspace tmp paths
|
|
113
|
+
let ciExamples = '';
|
|
114
|
+
if (workspaceTmpDir) {
|
|
115
|
+
ciExamples = `
|
|
116
|
+
CI investigation with workspace tmp directory.
|
|
117
|
+
- When downloading CI run logs:
|
|
118
|
+
gh run view RUN_ID --repo ${owner}/${repo} --log > ${workspaceTmpDir}/ci-logs/run-RUN_ID.log
|
|
119
|
+
- When downloading failed job logs:
|
|
120
|
+
gh run view RUN_ID --repo ${owner}/${repo} --log-failed > ${workspaceTmpDir}/ci-logs/run-RUN_ID-failed.log
|
|
121
|
+
- When listing CI runs with details:
|
|
122
|
+
gh run list --repo ${owner}/${repo} --branch ${branchName} --limit 5 --json databaseId,conclusion,createdAt,headSha > ${workspaceTmpDir}/ci-logs/recent-runs.json
|
|
123
|
+
- When saving PR diff for review:
|
|
124
|
+
gh pr diff ${prNumber} --repo ${owner}/${repo} > ${workspaceTmpDir}/diffs/pr-${prNumber}.diff
|
|
125
|
+
- When saving command output with stderr:
|
|
126
|
+
npm test 2>&1 | tee ${workspaceTmpDir}/test-output.log
|
|
127
|
+
- When investigating issue details:
|
|
128
|
+
gh issue view ${issueNumber} --repo ${owner}/${repo} --json body,comments > ${workspaceTmpDir}/issue-${issueNumber}.json
|
|
129
|
+
|
|
130
|
+
`;
|
|
131
|
+
}
|
|
132
|
+
|
|
93
133
|
return `You are AI issue solver using OpenCode.${thinkLine}
|
|
94
134
|
|
|
95
135
|
General guidelines.
|
|
@@ -103,7 +143,7 @@ General guidelines.
|
|
|
103
143
|
- When testing your assumptions, use the experiment scripts, and add it to experiments folder.
|
|
104
144
|
- When your experiments can show real world use case of the software, add it to examples folder.
|
|
105
145
|
- When you face something extremely hard, use divide and conquer — it always helps.
|
|
106
|
-
|
|
146
|
+
${workspaceInstructions}
|
|
107
147
|
Initial research.
|
|
108
148
|
- When you start, make sure you create detailed plan for yourself and follow your todo list step by step, make sure that as many points from these guidelines are added to your todo list to keep track of everything that can help you solve the issue with highest possible quality.
|
|
109
149
|
- When you read issue, read all details and comments thoroughly.
|
|
@@ -184,7 +224,7 @@ GitHub CLI command patterns.
|
|
|
184
224
|
- When adding PR comment, use gh pr comment NUMBER --body "text" --repo OWNER/REPO.
|
|
185
225
|
- When adding issue comment, use gh issue comment NUMBER --body "text" --repo OWNER/REPO.
|
|
186
226
|
- When viewing PR details, use gh pr view NUMBER --repo OWNER/REPO.
|
|
187
|
-
- When filtering with jq, use gh api repos/${owner}/${repo}/pulls/${prNumber}/comments --paginate --jq 'reverse | .[0:5]'.${getArchitectureCareSubPrompt(argv)}`;
|
|
227
|
+
- When filtering with jq, use gh api repos/${owner}/${repo}/pulls/${prNumber}/comments --paginate --jq 'reverse | .[0:5]'.${ciExamples}${getArchitectureCareSubPrompt(argv)}`;
|
|
188
228
|
};
|
|
189
229
|
|
|
190
230
|
// Export all functions as default object too
|
package/src/solve.config.lib.mjs
CHANGED
|
@@ -253,6 +253,11 @@ export const createYargsConfig = yargsInstance => {
|
|
|
253
253
|
choices: ['claude', 'opencode', 'codex', 'agent'],
|
|
254
254
|
default: 'claude',
|
|
255
255
|
})
|
|
256
|
+
.option('enable-workspaces', {
|
|
257
|
+
type: 'boolean',
|
|
258
|
+
description: 'Use separate workspace directory structure with repository/ and tmp/ folders. Works with all tools (claude, opencode, codex, agent). Experimental feature.',
|
|
259
|
+
default: false,
|
|
260
|
+
})
|
|
256
261
|
.option('interactive-mode', {
|
|
257
262
|
type: 'boolean',
|
|
258
263
|
description: '[EXPERIMENTAL] Post Claude output as PR comments in real-time. Only supported for --tool claude.',
|
package/src/solve.mjs
CHANGED
|
@@ -65,6 +65,9 @@ const { executeClaude } = claudeLib;
|
|
|
65
65
|
const githubLinking = await import('./github-linking.lib.mjs');
|
|
66
66
|
const { extractLinkedIssueNumber } = githubLinking;
|
|
67
67
|
|
|
68
|
+
const usageLimitLib = await import('./usage-limit.lib.mjs');
|
|
69
|
+
const { formatResetTimeWithRelative } = usageLimitLib;
|
|
70
|
+
|
|
68
71
|
const errorHandlers = await import('./solve.error-handlers.lib.mjs');
|
|
69
72
|
const { createUncaughtExceptionHandler, createUnhandledRejectionHandler, handleMainExecutionError } = errorHandlers;
|
|
70
73
|
|
|
@@ -508,7 +511,9 @@ if (isPrUrl) {
|
|
|
508
511
|
await log(`📝 Issue mode: Working with issue #${issueNumber}`);
|
|
509
512
|
}
|
|
510
513
|
// Create or find temporary directory for cloning the repository
|
|
511
|
-
|
|
514
|
+
// Pass workspace info for --enable-workspaces mode (works with all tools)
|
|
515
|
+
const workspaceInfo = argv.enableWorkspaces ? { owner, repo, issueNumber } : null;
|
|
516
|
+
const { tempDir, workspaceTmpDir } = await setupTempDirectory(argv, workspaceInfo);
|
|
512
517
|
// Populate cleanup context for signal handlers
|
|
513
518
|
cleanupContext.tempDir = tempDir;
|
|
514
519
|
cleanupContext.argv = argv;
|
|
@@ -761,6 +766,7 @@ try {
|
|
|
761
766
|
prUrl,
|
|
762
767
|
branchName,
|
|
763
768
|
tempDir,
|
|
769
|
+
workspaceTmpDir,
|
|
764
770
|
isContinueMode,
|
|
765
771
|
mergeStateStatus,
|
|
766
772
|
forkedRepo,
|
|
@@ -789,6 +795,7 @@ try {
|
|
|
789
795
|
prUrl,
|
|
790
796
|
branchName,
|
|
791
797
|
tempDir,
|
|
798
|
+
workspaceTmpDir,
|
|
792
799
|
isContinueMode,
|
|
793
800
|
mergeStateStatus,
|
|
794
801
|
forkedRepo,
|
|
@@ -817,6 +824,7 @@ try {
|
|
|
817
824
|
prUrl,
|
|
818
825
|
branchName,
|
|
819
826
|
tempDir,
|
|
827
|
+
workspaceTmpDir,
|
|
820
828
|
isContinueMode,
|
|
821
829
|
mergeStateStatus,
|
|
822
830
|
forkedRepo,
|
|
@@ -858,6 +866,7 @@ try {
|
|
|
858
866
|
prUrl,
|
|
859
867
|
branchName,
|
|
860
868
|
tempDir,
|
|
869
|
+
workspaceTmpDir,
|
|
861
870
|
isContinueMode,
|
|
862
871
|
mergeStateStatus,
|
|
863
872
|
forkedRepo,
|
|
@@ -961,7 +970,9 @@ try {
|
|
|
961
970
|
const tool = argv.tool || 'claude';
|
|
962
971
|
const resumeCmd = tool === 'claude' ? buildClaudeResumeCommand({ tempDir, sessionId, model: argv.model }) : null;
|
|
963
972
|
const resumeSection = resumeCmd ? `To resume after the limit resets, use:\n\`\`\`bash\n${resumeCmd}\n\`\`\`` : `Session ID: \`${sessionId}\``;
|
|
964
|
-
|
|
973
|
+
// Format the reset time with relative time if available
|
|
974
|
+
const formattedResetTime = resetTime ? formatResetTimeWithRelative(resetTime) : null;
|
|
975
|
+
const failureComment = formattedResetTime ? `❌ **Usage Limit Reached**\n\nThe AI tool has reached its usage limit. The limit will reset at: **${formattedResetTime}**\n\n${resumeSection}` : `❌ **Usage Limit Reached**\n\nThe AI tool has reached its usage limit. Please wait for the limit to reset.\n\n${resumeSection}`;
|
|
965
976
|
|
|
966
977
|
const commentResult = await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${failureComment}`;
|
|
967
978
|
if (commentResult.code === 0) {
|
|
@@ -199,11 +199,35 @@ export const validateForkParent = async (forkRepo, expectedUpstream) => {
|
|
|
199
199
|
}
|
|
200
200
|
};
|
|
201
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Build workspace directory name according to the specification:
|
|
204
|
+
* /tmp/hive-mind-solve-gh-{owner}/{repo}-issue-{issueNumber}-workspace-{timestamp}
|
|
205
|
+
*
|
|
206
|
+
* @param {string} owner - Repository owner
|
|
207
|
+
* @param {string} repo - Repository name
|
|
208
|
+
* @param {number|string} issueNumber - Issue number
|
|
209
|
+
* @param {number} timestamp - Unix timestamp
|
|
210
|
+
* @returns {string} The workspace directory path
|
|
211
|
+
*/
|
|
212
|
+
export const buildWorkspacePath = (owner, repo, issueNumber, timestamp) => {
|
|
213
|
+
// Format: /tmp/hive-mind-solve-gh-{owner}/{repo}-issue-{issueNumber}-workspace-{timestamp}
|
|
214
|
+
const baseDir = path.join(os.tmpdir(), `hive-mind-solve-gh-${owner}`);
|
|
215
|
+
const workspaceDir = path.join(baseDir, `${repo}-issue-${issueNumber}-workspace-${timestamp}`);
|
|
216
|
+
return workspaceDir;
|
|
217
|
+
};
|
|
218
|
+
|
|
202
219
|
// Create or find temporary directory for cloning the repository
|
|
203
|
-
|
|
220
|
+
// When --enable-workspaces is used, creates:
|
|
221
|
+
// {workspace}/repository - for the cloned repo
|
|
222
|
+
// {workspace}/tmp - for temp files, logs, downloads
|
|
223
|
+
export const setupTempDirectory = async (argv, workspaceInfo = null) => {
|
|
204
224
|
let tempDir;
|
|
225
|
+
let workspaceTmpDir = null;
|
|
205
226
|
let isResuming = argv.resume;
|
|
206
227
|
|
|
228
|
+
// Check if workspace mode should be enabled (works with all tools)
|
|
229
|
+
const useWorkspaces = argv.enableWorkspaces;
|
|
230
|
+
|
|
207
231
|
if (isResuming) {
|
|
208
232
|
// When resuming, try to find existing directory or create a new one
|
|
209
233
|
const scriptDir = path.dirname(process.argv[1]);
|
|
@@ -229,13 +253,34 @@ export const setupTempDirectory = async argv => {
|
|
|
229
253
|
await fs.mkdir(tempDir, { recursive: true });
|
|
230
254
|
await log(`Creating temporary directory for resumed session: ${tempDir}`);
|
|
231
255
|
}
|
|
256
|
+
} else if (useWorkspaces && workspaceInfo) {
|
|
257
|
+
// Workspace mode: create structured workspace with repository/ and tmp/ subdirectories
|
|
258
|
+
const { owner, repo, issueNumber } = workspaceInfo;
|
|
259
|
+
const timestamp = Date.now();
|
|
260
|
+
const workspaceDir = buildWorkspacePath(owner, repo, issueNumber, timestamp);
|
|
261
|
+
|
|
262
|
+
// Create the workspace structure:
|
|
263
|
+
// {workspace}/repository - where the repo will be cloned
|
|
264
|
+
// {workspace}/tmp - for temp files, logs, command outputs
|
|
265
|
+
const repoDir = path.join(workspaceDir, 'repository');
|
|
266
|
+
workspaceTmpDir = path.join(workspaceDir, 'tmp');
|
|
267
|
+
|
|
268
|
+
await fs.mkdir(repoDir, { recursive: true });
|
|
269
|
+
await fs.mkdir(workspaceTmpDir, { recursive: true });
|
|
270
|
+
|
|
271
|
+
tempDir = repoDir;
|
|
272
|
+
|
|
273
|
+
await log(`\n${formatAligned('📂', 'Workspace mode:', 'ENABLED')}`);
|
|
274
|
+
await log(formatAligned('', 'Workspace root:', workspaceDir, 2));
|
|
275
|
+
await log(formatAligned('', 'Repository dir:', repoDir, 2));
|
|
276
|
+
await log(formatAligned('', 'Temp dir:', workspaceTmpDir, 2));
|
|
232
277
|
} else {
|
|
233
278
|
tempDir = path.join(os.tmpdir(), `gh-issue-solver-${Date.now()}`);
|
|
234
279
|
await fs.mkdir(tempDir, { recursive: true });
|
|
235
280
|
await log(`\nCreating temporary directory: ${tempDir}`);
|
|
236
281
|
}
|
|
237
282
|
|
|
238
|
-
return { tempDir, isResuming };
|
|
283
|
+
return { tempDir, workspaceTmpDir, isResuming };
|
|
239
284
|
};
|
|
240
285
|
|
|
241
286
|
// Try to initialize an empty repository by creating a simple README.md
|
|
@@ -26,11 +26,16 @@ import { getCachedClaudeLimits, getCachedGitHubLimits, getCachedMemoryInfo, getC
|
|
|
26
26
|
/**
|
|
27
27
|
* Configuration constants for queue throttling
|
|
28
28
|
* All thresholds use ratios (0.0 - 1.0) representing usage percentage
|
|
29
|
+
*
|
|
30
|
+
* IMPORTANT: Running claude processes is NOT a blocking limit by itself.
|
|
31
|
+
* Commands can run in parallel as long as actual limits (CPU, API, etc.) are not exceeded.
|
|
32
|
+
* See: https://github.com/link-assistant/hive-mind/issues/1078
|
|
29
33
|
*/
|
|
30
34
|
export const QUEUE_CONFIG = {
|
|
31
35
|
// Resource thresholds (usage ratios: 0.0 - 1.0)
|
|
32
36
|
RAM_THRESHOLD: 0.5, // Stop if RAM usage > 50%
|
|
33
|
-
|
|
37
|
+
// CPU threshold uses 5-minute load average, not instantaneous CPU usage
|
|
38
|
+
CPU_THRESHOLD: 0.5, // Stop if 5-minute load average > 50% of CPU count
|
|
34
39
|
DISK_THRESHOLD: 0.95, // One-at-a-time if disk usage > 95%
|
|
35
40
|
|
|
36
41
|
// API limit thresholds (usage ratios: 0.0 - 1.0)
|
|
@@ -39,8 +44,11 @@ export const QUEUE_CONFIG = {
|
|
|
39
44
|
GITHUB_API_THRESHOLD: 0.8, // Stop if GitHub > 80% with parallel claude
|
|
40
45
|
|
|
41
46
|
// Timing
|
|
42
|
-
MIN_START_INTERVAL_MS:
|
|
43
|
-
|
|
47
|
+
// MIN_START_INTERVAL_MS: Time to allow solve command to start actual claude process
|
|
48
|
+
// This ensures that when API limits are checked, the running process is counted
|
|
49
|
+
MIN_START_INTERVAL_MS: 120000, // 2 minutes between starts (was 1 minute)
|
|
50
|
+
CONSUMER_POLL_INTERVAL_MS: 60000, // 1 minute between queue checks (was 5 seconds)
|
|
51
|
+
MESSAGE_UPDATE_INTERVAL_MS: 60000, // 1 minute between status message updates
|
|
44
52
|
|
|
45
53
|
// Process detection
|
|
46
54
|
CLAUDE_PROCESS_NAMES: ['claude'], // Process names to detect
|
|
@@ -163,6 +171,9 @@ class SolveQueueItem {
|
|
|
163
171
|
this.sessionName = null;
|
|
164
172
|
// Message tracking - forget after STARTED
|
|
165
173
|
this.messageInfo = null; // { chatId, messageId }
|
|
174
|
+
// Track when we last updated the Telegram message
|
|
175
|
+
// See: https://github.com/link-assistant/hive-mind/issues/1078
|
|
176
|
+
this.lastMessageUpdateTime = null;
|
|
166
177
|
}
|
|
167
178
|
|
|
168
179
|
/**
|
|
@@ -409,8 +420,10 @@ export class SolveQueue {
|
|
|
409
420
|
// "Claude process running" only blocks if there are OTHER reasons too
|
|
410
421
|
// This allows parallel execution when limits are not exceeded
|
|
411
422
|
if (hasRunningClaude && reasons.length > 0) {
|
|
412
|
-
// Add claude_running info
|
|
413
|
-
|
|
423
|
+
// Add claude_running info at the END (not beginning) of reasons
|
|
424
|
+
// Since it's supplementary info, not the primary blocking reason
|
|
425
|
+
// See: https://github.com/link-assistant/hive-mind/issues/1078
|
|
426
|
+
reasons.push(formatWaitingReason('claude_running', claudeProcs.count, 0) + ` (${claudeProcs.count} processes)`);
|
|
414
427
|
}
|
|
415
428
|
|
|
416
429
|
const canStart = reasons.length === 0;
|
|
@@ -430,6 +443,11 @@ export class SolveQueue {
|
|
|
430
443
|
|
|
431
444
|
/**
|
|
432
445
|
* Check system resources (RAM, CPU, disk) using cached values
|
|
446
|
+
*
|
|
447
|
+
* Uses 5-minute load average for CPU instead of instantaneous usage.
|
|
448
|
+
* This provides a more stable metric that isn't affected by brief spikes
|
|
449
|
+
* during claude process startup.
|
|
450
|
+
*
|
|
433
451
|
* @returns {Promise<{ok: boolean, reasons: string[], oneAtATime: boolean}>}
|
|
434
452
|
*/
|
|
435
453
|
async checkSystemResources() {
|
|
@@ -446,12 +464,25 @@ export class SolveQueue {
|
|
|
446
464
|
}
|
|
447
465
|
}
|
|
448
466
|
|
|
449
|
-
// Check CPU
|
|
467
|
+
// Check CPU using 5-minute load average (more stable than 1-minute)
|
|
468
|
+
// Cache TTL is 2 minutes, which is appropriate for this metric
|
|
450
469
|
const cpuResult = await getCachedCpuInfo(this.verbose);
|
|
451
470
|
if (cpuResult.success) {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
471
|
+
// Use loadAvg5 (5-minute average) instead of usagePercentage (1-minute based)
|
|
472
|
+
// This provides a more stable metric that isn't affected by transient spikes
|
|
473
|
+
const loadAvg5 = cpuResult.cpuLoad.loadAvg5;
|
|
474
|
+
const cpuCount = cpuResult.cpuLoad.cpuCount;
|
|
475
|
+
// Calculate usage ratio: loadAvg5 / cpuCount
|
|
476
|
+
// Load average of 1.0 per CPU = 100% utilization
|
|
477
|
+
const usageRatio = loadAvg5 / cpuCount;
|
|
478
|
+
const usagePercent = Math.min(100, Math.round(usageRatio * 100));
|
|
479
|
+
|
|
480
|
+
if (this.verbose) {
|
|
481
|
+
this.log(`CPU 5m load avg: ${loadAvg5.toFixed(2)}, cpus: ${cpuCount}, usage: ${usagePercent}%`);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (usageRatio > QUEUE_CONFIG.CPU_THRESHOLD) {
|
|
485
|
+
reasons.push(formatWaitingReason('cpu', usagePercent, QUEUE_CONFIG.CPU_THRESHOLD));
|
|
455
486
|
this.recordThrottle('cpu_high');
|
|
456
487
|
}
|
|
457
488
|
}
|
|
@@ -564,18 +595,33 @@ export class SolveQueue {
|
|
|
564
595
|
* Update item message in Telegram
|
|
565
596
|
* @param {SolveQueueItem} item
|
|
566
597
|
* @param {string} text
|
|
598
|
+
* @param {boolean} trackUpdateTime - Whether to track this as a periodic update (default: true)
|
|
567
599
|
*/
|
|
568
|
-
async updateItemMessage(item, text) {
|
|
600
|
+
async updateItemMessage(item, text, trackUpdateTime = true) {
|
|
569
601
|
if (!item.messageInfo || !item.ctx) return;
|
|
570
602
|
|
|
571
603
|
try {
|
|
572
604
|
const { chatId, messageId } = item.messageInfo;
|
|
573
605
|
await item.ctx.telegram.editMessageText(chatId, messageId, undefined, text, { parse_mode: 'Markdown' });
|
|
606
|
+
if (trackUpdateTime) {
|
|
607
|
+
item.lastMessageUpdateTime = Date.now();
|
|
608
|
+
}
|
|
574
609
|
} catch (error) {
|
|
575
610
|
this.log(`Failed to update message: ${error.message}`);
|
|
576
611
|
}
|
|
577
612
|
}
|
|
578
613
|
|
|
614
|
+
/**
|
|
615
|
+
* Check if an item's message should be updated periodically
|
|
616
|
+
* @param {SolveQueueItem} item
|
|
617
|
+
* @returns {boolean}
|
|
618
|
+
*/
|
|
619
|
+
shouldUpdateMessage(item) {
|
|
620
|
+
if (!item.messageInfo || !item.ctx) return false;
|
|
621
|
+
if (!item.lastMessageUpdateTime) return true; // Never updated
|
|
622
|
+
return Date.now() - item.lastMessageUpdateTime >= QUEUE_CONFIG.MESSAGE_UPDATE_INTERVAL_MS;
|
|
623
|
+
}
|
|
624
|
+
|
|
579
625
|
/**
|
|
580
626
|
* Consumer loop - processes items from the queue
|
|
581
627
|
*/
|
|
@@ -592,14 +638,20 @@ export class SolveQueue {
|
|
|
592
638
|
|
|
593
639
|
if (!check.canStart) {
|
|
594
640
|
// Update all queued items to waiting status with reason
|
|
641
|
+
// Also periodically refresh messages to show current status
|
|
642
|
+
// See: https://github.com/link-assistant/hive-mind/issues/1078
|
|
595
643
|
for (const item of this.queue) {
|
|
596
644
|
if (item.status === QueueItemStatus.QUEUED || item.status === QueueItemStatus.WAITING) {
|
|
597
645
|
const previousStatus = item.status;
|
|
598
646
|
const previousReason = item.waitingReason;
|
|
599
647
|
item.setWaiting(check.reason);
|
|
600
648
|
|
|
601
|
-
// Update message if
|
|
602
|
-
|
|
649
|
+
// Update message if:
|
|
650
|
+
// 1. Status or reason changed
|
|
651
|
+
// 2. OR it's time for a periodic update (every MESSAGE_UPDATE_INTERVAL_MS)
|
|
652
|
+
const shouldUpdate = previousStatus !== item.status || previousReason !== item.waitingReason || this.shouldUpdateMessage(item);
|
|
653
|
+
|
|
654
|
+
if (shouldUpdate) {
|
|
603
655
|
const position = this.queue.indexOf(item) + 1;
|
|
604
656
|
await this.updateItemMessage(item, `⏳ Waiting (position #${position})\n\n${item.infoBlock}\n\n*Reason:*\n${check.reason}`);
|
|
605
657
|
}
|
|
@@ -622,13 +674,10 @@ export class SolveQueue {
|
|
|
622
674
|
const item = this.queue.shift();
|
|
623
675
|
if (!item) continue;
|
|
624
676
|
|
|
625
|
-
//
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
await this.sleep(QUEUE_CONFIG.CONSUMER_POLL_INTERVAL_MS);
|
|
630
|
-
continue;
|
|
631
|
-
}
|
|
677
|
+
// NOTE: Running claude processes is NOT a blocking limit by itself
|
|
678
|
+
// Commands can run in parallel as long as actual limits (CPU, API, etc.) are not exceeded
|
|
679
|
+
// The MIN_START_INTERVAL_MS ensures enough time for processes to be counted
|
|
680
|
+
// See: https://github.com/link-assistant/hive-mind/issues/1078
|
|
632
681
|
|
|
633
682
|
// Update status to Starting
|
|
634
683
|
item.setStarting();
|
package/src/usage-limit.lib.mjs
CHANGED
|
@@ -142,6 +142,111 @@ export function detectUsageLimit(message) {
|
|
|
142
142
|
};
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Parse time string (e.g., "11:00 PM") and convert to Date object for today
|
|
147
|
+
*
|
|
148
|
+
* @param {string} timeStr - Time string in format "HH:MM AM/PM"
|
|
149
|
+
* @returns {Date|null} - Date object or null if parsing fails
|
|
150
|
+
*/
|
|
151
|
+
export function parseResetTime(timeStr) {
|
|
152
|
+
if (!timeStr || typeof timeStr !== 'string') {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Match pattern like "11:00 PM" or "11:00PM"
|
|
157
|
+
const match = timeStr.match(/(\d{1,2}):(\d{2})\s*(AM|PM)/i);
|
|
158
|
+
if (!match) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let hour = parseInt(match[1], 10);
|
|
163
|
+
const minute = parseInt(match[2], 10);
|
|
164
|
+
const ampm = match[3].toUpperCase();
|
|
165
|
+
|
|
166
|
+
// Convert to 24-hour format
|
|
167
|
+
if (ampm === 'PM' && hour !== 12) {
|
|
168
|
+
hour += 12;
|
|
169
|
+
} else if (ampm === 'AM' && hour === 12) {
|
|
170
|
+
hour = 0;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Create date for today with the parsed time
|
|
174
|
+
const now = new Date();
|
|
175
|
+
const resetDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), hour, minute, 0, 0);
|
|
176
|
+
|
|
177
|
+
// If the time is in the past today, assume it's tomorrow
|
|
178
|
+
if (resetDate <= now) {
|
|
179
|
+
resetDate.setDate(resetDate.getDate() + 1);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return resetDate;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Format relative time (e.g., "in 1h 23m")
|
|
187
|
+
*
|
|
188
|
+
* @param {Date} resetDate - Date object for reset time
|
|
189
|
+
* @returns {string} - Formatted relative time string
|
|
190
|
+
*/
|
|
191
|
+
export function formatRelativeTime(resetDate) {
|
|
192
|
+
if (!resetDate || !(resetDate instanceof Date)) {
|
|
193
|
+
return '';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const now = new Date();
|
|
197
|
+
const diffMs = resetDate - now;
|
|
198
|
+
|
|
199
|
+
if (diffMs <= 0) {
|
|
200
|
+
return 'now';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const totalSeconds = Math.floor(diffMs / 1000);
|
|
204
|
+
const totalMinutes = Math.floor(totalSeconds / 60);
|
|
205
|
+
const totalHours = Math.floor(totalMinutes / 60);
|
|
206
|
+
const totalDays = Math.floor(totalHours / 24);
|
|
207
|
+
|
|
208
|
+
const days = totalDays;
|
|
209
|
+
const hours = totalHours % 24;
|
|
210
|
+
const minutes = totalMinutes % 60;
|
|
211
|
+
|
|
212
|
+
const parts = [];
|
|
213
|
+
if (days > 0) parts.push(`${days}d`);
|
|
214
|
+
if (hours > 0) parts.push(`${hours}h`);
|
|
215
|
+
if (minutes > 0 || parts.length === 0) parts.push(`${minutes}m`);
|
|
216
|
+
|
|
217
|
+
return `in ${parts.join(' ')}`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Format reset time with relative time and UTC time
|
|
222
|
+
* Example: "in 1h 23m (11:00 PM UTC)"
|
|
223
|
+
*
|
|
224
|
+
* @param {string} resetTime - Time string in format "HH:MM AM/PM"
|
|
225
|
+
* @returns {string} - Formatted string with relative and absolute time
|
|
226
|
+
*/
|
|
227
|
+
export function formatResetTimeWithRelative(resetTime) {
|
|
228
|
+
if (!resetTime) {
|
|
229
|
+
return resetTime;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const resetDate = parseResetTime(resetTime);
|
|
233
|
+
if (!resetDate) {
|
|
234
|
+
// If we can't parse it, return the original time
|
|
235
|
+
return resetTime;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const relativeTime = formatRelativeTime(resetDate);
|
|
239
|
+
|
|
240
|
+
// Format the UTC time
|
|
241
|
+
const utcHours = resetDate.getUTCHours();
|
|
242
|
+
const utcMinutes = resetDate.getUTCMinutes();
|
|
243
|
+
const utcAmPm = utcHours >= 12 ? 'PM' : 'AM';
|
|
244
|
+
const utcHour12 = utcHours % 12 || 12;
|
|
245
|
+
const utcTimeStr = `${utcHour12}:${String(utcMinutes).padStart(2, '0')} ${utcAmPm} UTC`;
|
|
246
|
+
|
|
247
|
+
return `${relativeTime} (${utcTimeStr})`;
|
|
248
|
+
}
|
|
249
|
+
|
|
145
250
|
/**
|
|
146
251
|
* Format usage limit error message for console output
|
|
147
252
|
*
|