bosun 0.41.2 → 0.41.4

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 (73) hide show
  1. package/.env.example +1 -1
  2. package/agent/agent-pool.mjs +9 -2
  3. package/agent/agent-prompt-catalog.mjs +971 -0
  4. package/agent/agent-prompts.mjs +2 -970
  5. package/agent/agent-supervisor.mjs +119 -6
  6. package/agent/autofix-git.mjs +33 -0
  7. package/agent/autofix-prompts.mjs +151 -0
  8. package/agent/autofix.mjs +11 -175
  9. package/agent/bosun-skills.mjs +3 -2
  10. package/bosun.config.example.json +17 -0
  11. package/bosun.schema.json +87 -188
  12. package/cli.mjs +34 -1
  13. package/config/config-doctor.mjs +5 -250
  14. package/config/config-file-names.mjs +5 -0
  15. package/config/config.mjs +89 -493
  16. package/config/executor-config.mjs +493 -0
  17. package/config/repo-root.mjs +1 -2
  18. package/config/workspace-health.mjs +242 -0
  19. package/git/git-safety.mjs +15 -0
  20. package/github/github-oauth-portal.mjs +46 -0
  21. package/infra/library-manager-utils.mjs +22 -0
  22. package/infra/library-manager-well-known-sources.mjs +578 -0
  23. package/infra/library-manager.mjs +512 -1030
  24. package/infra/monitor.mjs +35 -9
  25. package/infra/session-tracker.mjs +10 -7
  26. package/kanban/kanban-adapter.mjs +17 -1
  27. package/lib/codebase-audit-manifests.mjs +117 -0
  28. package/lib/codebase-audit.mjs +18 -115
  29. package/package.json +18 -3
  30. package/server/setup-web-server.mjs +58 -5
  31. package/server/ui-server.mjs +1394 -79
  32. package/shell/codex-config-file.mjs +178 -0
  33. package/shell/codex-config.mjs +538 -575
  34. package/task/task-cli.mjs +54 -3
  35. package/task/task-executor.mjs +143 -13
  36. package/task/task-store.mjs +409 -1
  37. package/telegram/telegram-bot.mjs +127 -0
  38. package/tools/apply-pr-suggestions.mjs +401 -0
  39. package/tools/syntax-check.mjs +28 -9
  40. package/ui/app.js +3 -14
  41. package/ui/components/kanban-board.js +227 -4
  42. package/ui/components/session-list.js +85 -5
  43. package/ui/demo-defaults.js +338 -84
  44. package/ui/demo.html +155 -0
  45. package/ui/modules/session-api.js +96 -0
  46. package/ui/modules/settings-schema.js +1 -2
  47. package/ui/modules/state.js +43 -3
  48. package/ui/setup.html +4 -5
  49. package/ui/styles/components.css +58 -4
  50. package/ui/tabs/agents.js +12 -15
  51. package/ui/tabs/control.js +1 -0
  52. package/ui/tabs/library.js +484 -22
  53. package/ui/tabs/manual-flows.js +105 -29
  54. package/ui/tabs/tasks.js +848 -141
  55. package/ui/tabs/telemetry.js +129 -11
  56. package/ui/tabs/workflow-canvas-utils.mjs +130 -0
  57. package/ui/tabs/workflows.js +293 -23
  58. package/voice/voice-tool-definitions.mjs +757 -0
  59. package/voice/voice-tools.mjs +34 -778
  60. package/workflow/manual-flow-audit.mjs +165 -0
  61. package/workflow/manual-flows.mjs +164 -259
  62. package/workflow/workflow-engine.mjs +147 -58
  63. package/workflow/workflow-nodes/definitions.mjs +1207 -0
  64. package/workflow/workflow-nodes/transforms.mjs +612 -0
  65. package/workflow/workflow-nodes.mjs +358 -63
  66. package/workflow/workflow-templates.mjs +313 -191
  67. package/workflow-templates/_helpers.mjs +154 -0
  68. package/workflow-templates/agents.mjs +61 -4
  69. package/workflow-templates/code-quality.mjs +7 -7
  70. package/workflow-templates/github.mjs +20 -10
  71. package/workflow-templates/task-batch.mjs +44 -11
  72. package/workflow-templates/task-lifecycle.mjs +31 -6
  73. package/workspace/worktree-manager.mjs +277 -3
