@link-assistant/hive-mind 2.10.5 → 2.11.1
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 +12 -0
- package/README.hi.md +5 -0
- package/README.md +5 -0
- package/README.ru.md +5 -0
- package/README.zh.md +4 -0
- package/package.json +1 -1
- package/src/fix.ci-cd-issue.lib.mjs +137 -0
- package/src/fix.mjs +6 -116
- package/src/locales/en.lino +3 -3
- package/src/locales/hi.lino +3 -3
- package/src/locales/ru.lino +3 -3
- package/src/locales/zh.lino +3 -3
- package/src/pr-draft-state.lib.mjs +137 -0
- package/src/solve.auto-continue.lib.mjs +8 -0
- package/src/solve.restart-shared.lib.mjs +19 -0
- package/src/solve.session.lib.mjs +43 -60
- package/src/telegram-task-command.lib.mjs +47 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 2.11.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- aea5fe1: Put the pull request back into draft whenever a working session starts, restarts or resumes (issue #2123). Draft/ready transitions now live in one shared module (`src/pr-draft-state.lib.mjs`) that is called from `startWorkSession()` for every continue-mode session — the previous `--watch`/`--auto-continue` gate is gone — and from `executeToolIteration()`, which covers watch mode, temporary auto-restart on uncommitted changes, auto-restart-until-mergeable, escalate, keep-working and auto-ensure-requirements. Limit-reset auto-resume/auto-restart now also forwards `--auto-continue` so the resumed process re-attaches to the existing PR instead of running detached from it. The helper is a no-op for PRs that are already in the target state, merged or closed, and logs the observed `isDraft`/`state` under `--verbose`.
|
|
8
|
+
|
|
9
|
+
## 2.11.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 3613953: Add `/task --ci-cd <repository>` as a CI/CD remediation issue-only fallback for `/fix`.
|
|
14
|
+
|
|
3
15
|
## 2.10.5
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/README.hi.md
CHANGED
|
@@ -566,6 +566,11 @@ Examples:
|
|
|
566
566
|
`--no-solve` का उपयोग करें। विवरण के लिए
|
|
567
567
|
[स्वचालित CI/CD सुधार](docs/CI-CD-BEST-PRACTICES.md#automatic-cicd-remediation) देखें।
|
|
568
568
|
|
|
569
|
+
यदि पूरा `/fix` workflow उपलब्ध न हो, तो `/task --ci-cd <repository>` केवल वही CI/CD
|
|
570
|
+
issue-generation चरण चलाता है। यह बनाए गए issue का URL लौटाता है; सामान्य solve workflow
|
|
571
|
+
से remediation जारी रखने के लिए `/solve --development-log --deep-analysis --auto-merge`
|
|
572
|
+
से reply करें।
|
|
573
|
+
|
|
569
574
|
#### `/limits` - उपयोग सीमाएँ दिखाएँ
|
|
570
575
|
|
|
571
576
|
```
|
package/README.md
CHANGED
|
@@ -585,6 +585,11 @@ does not consume itself (e.g. `--tool`, `--model`, `--think`) is forwarded to
|
|
|
585
585
|
[Automatic CI/CD Remediation](docs/CI-CD-BEST-PRACTICES.md#automatic-cicd-remediation)
|
|
586
586
|
for details.
|
|
587
587
|
|
|
588
|
+
If the full `/fix` workflow is unavailable, `/task --ci-cd <repository>` runs
|
|
589
|
+
only the same CI/CD issue-generation step. It returns the created issue URL;
|
|
590
|
+
reply with `/solve --development-log --deep-analysis --auto-merge` to continue
|
|
591
|
+
the remediation through the normal solve workflow.
|
|
592
|
+
|
|
588
593
|
#### `/limits` - Show Usage Limits
|
|
589
594
|
|
|
590
595
|
```
|
package/README.ru.md
CHANGED
|
@@ -566,6 +566,11 @@ Examples:
|
|
|
566
566
|
`/solve`. Подробнее см.
|
|
567
567
|
[Автоматическое исправление CI/CD](docs/CI-CD-BEST-PRACTICES.md#automatic-cicd-remediation).
|
|
568
568
|
|
|
569
|
+
Если полный процесс `/fix` недоступен, `/task --ci-cd <repository>` выполняет
|
|
570
|
+
только тот же этап создания issue для CI/CD. Команда возвращает URL созданного
|
|
571
|
+
issue; ответьте `/solve --development-log --deep-analysis --auto-merge`, чтобы
|
|
572
|
+
продолжить исправление обычным процессом solve.
|
|
573
|
+
|
|
569
574
|
#### `/limits` — Показать лимиты использования
|
|
570
575
|
|
|
571
576
|
```
|
package/README.zh.md
CHANGED
|
@@ -560,6 +560,10 @@ issue 交给 `/solve --development-log --deep-analysis --auto-merge` 处理。
|
|
|
560
560
|
issue 的情况下预览,使用 `--no-solve` 可只创建 issue 而不启动 `/solve`。详见
|
|
561
561
|
[自动 CI/CD 修复](docs/CI-CD-BEST-PRACTICES.md#automatic-cicd-remediation)。
|
|
562
562
|
|
|
563
|
+
如果完整的 `/fix` 流程不可用,`/task --ci-cd <repository>` 只执行相同的 CI/CD issue
|
|
564
|
+
生成步骤。该命令会返回新建 issue 的 URL;回复
|
|
565
|
+
`/solve --development-log --deep-analysis --auto-merge` 即可通过常规 solve 流程继续修复。
|
|
566
|
+
|
|
563
567
|
#### `/limits` - 显示用量限制
|
|
564
568
|
|
|
565
569
|
```
|
package/package.json
CHANGED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared GitHub-backed CI/CD issue generation for `/fix --ci-cd` and
|
|
3
|
+
* `/task --ci-cd` (issues #1733 and #2121).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { spawn } from 'child_process';
|
|
7
|
+
import { CI_CD_ISSUE_LABELS, CI_CD_ISSUE_TYPE, buildCiCdIssueBody, buildCiCdIssueTitle } from './fix.ci-cd.lib.mjs';
|
|
8
|
+
import { createTaskIssue } from './task.issue-creation.lib.mjs';
|
|
9
|
+
|
|
10
|
+
function runCommand(command, args, options = {}) {
|
|
11
|
+
return new Promise(resolve => {
|
|
12
|
+
const child = spawn(command, args, {
|
|
13
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
14
|
+
env: process.env,
|
|
15
|
+
...options,
|
|
16
|
+
});
|
|
17
|
+
let stdout = '';
|
|
18
|
+
let stderr = '';
|
|
19
|
+
child.stdout.on('data', data => {
|
|
20
|
+
stdout += data.toString();
|
|
21
|
+
});
|
|
22
|
+
child.stderr.on('data', data => {
|
|
23
|
+
stderr += data.toString();
|
|
24
|
+
});
|
|
25
|
+
child.on('error', error => {
|
|
26
|
+
resolve({ code: 1, stdout, stderr: stderr || error.message });
|
|
27
|
+
});
|
|
28
|
+
child.on('close', code => {
|
|
29
|
+
resolve({ code, stdout, stderr });
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function commandOutput(run, command, args) {
|
|
35
|
+
const result = await run(command, args);
|
|
36
|
+
if (result.code !== 0) {
|
|
37
|
+
const output = `${result.stderr || ''}${result.stdout || ''}`.trim();
|
|
38
|
+
throw new Error(output || `${command} exited with code ${result.code}`);
|
|
39
|
+
}
|
|
40
|
+
return result.stdout.trim();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function detectLanguages(repository, run, warn) {
|
|
44
|
+
try {
|
|
45
|
+
const json = await commandOutput(run, 'gh', ['api', `repos/${repository.fullName}/languages`]);
|
|
46
|
+
return JSON.parse(json);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
warn(`⚠️ Could not detect languages: ${error.message}`);
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function getDefaultBranch(repository, run, warn) {
|
|
54
|
+
try {
|
|
55
|
+
return await commandOutput(run, 'gh', ['api', `repos/${repository.fullName}`, '--jq', '.default_branch']);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
warn(`⚠️ Could not determine default branch: ${error.message}`);
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function getLatestCommit(repository, branch, run, warn) {
|
|
63
|
+
if (!branch) return null;
|
|
64
|
+
try {
|
|
65
|
+
const json = await commandOutput(run, 'gh', ['api', `repos/${repository.fullName}/commits/${branch}`, '--jq', '{sha: .sha, message: .commit.message, url: .html_url}']);
|
|
66
|
+
return JSON.parse(json);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
warn(`⚠️ Could not fetch latest commit: ${error.message}`);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const RUNS_JQ = '[.workflow_runs[] | {name: .name, status: .status, conclusion: .conclusion, html_url: .html_url, head_sha: .head_sha}]';
|
|
74
|
+
|
|
75
|
+
async function getRunsForCommit(repository, sha, run, warn) {
|
|
76
|
+
if (!sha) return [];
|
|
77
|
+
try {
|
|
78
|
+
const json = await commandOutput(run, 'gh', ['api', `repos/${repository.fullName}/actions/runs?head_sha=${sha}&per_page=100`, '--jq', RUNS_JQ]);
|
|
79
|
+
const parsed = JSON.parse(json);
|
|
80
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
81
|
+
} catch (error) {
|
|
82
|
+
warn(`⚠️ Could not fetch CI/CD runs: ${error.message}`);
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function getRecentBranchRuns(repository, branch, run, warn) {
|
|
88
|
+
if (!branch) return [];
|
|
89
|
+
try {
|
|
90
|
+
const json = await commandOutput(run, 'gh', ['api', `repos/${repository.fullName}/actions/runs?branch=${encodeURIComponent(branch)}&per_page=20`, '--jq', RUNS_JQ]);
|
|
91
|
+
const parsed = JSON.parse(json);
|
|
92
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
93
|
+
} catch (error) {
|
|
94
|
+
warn(`⚠️ Could not fetch recent CI/CD runs for branch ${branch}: ${error.message}`);
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function prepareCiCdIssue({ repository, run = runCommand, warn = message => console.warn(message) }) {
|
|
100
|
+
const [languages, defaultBranch] = await Promise.all([detectLanguages(repository, run, warn), getDefaultBranch(repository, run, warn)]);
|
|
101
|
+
const commit = await getLatestCommit(repository, defaultBranch, run, warn);
|
|
102
|
+
let runs = await getRunsForCommit(repository, commit?.sha, run, warn);
|
|
103
|
+
let runsSource = 'commit';
|
|
104
|
+
|
|
105
|
+
if (runs.length === 0) {
|
|
106
|
+
const branchRuns = await getRecentBranchRuns(repository, defaultBranch, run, warn);
|
|
107
|
+
if (branchRuns.length > 0) {
|
|
108
|
+
runs = branchRuns;
|
|
109
|
+
runsSource = 'branch';
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
repository,
|
|
115
|
+
defaultBranch,
|
|
116
|
+
commit,
|
|
117
|
+
runs,
|
|
118
|
+
languages,
|
|
119
|
+
runsSource,
|
|
120
|
+
title: buildCiCdIssueTitle(),
|
|
121
|
+
body: buildCiCdIssueBody({ repository, defaultBranch, commit, runs, languages, runsSource }),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function createCiCdIssue({ repository, prepared = null, run = runCommand, log = null, warn = message => console.warn(message) }) {
|
|
126
|
+
const issueDraft = prepared || (await prepareCiCdIssue({ repository, run, warn }));
|
|
127
|
+
const issue = await createTaskIssue({
|
|
128
|
+
repository,
|
|
129
|
+
title: issueDraft.title,
|
|
130
|
+
body: issueDraft.body,
|
|
131
|
+
issueType: CI_CD_ISSUE_TYPE,
|
|
132
|
+
labels: [...CI_CD_ISSUE_LABELS],
|
|
133
|
+
run,
|
|
134
|
+
log,
|
|
135
|
+
});
|
|
136
|
+
return { ...issue, prepared: issueDraft };
|
|
137
|
+
}
|
package/src/fix.mjs
CHANGED
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
import path from 'path';
|
|
17
17
|
import { spawn } from 'child_process';
|
|
18
18
|
import { fileURLToPath } from 'url';
|
|
19
|
-
import {
|
|
19
|
+
import { buildSolveArgs, partitionFixArgs, summarizeRunFailures } from './fix.ci-cd.lib.mjs';
|
|
20
|
+
import { createCiCdIssue, prepareCiCdIssue } from './fix.ci-cd-issue.lib.mjs';
|
|
20
21
|
import { setupStdioLogInterceptor } from './lib.mjs';
|
|
21
22
|
|
|
22
23
|
setupStdioLogInterceptor();
|
|
@@ -44,95 +45,6 @@ Examples:
|
|
|
44
45
|
fix.mjs owner/repo --ci-cd --think max --no-solve`);
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
function runCommand(command, args, options = {}) {
|
|
48
|
-
return new Promise(resolve => {
|
|
49
|
-
const child = spawn(command, args, {
|
|
50
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
51
|
-
env: process.env,
|
|
52
|
-
...options,
|
|
53
|
-
});
|
|
54
|
-
let stdout = '';
|
|
55
|
-
let stderr = '';
|
|
56
|
-
child.stdout.on('data', data => {
|
|
57
|
-
stdout += data.toString();
|
|
58
|
-
});
|
|
59
|
-
child.stderr.on('data', data => {
|
|
60
|
-
stderr += data.toString();
|
|
61
|
-
});
|
|
62
|
-
child.on('error', error => {
|
|
63
|
-
resolve({ code: 1, stdout, stderr: stderr || error.message });
|
|
64
|
-
});
|
|
65
|
-
child.on('close', code => {
|
|
66
|
-
resolve({ code, stdout, stderr });
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async function commandOutput(command, args) {
|
|
72
|
-
const result = await runCommand(command, args);
|
|
73
|
-
if (result.code !== 0) {
|
|
74
|
-
const output = `${result.stderr || ''}${result.stdout || ''}`.trim();
|
|
75
|
-
throw new Error(output || `${command} exited with code ${result.code}`);
|
|
76
|
-
}
|
|
77
|
-
return result.stdout.trim();
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async function detectLanguages(repository) {
|
|
81
|
-
try {
|
|
82
|
-
const json = await commandOutput('gh', ['api', `repos/${repository.fullName}/languages`]);
|
|
83
|
-
return JSON.parse(json);
|
|
84
|
-
} catch (error) {
|
|
85
|
-
console.warn(`⚠️ Could not detect languages: ${error.message}`);
|
|
86
|
-
return {};
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async function getDefaultBranch(repository) {
|
|
91
|
-
try {
|
|
92
|
-
return await commandOutput('gh', ['api', `repos/${repository.fullName}`, '--jq', '.default_branch']);
|
|
93
|
-
} catch (error) {
|
|
94
|
-
console.warn(`⚠️ Could not determine default branch: ${error.message}`);
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
async function getLatestCommit(repository, branch) {
|
|
100
|
-
if (!branch) return null;
|
|
101
|
-
try {
|
|
102
|
-
const json = await commandOutput('gh', ['api', `repos/${repository.fullName}/commits/${branch}`, '--jq', '{sha: .sha, message: .commit.message, url: .html_url}']);
|
|
103
|
-
return JSON.parse(json);
|
|
104
|
-
} catch (error) {
|
|
105
|
-
console.warn(`⚠️ Could not fetch latest commit: ${error.message}`);
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const RUNS_JQ = '[.workflow_runs[] | {name: .name, status: .status, conclusion: .conclusion, html_url: .html_url, head_sha: .head_sha}]';
|
|
111
|
-
|
|
112
|
-
async function getRunsForCommit(repository, sha) {
|
|
113
|
-
if (!sha) return [];
|
|
114
|
-
try {
|
|
115
|
-
const json = await commandOutput('gh', ['api', `repos/${repository.fullName}/actions/runs?head_sha=${sha}&per_page=100`, '--jq', RUNS_JQ]);
|
|
116
|
-
const parsed = JSON.parse(json);
|
|
117
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
118
|
-
} catch (error) {
|
|
119
|
-
console.warn(`⚠️ Could not fetch CI/CD runs: ${error.message}`);
|
|
120
|
-
return [];
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
async function getRecentBranchRuns(repository, branch) {
|
|
125
|
-
if (!branch) return [];
|
|
126
|
-
try {
|
|
127
|
-
const json = await commandOutput('gh', ['api', `repos/${repository.fullName}/actions/runs?branch=${encodeURIComponent(branch)}&per_page=20`, '--jq', RUNS_JQ]);
|
|
128
|
-
const parsed = JSON.parse(json);
|
|
129
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
130
|
-
} catch (error) {
|
|
131
|
-
console.warn(`⚠️ Could not fetch recent CI/CD runs for branch ${branch}: ${error.message}`);
|
|
132
|
-
return [];
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
48
|
function resolveSolveCommand() {
|
|
137
49
|
return path.join(__dirname, 'solve.mjs');
|
|
138
50
|
}
|
|
@@ -171,29 +83,14 @@ async function main() {
|
|
|
171
83
|
const repository = parsed.repository;
|
|
172
84
|
console.log(`🔧 /fix --ci-cd for ${repository.fullName}`);
|
|
173
85
|
|
|
174
|
-
const
|
|
175
|
-
const commit
|
|
176
|
-
let runs = await getRunsForCommit(repository, commit?.sha);
|
|
177
|
-
let runsSource = 'commit';
|
|
178
|
-
|
|
179
|
-
// Release/tag commits frequently produce no runs of their own. Fall back to
|
|
180
|
-
// the most recent runs on the default branch so the issue stays actionable.
|
|
181
|
-
if (runs.length === 0) {
|
|
182
|
-
const branchRuns = await getRecentBranchRuns(repository, defaultBranch);
|
|
183
|
-
if (branchRuns.length > 0) {
|
|
184
|
-
runs = branchRuns;
|
|
185
|
-
runsSource = 'branch';
|
|
186
|
-
}
|
|
187
|
-
}
|
|
86
|
+
const prepared = await prepareCiCdIssue({ repository });
|
|
87
|
+
const { defaultBranch, commit, runs, runsSource, title, body } = prepared;
|
|
188
88
|
|
|
189
89
|
const { total, failing } = summarizeRunFailures(runs);
|
|
190
90
|
console.log(` Default branch: ${defaultBranch || 'unknown'}`);
|
|
191
91
|
console.log(` Latest commit: ${commit?.sha ? commit.sha.slice(0, 7) : 'unknown'}`);
|
|
192
92
|
console.log(` CI/CD runs: ${total} (${failing} not passing)${runsSource === 'branch' ? ' [recent branch runs]' : ''}`);
|
|
193
93
|
|
|
194
|
-
const title = buildCiCdIssueTitle();
|
|
195
|
-
const body = buildCiCdIssueBody({ repository, defaultBranch, commit, runs, languages, runsSource });
|
|
196
|
-
|
|
197
94
|
if (parsed.dryRun) {
|
|
198
95
|
console.log('\n--- DRY RUN: issue that would be created ---\n');
|
|
199
96
|
console.log(`Title: ${title}\n`);
|
|
@@ -202,16 +99,9 @@ async function main() {
|
|
|
202
99
|
}
|
|
203
100
|
|
|
204
101
|
console.log('\n📝 Creating remediation issue...');
|
|
205
|
-
const
|
|
206
|
-
const issue = await createTaskIssue({
|
|
102
|
+
const issue = await createCiCdIssue({
|
|
207
103
|
repository,
|
|
208
|
-
|
|
209
|
-
body,
|
|
210
|
-
// The Bug type is what makes /solve --deep-analysis emit the root-cause
|
|
211
|
-
// instructions this body omits (issue #1733).
|
|
212
|
-
issueType: CI_CD_ISSUE_TYPE,
|
|
213
|
-
labels: [...CI_CD_ISSUE_LABELS],
|
|
214
|
-
run: runCommand,
|
|
104
|
+
prepared,
|
|
215
105
|
log: message => console.log(message),
|
|
216
106
|
});
|
|
217
107
|
console.log(`✅ Created issue: ${issue.url}`);
|
package/src/locales/en.lino
CHANGED
|
@@ -556,9 +556,9 @@ en
|
|
|
556
556
|
locked
|
|
557
557
|
options "🔒 Locked options: `{{options}}`"
|
|
558
558
|
task
|
|
559
|
-
enabled "*/task* - Create a GitHub issue from
|
|
560
|
-
usage "Usage: `/task <github-repository-url>` followed by issue text, or
|
|
561
|
-
example "Example: `/task https://github.com/owner/repo`
|
|
559
|
+
enabled "*/task* - Create a GitHub issue from text or CI/CD context"
|
|
560
|
+
usage "Usage: `/task <github-repository-url>` followed by issue text, or `/task --ci-cd <github-repository-url>`"
|
|
561
|
+
example "Example: `/task --ci-cd https://github.com/owner/repo` creates only the CI/CD remediation issue"
|
|
562
562
|
disabled "*/task* / */split* - ❌ Disabled"
|
|
563
563
|
split
|
|
564
564
|
enabled "*/split* - Split a GitHub issue into smaller issues"
|
package/src/locales/hi.lino
CHANGED
|
@@ -556,9 +556,9 @@ hi
|
|
|
556
556
|
locked
|
|
557
557
|
options "🔒 लॉक किए गए विकल्प: `{{options}}`"
|
|
558
558
|
task
|
|
559
|
-
enabled "*/task* -
|
|
560
|
-
usage "उपयोग: `/task <github-repository-url>` के बाद issue text, या `/task
|
|
561
|
-
example "उदाहरण: `/task https://github.com/owner/repo
|
|
559
|
+
enabled "*/task* - text या CI/CD context से GitHub issue बनाएँ"
|
|
560
|
+
usage "उपयोग: `/task <github-repository-url>` के बाद issue text, या `/task --ci-cd <github-repository-url>`"
|
|
561
|
+
example "उदाहरण: `/task --ci-cd https://github.com/owner/repo` केवल CI/CD remediation issue बनाता है"
|
|
562
562
|
disabled "*/task* / */split* - ❌ अक्षम"
|
|
563
563
|
split
|
|
564
564
|
enabled "*/split* - GitHub issue को छोटे issues में बाँटें"
|
package/src/locales/ru.lino
CHANGED
|
@@ -556,9 +556,9 @@ ru
|
|
|
556
556
|
locked
|
|
557
557
|
options "🔒 Заблокированные опции: `{{options}}`"
|
|
558
558
|
task
|
|
559
|
-
enabled "*/task* - Создать
|
|
560
|
-
usage "Использование: `/task <github-repository-url>` с текстом
|
|
561
|
-
example "Пример: `/task https://github.com/owner/repo
|
|
559
|
+
enabled "*/task* - Создать issue GitHub из текста или контекста CI/CD"
|
|
560
|
+
usage "Использование: `/task <github-repository-url>` с текстом или `/task --ci-cd <github-repository-url>`"
|
|
561
|
+
example "Пример: `/task --ci-cd https://github.com/owner/repo` создаёт только issue для исправления CI/CD"
|
|
562
562
|
disabled "*/task* / */split* - ❌ Отключено"
|
|
563
563
|
split
|
|
564
564
|
enabled "*/split* - Разделить задачу GitHub на меньшие задачи"
|
package/src/locales/zh.lino
CHANGED
|
@@ -556,9 +556,9 @@ zh
|
|
|
556
556
|
locked
|
|
557
557
|
options "🔒 锁定选项:`{{options}}`"
|
|
558
558
|
task
|
|
559
|
-
enabled "*/task* -
|
|
560
|
-
usage "用法:`/task <github-repository-url>` 后接 issue
|
|
561
|
-
example "示例:`/task https://github.com/owner/repo
|
|
559
|
+
enabled "*/task* - 根据文本或 CI/CD 上下文创建 GitHub issue"
|
|
560
|
+
usage "用法:`/task <github-repository-url>` 后接 issue 文本,或 `/task --ci-cd <github-repository-url>`"
|
|
561
|
+
example "示例:`/task --ci-cd https://github.com/owner/repo` 只创建 CI/CD 修复 issue"
|
|
562
562
|
disabled "*/task* / */split* - ❌ 已禁用"
|
|
563
563
|
split
|
|
564
564
|
enabled "*/split* - 将 GitHub issue 拆分为更小的 issue"
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for pull request draft/ready state transitions.
|
|
3
|
+
*
|
|
4
|
+
* Issue #2123: every place that starts, restarts or resumes a working session must
|
|
5
|
+
* put the pull request back into draft mode (if it is not already a draft), and every
|
|
6
|
+
* place that ends a working session must convert it back to ready for review.
|
|
7
|
+
*
|
|
8
|
+
* Before this module the logic was duplicated inline in solve.session.lib.mjs and was
|
|
9
|
+
* gated behind `argv.watch || argv.autoContinue`, so auto-restart / auto-resume
|
|
10
|
+
* sessions (temporary watch mode, auto-restart-until-mergeable, escalate,
|
|
11
|
+
* keep-working, auto-ensure, PR-placeholder restart) kept the PR marked as
|
|
12
|
+
* "ready for review" while the AI was actively working on it.
|
|
13
|
+
*
|
|
14
|
+
* @see https://github.com/link-assistant/hive-mind/issues/2123
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// rate-limit marker (#1726): callers pass in a `$` already wrapped by wrapDollarWithGhRetry.
|
|
18
|
+
import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry } from './github-rate-limit.lib.mjs';
|
|
19
|
+
|
|
20
|
+
const noopLog = async () => {};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Fetch the draft/open state of a pull request.
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} options
|
|
26
|
+
* @param {string} options.owner - Repository owner
|
|
27
|
+
* @param {string} options.repo - Repository name
|
|
28
|
+
* @param {number|string} options.prNumber - Pull request number
|
|
29
|
+
* @param {Function} options.$ - command-stream style tagged template executor
|
|
30
|
+
* @param {Function} [options.log] - Logger
|
|
31
|
+
* @returns {Promise<{ok: boolean, isDraft: (boolean|null), state: (string|null), merged: boolean, error: (string|null)}>}
|
|
32
|
+
*/
|
|
33
|
+
export const getPullRequestDraftState = async ({ owner, repo, prNumber, $, log = noopLog }) => {
|
|
34
|
+
try {
|
|
35
|
+
const result = await $`gh pr view ${prNumber} --repo ${owner}/${repo} --json isDraft,state`;
|
|
36
|
+
if (result.code !== 0) {
|
|
37
|
+
const stderr = result.stderr ? result.stderr.toString().trim() : '';
|
|
38
|
+
return { ok: false, isDraft: null, state: null, merged: false, error: stderr || `gh exited with code ${result.code}` };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const raw = result.stdout.toString().trim();
|
|
42
|
+
let parsed;
|
|
43
|
+
try {
|
|
44
|
+
parsed = JSON.parse(raw);
|
|
45
|
+
} catch {
|
|
46
|
+
return { ok: false, isDraft: null, state: null, merged: false, error: `Could not parse gh output: ${raw.slice(0, 200)}` };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const state = typeof parsed.state === 'string' ? parsed.state.toUpperCase() : null;
|
|
50
|
+
await log(` 🔍 PR #${prNumber} draft state: isDraft=${parsed.isDraft}, state=${state}`, { verbose: true });
|
|
51
|
+
|
|
52
|
+
return { ok: true, isDraft: parsed.isDraft === true, state, merged: state === 'MERGED', error: null };
|
|
53
|
+
} catch (error) {
|
|
54
|
+
return { ok: false, isDraft: null, state: null, merged: false, error: error.message };
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Internal helper shared by ensurePullRequestIsDraft/ensurePullRequestIsReady.
|
|
60
|
+
*
|
|
61
|
+
* @param {Object} options
|
|
62
|
+
* @param {'draft'|'ready'} options.target - Desired state
|
|
63
|
+
* @returns {Promise<{ok: boolean, changed: boolean, skipped: boolean, reason: (string|null), error: (string|null)}>}
|
|
64
|
+
*/
|
|
65
|
+
const setPullRequestDraftState = async ({ target, owner, repo, prNumber, $, log = noopLog, formatAligned = null, indent = 2, reason = null, reportError = null }) => {
|
|
66
|
+
const wantDraft = target === 'draft';
|
|
67
|
+
const label = wantDraft ? 'draft mode' : 'ready for review';
|
|
68
|
+
const write = async (icon, key, value) => {
|
|
69
|
+
await log(formatAligned ? formatAligned(icon, key, value, indent) : `${icon} ${key} ${value}`);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
if (!owner || !repo || !prNumber) {
|
|
73
|
+
return { ok: false, changed: false, skipped: true, reason: 'missing_pr_context', error: null };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const status = await getPullRequestDraftState({ owner, repo, prNumber, $, log });
|
|
78
|
+
|
|
79
|
+
if (!status.ok) {
|
|
80
|
+
await log(`Warning: Could not check PR #${prNumber} draft status: ${status.error}`, { level: 'warning' });
|
|
81
|
+
return { ok: false, changed: false, skipped: false, reason: 'status_check_failed', error: status.error };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// A merged or closed pull request cannot change its draft state; GitHub rejects it.
|
|
85
|
+
if (status.state && status.state !== 'OPEN') {
|
|
86
|
+
await write('ℹ️', 'PR status:', `${status.state.toLowerCase()} - skipping ${label} conversion`);
|
|
87
|
+
return { ok: true, changed: false, skipped: true, reason: `pr_${status.state.toLowerCase()}`, error: null };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (status.isDraft === wantDraft) {
|
|
91
|
+
await write('✅', 'PR status:', `Already in ${label}`);
|
|
92
|
+
return { ok: true, changed: false, skipped: true, reason: 'already_in_target_state', error: null };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
await write('📝', 'Converting PR:', `To ${label}${reason ? ` (${reason})` : ''}...`);
|
|
96
|
+
const convertResult = wantDraft ? await $`gh pr ready ${prNumber} --repo ${owner}/${repo} --undo` : await $`gh pr ready ${prNumber} --repo ${owner}/${repo}`;
|
|
97
|
+
|
|
98
|
+
if (convertResult.code === 0) {
|
|
99
|
+
await write('✅', 'PR converted:', `Now in ${label}`);
|
|
100
|
+
return { ok: true, changed: true, skipped: false, reason: null, error: null };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const stderr = convertResult.stderr ? convertResult.stderr.toString().trim() : '';
|
|
104
|
+
await log(`Warning: Could not convert PR #${prNumber} to ${label}${stderr ? `: ${stderr}` : ''}`, { level: 'warning' });
|
|
105
|
+
return { ok: false, changed: false, skipped: false, reason: 'conversion_failed', error: stderr || `gh exited with code ${convertResult.code}` };
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (typeof reportError === 'function') {
|
|
108
|
+
reportError(error, {
|
|
109
|
+
context: wantDraft ? 'convert_pr_to_draft' : 'convert_pr_to_ready',
|
|
110
|
+
prNumber,
|
|
111
|
+
owner,
|
|
112
|
+
repo,
|
|
113
|
+
operation: 'pr_status_change',
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
await log(`Warning: Could not check/convert PR #${prNumber} draft status: ${error.message}`, { level: 'warning' });
|
|
117
|
+
return { ok: false, changed: false, skipped: false, reason: 'exception', error: error.message };
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Put a pull request into draft mode when a working session starts/restarts/resumes.
|
|
123
|
+
* No-op when the PR is already a draft, merged, or closed.
|
|
124
|
+
*/
|
|
125
|
+
export const ensurePullRequestIsDraft = async options => setPullRequestDraftState({ ...options, target: 'draft' });
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Put a pull request back to "ready for review" when a working session ends.
|
|
129
|
+
* No-op when the PR is already ready, merged, or closed.
|
|
130
|
+
*/
|
|
131
|
+
export const ensurePullRequestIsReady = async options => setPullRequestDraftState({ ...options, target: 'ready' });
|
|
132
|
+
|
|
133
|
+
export default {
|
|
134
|
+
getPullRequestDraftState,
|
|
135
|
+
ensurePullRequestIsDraft,
|
|
136
|
+
ensurePullRequestIsReady,
|
|
137
|
+
};
|
|
@@ -162,6 +162,14 @@ export const autoContinueWhenLimitResets = async (issueUrl, sessionId, argv, sho
|
|
|
162
162
|
await log(`🔄 Session will be RESTARTED (fresh start without previous context)`);
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
// Issue #2123: the resumed/restarted process is launched with the ISSUE url, so without
|
|
166
|
+
// --auto-continue it would not enter continue mode, would not find the existing PR, and
|
|
167
|
+
// therefore would never convert that PR back to draft (nor post the auto-resume/auto-restart
|
|
168
|
+
// session comment). Preserve the flag so the new session attaches to the same PR.
|
|
169
|
+
if (argv.autoContinue) {
|
|
170
|
+
resumeArgs.push('--auto-continue');
|
|
171
|
+
}
|
|
172
|
+
|
|
165
173
|
// Preserve auto-resume/auto-restart flag for subsequent limit hits
|
|
166
174
|
if (argv.autoResumeOnLimitReset) {
|
|
167
175
|
resumeArgs.push('--auto-resume-on-limit-reset');
|
|
@@ -33,6 +33,8 @@ const lib = await import('./lib.mjs');
|
|
|
33
33
|
const { log, formatAligned, extractToolErrorCore } = lib;
|
|
34
34
|
const { ensurePullRequestBaseBranch } = await import('./solve.pr-base-guard.lib.mjs');
|
|
35
35
|
const { RESOURCE_PHASE_RESTART_AFTER, RESOURCE_PHASE_RESTART_BEFORE, recordResourceSnapshot } = await import('./solve.resource-diagnostics.lib.mjs');
|
|
36
|
+
// Issue #2123: shared draft/ready transitions for working sessions.
|
|
37
|
+
const { ensurePullRequestIsDraft } = await import('./pr-draft-state.lib.mjs');
|
|
36
38
|
|
|
37
39
|
// Import Sentry integration
|
|
38
40
|
const sentryLib = await import('./sentry.lib.mjs');
|
|
@@ -185,6 +187,23 @@ export const executeToolIteration = async params => {
|
|
|
185
187
|
label: 'before AI restart iteration',
|
|
186
188
|
});
|
|
187
189
|
|
|
190
|
+
// Issue #2123: every restart/resume iteration is a new working session, so the PR must be
|
|
191
|
+
// put back into draft before the AI starts changing it. This single call covers watch mode,
|
|
192
|
+
// temporary auto-restart, auto-restart-until-mergeable, escalate, keep-working,
|
|
193
|
+
// auto-ensure-requirements and the PR-placeholder restart, which all funnel through here.
|
|
194
|
+
if (prNumber) {
|
|
195
|
+
await ensurePullRequestIsDraft({
|
|
196
|
+
owner,
|
|
197
|
+
repo,
|
|
198
|
+
prNumber,
|
|
199
|
+
$,
|
|
200
|
+
log,
|
|
201
|
+
formatAligned,
|
|
202
|
+
reason: 'restart iteration',
|
|
203
|
+
reportError,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
188
207
|
// Import necessary modules for tool execution
|
|
189
208
|
const memoryCheck = await import('./memory-check.mjs');
|
|
190
209
|
const { getResourceSnapshot } = memoryCheck;
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
import { AI_WORK_SESSION_STARTED_MARKER, AI_WORK_SESSION_COMPLETED_MARKER, AI_WORK_SESSION_RESUMED_MARKER, AUTO_RESUME_ON_LIMIT_RESET_MARKER, AUTO_RESTART_ON_LIMIT_RESET_MARKER, postTrackedComment } from './tool-comments.lib.mjs';
|
|
10
10
|
|
|
11
11
|
import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller
|
|
12
|
+
|
|
13
|
+
// Issue #2123: draft/ready transitions live in one shared module so every session
|
|
14
|
+
// start/restart/resume path behaves identically.
|
|
15
|
+
import { ensurePullRequestIsDraft, ensurePullRequestIsReady } from './pr-draft-state.lib.mjs';
|
|
12
16
|
/**
|
|
13
17
|
* Session type definitions for different work session contexts
|
|
14
18
|
* See: https://github.com/link-assistant/hive-mind/issues/1152
|
|
@@ -70,39 +74,30 @@ function getSessionCommentContent(sessionType, timestamp) {
|
|
|
70
74
|
* @param {string} [options.sessionType='new'] - One of SESSION_TYPES values
|
|
71
75
|
*/
|
|
72
76
|
export async function startWorkSession({ isContinueMode, prNumber, argv, log, formatAligned, $, sessionType = SESSION_TYPES.NEW }) {
|
|
73
|
-
// Record work start time and convert PR to draft
|
|
77
|
+
// Record work start time and convert PR to draft.
|
|
78
|
+
//
|
|
79
|
+
// Issue #2123: the draft conversion used to be gated behind `argv.watch || argv.autoContinue`,
|
|
80
|
+
// so plain `--resume`/continue-mode sessions left the PR marked "ready for review" while the
|
|
81
|
+
// AI was still working on it. Any continue-mode session with a PR now converts it to draft.
|
|
74
82
|
const workStartTime = new Date();
|
|
75
|
-
|
|
83
|
+
const shouldPostSessionComment = argv.watch || argv.autoContinue;
|
|
84
|
+
if (isContinueMode && prNumber) {
|
|
76
85
|
await log(`\n${formatAligned('🚀', 'Starting work session:', workStartTime.toISOString())}`);
|
|
77
86
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
} else {
|
|
92
|
-
await log(formatAligned('✅', 'PR status:', 'Already in draft mode', 2));
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
} catch (error) {
|
|
96
|
-
const sentryLib = await import('./sentry.lib.mjs');
|
|
97
|
-
const { reportError } = sentryLib;
|
|
98
|
-
reportError(error, {
|
|
99
|
-
context: 'convert_pr_to_draft',
|
|
100
|
-
prNumber,
|
|
101
|
-
operation: 'pr_status_change',
|
|
102
|
-
});
|
|
103
|
-
await log('Warning: Could not check/convert PR draft status', { level: 'warning' });
|
|
104
|
-
}
|
|
87
|
+
const { reportError } = await import('./sentry.lib.mjs');
|
|
88
|
+
await ensurePullRequestIsDraft({
|
|
89
|
+
owner: global.owner,
|
|
90
|
+
repo: global.repo,
|
|
91
|
+
prNumber,
|
|
92
|
+
$,
|
|
93
|
+
log,
|
|
94
|
+
formatAligned,
|
|
95
|
+
reason: `session start: ${sessionType}`,
|
|
96
|
+
reportError,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
105
99
|
|
|
100
|
+
if (isContinueMode && prNumber && shouldPostSessionComment) {
|
|
106
101
|
// Post a comment marking the start of work session with appropriate header based on session type.
|
|
107
102
|
// Issue #1625: Use postTrackedComment so the comment ID is registered in-memory and can be
|
|
108
103
|
// excluded from the "did the AI post anything?" check in checkForAiCreatedComments().
|
|
@@ -131,14 +126,17 @@ export async function startWorkSession({ isContinueMode, prNumber, argv, log, fo
|
|
|
131
126
|
}
|
|
132
127
|
|
|
133
128
|
export async function endWorkSession({ isContinueMode, prNumber, argv, log, formatAligned, $, logsAttached = false }) {
|
|
134
|
-
// Post end work session comment and convert PR back to ready if in continue mode
|
|
135
|
-
|
|
129
|
+
// Post end work session comment and convert PR back to ready if in continue mode.
|
|
130
|
+
// Issue #2123: the ready conversion mirrors startWorkSession's draft conversion, so it must
|
|
131
|
+
// run for every continue-mode session, not only for --watch/--auto-continue ones.
|
|
132
|
+
if (isContinueMode && prNumber) {
|
|
136
133
|
const workEndTime = new Date();
|
|
134
|
+
const shouldPostSessionComment = argv.watch || argv.autoContinue;
|
|
137
135
|
await log(`\n${formatAligned('🏁', 'Ending work session:', workEndTime.toISOString())}`);
|
|
138
136
|
|
|
139
137
|
// Only post end comment if logs were NOT already attached
|
|
140
138
|
// The attachLogToGitHub comment already serves as finishing status with "Now working session is ended" text
|
|
141
|
-
if (!logsAttached) {
|
|
139
|
+
if (shouldPostSessionComment && !logsAttached) {
|
|
142
140
|
// Post a comment marking the end of work session.
|
|
143
141
|
// Issue #1625: Track the comment ID so it won't be mistaken for AI-authored content.
|
|
144
142
|
try {
|
|
@@ -159,36 +157,21 @@ export async function endWorkSession({ isContinueMode, prNumber, argv, log, form
|
|
|
159
157
|
});
|
|
160
158
|
await log('Warning: Could not post work end comment', { level: 'warning' });
|
|
161
159
|
}
|
|
162
|
-
} else {
|
|
160
|
+
} else if (shouldPostSessionComment) {
|
|
163
161
|
await log(formatAligned('ℹ️', 'Skipping:', 'End comment (logs already attached with session end message)', 2));
|
|
164
162
|
}
|
|
165
163
|
|
|
166
|
-
// Convert PR back to ready for review
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
} else {
|
|
180
|
-
await log(formatAligned('✅', 'PR status:', 'Already ready for review', 2));
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
} catch (error) {
|
|
184
|
-
const sentryLib = await import('./sentry.lib.mjs');
|
|
185
|
-
const { reportError } = sentryLib;
|
|
186
|
-
reportError(error, {
|
|
187
|
-
context: 'convert_pr_to_ready',
|
|
188
|
-
prNumber,
|
|
189
|
-
operation: 'pr_status_change',
|
|
190
|
-
});
|
|
191
|
-
await log('Warning: Could not convert PR to ready status', { level: 'warning' });
|
|
192
|
-
}
|
|
164
|
+
// Convert PR back to ready for review (issue #2123: shared implementation)
|
|
165
|
+
const { reportError } = await import('./sentry.lib.mjs');
|
|
166
|
+
await ensurePullRequestIsReady({
|
|
167
|
+
owner: global.owner,
|
|
168
|
+
repo: global.repo,
|
|
169
|
+
prNumber,
|
|
170
|
+
$,
|
|
171
|
+
log,
|
|
172
|
+
formatAligned,
|
|
173
|
+
reason: 'session end',
|
|
174
|
+
reportError,
|
|
175
|
+
});
|
|
193
176
|
}
|
|
194
177
|
}
|
|
@@ -2,6 +2,8 @@ import { buildUserMention } from './buildUserMention.lib.mjs';
|
|
|
2
2
|
import { validateModelName } from './models/index.mjs';
|
|
3
3
|
import { getLinoYargsFactory } from './cli-arguments.lib.mjs';
|
|
4
4
|
import { createYargsConfig as createTaskYargsConfig } from './task.config.lib.mjs';
|
|
5
|
+
import { createCiCdIssue } from './fix.ci-cd-issue.lib.mjs';
|
|
6
|
+
import { parseFixRepository } from './fix.ci-cd.lib.mjs';
|
|
5
7
|
import { createTaskIssue, parseTaskIssueCreationInput, resolveTaskIssueCreationInput } from './task.issue-creation.lib.mjs';
|
|
6
8
|
import { parseTaskIssueUrl } from './task.split.lib.mjs';
|
|
7
9
|
import { escapeMarkdown } from './telegram-markdown.lib.mjs';
|
|
@@ -23,6 +25,10 @@ export function hasTaskSplitFlag(args) {
|
|
|
23
25
|
return args.includes('--split') || args.some(arg => arg.startsWith('--split='));
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
export function hasTaskCiCdFlag(args) {
|
|
29
|
+
return args.includes('--ci-cd');
|
|
30
|
+
}
|
|
31
|
+
|
|
26
32
|
export function applyTaskCommandDefaults(args, commandName = 'task') {
|
|
27
33
|
if (commandName !== 'split') return args;
|
|
28
34
|
const hasSplit = args.includes('--split') || args.some(arg => arg.startsWith('--split='));
|
|
@@ -66,6 +72,16 @@ export function buildTaskCommandArgs(text) {
|
|
|
66
72
|
};
|
|
67
73
|
}
|
|
68
74
|
|
|
75
|
+
export function buildTaskCiCdCommandArgs(text) {
|
|
76
|
+
const args = parseCommandArgs(text);
|
|
77
|
+
const repositoryRaw = args.find(arg => !arg.startsWith('-') && parseFixRepository(arg)) || null;
|
|
78
|
+
return {
|
|
79
|
+
args,
|
|
80
|
+
repositoryRaw,
|
|
81
|
+
repository: repositoryRaw ? parseFixRepository(repositoryRaw) : null,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
69
85
|
function getReplyText(message) {
|
|
70
86
|
const reply = message?.reply_to_message;
|
|
71
87
|
if (!reply || reply.forum_topic_created) return '';
|
|
@@ -97,7 +113,7 @@ function injectLanguageIfMissing(args, locale) {
|
|
|
97
113
|
}
|
|
98
114
|
|
|
99
115
|
export function registerTaskCommands(bot, options) {
|
|
100
|
-
const { VERBOSE, taskEnabled, addBreadcrumb, isOldMessage, isForwarded, isGroupChat, isTopicAuthorized, buildAuthErrorMessage, isChatStopped, getStoppedChatRejectMessage, safeReply, executeAndUpdateMessage, createTaskIssue: createTaskIssueFn = createTaskIssue, resolveLocale = null } = options;
|
|
116
|
+
const { VERBOSE, taskEnabled, addBreadcrumb, isOldMessage, isForwarded, isGroupChat, isTopicAuthorized, buildAuthErrorMessage, isChatStopped, getStoppedChatRejectMessage, safeReply, executeAndUpdateMessage, createTaskIssue: createTaskIssueFn = createTaskIssue, createCiCdIssue: createCiCdIssueFn = createCiCdIssue, resolveLocale = null } = options;
|
|
101
117
|
|
|
102
118
|
async function handleTaskCommand(ctx) {
|
|
103
119
|
const commandName = getTaskCommandNameFromText(ctx.message?.text) || 'task';
|
|
@@ -138,6 +154,36 @@ export function registerTaskCommands(bot, options) {
|
|
|
138
154
|
|
|
139
155
|
const parsedArgs = parseCommandArgs(ctx.message.text);
|
|
140
156
|
const splitMode = commandName === 'split' || hasTaskSplitFlag(parsedArgs);
|
|
157
|
+
const ciCdMode = commandName === 'task' && hasTaskCiCdFlag(parsedArgs);
|
|
158
|
+
|
|
159
|
+
if (splitMode && ciCdMode) {
|
|
160
|
+
await safeReply(ctx, '❌ `--ci-cd` and `--split` cannot be used together.', { reply_to_message_id: ctx.message.message_id });
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (ciCdMode) {
|
|
165
|
+
const built = buildTaskCiCdCommandArgs(ctx.message.text);
|
|
166
|
+
if (!built.repository) {
|
|
167
|
+
await safeReply(ctx, `❌ Missing GitHub repository URL. Usage: \`${commandDisplay} --ci-cd <github-repository-url>\`\n\nExample: \`${commandDisplay} --ci-cd https://github.com/owner/repo\``, { reply_to_message_id: ctx.message.message_id });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const statusMessage = await ctx.reply(`Collecting CI/CD context and creating a GitHub issue in ${built.repository.fullName}...`, {
|
|
172
|
+
reply_to_message_id: ctx.message.message_id,
|
|
173
|
+
disable_web_page_preview: true,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const createdIssue = await createCiCdIssueFn({
|
|
178
|
+
repository: built.repository,
|
|
179
|
+
log: message => VERBOSE && console.log(`[VERBOSE] ${message}`),
|
|
180
|
+
});
|
|
181
|
+
await editTelegramMessage(ctx, statusMessage, `Created GitHub issue:\n${createdIssue.url}\n\nReply to this message with /solve --development-log --deep-analysis --auto-merge to continue the full CI/CD remediation workflow.`);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
await editTelegramMessage(ctx, statusMessage, `Error creating CI/CD issue:\n${error.message || String(error)}`);
|
|
184
|
+
}
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
141
187
|
|
|
142
188
|
if (!splitMode) {
|
|
143
189
|
const replyText = getReplyText(ctx.message);
|