@qelos/aidev 0.5.0 → 0.5.2
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/.env.aidev.example +8 -0
- package/README.md +137 -2
- package/dist/cli.js +14 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/accepted.d.ts +7 -2
- package/dist/commands/accepted.d.ts.map +1 -1
- package/dist/commands/accepted.js +17 -2
- package/dist/commands/accepted.js.map +1 -1
- package/dist/commands/help.d.ts.map +1 -1
- package/dist/commands/help.js +13 -0
- package/dist/commands/help.js.map +1 -1
- package/dist/commands/run.d.ts +16 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +408 -20
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/tasks.d.ts +1 -0
- package/dist/commands/tasks.d.ts.map +1 -1
- package/dist/commands/tasks.js +29 -0
- package/dist/commands/tasks.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +6 -0
- package/dist/config.js.map +1 -1
- package/dist/github.d.ts +2 -0
- package/dist/github.d.ts.map +1 -1
- package/dist/github.js +28 -0
- package/dist/github.js.map +1 -1
- package/dist/hooks.d.ts +16 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +1 -0
- package/dist/hooks.js.map +1 -1
- package/dist/providers/linear.js +1 -1
- package/dist/providers/linear.js.map +1 -1
- package/dist/sessions.d.ts +20 -0
- package/dist/sessions.d.ts.map +1 -0
- package/dist/sessions.js +171 -0
- package/dist/sessions.js.map +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/__tests__/ai-runners.test.d.ts +0 -2
- package/dist/__tests__/ai-runners.test.d.ts.map +0 -1
- package/dist/__tests__/ai-runners.test.js +0 -367
- package/dist/__tests__/ai-runners.test.js.map +0 -1
- package/dist/__tests__/clickup-format.test.d.ts +0 -2
- package/dist/__tests__/clickup-format.test.d.ts.map +0 -1
- package/dist/__tests__/clickup-format.test.js +0 -256
- package/dist/__tests__/clickup-format.test.js.map +0 -1
- package/dist/__tests__/config.test.d.ts +0 -2
- package/dist/__tests__/config.test.d.ts.map +0 -1
- package/dist/__tests__/config.test.js +0 -418
- package/dist/__tests__/config.test.js.map +0 -1
- package/dist/__tests__/git.test.d.ts +0 -2
- package/dist/__tests__/git.test.d.ts.map +0 -1
- package/dist/__tests__/git.test.js +0 -697
- package/dist/__tests__/git.test.js.map +0 -1
- package/dist/__tests__/github.test.d.ts +0 -2
- package/dist/__tests__/github.test.d.ts.map +0 -1
- package/dist/__tests__/github.test.js +0 -209
- package/dist/__tests__/github.test.js.map +0 -1
- package/dist/__tests__/help.test.d.ts +0 -2
- package/dist/__tests__/help.test.d.ts.map +0 -1
- package/dist/__tests__/help.test.js +0 -34
- package/dist/__tests__/help.test.js.map +0 -1
- package/dist/__tests__/hooks.test.d.ts +0 -2
- package/dist/__tests__/hooks.test.d.ts.map +0 -1
- package/dist/__tests__/hooks.test.js +0 -333
- package/dist/__tests__/hooks.test.js.map +0 -1
- package/dist/__tests__/init.test.d.ts +0 -2
- package/dist/__tests__/init.test.d.ts.map +0 -1
- package/dist/__tests__/init.test.js +0 -596
- package/dist/__tests__/init.test.js.map +0 -1
- package/dist/__tests__/local-provider.test.d.ts +0 -2
- package/dist/__tests__/local-provider.test.d.ts.map +0 -1
- package/dist/__tests__/local-provider.test.js +0 -631
- package/dist/__tests__/local-provider.test.js.map +0 -1
- package/dist/__tests__/lockfile.test.d.ts +0 -2
- package/dist/__tests__/lockfile.test.d.ts.map +0 -1
- package/dist/__tests__/lockfile.test.js +0 -144
- package/dist/__tests__/lockfile.test.js.map +0 -1
- package/dist/__tests__/permissions.test.d.ts +0 -2
- package/dist/__tests__/permissions.test.d.ts.map +0 -1
- package/dist/__tests__/permissions.test.js +0 -151
- package/dist/__tests__/permissions.test.js.map +0 -1
- package/dist/__tests__/platform.test.d.ts +0 -2
- package/dist/__tests__/platform.test.d.ts.map +0 -1
- package/dist/__tests__/platform.test.js +0 -329
- package/dist/__tests__/platform.test.js.map +0 -1
- package/dist/__tests__/providers.test.d.ts +0 -2
- package/dist/__tests__/providers.test.d.ts.map +0 -1
- package/dist/__tests__/providers.test.js +0 -925
- package/dist/__tests__/providers.test.js.map +0 -1
- package/dist/__tests__/run.test.d.ts +0 -2
- package/dist/__tests__/run.test.d.ts.map +0 -1
- package/dist/__tests__/run.test.js +0 -529
- package/dist/__tests__/run.test.js.map +0 -1
- package/dist/__tests__/schedule.test.d.ts +0 -2
- package/dist/__tests__/schedule.test.d.ts.map +0 -1
- package/dist/__tests__/schedule.test.js +0 -258
- package/dist/__tests__/schedule.test.js.map +0 -1
package/dist/commands/run.js
CHANGED
|
@@ -39,12 +39,18 @@ exports.getOpenStatus = getOpenStatus;
|
|
|
39
39
|
exports.getInReviewStatus = getInReviewStatus;
|
|
40
40
|
exports.getRunSkipReason = getRunSkipReason;
|
|
41
41
|
exports.sortTasksByPriority = sortTasksByPriority;
|
|
42
|
+
exports.writeTaskPlan = writeTaskPlan;
|
|
43
|
+
exports.readTaskPlan = readTaskPlan;
|
|
44
|
+
exports.formatSubtaskId = formatSubtaskId;
|
|
45
|
+
exports.subtaskDepth = subtaskDepth;
|
|
46
|
+
exports.formatSubtaskList = formatSubtaskList;
|
|
42
47
|
exports.runCommand = runCommand;
|
|
43
48
|
exports.hasHumanReply = hasHumanReply;
|
|
44
49
|
exports.hasTriggerWord = hasTriggerWord;
|
|
45
50
|
exports.checkNeedsClarification = checkNeedsClarification;
|
|
46
51
|
exports.buildConflictResolutionPrompt = buildConflictResolutionPrompt;
|
|
47
52
|
exports.buildImplementPrompt = buildImplementPrompt;
|
|
53
|
+
exports.splitFailedSubtask = splitFailedSubtask;
|
|
48
54
|
exports.buildPRBody = buildPRBody;
|
|
49
55
|
exports.tryCreatePR = tryCreatePR;
|
|
50
56
|
exports.buildPRUrl = buildPRUrl;
|
|
@@ -53,6 +59,9 @@ exports.buildNonCodePrompt = buildNonCodePrompt;
|
|
|
53
59
|
exports.buildNonCodeCompletionComment = buildNonCodeCompletionComment;
|
|
54
60
|
exports.hasAidevComment = hasAidevComment;
|
|
55
61
|
exports.filterAutomatedComments = filterAutomatedComments;
|
|
62
|
+
exports.parseReplyDirectives = parseReplyDirectives;
|
|
63
|
+
exports.buildReviewPrompt = buildReviewPrompt;
|
|
64
|
+
exports.buildReviewCompletionComment = buildReviewCompletionComment;
|
|
56
65
|
const fs = __importStar(require("node:fs"));
|
|
57
66
|
const path = __importStar(require("node:path"));
|
|
58
67
|
const logger_1 = require("../logger");
|
|
@@ -62,6 +71,7 @@ const github_1 = require("../github");
|
|
|
62
71
|
const diagnostics_1 = require("../diagnostics");
|
|
63
72
|
const lockfile_1 = require("../lockfile");
|
|
64
73
|
const hooks_1 = require("../hooks");
|
|
74
|
+
const sessions_1 = require("../sessions");
|
|
65
75
|
const SKIP_STATUSES = new Set(['closed', 'done', 'cancelled', 'complete']);
|
|
66
76
|
const NO_PRIORITY = Number.MAX_SAFE_INTEGER;
|
|
67
77
|
const SLEEPING_MARKER = 'machine appears to be asleep';
|
|
@@ -146,17 +156,43 @@ function cleanupThinkingFiles(taskId) {
|
|
|
146
156
|
function writeTaskPlan(plan) {
|
|
147
157
|
fs.writeFileSync(taskPlanPath(plan.taskId), JSON.stringify(plan, null, 2), 'utf8');
|
|
148
158
|
}
|
|
159
|
+
function truncateError(msg) {
|
|
160
|
+
const MAX_LEN = 4096;
|
|
161
|
+
if (msg.length <= MAX_LEN)
|
|
162
|
+
return msg;
|
|
163
|
+
return msg.slice(0, MAX_LEN) + '\n... (truncated)';
|
|
164
|
+
}
|
|
149
165
|
function readTaskPlan(taskId) {
|
|
150
166
|
const p = taskPlanPath(taskId);
|
|
151
167
|
if (!fs.existsSync(p))
|
|
152
168
|
return null;
|
|
153
169
|
try {
|
|
154
|
-
|
|
170
|
+
const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
171
|
+
// Backward compat: older plans may be missing `attempts` / `lastError`.
|
|
172
|
+
raw.subtasks = (raw.subtasks || []).map((s) => ({
|
|
173
|
+
id: s.id,
|
|
174
|
+
title: s.title,
|
|
175
|
+
description: s.description,
|
|
176
|
+
status: s.status,
|
|
177
|
+
attempts: typeof s.attempts === 'number' ? s.attempts : 0,
|
|
178
|
+
lastError: s.lastError,
|
|
179
|
+
}));
|
|
180
|
+
return raw;
|
|
155
181
|
}
|
|
156
182
|
catch {
|
|
157
183
|
return null;
|
|
158
184
|
}
|
|
159
185
|
}
|
|
186
|
+
function formatSubtaskId(id) {
|
|
187
|
+
// Decimal string IDs like "3.1" already contain a dot; plain numeric IDs get
|
|
188
|
+
// a trailing dot so the list reads "1. Title", "2. Title", "3.1 Title".
|
|
189
|
+
return typeof id === 'string' ? id : `${id}.`;
|
|
190
|
+
}
|
|
191
|
+
function subtaskDepth(id) {
|
|
192
|
+
if (typeof id === 'number')
|
|
193
|
+
return 0;
|
|
194
|
+
return (id.match(/\./g) || []).length;
|
|
195
|
+
}
|
|
160
196
|
function formatSubtaskList(plan) {
|
|
161
197
|
const icons = {
|
|
162
198
|
pending: '⬜',
|
|
@@ -165,7 +201,7 @@ function formatSubtaskList(plan) {
|
|
|
165
201
|
failed: '❌',
|
|
166
202
|
};
|
|
167
203
|
return plan.subtasks
|
|
168
|
-
.map((s) => `${icons[s.status]} **${s.id}
|
|
204
|
+
.map((s) => `${icons[s.status]} **${formatSubtaskId(s.id)}** ${s.title} — _${s.status}_`)
|
|
169
205
|
.join('\n');
|
|
170
206
|
}
|
|
171
207
|
async function runCommand(filter, config, provider, runners, nonCodeProvider, hooks = {}, vm) {
|
|
@@ -210,6 +246,40 @@ async function runCommand(filter, config, provider, runners, nonCodeProvider, ho
|
|
|
210
246
|
skipped++;
|
|
211
247
|
}
|
|
212
248
|
}
|
|
249
|
+
// Review task phase: check tasks in review status for unresolved code review comments
|
|
250
|
+
if ((0, github_1.isGitHubRemote)(config.gitRemote) && (0, github_1.isGhInstalled)() && (0, github_1.isGhAuthenticated)() && config.githubRepo) {
|
|
251
|
+
const reviewStatus = getInReviewStatus(config);
|
|
252
|
+
logger_1.logger.info(`Fetching tasks in "${reviewStatus}" status for code review checks...`);
|
|
253
|
+
try {
|
|
254
|
+
const reviewTasks = await provider.fetchTasksByStatus([reviewStatus]);
|
|
255
|
+
const mainTag = (config.clickupTag || '').toLowerCase();
|
|
256
|
+
const reviewCandidates = mainTag
|
|
257
|
+
? reviewTasks.filter((t) => t.tags.some((tag) => tag.toLowerCase() === mainTag))
|
|
258
|
+
: reviewTasks;
|
|
259
|
+
if (reviewCandidates.length > 0) {
|
|
260
|
+
logger_1.logger.info(`Found ${reviewCandidates.length} task(s) in review — checking for unresolved code reviews`);
|
|
261
|
+
for (const task of reviewCandidates) {
|
|
262
|
+
if (!screenAvailable) {
|
|
263
|
+
logger_1.logger.info(`[${task.id}] Skipping review check — screen not available`);
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
const result = await processReviewTask(task, config, provider, runners, screenAvailable, hooks, vm);
|
|
267
|
+
if (result === 'processed')
|
|
268
|
+
processed++;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
logger_1.logger.warn(`Failed to fetch review tasks: ${err instanceof Error ? err.message : err}`);
|
|
274
|
+
}
|
|
275
|
+
// Checkout back to base branch after processing review tasks
|
|
276
|
+
git.fetchAndCheckout(config.gitRemote, config.githubBaseBranch);
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
if (!(0, github_1.isGhInstalled)() || !(0, github_1.isGhAuthenticated)()) {
|
|
280
|
+
logger_1.logger.debug('gh CLI not available — skipping review task checks');
|
|
281
|
+
}
|
|
282
|
+
}
|
|
213
283
|
// afterRun hook
|
|
214
284
|
if (vm) {
|
|
215
285
|
const afterCtx = {
|
|
@@ -565,10 +635,7 @@ async function implementTask(task, branchName, branchExists, config, provider, r
|
|
|
565
635
|
let context = '';
|
|
566
636
|
try {
|
|
567
637
|
const comments = await provider.getComments(task.id);
|
|
568
|
-
|
|
569
|
-
if (humanComments.length > 0) {
|
|
570
|
-
context = '\n\nConversation context:\n' + humanComments.map((c) => `${c.author}: ${c.text}`).join('\n');
|
|
571
|
-
}
|
|
638
|
+
context = await buildConversationContext(task.id, comments, config, runners);
|
|
572
639
|
}
|
|
573
640
|
catch {
|
|
574
641
|
// ignore
|
|
@@ -728,6 +795,7 @@ Keep sub-tasks focused: 2-6 sub-tasks is ideal. Order them by dependency (founda
|
|
|
728
795
|
title: s.title,
|
|
729
796
|
description: s.description,
|
|
730
797
|
status: 'pending',
|
|
798
|
+
attempts: 0,
|
|
731
799
|
})),
|
|
732
800
|
};
|
|
733
801
|
fs.writeFileSync(taskInstructionsPath(task.id), parsed.instructions || `# Implementation Plan: ${task.name}\n\nSee ${task.id}.aidev.task.json for sub-tasks.`, 'utf8');
|
|
@@ -739,7 +807,87 @@ Keep sub-tasks focused: 2-6 sub-tasks is ideal. Order them by dependency (founda
|
|
|
739
807
|
return null;
|
|
740
808
|
}
|
|
741
809
|
}
|
|
742
|
-
async function
|
|
810
|
+
async function splitFailedSubtask(parentTask, plan, failedSubtask, runners) {
|
|
811
|
+
const runner = runners.find((r) => r.isAvailable());
|
|
812
|
+
if (!runner) {
|
|
813
|
+
logger_1.logger.error('No AI runner available for sub-task split');
|
|
814
|
+
return null;
|
|
815
|
+
}
|
|
816
|
+
const siblings = plan.subtasks
|
|
817
|
+
.filter((s) => s.id !== failedSubtask.id)
|
|
818
|
+
.map((s) => ` - [${s.status}] ${formatSubtaskId(s.id)} ${s.title}`)
|
|
819
|
+
.join('\n');
|
|
820
|
+
const diagnostics = failedSubtask.lastError && failedSubtask.lastError !== '__git__'
|
|
821
|
+
? failedSubtask.lastError
|
|
822
|
+
: '(no diagnostics captured)';
|
|
823
|
+
const splitPrompt = `You are a senior software architect helping recover a stalled implementation step by splitting it into exactly two smaller, sequential sub-tasks.
|
|
824
|
+
|
|
825
|
+
Overall task: ${parentTask.name}
|
|
826
|
+
${parentTask.description ? `\nTask description:\n${parentTask.description}\n` : ''}
|
|
827
|
+
## Surrounding plan
|
|
828
|
+
${siblings || '(no sibling sub-tasks)'}
|
|
829
|
+
|
|
830
|
+
## Failed sub-task
|
|
831
|
+
ID: ${formatSubtaskId(failedSubtask.id)}
|
|
832
|
+
Title: ${failedSubtask.title}
|
|
833
|
+
Description: ${failedSubtask.description}
|
|
834
|
+
|
|
835
|
+
## Previous failure diagnostics
|
|
836
|
+
${diagnostics}
|
|
837
|
+
|
|
838
|
+
Split the failed sub-task above into exactly two smaller, independently implementable sub-tasks that together achieve the original goal. Each new sub-task should be a coherent unit of work that can be committed separately, ordered by dependency (foundation first). Take the diagnostics into account so the split actually addresses what broke.
|
|
839
|
+
|
|
840
|
+
Respond with valid JSON only — no markdown fences, no extra text:
|
|
841
|
+
{
|
|
842
|
+
"subtasks": [
|
|
843
|
+
{ "title": "Short title for the first new sub-task", "description": "Detailed description of what to implement in this step" },
|
|
844
|
+
{ "title": "Short title for the second new sub-task", "description": "Detailed description of what to implement in this step" }
|
|
845
|
+
]
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
Exactly two entries — no more, no fewer.`;
|
|
849
|
+
logger_1.logger.info(`Splitting failed sub-task ${formatSubtaskId(failedSubtask.id)} into two smaller steps...`);
|
|
850
|
+
const result = await runner.run(splitPrompt);
|
|
851
|
+
if (!result.success) {
|
|
852
|
+
logger_1.logger.error('Sub-task split failed');
|
|
853
|
+
return null;
|
|
854
|
+
}
|
|
855
|
+
try {
|
|
856
|
+
const jsonMatch = result.output.match(/\{[\s\S]*\}/);
|
|
857
|
+
if (!jsonMatch) {
|
|
858
|
+
logger_1.logger.error('Could not parse split response — no JSON found');
|
|
859
|
+
return null;
|
|
860
|
+
}
|
|
861
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
862
|
+
if (!parsed.subtasks || parsed.subtasks.length !== 2) {
|
|
863
|
+
logger_1.logger.error(`Split response must contain exactly 2 sub-tasks (got ${parsed.subtasks?.length ?? 0})`);
|
|
864
|
+
return null;
|
|
865
|
+
}
|
|
866
|
+
const newSubtasks = [];
|
|
867
|
+
for (let i = 0; i < 2; i++) {
|
|
868
|
+
const entry = parsed.subtasks[i];
|
|
869
|
+
const title = (entry?.title || '').trim();
|
|
870
|
+
const description = (entry?.description || '').trim();
|
|
871
|
+
if (!title || !description) {
|
|
872
|
+
logger_1.logger.error(`Split response sub-task ${i + 1} has empty title or description`);
|
|
873
|
+
return null;
|
|
874
|
+
}
|
|
875
|
+
newSubtasks.push({
|
|
876
|
+
id: `${failedSubtask.id}.${i + 1}`,
|
|
877
|
+
title,
|
|
878
|
+
description,
|
|
879
|
+
status: 'pending',
|
|
880
|
+
attempts: 0,
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
return newSubtasks;
|
|
884
|
+
}
|
|
885
|
+
catch (err) {
|
|
886
|
+
logger_1.logger.error(`Failed to parse split response: ${err}`);
|
|
887
|
+
return null;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
async function executeSubTask(subtask, task, plan, config, runners, reviewContext, previousError) {
|
|
743
891
|
const instructionsPath = taskInstructionsPath(task.id);
|
|
744
892
|
const instructions = fs.existsSync(instructionsPath)
|
|
745
893
|
? fs.readFileSync(instructionsPath, 'utf8')
|
|
@@ -748,6 +896,9 @@ async function executeSubTask(subtask, task, plan, config, runners, reviewContex
|
|
|
748
896
|
.filter((s) => s.status === 'done')
|
|
749
897
|
.map((s) => ` - [done] ${s.id}. ${s.title}`)
|
|
750
898
|
.join('\n');
|
|
899
|
+
const retrySection = previousError && previousError !== '__git__'
|
|
900
|
+
? `\n## Previous attempt failure diagnostics\nThis step failed on a previous attempt. Diagnostics below — please take them into account and avoid repeating the same failure.\n\n${previousError}\n`
|
|
901
|
+
: '';
|
|
751
902
|
const prompt = `You are implementing step ${subtask.id} of a multi-step task.
|
|
752
903
|
|
|
753
904
|
Overall task: ${task.name}
|
|
@@ -755,7 +906,7 @@ ${task.description ? `\nTask description:\n${task.description}` : ''}
|
|
|
755
906
|
|
|
756
907
|
## Full implementation instructions
|
|
757
908
|
${instructions}
|
|
758
|
-
${reviewContext || ''}
|
|
909
|
+
${reviewContext || ''}${retrySection}
|
|
759
910
|
## Progress
|
|
760
911
|
${completedSteps || '(no steps completed yet)'}
|
|
761
912
|
|
|
@@ -813,10 +964,7 @@ async function implementThinkingTask(task, branchName, branchExists, config, pro
|
|
|
813
964
|
let context = '';
|
|
814
965
|
try {
|
|
815
966
|
const comments = await provider.getComments(task.id);
|
|
816
|
-
|
|
817
|
-
if (humanComments.length > 0) {
|
|
818
|
-
context = '\n\nConversation context:\n' + humanComments.map((c) => `${c.author}: ${c.text}`).join('\n');
|
|
819
|
-
}
|
|
967
|
+
context = await buildConversationContext(task.id, comments, config, runners);
|
|
820
968
|
}
|
|
821
969
|
catch { /* ignore */ }
|
|
822
970
|
if (branchExists) {
|
|
@@ -882,26 +1030,68 @@ async function implementThinkingTask(task, branchName, branchExists, config, pro
|
|
|
882
1030
|
title: s.title,
|
|
883
1031
|
description: s.description,
|
|
884
1032
|
status: (prev?.status ?? s.status),
|
|
1033
|
+
attempts: prev?.attempts ?? 0,
|
|
1034
|
+
lastError: prev?.lastError,
|
|
885
1035
|
};
|
|
886
1036
|
});
|
|
887
1037
|
writeTaskPlan(plan);
|
|
888
1038
|
}
|
|
889
1039
|
let allSucceeded = true;
|
|
890
|
-
for (
|
|
1040
|
+
for (let i = 0; i < plan.subtasks.length; i++) {
|
|
1041
|
+
const subtask = plan.subtasks[i];
|
|
891
1042
|
if (subtask.status === 'done') {
|
|
892
1043
|
logger_1.logger.info(` Step ${subtask.id} already done — skipping`);
|
|
893
1044
|
continue;
|
|
894
1045
|
}
|
|
1046
|
+
// Failure recovery on resume: a sub-task that ended in 'failed' on a prior run
|
|
1047
|
+
// gets a chance to be split into two smaller steps before we retry it.
|
|
1048
|
+
if (subtask.status === 'failed') {
|
|
1049
|
+
const isGitFailure = subtask.lastError === '__git__';
|
|
1050
|
+
const depth = subtaskDepth(subtask.id);
|
|
1051
|
+
const attempts = subtask.attempts ?? 0;
|
|
1052
|
+
const shouldSplit = !isGitFailure && attempts >= 2 && depth < 2;
|
|
1053
|
+
if (shouldSplit) {
|
|
1054
|
+
const failedId = subtask.id;
|
|
1055
|
+
const newSubtasks = await splitFailedSubtask(task, plan, subtask, runners);
|
|
1056
|
+
if (newSubtasks) {
|
|
1057
|
+
plan.subtasks.splice(i, 1, ...newSubtasks);
|
|
1058
|
+
writeTaskPlan(plan);
|
|
1059
|
+
const newIds = newSubtasks.map((s) => s.id).join(', ');
|
|
1060
|
+
logger_1.logger.info(` Step ${failedId} was split into ${newIds}`);
|
|
1061
|
+
try {
|
|
1062
|
+
await provider.postComment(task.id, `${config.commentPrefix} Step ${failedId} was split into ${newIds}:\n\n${formatSubtaskList(plan)}`);
|
|
1063
|
+
}
|
|
1064
|
+
catch { /* ignore */ }
|
|
1065
|
+
i--; // re-process this index — it now points at the first new sub-task
|
|
1066
|
+
continue;
|
|
1067
|
+
}
|
|
1068
|
+
logger_1.logger.warn(` Could not split failed step ${failedId} — falling back to plain retry`);
|
|
1069
|
+
try {
|
|
1070
|
+
await provider.postComment(task.id, `${config.commentPrefix} Failed to automatically split step ${failedId}. Retrying as-is.`);
|
|
1071
|
+
}
|
|
1072
|
+
catch { /* ignore */ }
|
|
1073
|
+
}
|
|
1074
|
+
else if (!isGitFailure && attempts >= 2 && depth >= 2) {
|
|
1075
|
+
logger_1.logger.warn(` Step ${subtask.id} has reached the split-depth cap — manual intervention may be needed`);
|
|
1076
|
+
try {
|
|
1077
|
+
await provider.postComment(task.id, `${config.commentPrefix} Step ${subtask.id} has already been split twice and is still failing. Retrying as-is — please consider manual intervention.`);
|
|
1078
|
+
}
|
|
1079
|
+
catch { /* ignore */ }
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
const previousError = subtask.lastError;
|
|
895
1083
|
subtask.status = 'running';
|
|
1084
|
+
subtask.attempts = (subtask.attempts ?? 0) + 1;
|
|
896
1085
|
writeTaskPlan(plan);
|
|
897
|
-
logger_1.logger.info(` Starting step ${subtask.id}: ${subtask.title}`);
|
|
898
|
-
const success = await executeSubTask(subtask, task, plan, config, runners, reviewContext);
|
|
1086
|
+
logger_1.logger.info(` Starting step ${subtask.id}: ${subtask.title} (attempt ${subtask.attempts})`);
|
|
1087
|
+
const success = await executeSubTask(subtask, task, plan, config, runners, reviewContext, previousError);
|
|
899
1088
|
if (!success) {
|
|
1089
|
+
const diagnostics = (0, diagnostics_1.collectAndLogDiagnostics)();
|
|
900
1090
|
subtask.status = 'failed';
|
|
1091
|
+
subtask.lastError = truncateError(diagnostics);
|
|
901
1092
|
writeTaskPlan(plan);
|
|
902
1093
|
allSucceeded = false;
|
|
903
1094
|
logger_1.logger.error(` Step ${subtask.id} failed: ${subtask.title}`);
|
|
904
|
-
const diagnostics = (0, diagnostics_1.collectAndLogDiagnostics)();
|
|
905
1095
|
try {
|
|
906
1096
|
await provider.postComment(task.id, `${config.commentPrefix} Step ${subtask.id} failed: ${subtask.title}\n\n${formatSubtaskList(plan)}\n\n${diagnostics}`);
|
|
907
1097
|
}
|
|
@@ -910,6 +1100,7 @@ async function implementThinkingTask(task, branchName, branchExists, config, pro
|
|
|
910
1100
|
}
|
|
911
1101
|
if (!git.addAll() || !git.commit(`${config.commentPrefix} Step ${subtask.id}: ${subtask.title}\n\nTask: ${task.url}`, branchName)) {
|
|
912
1102
|
subtask.status = 'failed';
|
|
1103
|
+
subtask.lastError = '__git__';
|
|
913
1104
|
writeTaskPlan(plan);
|
|
914
1105
|
allSucceeded = false;
|
|
915
1106
|
logger_1.logger.error(` Failed to commit step ${subtask.id}`);
|
|
@@ -917,6 +1108,7 @@ async function implementThinkingTask(task, branchName, branchExists, config, pro
|
|
|
917
1108
|
}
|
|
918
1109
|
if (!git.push(config.gitRemote, branchName)) {
|
|
919
1110
|
subtask.status = 'failed';
|
|
1111
|
+
subtask.lastError = '__git__';
|
|
920
1112
|
writeTaskPlan(plan);
|
|
921
1113
|
allSucceeded = false;
|
|
922
1114
|
logger_1.logger.error(` Failed to push step ${subtask.id}`);
|
|
@@ -1057,6 +1249,21 @@ function hasAidevComment(comments, commentPrefix = '[aidev]') {
|
|
|
1057
1249
|
function filterAutomatedComments(comments, commentPrefix = '[aidev]') {
|
|
1058
1250
|
return comments.filter((c) => !c.text.includes(commentPrefix));
|
|
1059
1251
|
}
|
|
1252
|
+
async function buildConversationContext(taskId, comments, config, runners) {
|
|
1253
|
+
const humanComments = filterAutomatedComments(comments, config.commentPrefix);
|
|
1254
|
+
if (humanComments.length === 0)
|
|
1255
|
+
return '';
|
|
1256
|
+
const rawLength = '\n\nConversation context:\n'.length +
|
|
1257
|
+
humanComments.reduce((n, c, i) => n + c.author.length + 2 + c.text.length + (i > 0 ? 1 : 0), 0);
|
|
1258
|
+
const context = await (0, sessions_1.buildCompressedContext)(humanComments, taskId, runners, config);
|
|
1259
|
+
if (config.autoCompress &&
|
|
1260
|
+
humanComments.length > 1 &&
|
|
1261
|
+
rawLength > config.compressThreshold &&
|
|
1262
|
+
context.startsWith('\n\nSummary of earlier conversation')) {
|
|
1263
|
+
logger_1.logger.info(`[${taskId}] Auto-compressed conversation context: ${rawLength} → ${context.length} chars`);
|
|
1264
|
+
}
|
|
1265
|
+
return context;
|
|
1266
|
+
}
|
|
1060
1267
|
async function processNonCodeTask(task, filter, config, provider, runners, screenAvailable, hooks = {}, vm) {
|
|
1061
1268
|
const pendingStatus = getPendingStatus(config);
|
|
1062
1269
|
const openStatus = getOpenStatus(config);
|
|
@@ -1125,10 +1332,7 @@ async function implementNonCodeTask(task, config, provider, runners, hooks = {},
|
|
|
1125
1332
|
let context = '';
|
|
1126
1333
|
try {
|
|
1127
1334
|
const comments = await provider.getComments(task.id);
|
|
1128
|
-
|
|
1129
|
-
if (humanComments.length > 0) {
|
|
1130
|
-
context = '\n\nConversation context:\n' + humanComments.map((c) => `${c.author}: ${c.text}`).join('\n');
|
|
1131
|
-
}
|
|
1335
|
+
context = await buildConversationContext(task.id, comments, config, runners);
|
|
1132
1336
|
}
|
|
1133
1337
|
catch {
|
|
1134
1338
|
// ignore
|
|
@@ -1181,4 +1385,188 @@ async function implementNonCodeTask(task, config, provider, runners, hooks = {},
|
|
|
1181
1385
|
}
|
|
1182
1386
|
logger_1.logger.success(`Non-code task complete: ${task.name}`);
|
|
1183
1387
|
}
|
|
1388
|
+
// ─── Review task processing ─────────────────────────────────────────────────
|
|
1389
|
+
const REPLY_REGEX = /<!-- AIDEV-REPLY ([\w=+/]+) -->([\s\S]*?)<!-- \/AIDEV-REPLY -->/g;
|
|
1390
|
+
function parseReplyDirectives(output) {
|
|
1391
|
+
const replies = [];
|
|
1392
|
+
let match;
|
|
1393
|
+
while ((match = REPLY_REGEX.exec(output)) !== null) {
|
|
1394
|
+
replies.push({ threadId: match[1], body: match[2].trim() });
|
|
1395
|
+
}
|
|
1396
|
+
return replies;
|
|
1397
|
+
}
|
|
1398
|
+
async function processReviewTask(task, config, provider, runners, screenAvailable, hooks = {}, vm) {
|
|
1399
|
+
const branchName = `${task.id}/${git.slugify(task.name)}`;
|
|
1400
|
+
logger_1.logger.task(`[${task.id}] "${task.name}" [review] — checking code reviews`);
|
|
1401
|
+
if (!config.githubRepo) {
|
|
1402
|
+
logger_1.logger.debug(`[${task.id}] No githubRepo configured — skipping review check`);
|
|
1403
|
+
return 'skipped';
|
|
1404
|
+
}
|
|
1405
|
+
const prNumber = (0, github_1.getPrNumberForBranch)(branchName);
|
|
1406
|
+
if (!prNumber) {
|
|
1407
|
+
logger_1.logger.debug(`[${task.id}] No PR found for branch ${branchName} — skipping`);
|
|
1408
|
+
return 'skipped';
|
|
1409
|
+
}
|
|
1410
|
+
const parts = config.githubRepo.split('/');
|
|
1411
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
1412
|
+
logger_1.logger.debug(`[${task.id}] Invalid githubRepo format — skipping`);
|
|
1413
|
+
return 'skipped';
|
|
1414
|
+
}
|
|
1415
|
+
const allThreads = (0, github_1.fetchUnresolvedReviewThreads)(parts[0], parts[1], prNumber);
|
|
1416
|
+
if (allThreads.length === 0) {
|
|
1417
|
+
logger_1.logger.debug(`[${task.id}] No unresolved review threads — skipping`);
|
|
1418
|
+
return 'skipped';
|
|
1419
|
+
}
|
|
1420
|
+
const actionableThreads = (0, github_1.filterUnresolvedByNonAidev)(allThreads, config.commentPrefix);
|
|
1421
|
+
if (actionableThreads.length === 0) {
|
|
1422
|
+
logger_1.logger.debug(`[${task.id}] All unresolved threads are from aidev — skipping`);
|
|
1423
|
+
return 'skipped';
|
|
1424
|
+
}
|
|
1425
|
+
if (!screenAvailable) {
|
|
1426
|
+
await notifySleeping(task, provider, config.commentPrefix);
|
|
1427
|
+
return 'skipped';
|
|
1428
|
+
}
|
|
1429
|
+
logger_1.logger.info(`[${task.id}] Found ${actionableThreads.length} actionable review thread(s) — resolving`);
|
|
1430
|
+
await implementReviewTask(task, branchName, config, provider, runners, actionableThreads, hooks, vm);
|
|
1431
|
+
return 'processed';
|
|
1432
|
+
}
|
|
1433
|
+
async function implementReviewTask(task, branchName, config, provider, runners, threads, hooks = {}, vm) {
|
|
1434
|
+
logger_1.logger.info(`Resolving review comments for: ${task.name}`);
|
|
1435
|
+
if (!git.fetchAndCheckoutBranch(config.gitRemote, branchName)) {
|
|
1436
|
+
logger_1.logger.error(`[${task.id}] Failed to checkout branch ${branchName}`);
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
let reviewPrompt = buildReviewPrompt(task, threads);
|
|
1440
|
+
// beforeReviewTask hook
|
|
1441
|
+
if (vm) {
|
|
1442
|
+
const reviewCtx = { task, config, branchName, threads, prompt: reviewPrompt };
|
|
1443
|
+
const modified = await (0, hooks_1.executeHook)(hooks, 'beforeReviewTask', reviewCtx, vm);
|
|
1444
|
+
reviewPrompt = modified.prompt;
|
|
1445
|
+
}
|
|
1446
|
+
let success = false;
|
|
1447
|
+
let agentOutput = '';
|
|
1448
|
+
let previousNotes = '';
|
|
1449
|
+
for (const runner of runners) {
|
|
1450
|
+
if (!runner.isAvailable())
|
|
1451
|
+
continue;
|
|
1452
|
+
logger_1.logger.info(`Running ${runner.name} to address review comments...`);
|
|
1453
|
+
const result = await runner.run(reviewPrompt, previousNotes || undefined);
|
|
1454
|
+
if (result.success) {
|
|
1455
|
+
success = true;
|
|
1456
|
+
agentOutput = result.output;
|
|
1457
|
+
break;
|
|
1458
|
+
}
|
|
1459
|
+
logger_1.logger.warn(`${runner.name} failed — trying next runner`);
|
|
1460
|
+
previousNotes = `Previous runner (${runner.name}) output:\n${result.output}\nErrors:\n${result.error}`;
|
|
1461
|
+
}
|
|
1462
|
+
if (!success) {
|
|
1463
|
+
logger_1.logger.error(`[${task.id}] All AI runners failed to address review comments`);
|
|
1464
|
+
const diagnostics = (0, diagnostics_1.collectAndLogDiagnostics)();
|
|
1465
|
+
try {
|
|
1466
|
+
await provider.postComment(task.id, `${config.commentPrefix} Failed to address code review comments automatically.\n\n${diagnostics}`);
|
|
1467
|
+
}
|
|
1468
|
+
catch { /* ignore */ }
|
|
1469
|
+
// afterReviewTask hook — failure
|
|
1470
|
+
if (vm) {
|
|
1471
|
+
const afterCtx = {
|
|
1472
|
+
task, config, branchName, threads, prompt: reviewPrompt, success: false, resolvedCount: 0,
|
|
1473
|
+
};
|
|
1474
|
+
await (0, hooks_1.executeHook)(hooks, 'afterReviewTask', afterCtx, vm);
|
|
1475
|
+
}
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
let resolvedCount = 0;
|
|
1479
|
+
let repliedCount = 0;
|
|
1480
|
+
// Handle code fixes: commit and push if agent made changes
|
|
1481
|
+
if (git.hasChanges()) {
|
|
1482
|
+
if (git.addAll() && git.commit(`${config.commentPrefix} Address code review comments\n\nTask: ${task.url}`, branchName)) {
|
|
1483
|
+
if (git.push(config.gitRemote, branchName)) {
|
|
1484
|
+
// Resolve threads that were addressed via code changes
|
|
1485
|
+
resolveHandledThreads(threads);
|
|
1486
|
+
resolvedCount = threads.length;
|
|
1487
|
+
logger_1.logger.info(`Pushed code fixes and resolved ${resolvedCount} thread(s)`);
|
|
1488
|
+
}
|
|
1489
|
+
else {
|
|
1490
|
+
logger_1.logger.error(`[${task.id}] Failed to push review fixes`);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
else {
|
|
1494
|
+
logger_1.logger.error(`[${task.id}] Failed to commit review fixes`);
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
// Handle reply directives from agent output
|
|
1498
|
+
const replies = parseReplyDirectives(agentOutput);
|
|
1499
|
+
for (const reply of replies) {
|
|
1500
|
+
const prefixedBody = `${config.commentPrefix} ${reply.body}`;
|
|
1501
|
+
if ((0, github_1.replyToReviewThread)(reply.threadId, prefixedBody)) {
|
|
1502
|
+
repliedCount++;
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
if (repliedCount > 0) {
|
|
1506
|
+
logger_1.logger.info(`Posted ${repliedCount} reply(ies) to review threads`);
|
|
1507
|
+
}
|
|
1508
|
+
// Post completion comment on task provider
|
|
1509
|
+
if (resolvedCount > 0 || repliedCount > 0) {
|
|
1510
|
+
try {
|
|
1511
|
+
const comment = buildReviewCompletionComment(config, resolvedCount, repliedCount);
|
|
1512
|
+
await provider.postComment(task.id, comment);
|
|
1513
|
+
}
|
|
1514
|
+
catch { /* ignore */ }
|
|
1515
|
+
}
|
|
1516
|
+
// afterReviewTask hook
|
|
1517
|
+
if (vm) {
|
|
1518
|
+
const afterCtx = {
|
|
1519
|
+
task, config, branchName, threads, prompt: reviewPrompt, success: true, resolvedCount,
|
|
1520
|
+
};
|
|
1521
|
+
await (0, hooks_1.executeHook)(hooks, 'afterReviewTask', afterCtx, vm);
|
|
1522
|
+
}
|
|
1523
|
+
logger_1.logger.success(`Review comments addressed for: ${task.name}`);
|
|
1524
|
+
}
|
|
1525
|
+
function buildReviewPrompt(task, threads) {
|
|
1526
|
+
let prompt = `You are addressing code review comments on a pull request for a software development task.
|
|
1527
|
+
|
|
1528
|
+
Task: ${task.name}
|
|
1529
|
+
|
|
1530
|
+
Description:
|
|
1531
|
+
${task.description || '(no description provided)'}
|
|
1532
|
+
|
|
1533
|
+
## Unresolved Code Review Threads
|
|
1534
|
+
|
|
1535
|
+
The following review threads need to be addressed. For each thread, either:
|
|
1536
|
+
- Fix the code as requested (make the changes directly in the files)
|
|
1537
|
+
- Or, if it's a discussion/question that doesn't require code changes, output a REPLY block:
|
|
1538
|
+
<!-- AIDEV-REPLY thread_id -->Your reply here<!-- /AIDEV-REPLY -->
|
|
1539
|
+
|
|
1540
|
+
Replace "thread_id" with the actual thread ID shown below.
|
|
1541
|
+
|
|
1542
|
+
`;
|
|
1543
|
+
for (const thread of threads) {
|
|
1544
|
+
const location = thread.line
|
|
1545
|
+
? `\`${thread.path}\` (line ${thread.line})`
|
|
1546
|
+
: `\`${thread.path}\``;
|
|
1547
|
+
prompt += `### Thread ${thread.id} — ${location}\n`;
|
|
1548
|
+
for (const comment of thread.comments) {
|
|
1549
|
+
prompt += `> **${comment.author}**: ${comment.body}\n`;
|
|
1550
|
+
}
|
|
1551
|
+
prompt += '\n';
|
|
1552
|
+
}
|
|
1553
|
+
prompt += `## Instructions
|
|
1554
|
+
|
|
1555
|
+
1. Read each review thread carefully
|
|
1556
|
+
2. For code change requests: make the fix directly in the relevant file(s)
|
|
1557
|
+
3. For questions or discussions: output a REPLY block with a clear, helpful response
|
|
1558
|
+
4. You may handle multiple threads — some with code fixes, others with replies
|
|
1559
|
+
5. Focus on correctness and follow the existing code style`;
|
|
1560
|
+
return prompt;
|
|
1561
|
+
}
|
|
1562
|
+
function buildReviewCompletionComment(config, resolvedCount, repliedCount) {
|
|
1563
|
+
const parts = [`${config.commentPrefix} Code review comments addressed!`];
|
|
1564
|
+
if (resolvedCount > 0) {
|
|
1565
|
+
parts.push(`Resolved ${resolvedCount} thread(s) with code fixes.`);
|
|
1566
|
+
}
|
|
1567
|
+
if (repliedCount > 0) {
|
|
1568
|
+
parts.push(`Replied to ${repliedCount} thread(s).`);
|
|
1569
|
+
}
|
|
1570
|
+
return parts.join('\n');
|
|
1571
|
+
}
|
|
1184
1572
|
//# sourceMappingURL=run.js.map
|