@@ -0,0 +1,165 @@
1
+ import { mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+
4
+ export async function executeAnnotationAudit(formValues, rootDir, context) {
5
+ const {
6
+ targetDir = "",
7
+ fileExtensions = ".mjs, .js, .ts, .tsx, .jsx, .py",
8
+ skipGenerated = true,
9
+ phases = "all",
10
+ dryRun = false,
11
+ } = formValues;
12
+
13
+ const extensions = fileExtensions
14
+ .split(",")
15
+ .map((extension) => extension.trim())
16
+ .filter(Boolean);
17
+
18
+ const scanRoot = targetDir ? resolve(rootDir, targetDir) : rootDir;
19
+ const inventory = buildInventory(scanRoot, extensions, skipGenerated, rootDir);
20
+
21
+ if (dryRun) {
22
+ return {
23
+ mode: "dry-run",
24
+ filesScanned: inventory.length,
25
+ filesNeedingSummary: inventory.filter((file) => !file.has_summary).length,
26
+ filesNeedingWarn: inventory.filter((file) => !file.has_warn).length,
27
+ phases,
28
+ inventory,
29
+ };
30
+ }
31
+
32
+ if (context.taskManager && typeof context.taskManager.createTask === "function") {
33
+ const taskDescription = buildAuditTaskDescription(formValues, inventory);
34
+ const task = await context.taskManager.createTask({
35
+ title: `docs(audit): codebase annotation audit`,
36
+ description: taskDescription,
37
+ priority: "high",
38
+ labels: ["audit", "documentation", "annotation"],
39
+ skills: ["codebase-annotation-audit"],
40
+ });
41
+ return {
42
+ mode: "task-dispatched",
43
+ taskId: task.id || task._id,
44
+ filesScanned: inventory.length,
45
+ filesNeedingSummary: inventory.filter((file) => !file.has_summary).length,
46
+ phases,
47
+ };
48
+ }
49
+
50
+ const auditDir = resolve(rootDir, ".bosun", "audit");
51
+ mkdirSync(auditDir, { recursive: true });
52
+ writeFileSync(
53
+ resolve(auditDir, "inventory.json"),
54
+ JSON.stringify(inventory, null, 2) + "\n",
55
+ "utf8",
56
+ );
57
+
58
+ return {
59
+ mode: "inventory-saved",
60
+ inventoryPath: resolve(auditDir, "inventory.json"),
61
+ filesScanned: inventory.length,
62
+ filesNeedingSummary: inventory.filter((file) => !file.has_summary).length,
63
+ filesNeedingWarn: inventory.filter((file) => !file.has_warn).length,
64
+ phases,
65
+ instructions:
66
+ "Inventory saved. Assign a docs(audit) task to an agent with the codebase-annotation-audit skill to complete annotation.",
67
+ };
68
+ }
69
+
70
+ function buildInventory(scanDir, extensions, skipGenerated, repoRoot) {
71
+ const inventory = [];
72
+ const generatedPatterns = [
73
+ /node_modules/,
74
+ /\.min\.\w+$/,
75
+ /package-lock\.json$/,
76
+ /yarn\.lock$/,
77
+ /pnpm-lock\.yaml$/,
78
+ /\.next\//,
79
+ /dist\//,
80
+ /build\//,
81
+ /coverage\//,
82
+ /\.bosun-worktrees\//,
83
+ /\.git\//,
84
+ ];
85
+
86
+ function walk(dir) {
87
+ let entries;
88
+ try {
89
+ entries = readdirSync(dir, { withFileTypes: true });
90
+ } catch {
91
+ return;
92
+ }
93
+
94
+ for (const entry of entries) {
95
+ const fullPath = resolve(dir, entry.name);
96
+ const relPath = fullPath.replace(repoRoot, "").replace(/\\/g, "/").replace(/^\//, "");
97
+
98
+ if (entry.isDirectory()) {
99
+ if (skipGenerated && generatedPatterns.some((pattern) => pattern.test(relPath))) continue;
100
+ if (entry.name.startsWith(".") && entry.name !== ".bosun") continue;
101
+ walk(fullPath);
102
+ continue;
103
+ }
104
+
105
+ if (!entry.isFile()) continue;
106
+ if (extensions.length > 0 && !extensions.some((ext) => entry.name.endsWith(ext))) continue;
107
+ if (skipGenerated && generatedPatterns.some((pattern) => pattern.test(relPath))) continue;
108
+
109
+ let content = "";
110
+ let lines = 0;
111
+ let hasSummary = false;
112
+ let hasWarn = false;
113
+
114
+ try {
115
+ content = readFileSync(fullPath, "utf8");
116
+ lines = content.split("\n").length;
117
+ hasSummary = /(?:CLAUDE|BOSUN):SUMMARY/i.test(content);
118
+ hasWarn = /(?:CLAUDE|BOSUN):WARN/i.test(content);
119
+ } catch {
120
+ }
121
+
122
+ const ext = entry.name.includes(".") ? entry.name.slice(entry.name.lastIndexOf(".")) : "";
123
+ inventory.push({
124
+ path: relPath,
125
+ lang: ext,
126
+ lines,
127
+ has_summary: hasSummary,
128
+ has_warn: hasWarn,
129
+ category: categorizeFile(relPath),
130
+ });
131
+ }
132
+ }
133
+
134
+ walk(scanDir);
135
+ return inventory;
136
+ }
137
+
138
+ function categorizeFile(relPath) {
139
+ if (/test|spec|__tests__/i.test(relPath)) return "test";
140
+ if (/\.config\.|tsconfig|jest\.config|webpack|vite\.config|\.env/i.test(relPath)) return "config";
141
+ if (/\.min\.|dist\/|build\/|generated/i.test(relPath)) return "generated";
142
+ if (/util|helper|lib\//i.test(relPath)) return "util";
143
+ return "core";
144
+ }
145
+
146
+ function buildAuditTaskDescription(formValues, inventory) {
147
+ const needsSummary = inventory.filter((file) => !file.has_summary).length;
148
+ const needsWarn = inventory.filter((file) => !file.has_warn).length;
149
+ return `## Codebase Annotation Audit
150
+
151
+ **Phases:** ${formValues.phases || "all"}
152
+ **Target:** ${formValues.targetDir || "(entire repo)"}
153
+ **Extensions:** ${formValues.fileExtensions || "all source files"}
154
+
155
+ ### Inventory Summary
156
+ - Total files: ${inventory.length}
157
+ - Files needing CLAUDE:SUMMARY: ${needsSummary}
158
+ - Files needing CLAUDE:WARN review: ${needsWarn}
159
+
160
+ ### Instructions
161
+ Follow the codebase-annotation-audit skill (loaded in your skills).
162
+ Run phases as specified above. Do NOT change any program behavior — documentation only.
163
+ ${formValues.commitMessage ? `\nCommit with: \`${formValues.commitMessage}\`` : ""}
164
+ `;
165
+ }
@@ -7,6 +7,7 @@
7
7
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, unlinkSync } from "node:fs";
8
8
  import { dirname, resolve } from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
+ import { executeAnnotationAudit } from "./manual-flow-audit.mjs";
10
11
 
11
12
  const __filename = fileURLToPath(import.meta.url);
12
13
  const __dirname = dirname(__filename);
@@ -48,6 +49,7 @@ const __dirname = dirname(__filename);
48
49
  * @property {string} [completedAt]
49
50
  * @property {Object} [result] — executor output
50
51
  * @property {string} [error]
52
+ * @property {Object} [metadata]
51
53
  */
52
54
 
53
55
  // ── Directories ──────────────────────────────────────────────────────────────
@@ -839,22 +841,30 @@ export function deleteFlowTemplate(templateId, rootDir) {
839
841
  * @param {string} rootDir
840
842
  * @returns {ManualFlowRun}
841
843
  */
842
- export function createRun(templateId, formValues, rootDir) {
844
+ export function createRun(templateId, formValues, rootDir, opts = {}) {
843
845
  ensureDirs(rootDir);
844
846
  const template = getFlowTemplate(templateId, rootDir);
845
847
  if (!template) throw new Error(`Template not found: ${templateId}`);
846
848
 
847
- // Validate required fields
849
+ validateRequiredManualFlowFields(template, formValues);
850
+ const resolved = resolveManualFlowValues(template, formValues);
851
+ const run = createManualFlowRunRecord(templateId, template.name, resolved, opts);
852
+
853
+ writeRunToDisk(run, rootDir);
854
+ return run;
855
+ }
856
+
857
+ function validateRequiredManualFlowFields(template, formValues) {
848
858
  for (const field of template.fields) {
849
- if (field.required) {
850
- const val = formValues[field.id];
851
- if (val === undefined || val === null || val === "") {
852
- throw new Error(`Required field missing: ${field.label} (${field.id})`);
853
- }
859
+ if (!field.required) continue;
860
+ const value = formValues[field.id];
861
+ if (value === undefined || value === null || value === "") {
862
+ throw new Error(`Required field missing: ${field.label} (${field.id})`);
854
863
  }
855
864
  }
865
+ }
856
866
 
857
- // Apply defaults for missing optional fields
867
+ function resolveManualFlowValues(template, formValues) {
858
868
  const resolved = {};
859
869
  for (const field of template.fields) {
860
870
  if (formValues[field.id] !== undefined && formValues[field.id] !== null) {
@@ -863,21 +873,49 @@ export function createRun(templateId, formValues, rootDir) {
863
873
  resolved[field.id] = field.defaultValue;
864
874
  }
865
875
  }
876
+ return resolved;
877
+ }
878
+
879
+ function normalizeManualFlowRunMetadata(metadata = {}) {
880
+ if (!metadata || typeof metadata !== "object") return null;
881
+ const normalized = {};
882
+
883
+ const repository = String(
884
+ metadata.repository || metadata.targetRepo || metadata.repo || "",
885
+ ).trim();
886
+ if (repository) {
887
+ normalized.repository = repository;
888
+ normalized.targetRepo = repository;
889
+ }
866
890
 
867
- const run = {
891
+ const workspaceId = String(metadata.workspaceId || metadata.workspace || "").trim();
892
+ if (workspaceId) normalized.workspaceId = workspaceId;
893
+
894
+ const workspaceDir = String(metadata.workspaceDir || metadata.rootDir || "").trim();
895
+ if (workspaceDir) normalized.workspaceDir = workspaceDir;
896
+
897
+ const projectId = String(metadata.projectId || metadata.project || "").trim();
898
+ if (projectId) normalized.projectId = projectId;
899
+
900
+ const triggerSource = String(metadata.triggerSource || metadata.source || "").trim();
901
+ if (triggerSource) normalized.triggerSource = triggerSource;
902
+
903
+ return Object.keys(normalized).length > 0 ? normalized : null;
904
+ }
905
+
906
+ function createManualFlowRunRecord(templateId, templateName, formValues, opts = {}) {
907
+ return {
868
908
  id: `mfr-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`,
869
909
  templateId,
870
- templateName: template.name,
871
- formValues: resolved,
910
+ templateName,
911
+ formValues,
872
912
  status: "pending",
873
913
  startedAt: new Date().toISOString(),
874
914
  completedAt: null,
875
915
  result: null,
876
916
  error: null,
917
+ metadata: normalizeManualFlowRunMetadata(opts?.metadata),
877
918
  };
878
-
879
- writeRunToDisk(run, rootDir);
880
- return run;
881
919
  }
882
920
 
883
921
  /**
@@ -998,7 +1036,9 @@ export function listRuns(rootDir, opts = {}) {
998
1036
  * @returns {Promise<ManualFlowRun>}
999
1037
  */
1000
1038
  export async function executeFlow(templateId, formValues, rootDir, context = {}) {
1001
- const run = createRun(templateId, formValues, rootDir);
1039
+ const run = createRun(templateId, formValues, rootDir, {
1040
+ metadata: context?.runMetadata,
1041
+ });
1002
1042
 
1003
1043
  try {
1004
1044
  startRun(run.id, rootDir);
@@ -1036,186 +1076,8 @@ export async function executeFlow(templateId, formValues, rootDir, context = {})
1036
1076
  return completeRun(run.id, result, rootDir);
1037
1077
  } catch (err) {
1038
1078
  console.warn("[manual-flows] execution failed for " + run.id + ": " + (err?.message || String(err)));
1039
- return failRun(run.id, "Execution failed", rootDir);
1040
- }
1041
- }
1042
-
1043
- // ── Built-in Executors ───────────────────────────────────────────────────────
1044
-
1045
- /**
1046
- * Execute the Codebase Annotation Audit flow.
1047
- * Creates a Bosun task with the audit skill injected, or runs a lightweight
1048
- * inventory inline for dry runs.
1049
- */
1050
- async function executeAnnotationAudit(formValues, rootDir, context) {
1051
- const {
1052
- targetDir = "",
1053
- fileExtensions = ".mjs, .js, .ts, .tsx, .jsx, .py",
1054
- skipGenerated = true,
1055
- phases = "all",
1056
- dryRun = false,
1057
- } = formValues;
1058
-
1059
- // Parse extensions
1060
- const extensions = fileExtensions
1061
- .split(",")
1062
- .map((e) => e.trim())
1063
- .filter(Boolean);
1064
-
1065
- // Build inventory inline (lightweight — no agent needed)
1066
- const scanRoot = targetDir ? resolve(rootDir, targetDir) : rootDir;
1067
- const inventory = buildInventory(scanRoot, extensions, skipGenerated, rootDir);
1068
-
1069
- if (dryRun) {
1070
- return {
1071
- mode: "dry-run",
1072
- filesScanned: inventory.length,
1073
- filesNeedingSummary: inventory.filter((f) => !f.has_summary).length,
1074
- filesNeedingWarn: inventory.filter((f) => !f.has_warn).length,
1075
- phases,
1076
- inventory,
1077
- };
1078
- }
1079
-
1080
- // For actual execution, create a task that the agent will pick up
1081
- if (context.taskManager && typeof context.taskManager.createTask === "function") {
1082
- const taskDescription = buildAuditTaskDescription(formValues, inventory);
1083
- const task = await context.taskManager.createTask({
1084
- title: `docs(audit): codebase annotation audit`,
1085
- description: taskDescription,
1086
- priority: "high",
1087
- labels: ["audit", "documentation", "annotation"],
1088
- skills: ["codebase-annotation-audit"],
1089
- });
1090
- return {
1091
- mode: "task-dispatched",
1092
- taskId: task.id || task._id,
1093
- filesScanned: inventory.length,
1094
- filesNeedingSummary: inventory.filter((f) => !f.has_summary).length,
1095
- phases,
1096
- };
1079
+ return failRun(run.id, err?.message || "Execution failed", rootDir);
1097
1080
  }
1098
-
1099
- // Fallback: return inventory with instructions for manual execution
1100
- const auditDir = resolve(rootDir, ".bosun", "audit");
1101
- mkdirSync(auditDir, { recursive: true });
1102
- writeFileSync(
1103
- resolve(auditDir, "inventory.json"),
1104
- JSON.stringify(inventory, null, 2) + "\n",
1105
- "utf8",
1106
- );
1107
-
1108
- return {
1109
- mode: "inventory-saved",
1110
- inventoryPath: resolve(auditDir, "inventory.json"),
1111
- filesScanned: inventory.length,
1112
- filesNeedingSummary: inventory.filter((f) => !f.has_summary).length,
1113
- filesNeedingWarn: inventory.filter((f) => !f.has_warn).length,
1114
- phases,
1115
- instructions:
1116
- "Inventory saved. Assign a docs(audit) task to an agent with the codebase-annotation-audit skill to complete annotation.",
1117
- };
1118
- }
1119
-
1120
- /**
1121
- * Scan files to build an audit inventory.
1122
- */
1123
- function buildInventory(scanDir, extensions, skipGenerated, repoRoot) {
1124
- const inventory = [];
1125
- const GENERATED_PATTERNS = [
1126
- /node_modules/,
1127
- /\.min\.\w+$/,
1128
- /package-lock\.json$/,
1129
- /yarn\.lock$/,
1130
- /pnpm-lock\.yaml$/,
1131
- /\.next\//,
1132
- /dist\//,
1133
- /build\//,
1134
- /coverage\//,
1135
- /\.bosun-worktrees\//,
1136
- /\.git\//,
1137
- ];
1138
-
1139
- function walk(dir) {
1140
- let entries;
1141
- try {
1142
- entries = readdirSync(dir, { withFileTypes: true });
1143
- } catch {
1144
- return;
1145
- }
1146
-
1147
- for (const entry of entries) {
1148
- const fullPath = resolve(dir, entry.name);
1149
- const relPath = fullPath.replace(repoRoot, "").replace(/\\/g, "/").replace(/^\//, "");
1150
-
1151
- if (entry.isDirectory()) {
1152
- if (skipGenerated && GENERATED_PATTERNS.some((p) => p.test(relPath))) continue;
1153
- if (entry.name.startsWith(".") && entry.name !== ".bosun") continue;
1154
- walk(fullPath);
1155
- continue;
1156
- }
1157
-
1158
- if (!entry.isFile()) continue;
1159
- if (extensions.length > 0 && !extensions.some((ext) => entry.name.endsWith(ext))) continue;
1160
- if (skipGenerated && GENERATED_PATTERNS.some((p) => p.test(relPath))) continue;
1161
-
1162
- let content = "";
1163
- let lines = 0;
1164
- let hasSummary = false;
1165
- let hasWarn = false;
1166
-
1167
- try {
1168
- content = readFileSync(fullPath, "utf8");
1169
- lines = content.split("\n").length;
1170
- hasSummary = /(?:CLAUDE|BOSUN):SUMMARY/i.test(content);
1171
- hasWarn = /(?:CLAUDE|BOSUN):WARN/i.test(content);
1172
- } catch { /* unreadable */ }
1173
-
1174
- const ext = entry.name.includes(".") ? entry.name.slice(entry.name.lastIndexOf(".")) : "";
1175
- const category = categorizeFile(relPath, ext);
1176
-
1177
- inventory.push({
1178
- path: relPath,
1179
- lang: ext,
1180
- lines,
1181
- has_summary: hasSummary,
1182
- has_warn: hasWarn,
1183
- category,
1184
- });
1185
- }
1186
- }
1187
-
1188
- walk(scanDir);
1189
- return inventory;
1190
- }
1191
-
1192
- function categorizeFile(relPath, ext) {
1193
- if (/test|spec|__tests__/i.test(relPath)) return "test";
1194
- if (/\.config\.|tsconfig|jest\.config|webpack|vite\.config|\.env/i.test(relPath)) return "config";
1195
- if (/\.min\.|dist\/|build\/|generated/i.test(relPath)) return "generated";
1196
- if (/util|helper|lib\//i.test(relPath)) return "util";
1197
- return "core";
1198
- }
1199
-
1200
- function buildAuditTaskDescription(formValues, inventory) {
1201
- const needsSummary = inventory.filter((f) => !f.has_summary).length;
1202
- const needsWarn = inventory.filter((f) => !f.has_warn).length;
1203
- return `## Codebase Annotation Audit
1204
-
1205
- **Phases:** ${formValues.phases || "all"}
1206
- **Target:** ${formValues.targetDir || "(entire repo)"}
1207
- **Extensions:** ${formValues.fileExtensions || "all source files"}
1208
-
1209
- ### Inventory Summary
1210
- - Total files: ${inventory.length}
1211
- - Files needing CLAUDE:SUMMARY: ${needsSummary}
1212
- - Files needing CLAUDE:WARN review: ${needsWarn}
1213
-
1214
- ### Instructions
1215
- Follow the codebase-annotation-audit skill (loaded in your skills).
1216
- Run phases as specified above. Do NOT change any program behavior — documentation only.
1217
- ${formValues.commitMessage ? `\nCommit with: \`${formValues.commitMessage}\`` : ""}
1218
- `;
1219
1081
  }
1220
1082
 
1221
1083
  /** Stub executor for skill generation flow. */
@@ -1302,28 +1164,11 @@ async function executeContextIndexFull(formValues, rootDir, _context = {}) {
1302
1164
  }
1303
1165
 
1304
1166
  async function executeResearchAgent(formValues, rootDir, context = {}) {
1305
- const problem = String(formValues?.problem || "").trim();
1306
- if (!problem) {
1307
- throw new Error("Research problem is required.");
1308
- }
1309
-
1310
- const domain = String(formValues?.domain || "computer-science").trim() || "computer-science";
1311
- const maxIterationsRaw = Number(formValues?.maxIterations);
1312
- const maxIterations = Number.isFinite(maxIterationsRaw)
1313
- ? Math.min(50, Math.max(1, Math.floor(maxIterationsRaw)))
1314
- : 10;
1315
- const searchLiterature = formValues?.searchLiterature !== false;
1316
- const executionMode = String(formValues?.executionMode || "workflow").trim().toLowerCase();
1167
+ const researchConfig = resolveResearchAgentConfig(formValues);
1168
+ const { problem, domain, maxIterations, searchLiterature, executionMode } = researchConfig;
1317
1169
 
1318
1170
  if (executionMode === "task") {
1319
- const taskDescription =
1320
- `Run iterative research for the following problem:\n\n` +
1321
- `${problem}\n\n` +
1322
- `Domain: ${domain}\n` +
1323
- `Max iterations: ${maxIterations}\n` +
1324
- `Search literature first: ${searchLiterature}\n\n` +
1325
- `Use a generate -> verify -> revise loop. If verification identifies critical flaws, ` +
1326
- `regenerate from a fundamentally different approach.`;
1171
+ const taskDescription = buildResearchTaskDescription(researchConfig);
1327
1172
  if (context.taskManager && typeof context.taskManager.createTask === "function") {
1328
1173
  const task = await context.taskManager.createTask({
1329
1174
  title: `research: iterative agent (${domain})`,
@@ -1340,27 +1185,18 @@ async function executeResearchAgent(formValues, rootDir, context = {}) {
1340
1185
  searchLiterature,
1341
1186
  };
1342
1187
  }
1343
- return {
1344
- mode: "instructions",
1345
- problem,
1346
- domain,
1347
- maxIterations,
1348
- searchLiterature,
1349
- instructions: "Task manager unavailable. Create a high-priority research task using the provided configuration.",
1350
- };
1188
+ return buildResearchInstructionsResult(
1189
+ researchConfig,
1190
+ "Task manager unavailable. Create a high-priority research task using the provided configuration.",
1191
+ );
1351
1192
  }
1352
1193
 
1353
1194
  const engine = context.engine;
1354
1195
  if (!engine || typeof engine.execute !== "function") {
1355
- return {
1356
- mode: "instructions",
1357
- problem,
1358
- domain,
1359
- maxIterations,
1360
- searchLiterature,
1361
- instructions:
1362
- "Workflow execution mode requires an active workflow engine. Retry from the Workflows launcher or switch to Task mode.",
1363
- };
1196
+ return buildResearchInstructionsResult(
1197
+ researchConfig,
1198
+ "Workflow execution mode requires an active workflow engine. Retry from the Workflows launcher or switch to Task mode.",
1199
+ );
1364
1200
  }
1365
1201
 
1366
1202
  const { installTemplate } = await import("./workflow-templates.mjs");
@@ -1369,14 +1205,7 @@ async function executeResearchAgent(formValues, rootDir, context = {}) {
1369
1205
  installTemplate(templateId, engine);
1370
1206
  }
1371
1207
 
1372
- const input = {
1373
- problem,
1374
- domain,
1375
- maxIterations,
1376
- searchLiterature,
1377
- _previousFeedback: "",
1378
- triggerSource: "manual",
1379
- };
1208
+ const input = buildResearchWorkflowInput(researchConfig);
1380
1209
 
1381
1210
  Promise.resolve()
1382
1211
  .then(() => engine.execute(templateId, input, { force: true, triggerSource: "manual" }))
@@ -1396,30 +1225,76 @@ async function executeResearchAgent(formValues, rootDir, context = {}) {
1396
1225
  };
1397
1226
  }
1398
1227
 
1399
- /** Executor for user-created custom templates. */
1400
- async function executeCustomFlow(template, formValues, rootDir, context) {
1401
- const templateValues = {
1402
- ...(formValues || {}),
1403
- templateName: template?.name || "",
1404
- templateId: template?.id || "",
1405
- category: template?.category || "custom",
1228
+ function resolveResearchAgentConfig(formValues) {
1229
+ const problem = String(formValues?.problem || "").trim();
1230
+ if (!problem) {
1231
+ throw new Error("Research problem is required.");
1232
+ }
1233
+
1234
+ const domain = String(formValues?.domain || "computer-science").trim() || "computer-science";
1235
+ const maxIterationsRaw = Number(formValues?.maxIterations);
1236
+ const maxIterations = Number.isFinite(maxIterationsRaw)
1237
+ ? Math.min(50, Math.max(1, Math.floor(maxIterationsRaw)))
1238
+ : 10;
1239
+
1240
+ return {
1241
+ problem,
1242
+ domain,
1243
+ maxIterations,
1244
+ searchLiterature: formValues?.searchLiterature !== false,
1245
+ executionMode: String(formValues?.executionMode || "workflow").trim().toLowerCase(),
1406
1246
  };
1247
+ }
1407
1248
 
1408
- const action = template?.action && typeof template.action === "object"
1409
- ? template.action
1410
- : { kind: "task" };
1249
+ function buildResearchTaskDescription({ problem, domain, maxIterations, searchLiterature }) {
1250
+ return (
1251
+ `Run iterative research for the following problem:\n\n` +
1252
+ `${problem}\n\n` +
1253
+ `Domain: ${domain}\n` +
1254
+ `Max iterations: ${maxIterations}\n` +
1255
+ `Search literature first: ${searchLiterature}\n\n` +
1256
+ `Use a generate -> verify -> revise loop. If verification identifies critical flaws, ` +
1257
+ `regenerate from a fundamentally different approach.`
1258
+ );
1259
+ }
1260
+
1261
+ function buildResearchInstructionsResult({ problem, domain, maxIterations, searchLiterature }, instructions) {
1262
+ return {
1263
+ mode: "instructions",
1264
+ problem,
1265
+ domain,
1266
+ maxIterations,
1267
+ searchLiterature,
1268
+ instructions,
1269
+ };
1270
+ }
1271
+
1272
+ function buildResearchWorkflowInput({ problem, domain, maxIterations, searchLiterature }) {
1273
+ return {
1274
+ problem,
1275
+ domain,
1276
+ maxIterations,
1277
+ searchLiterature,
1278
+ _previousFeedback: "",
1279
+ triggerSource: "manual",
1280
+ };
1281
+ }
1282
+
1283
+ /** Executor for user-created custom templates. */
1284
+ async function executeCustomFlow(template, formValues, rootDir, context) {
1285
+ const templateValues = buildCustomFlowTemplateValues(template, formValues, {
1286
+ repository:
1287
+ context?.runMetadata?.repository ||
1288
+ context?.runMetadata?.targetRepo ||
1289
+ "",
1290
+ workspaceId: context?.runMetadata?.workspaceId || "",
1291
+ });
1292
+ const action = resolveCustomFlowAction(template);
1411
1293
  const actionKind = String(action?.kind || "task").trim().toLowerCase();
1412
1294
 
1413
1295
  if (context.taskManager && typeof context.taskManager.createTask === "function") {
1414
- const taskAction = actionKind === "task" && action?.task && typeof action.task === "object"
1415
- ? action.task
1416
- : {};
1417
- const taskTitleTemplate = String(taskAction?.title || "").trim();
1418
- const taskDescriptionTemplate = String(taskAction?.description || "").trim();
1419
- const taskPriority = String(taskAction?.priority || "medium").trim() || "medium";
1420
- const taskLabels = Array.isArray(taskAction?.labels)
1421
- ? taskAction.labels.map((label) => String(label || "").trim()).filter(Boolean)
1422
- : [];
1296
+ const { taskTitleTemplate, taskDescriptionTemplate, taskPriority, taskLabels } =
1297
+ resolveCustomFlowTaskConfig(actionKind, action);
1423
1298
 
1424
1299
  const renderedTitle = renderTemplateString(taskTitleTemplate, templateValues);
1425
1300
  const renderedDescription = renderTemplateString(taskDescriptionTemplate, templateValues);
@@ -1460,6 +1335,37 @@ async function executeCustomFlow(template, formValues, rootDir, context) {
1460
1335
  };
1461
1336
  }
1462
1337
 
1338
+ function buildCustomFlowTemplateValues(template, formValues, extraValues = {}) {
1339
+ return {
1340
+ ...(formValues || {}),
1341
+ ...(extraValues || {}),
1342
+ templateName: template?.name || "",
1343
+ templateId: template?.id || "",
1344
+ category: template?.category || "custom",
1345
+ };
1346
+ }
1347
+
1348
+ function resolveCustomFlowAction(template) {
1349
+ return template?.action && typeof template.action === "object"
1350
+ ? template.action
1351
+ : { kind: "task" };
1352
+ }
1353
+
1354
+ function resolveCustomFlowTaskConfig(actionKind, action) {
1355
+ const taskAction = actionKind === "task" && action?.task && typeof action.task === "object"
1356
+ ? action.task
1357
+ : {};
1358
+
1359
+ return {
1360
+ taskTitleTemplate: String(taskAction?.title || "").trim(),
1361
+ taskDescriptionTemplate: String(taskAction?.description || "").trim(),
1362
+ taskPriority: String(taskAction?.priority || "medium").trim() || "medium",
1363
+ taskLabels: Array.isArray(taskAction?.labels)
1364
+ ? taskAction.labels.map((label) => String(label || "").trim()).filter(Boolean)
1365
+ : [],
1366
+ };
1367
+ }
1368
+
1463
1369
  function renderTemplateString(templateText = "", values = {}) {
1464
1370
  const raw = String(templateText || "");
1465
1371
  if (!raw) return "";
@@ -1484,4 +1390,3 @@ function writeRunToDisk(run, rootDir) {
1484
1390
  mkdirSync(dir, { recursive: true });
1485
1391
  writeFileSync(resolve(dir, `${run.id}.json`), JSON.stringify(run, null, 2) + "\n", "utf8");
1486
1392
  }
1487
-