@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.
Files changed (99) hide show
  1. package/.env.aidev.example +8 -0
  2. package/README.md +137 -2
  3. package/dist/cli.js +14 -1
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/accepted.d.ts +7 -2
  6. package/dist/commands/accepted.d.ts.map +1 -1
  7. package/dist/commands/accepted.js +17 -2
  8. package/dist/commands/accepted.js.map +1 -1
  9. package/dist/commands/help.d.ts.map +1 -1
  10. package/dist/commands/help.js +13 -0
  11. package/dist/commands/help.js.map +1 -1
  12. package/dist/commands/run.d.ts +16 -1
  13. package/dist/commands/run.d.ts.map +1 -1
  14. package/dist/commands/run.js +408 -20
  15. package/dist/commands/run.js.map +1 -1
  16. package/dist/commands/tasks.d.ts +1 -0
  17. package/dist/commands/tasks.d.ts.map +1 -1
  18. package/dist/commands/tasks.js +29 -0
  19. package/dist/commands/tasks.js.map +1 -1
  20. package/dist/config.d.ts.map +1 -1
  21. package/dist/config.js +6 -0
  22. package/dist/config.js.map +1 -1
  23. package/dist/github.d.ts +2 -0
  24. package/dist/github.d.ts.map +1 -1
  25. package/dist/github.js +28 -0
  26. package/dist/github.js.map +1 -1
  27. package/dist/hooks.d.ts +16 -1
  28. package/dist/hooks.d.ts.map +1 -1
  29. package/dist/hooks.js +1 -0
  30. package/dist/hooks.js.map +1 -1
  31. package/dist/providers/linear.js +1 -1
  32. package/dist/providers/linear.js.map +1 -1
  33. package/dist/sessions.d.ts +20 -0
  34. package/dist/sessions.d.ts.map +1 -0
  35. package/dist/sessions.js +171 -0
  36. package/dist/sessions.js.map +1 -0
  37. package/dist/types.d.ts +2 -0
  38. package/dist/types.d.ts.map +1 -1
  39. package/package.json +1 -1
  40. package/dist/__tests__/ai-runners.test.d.ts +0 -2
  41. package/dist/__tests__/ai-runners.test.d.ts.map +0 -1
  42. package/dist/__tests__/ai-runners.test.js +0 -367
  43. package/dist/__tests__/ai-runners.test.js.map +0 -1
  44. package/dist/__tests__/clickup-format.test.d.ts +0 -2
  45. package/dist/__tests__/clickup-format.test.d.ts.map +0 -1
  46. package/dist/__tests__/clickup-format.test.js +0 -256
  47. package/dist/__tests__/clickup-format.test.js.map +0 -1
  48. package/dist/__tests__/config.test.d.ts +0 -2
  49. package/dist/__tests__/config.test.d.ts.map +0 -1
  50. package/dist/__tests__/config.test.js +0 -418
  51. package/dist/__tests__/config.test.js.map +0 -1
  52. package/dist/__tests__/git.test.d.ts +0 -2
  53. package/dist/__tests__/git.test.d.ts.map +0 -1
  54. package/dist/__tests__/git.test.js +0 -697
  55. package/dist/__tests__/git.test.js.map +0 -1
  56. package/dist/__tests__/github.test.d.ts +0 -2
  57. package/dist/__tests__/github.test.d.ts.map +0 -1
  58. package/dist/__tests__/github.test.js +0 -209
  59. package/dist/__tests__/github.test.js.map +0 -1
  60. package/dist/__tests__/help.test.d.ts +0 -2
  61. package/dist/__tests__/help.test.d.ts.map +0 -1
  62. package/dist/__tests__/help.test.js +0 -34
  63. package/dist/__tests__/help.test.js.map +0 -1
  64. package/dist/__tests__/hooks.test.d.ts +0 -2
  65. package/dist/__tests__/hooks.test.d.ts.map +0 -1
  66. package/dist/__tests__/hooks.test.js +0 -333
  67. package/dist/__tests__/hooks.test.js.map +0 -1
  68. package/dist/__tests__/init.test.d.ts +0 -2
  69. package/dist/__tests__/init.test.d.ts.map +0 -1
  70. package/dist/__tests__/init.test.js +0 -596
  71. package/dist/__tests__/init.test.js.map +0 -1
  72. package/dist/__tests__/local-provider.test.d.ts +0 -2
  73. package/dist/__tests__/local-provider.test.d.ts.map +0 -1
  74. package/dist/__tests__/local-provider.test.js +0 -631
  75. package/dist/__tests__/local-provider.test.js.map +0 -1
  76. package/dist/__tests__/lockfile.test.d.ts +0 -2
  77. package/dist/__tests__/lockfile.test.d.ts.map +0 -1
  78. package/dist/__tests__/lockfile.test.js +0 -144
  79. package/dist/__tests__/lockfile.test.js.map +0 -1
  80. package/dist/__tests__/permissions.test.d.ts +0 -2
  81. package/dist/__tests__/permissions.test.d.ts.map +0 -1
  82. package/dist/__tests__/permissions.test.js +0 -151
  83. package/dist/__tests__/permissions.test.js.map +0 -1
  84. package/dist/__tests__/platform.test.d.ts +0 -2
  85. package/dist/__tests__/platform.test.d.ts.map +0 -1
  86. package/dist/__tests__/platform.test.js +0 -329
  87. package/dist/__tests__/platform.test.js.map +0 -1
  88. package/dist/__tests__/providers.test.d.ts +0 -2
  89. package/dist/__tests__/providers.test.d.ts.map +0 -1
  90. package/dist/__tests__/providers.test.js +0 -925
  91. package/dist/__tests__/providers.test.js.map +0 -1
  92. package/dist/__tests__/run.test.d.ts +0 -2
  93. package/dist/__tests__/run.test.d.ts.map +0 -1
  94. package/dist/__tests__/run.test.js +0 -529
  95. package/dist/__tests__/run.test.js.map +0 -1
  96. package/dist/__tests__/schedule.test.d.ts +0 -2
  97. package/dist/__tests__/schedule.test.d.ts.map +0 -1
  98. package/dist/__tests__/schedule.test.js +0 -258
  99. package/dist/__tests__/schedule.test.js.map +0 -1
@@ -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
- return JSON.parse(fs.readFileSync(p, 'utf8'));
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}.** ${s.title} — _${s.status}_`)
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
- const humanComments = filterAutomatedComments(comments, config.commentPrefix);
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 executeSubTask(subtask, task, plan, config, runners, reviewContext) {
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
- const humanComments = filterAutomatedComments(comments, config.commentPrefix);
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 (const subtask of plan.subtasks) {
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
- const humanComments = filterAutomatedComments(comments, config.commentPrefix);
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