bosun 0.29.0 → 0.29.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/agent-endpoint.mjs +317 -0
- package/agent-pool.mjs +116 -14
- package/agent-prompts.mjs +38 -0
- package/cli.mjs +41 -2
- package/codex-config.mjs +107 -29
- package/config.mjs +79 -9
- package/fetch-runtime.mjs +5 -1
- package/monitor.mjs +88 -4
- package/package.json +8 -3
- package/repo-root.mjs +113 -3
- package/setup.mjs +153 -11
- package/task-cli.mjs +826 -0
- package/task-store.mjs +2 -2
- package/ui/app.js +213 -122
- package/ui/components/agent-selector.js +5 -2
- package/ui/components/chat-view.js +89 -33
- package/ui/components/command-palette.js +139 -17
- package/ui/components/diff-viewer.js +32 -32
- package/ui/components/forms.js +98 -216
- package/ui/components/session-list.js +114 -63
- package/ui/components/shared.js +86 -86
- package/ui/components/workspace-switcher.js +66 -69
- package/ui/index.html +0 -91
- package/ui/modules/settings-schema.js +1 -1
- package/ui/modules/streaming.js +22 -6
- package/ui/styles/components.css +3414 -322
- package/ui/styles/layout.css +488 -5
- package/ui/styles/sessions.css +1 -1
- package/ui/styles.css +225 -0
- package/ui/tabs/agents.js +504 -533
- package/ui/tabs/chat.js +159 -55
- package/ui/tabs/control.js +74 -76
- package/ui/tabs/dashboard.js +107 -113
- package/ui/tabs/infra.js +67 -76
- package/ui/tabs/logs.js +62 -71
- package/ui/tabs/settings.js +71 -27
- package/ui/tabs/tasks.js +53 -56
- package/ui/tabs/telemetry.js +30 -30
- package/worktree-manager.mjs +8 -0
- package/desktop/AGENTS.md +0 -62
- package/desktop/package-lock.json +0 -4193
package/agent-endpoint.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import { createServer } from "node:http";
|
|
|
20
20
|
import { resolve, dirname } from "node:path";
|
|
21
21
|
import { fileURLToPath } from "node:url";
|
|
22
22
|
import { writeFileSync, mkdirSync, unlinkSync, readFileSync } from "node:fs";
|
|
23
|
+
import { randomUUID } from "node:crypto";
|
|
23
24
|
|
|
24
25
|
const __filename = fileURLToPath(import.meta.url);
|
|
25
26
|
const __dirname = dirname(__filename);
|
|
@@ -505,6 +506,20 @@ export class AgentEndpoint {
|
|
|
505
506
|
return await this._handleListTasks(url, res);
|
|
506
507
|
}
|
|
507
508
|
|
|
509
|
+
if (method === "POST" && pathname === "/api/tasks/create") {
|
|
510
|
+
const body = await parseBody(req);
|
|
511
|
+
return await this._handleCreateTask(body, res);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (method === "GET" && pathname === "/api/tasks/stats") {
|
|
515
|
+
return await this._handleTaskStats(res);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (method === "POST" && pathname === "/api/tasks/import") {
|
|
519
|
+
const body = await parseBody(req);
|
|
520
|
+
return await this._handleImportTasks(body, res);
|
|
521
|
+
}
|
|
522
|
+
|
|
508
523
|
if (method === "GET" && pathname === "/api/executor") {
|
|
509
524
|
return this._handleExecutorStatus(res);
|
|
510
525
|
}
|
|
@@ -545,6 +560,15 @@ export class AgentEndpoint {
|
|
|
545
560
|
const body = await parseBody(req);
|
|
546
561
|
return await this._handleError(taskId, body, res);
|
|
547
562
|
}
|
|
563
|
+
|
|
564
|
+
if (method === "POST" && pathname === `/api/tasks/${taskId}/update`) {
|
|
565
|
+
const body = await parseBody(req);
|
|
566
|
+
return await this._handleUpdateTask(taskId, body, res);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (method === "DELETE" && pathname === `/api/tasks/${taskId}`) {
|
|
570
|
+
return await this._handleDeleteTask(taskId, res);
|
|
571
|
+
}
|
|
548
572
|
}
|
|
549
573
|
|
|
550
574
|
// ── 404 ─────────────────────────────────────────────────────────
|
|
@@ -864,6 +888,299 @@ export class AgentEndpoint {
|
|
|
864
888
|
sendJson(res, 200, { ok: true, task, nextAction });
|
|
865
889
|
}
|
|
866
890
|
|
|
891
|
+
// ── Task CRUD Handlers ──────────────────────────────────────────────────
|
|
892
|
+
|
|
893
|
+
async _handleCreateTask(body, res) {
|
|
894
|
+
if (!this._taskStore) {
|
|
895
|
+
sendJson(res, 503, { error: "Task store not configured" });
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
const { title, description, status, priority, tags, baseBranch, base_branch, workspace, repository, repositories, implementation_steps, acceptance_criteria, verification, draft } = body;
|
|
900
|
+
|
|
901
|
+
if (!title) {
|
|
902
|
+
sendJson(res, 400, { error: "Missing 'title' in body" });
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
try {
|
|
907
|
+
// Build description from structured fields if provided
|
|
908
|
+
let fullDescription = description || "";
|
|
909
|
+
if (implementation_steps?.length || acceptance_criteria?.length || verification?.length) {
|
|
910
|
+
const parts = [fullDescription];
|
|
911
|
+
if (implementation_steps?.length) {
|
|
912
|
+
parts.push("", "## Implementation Steps");
|
|
913
|
+
for (const step of implementation_steps) parts.push(`- ${step}`);
|
|
914
|
+
}
|
|
915
|
+
if (acceptance_criteria?.length) {
|
|
916
|
+
parts.push("", "## Acceptance Criteria");
|
|
917
|
+
for (const c of acceptance_criteria) parts.push(`- ${c}`);
|
|
918
|
+
}
|
|
919
|
+
if (verification?.length) {
|
|
920
|
+
parts.push("", "## Verification");
|
|
921
|
+
for (const v of verification) parts.push(`- ${v}`);
|
|
922
|
+
}
|
|
923
|
+
fullDescription = parts.join("\n");
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
let task;
|
|
927
|
+
if (typeof this._taskStore.createTask === "function") {
|
|
928
|
+
// kanban adapter — handles ID generation
|
|
929
|
+
task = await this._taskStore.createTask(null, {
|
|
930
|
+
title,
|
|
931
|
+
description: fullDescription,
|
|
932
|
+
status: status || "draft",
|
|
933
|
+
priority: priority || "medium",
|
|
934
|
+
tags: tags || [],
|
|
935
|
+
baseBranch: baseBranch || base_branch || "main",
|
|
936
|
+
workspace: workspace || "",
|
|
937
|
+
repository: repository || "",
|
|
938
|
+
repositories: repositories || [],
|
|
939
|
+
draft: draft ?? (status === "draft" || !status),
|
|
940
|
+
});
|
|
941
|
+
} else if (typeof this._taskStore.addTask === "function") {
|
|
942
|
+
// raw task-store
|
|
943
|
+
task = this._taskStore.addTask({
|
|
944
|
+
id: randomUUID(),
|
|
945
|
+
title,
|
|
946
|
+
description: fullDescription,
|
|
947
|
+
status: status || "draft",
|
|
948
|
+
priority: priority || "medium",
|
|
949
|
+
tags: tags || [],
|
|
950
|
+
baseBranch: baseBranch || base_branch || "main",
|
|
951
|
+
workspace: workspace || "",
|
|
952
|
+
repository: repository || "",
|
|
953
|
+
repositories: repositories || [],
|
|
954
|
+
draft: draft ?? (status === "draft" || !status),
|
|
955
|
+
});
|
|
956
|
+
} else {
|
|
957
|
+
sendJson(res, 501, { error: "Task store does not support creation" });
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
if (!task) {
|
|
962
|
+
sendJson(res, 500, { error: "Failed to create task" });
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
console.log(`${TAG} Created task ${task.id}: ${task.title}`);
|
|
967
|
+
sendJson(res, 201, { ok: true, task });
|
|
968
|
+
} catch (err) {
|
|
969
|
+
console.error(`${TAG} createTask error:`, err.message);
|
|
970
|
+
sendJson(res, 500, { error: `Failed to create task: ${err.message}` });
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
async _handleUpdateTask(taskId, body, res) {
|
|
975
|
+
if (!this._taskStore) {
|
|
976
|
+
sendJson(res, 503, { error: "Task store not configured" });
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
try {
|
|
981
|
+
// Verify task exists
|
|
982
|
+
let existing = null;
|
|
983
|
+
if (typeof this._taskStore.getTask === "function") {
|
|
984
|
+
existing = await this._taskStore.getTask(taskId);
|
|
985
|
+
} else if (typeof this._taskStore.get === "function") {
|
|
986
|
+
existing = await this._taskStore.get(taskId);
|
|
987
|
+
}
|
|
988
|
+
if (!existing) {
|
|
989
|
+
sendJson(res, 404, { error: "Task not found" });
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
const updates = { ...body };
|
|
994
|
+
delete updates.id; // Never allow ID change
|
|
995
|
+
|
|
996
|
+
// Normalize base_branch → baseBranch
|
|
997
|
+
if (updates.base_branch) {
|
|
998
|
+
updates.baseBranch = updates.base_branch;
|
|
999
|
+
delete updates.base_branch;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// Handle status change with history tracking
|
|
1003
|
+
if (updates.status && updates.status !== existing.status) {
|
|
1004
|
+
if (typeof this._taskStore.updateTaskStatus === "function") {
|
|
1005
|
+
await this._taskStore.updateTaskStatus(taskId, updates.status);
|
|
1006
|
+
}
|
|
1007
|
+
delete updates.status;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// Apply remaining updates
|
|
1011
|
+
let updatedTask;
|
|
1012
|
+
if (Object.keys(updates).length > 0) {
|
|
1013
|
+
if (typeof this._taskStore.updateTask === "function") {
|
|
1014
|
+
updatedTask = await this._taskStore.updateTask(taskId, updates);
|
|
1015
|
+
} else if (typeof this._taskStore.update === "function") {
|
|
1016
|
+
updatedTask = await this._taskStore.update(taskId, updates);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// Re-fetch to get final state
|
|
1021
|
+
if (typeof this._taskStore.getTask === "function") {
|
|
1022
|
+
updatedTask = await this._taskStore.getTask(taskId);
|
|
1023
|
+
} else if (typeof this._taskStore.get === "function") {
|
|
1024
|
+
updatedTask = await this._taskStore.get(taskId);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
console.log(`${TAG} Updated task ${taskId}`);
|
|
1028
|
+
sendJson(res, 200, { ok: true, task: updatedTask || { id: taskId } });
|
|
1029
|
+
} catch (err) {
|
|
1030
|
+
console.error(`${TAG} updateTask(${taskId}) error:`, err.message);
|
|
1031
|
+
sendJson(res, 500, { error: `Failed to update task: ${err.message}` });
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
async _handleDeleteTask(taskId, res) {
|
|
1036
|
+
if (!this._taskStore) {
|
|
1037
|
+
sendJson(res, 503, { error: "Task store not configured" });
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
try {
|
|
1042
|
+
let removed = false;
|
|
1043
|
+
if (typeof this._taskStore.deleteTask === "function") {
|
|
1044
|
+
await this._taskStore.deleteTask(taskId);
|
|
1045
|
+
removed = true;
|
|
1046
|
+
} else if (typeof this._taskStore.removeTask === "function") {
|
|
1047
|
+
removed = this._taskStore.removeTask(taskId);
|
|
1048
|
+
} else {
|
|
1049
|
+
sendJson(res, 501, { error: "Task store does not support deletion" });
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
if (!removed) {
|
|
1054
|
+
sendJson(res, 404, { error: "Task not found" });
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
console.log(`${TAG} Deleted task ${taskId}`);
|
|
1059
|
+
sendJson(res, 200, { ok: true, deleted: taskId });
|
|
1060
|
+
} catch (err) {
|
|
1061
|
+
console.error(`${TAG} deleteTask(${taskId}) error:`, err.message);
|
|
1062
|
+
sendJson(res, 500, { error: `Failed to delete task: ${err.message}` });
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
async _handleTaskStats(res) {
|
|
1067
|
+
if (!this._taskStore) {
|
|
1068
|
+
sendJson(res, 503, { error: "Task store not configured" });
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
try {
|
|
1073
|
+
let stats;
|
|
1074
|
+
if (typeof this._taskStore.getStats === "function") {
|
|
1075
|
+
stats = this._taskStore.getStats();
|
|
1076
|
+
} else {
|
|
1077
|
+
// Compute from list
|
|
1078
|
+
let tasks = [];
|
|
1079
|
+
if (typeof this._taskStore.listTasks === "function") {
|
|
1080
|
+
tasks = await this._taskStore.listTasks(null, {});
|
|
1081
|
+
} else if (typeof this._taskStore.list === "function") {
|
|
1082
|
+
tasks = await this._taskStore.list({});
|
|
1083
|
+
}
|
|
1084
|
+
const list = Array.isArray(tasks) ? tasks : [];
|
|
1085
|
+
stats = {
|
|
1086
|
+
draft: list.filter((t) => t.status === "draft").length,
|
|
1087
|
+
todo: list.filter((t) => t.status === "todo").length,
|
|
1088
|
+
inprogress: list.filter((t) => t.status === "inprogress").length,
|
|
1089
|
+
inreview: list.filter((t) => t.status === "inreview").length,
|
|
1090
|
+
done: list.filter((t) => t.status === "done").length,
|
|
1091
|
+
blocked: list.filter((t) => t.status === "blocked").length,
|
|
1092
|
+
total: list.length,
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
sendJson(res, 200, { ok: true, stats });
|
|
1097
|
+
} catch (err) {
|
|
1098
|
+
console.error(`${TAG} taskStats error:`, err.message);
|
|
1099
|
+
sendJson(res, 500, { error: `Failed to get stats: ${err.message}` });
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
async _handleImportTasks(body, res) {
|
|
1104
|
+
if (!this._taskStore) {
|
|
1105
|
+
sendJson(res, 503, { error: "Task store not configured" });
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
const tasks = body.tasks || body.backlog || (Array.isArray(body) ? body : null);
|
|
1110
|
+
if (!tasks || !Array.isArray(tasks)) {
|
|
1111
|
+
sendJson(res, 400, { error: "Body must contain 'tasks' array" });
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
const results = { created: [], failed: [] };
|
|
1116
|
+
for (const t of tasks) {
|
|
1117
|
+
try {
|
|
1118
|
+
// Recursively use our create handler logic
|
|
1119
|
+
let fullDescription = t.description || "";
|
|
1120
|
+
if (t.implementation_steps?.length || t.acceptance_criteria?.length || t.verification?.length) {
|
|
1121
|
+
const parts = [fullDescription];
|
|
1122
|
+
if (t.implementation_steps?.length) {
|
|
1123
|
+
parts.push("", "## Implementation Steps");
|
|
1124
|
+
for (const step of t.implementation_steps) parts.push(`- ${step}`);
|
|
1125
|
+
}
|
|
1126
|
+
if (t.acceptance_criteria?.length) {
|
|
1127
|
+
parts.push("", "## Acceptance Criteria");
|
|
1128
|
+
for (const c of t.acceptance_criteria) parts.push(`- ${c}`);
|
|
1129
|
+
}
|
|
1130
|
+
if (t.verification?.length) {
|
|
1131
|
+
parts.push("", "## Verification");
|
|
1132
|
+
for (const v of t.verification) parts.push(`- ${v}`);
|
|
1133
|
+
}
|
|
1134
|
+
fullDescription = parts.join("\n");
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
let task;
|
|
1138
|
+
if (typeof this._taskStore.createTask === "function") {
|
|
1139
|
+
task = await this._taskStore.createTask(null, {
|
|
1140
|
+
title: t.title,
|
|
1141
|
+
description: fullDescription,
|
|
1142
|
+
status: t.status || "draft",
|
|
1143
|
+
priority: t.priority || "medium",
|
|
1144
|
+
tags: t.tags || [],
|
|
1145
|
+
baseBranch: t.baseBranch || t.base_branch || "main",
|
|
1146
|
+
workspace: t.workspace || "",
|
|
1147
|
+
repository: t.repository || "",
|
|
1148
|
+
draft: t.draft ?? true,
|
|
1149
|
+
});
|
|
1150
|
+
} else if (typeof this._taskStore.addTask === "function") {
|
|
1151
|
+
task = this._taskStore.addTask({
|
|
1152
|
+
id: randomUUID(),
|
|
1153
|
+
title: t.title,
|
|
1154
|
+
description: fullDescription,
|
|
1155
|
+
status: t.status || "draft",
|
|
1156
|
+
priority: t.priority || "medium",
|
|
1157
|
+
tags: t.tags || [],
|
|
1158
|
+
baseBranch: t.baseBranch || t.base_branch || "main",
|
|
1159
|
+
workspace: t.workspace || "",
|
|
1160
|
+
repository: t.repository || "",
|
|
1161
|
+
draft: t.draft ?? true,
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
if (task) {
|
|
1166
|
+
results.created.push({ id: task.id, title: task.title });
|
|
1167
|
+
} else {
|
|
1168
|
+
results.failed.push({ title: t.title, error: "addTask returned null" });
|
|
1169
|
+
}
|
|
1170
|
+
} catch (err) {
|
|
1171
|
+
results.failed.push({ title: t.title || "untitled", error: err.message });
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
console.log(`${TAG} Import: ${results.created.length} created, ${results.failed.length} failed`);
|
|
1176
|
+
sendJson(res, 200, {
|
|
1177
|
+
ok: true,
|
|
1178
|
+
created: results.created.length,
|
|
1179
|
+
failed: results.failed.length,
|
|
1180
|
+
results,
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
|
|
867
1184
|
async _handleError(taskId, body, res) {
|
|
868
1185
|
const { error: errorMsg, pattern, output } = body;
|
|
869
1186
|
|
package/agent-pool.mjs
CHANGED
|
@@ -42,14 +42,26 @@
|
|
|
42
42
|
import { resolve, dirname } from "node:path";
|
|
43
43
|
import { fileURLToPath } from "node:url";
|
|
44
44
|
import { loadConfig } from "./config.mjs";
|
|
45
|
-
import { resolveRepoRoot } from "./repo-root.mjs";
|
|
45
|
+
import { resolveRepoRoot, resolveAgentRepoRoot } from "./repo-root.mjs";
|
|
46
46
|
import { resolveCodexProfileRuntime } from "./codex-model-profiles.mjs";
|
|
47
47
|
|
|
48
48
|
const __filename = fileURLToPath(import.meta.url);
|
|
49
49
|
const __dirname = dirname(__filename);
|
|
50
50
|
|
|
51
|
-
/**
|
|
52
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Repository root for the active workspace.
|
|
53
|
+
* Uses lazy resolution so workspace config changes are picked up per-call.
|
|
54
|
+
* Prefers workspace-aware agent root over raw REPO_ROOT.
|
|
55
|
+
*/
|
|
56
|
+
let _cachedRepoRoot = null;
|
|
57
|
+
function getAgentRepoRoot() {
|
|
58
|
+
if (!_cachedRepoRoot) {
|
|
59
|
+
_cachedRepoRoot = resolveAgentRepoRoot();
|
|
60
|
+
}
|
|
61
|
+
return _cachedRepoRoot;
|
|
62
|
+
}
|
|
63
|
+
/** @deprecated Use getAgentRepoRoot() — kept for backward compat */
|
|
64
|
+
const REPO_ROOT = resolveAgentRepoRoot();
|
|
53
65
|
|
|
54
66
|
/** Default timeout: 6 hours — agents should run until the stream-based watchdog detects real issues */
|
|
55
67
|
const DEFAULT_TIMEOUT_MS = 6 * 60 * 60 * 1000;
|
|
@@ -102,12 +114,74 @@ function shouldFallbackForSdkError(error) {
|
|
|
102
114
|
if (!error) return false;
|
|
103
115
|
const message = String(error).toLowerCase();
|
|
104
116
|
if (!message) return false;
|
|
117
|
+
// SDK not installed / not found
|
|
105
118
|
if (message.includes("not available")) return true;
|
|
119
|
+
// Missing finish_reason (incomplete response)
|
|
106
120
|
if (message.includes("missing finish_reason")) return true;
|
|
107
121
|
if (message.includes("missing") && message.includes("finish_reason")) return true;
|
|
122
|
+
// Model-related errors — SDK doesn't support the requested model
|
|
123
|
+
if (message.includes("not supported")) return true;
|
|
124
|
+
if (message.includes("model not found")) return true;
|
|
125
|
+
if (message.includes("model_not_found")) return true;
|
|
126
|
+
if (message.includes("does not exist")) return true;
|
|
127
|
+
if (message.includes("invalid model")) return true;
|
|
128
|
+
// Auth / key errors — SDK isn't properly configured
|
|
129
|
+
if (message.includes("unauthorized") || message.includes("401")) return true;
|
|
130
|
+
if (message.includes("api key") && (message.includes("invalid") || message.includes("missing") || message.includes("required"))) return true;
|
|
131
|
+
if (message.includes("authentication") && (message.includes("failed") || message.includes("required") || message.includes("error"))) return true;
|
|
132
|
+
if (message.includes("forbidden") || message.includes("403")) return true;
|
|
133
|
+
// Connection errors — SDK endpoint is unreachable
|
|
134
|
+
if (message.includes("econnrefused")) return true;
|
|
135
|
+
if (message.includes("enotfound")) return true;
|
|
136
|
+
if (message.includes("connection refused")) return true;
|
|
108
137
|
return false;
|
|
109
138
|
}
|
|
110
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Check whether an SDK has the minimum prerequisites to be attempted.
|
|
142
|
+
* This prevents wasting time and error logs trying unconfigured SDKs.
|
|
143
|
+
*
|
|
144
|
+
* @param {string} name SDK canonical name
|
|
145
|
+
* @returns {{ ok: boolean, reason: string|null }}
|
|
146
|
+
*/
|
|
147
|
+
function hasSdkPrerequisites(name) {
|
|
148
|
+
// Test mocks bypass prerequisite checks
|
|
149
|
+
if (process.env[`__MOCK_${name.toUpperCase()}_AVAILABLE`] === "1") {
|
|
150
|
+
return { ok: true, reason: null };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (name === "codex") {
|
|
154
|
+
// Codex needs an OpenAI API key (or Azure key, or profile-specific key)
|
|
155
|
+
const hasKey =
|
|
156
|
+
process.env.OPENAI_API_KEY ||
|
|
157
|
+
process.env.AZURE_OPENAI_API_KEY ||
|
|
158
|
+
process.env.CODEX_MODEL_PROFILE_XL_API_KEY ||
|
|
159
|
+
process.env.CODEX_MODEL_PROFILE_M_API_KEY;
|
|
160
|
+
if (!hasKey) {
|
|
161
|
+
return { ok: false, reason: "no API key (OPENAI_API_KEY / AZURE_OPENAI_API_KEY)" };
|
|
162
|
+
}
|
|
163
|
+
return { ok: true, reason: null };
|
|
164
|
+
}
|
|
165
|
+
if (name === "copilot") {
|
|
166
|
+
// Copilot needs either a token or VS Code context
|
|
167
|
+
const hasToken = process.env.COPILOT_CLI_TOKEN || process.env.GITHUB_TOKEN;
|
|
168
|
+
// Copilot also works from VS Code extension context — check for common indicators
|
|
169
|
+
const hasVsCode = process.env.VSCODE_PID || process.env.COPILOT_AGENT_HOST;
|
|
170
|
+
if (!hasToken && !hasVsCode) {
|
|
171
|
+
return { ok: false, reason: "no COPILOT_CLI_TOKEN or GITHUB_TOKEN" };
|
|
172
|
+
}
|
|
173
|
+
return { ok: true, reason: null };
|
|
174
|
+
}
|
|
175
|
+
if (name === "claude") {
|
|
176
|
+
const hasKey = process.env.ANTHROPIC_API_KEY;
|
|
177
|
+
if (!hasKey) {
|
|
178
|
+
return { ok: false, reason: "no ANTHROPIC_API_KEY" };
|
|
179
|
+
}
|
|
180
|
+
return { ok: true, reason: null };
|
|
181
|
+
}
|
|
182
|
+
return { ok: true, reason: null };
|
|
183
|
+
}
|
|
184
|
+
|
|
111
185
|
/**
|
|
112
186
|
* Detect if an error is a context/token overflow that should trigger a
|
|
113
187
|
* new thread instead of retrying on the same (exhausted) session.
|
|
@@ -1452,18 +1526,25 @@ export async function launchEphemeralThread(
|
|
|
1452
1526
|
|
|
1453
1527
|
// ── Try primary SDK ──────────────────────────────────────────────────────
|
|
1454
1528
|
if (primaryAdapter && !isDisabled(primaryName)) {
|
|
1455
|
-
const
|
|
1456
|
-
|
|
1529
|
+
const prereq = hasSdkPrerequisites(primaryName);
|
|
1530
|
+
if (!prereq.ok) {
|
|
1531
|
+
console.warn(
|
|
1532
|
+
`${TAG} primary SDK "${primaryName}" missing prerequisites: ${prereq.reason}; trying fallback chain`,
|
|
1533
|
+
);
|
|
1534
|
+
} else {
|
|
1535
|
+
const launcher = await primaryAdapter.load();
|
|
1536
|
+
const result = await launcher(prompt, cwd, timeoutMs, extra);
|
|
1457
1537
|
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1538
|
+
// If it succeeded, or if the error isn't fallback-worthy, return as-is
|
|
1539
|
+
if (result.success || !shouldFallbackForSdkError(result.error)) {
|
|
1540
|
+
return result;
|
|
1541
|
+
}
|
|
1462
1542
|
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1543
|
+
// Primary SDK not installed — fall through to fallback chain
|
|
1544
|
+
console.warn(
|
|
1545
|
+
`${TAG} primary SDK "${primaryName}" failed (${result.error}); trying fallback chain`,
|
|
1546
|
+
);
|
|
1547
|
+
}
|
|
1467
1548
|
}
|
|
1468
1549
|
|
|
1469
1550
|
// ── Fallback chain ───────────────────────────────────────────────────────
|
|
@@ -1474,6 +1555,15 @@ export async function launchEphemeralThread(
|
|
|
1474
1555
|
const adapter = SDK_ADAPTERS[name];
|
|
1475
1556
|
if (!adapter) continue;
|
|
1476
1557
|
|
|
1558
|
+
// Check prerequisites before wasting time trying an unconfigured SDK
|
|
1559
|
+
const prereq = hasSdkPrerequisites(name);
|
|
1560
|
+
if (!prereq.ok) {
|
|
1561
|
+
console.log(
|
|
1562
|
+
`${TAG} skipping fallback SDK "${name}": ${prereq.reason}`,
|
|
1563
|
+
);
|
|
1564
|
+
continue;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1477
1567
|
console.log(`${TAG} trying fallback SDK: ${name}`);
|
|
1478
1568
|
const launcher = await adapter.load();
|
|
1479
1569
|
const result = await launcher(prompt, cwd, timeoutMs, extra);
|
|
@@ -1485,11 +1575,23 @@ export async function launchEphemeralThread(
|
|
|
1485
1575
|
|
|
1486
1576
|
// ── All SDKs exhausted ───────────────────────────────────────────────────
|
|
1487
1577
|
const triedSdks = SDK_FALLBACK_ORDER.filter((n) => !isDisabled(n));
|
|
1578
|
+
const configuredSdks = triedSdks.filter((n) => hasSdkPrerequisites(n).ok);
|
|
1579
|
+
const skippedSdks = triedSdks.filter((n) => !hasSdkPrerequisites(n).ok);
|
|
1580
|
+
let errorMsg = `${TAG} no SDK available.`;
|
|
1581
|
+
if (configuredSdks.length > 0) {
|
|
1582
|
+
errorMsg += ` Tried: ${configuredSdks.join(", ")}.`;
|
|
1583
|
+
}
|
|
1584
|
+
if (skippedSdks.length > 0) {
|
|
1585
|
+
errorMsg += ` Skipped (missing credentials): ${skippedSdks.map((n) => `${n} (${hasSdkPrerequisites(n).reason})`).join(", ")}.`;
|
|
1586
|
+
}
|
|
1587
|
+
if (triedSdks.length === 0) {
|
|
1588
|
+
errorMsg += " All SDKs are disabled.";
|
|
1589
|
+
}
|
|
1488
1590
|
return {
|
|
1489
1591
|
success: false,
|
|
1490
1592
|
output: "",
|
|
1491
1593
|
items: [],
|
|
1492
|
-
error:
|
|
1594
|
+
error: errorMsg,
|
|
1493
1595
|
sdk: primaryName,
|
|
1494
1596
|
threadId: null,
|
|
1495
1597
|
};
|
package/agent-prompts.mjs
CHANGED
|
@@ -113,6 +113,12 @@ const PROMPT_DEFS = [
|
|
|
113
113
|
filename: "monitor-restart-loop-fix.md",
|
|
114
114
|
description: "Prompt used when monitor/orchestrator enters restart loops.",
|
|
115
115
|
},
|
|
116
|
+
{
|
|
117
|
+
key: "taskManager",
|
|
118
|
+
filename: "task-manager.md",
|
|
119
|
+
description:
|
|
120
|
+
"Task management agent prompt with full CRUD access via CLI and REST API.",
|
|
121
|
+
},
|
|
116
122
|
];
|
|
117
123
|
|
|
118
124
|
export const AGENT_PROMPT_DEFINITIONS = Object.freeze(
|
|
@@ -542,6 +548,38 @@ Constraints:
|
|
|
542
548
|
2. Keep behavior stable and production-safe.
|
|
543
549
|
3. Avoid unrelated refactors.
|
|
544
550
|
4. Prefer small guardrails.
|
|
551
|
+
`,
|
|
552
|
+
taskManager: `# Bosun Task Manager Agent
|
|
553
|
+
|
|
554
|
+
You manage the backlog via CLI, REST API, or Node.js API.
|
|
555
|
+
|
|
556
|
+
## Quick Reference
|
|
557
|
+
|
|
558
|
+
CLI:
|
|
559
|
+
bosun task list [--status s] [--json]
|
|
560
|
+
bosun task create '{"title":"..."}' | --title "..." --priority high
|
|
561
|
+
bosun task get <id> [--json]
|
|
562
|
+
bosun task update <id> --status todo --priority critical
|
|
563
|
+
bosun task delete <id>
|
|
564
|
+
bosun task stats [--json]
|
|
565
|
+
bosun task import <file.json>
|
|
566
|
+
bosun task plan --count N
|
|
567
|
+
|
|
568
|
+
REST API (port 18432):
|
|
569
|
+
GET /api/tasks[?status=todo]
|
|
570
|
+
GET /api/tasks/<id>
|
|
571
|
+
POST /api/tasks/create {"title":"...","description":"...","priority":"high"}
|
|
572
|
+
POST /api/tasks/<id>/update {"status":"todo","priority":"critical"}
|
|
573
|
+
DELETE /api/tasks/<id>
|
|
574
|
+
GET /api/tasks/stats
|
|
575
|
+
POST /api/tasks/import {"tasks":[...]}
|
|
576
|
+
|
|
577
|
+
Task title format: [size] type(scope): description
|
|
578
|
+
Sizes: [xs] [s] [m] [l] [xl]
|
|
579
|
+
Types: feat, fix, docs, refactor, test, chore
|
|
580
|
+
Statuses: draft → todo → inprogress → inreview → done
|
|
581
|
+
|
|
582
|
+
See .bosun/agents/task-manager.md for full documentation.
|
|
545
583
|
`,
|
|
546
584
|
};
|
|
547
585
|
|
package/cli.mjs
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
mkdirSync,
|
|
25
25
|
} from "node:fs";
|
|
26
26
|
import { fileURLToPath } from "node:url";
|
|
27
|
-
import { execFileSync, fork, spawn } from "node:child_process";
|
|
27
|
+
import { execFileSync, execSync, fork, spawn } from "node:child_process";
|
|
28
28
|
import os from "node:os";
|
|
29
29
|
import { createDaemonCrashTracker } from "./daemon-restart-policy.mjs";
|
|
30
30
|
import {
|
|
@@ -119,6 +119,16 @@ function showHelp() {
|
|
|
119
119
|
--workspace-switch <id> Switch active workspace
|
|
120
120
|
--workspace-add-repo Add repo to workspace (interactive)
|
|
121
121
|
|
|
122
|
+
TASK MANAGEMENT
|
|
123
|
+
task list [--status s] [--json] List tasks with optional filters
|
|
124
|
+
task create <json|flags> Create a new task from JSON or flags
|
|
125
|
+
task get <id> [--json] Show task details by ID (prefix match)
|
|
126
|
+
task update <id> <patch> Update task fields (JSON or flags)
|
|
127
|
+
task delete <id> Delete a task
|
|
128
|
+
task stats [--json] Show aggregate task statistics
|
|
129
|
+
task import <file.json> Bulk import tasks from JSON
|
|
130
|
+
task plan [--count N] Trigger AI task planner
|
|
131
|
+
|
|
122
132
|
VIBE-KANBAN
|
|
123
133
|
--no-vk-spawn Don't auto-spawn Vibe-Kanban
|
|
124
134
|
--vk-ensure-interval <ms> VK health check interval (default: 60000)
|
|
@@ -440,7 +450,27 @@ function startDaemon() {
|
|
|
440
450
|
detached: true,
|
|
441
451
|
stdio: "ignore",
|
|
442
452
|
windowsHide: process.platform === "win32",
|
|
443
|
-
env: {
|
|
453
|
+
env: {
|
|
454
|
+
...process.env,
|
|
455
|
+
BOSUN_DAEMON: "1",
|
|
456
|
+
// Propagate the bosun package directory so repo-root detection works
|
|
457
|
+
// even when the daemon child's cwd is not inside a git repo.
|
|
458
|
+
BOSUN_DIR: process.env.BOSUN_DIR || fileURLToPath(new URL(".", import.meta.url)),
|
|
459
|
+
// Propagate REPO_ROOT if available; otherwise resolve from cwd before detaching
|
|
460
|
+
...(process.env.REPO_ROOT
|
|
461
|
+
? {}
|
|
462
|
+
: (() => {
|
|
463
|
+
try {
|
|
464
|
+
const gitRoot = execSync("git rev-parse --show-toplevel", {
|
|
465
|
+
encoding: "utf8",
|
|
466
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
467
|
+
}).trim();
|
|
468
|
+
return gitRoot ? { REPO_ROOT: gitRoot } : {};
|
|
469
|
+
} catch {
|
|
470
|
+
return {};
|
|
471
|
+
}
|
|
472
|
+
})()),
|
|
473
|
+
},
|
|
444
474
|
// Use home dir so spawn never inherits a deleted CWD (e.g. old git worktree)
|
|
445
475
|
cwd: os.homedir(),
|
|
446
476
|
},
|
|
@@ -595,6 +625,15 @@ async function main() {
|
|
|
595
625
|
return;
|
|
596
626
|
}
|
|
597
627
|
|
|
628
|
+
// Handle 'task' subcommand — must come before flag-based routing
|
|
629
|
+
if (args[0] === "task" || args.includes("--task")) {
|
|
630
|
+
const { runTaskCli } = await import("./task-cli.mjs");
|
|
631
|
+
// Pass everything after "task" to the task CLI
|
|
632
|
+
const taskArgs = args[0] === "task" ? args.slice(1) : args.slice(args.indexOf("--task") + 1);
|
|
633
|
+
await runTaskCli(taskArgs);
|
|
634
|
+
process.exit(0);
|
|
635
|
+
}
|
|
636
|
+
|
|
598
637
|
// Handle --doctor
|
|
599
638
|
if (args.includes("--doctor") || args.includes("doctor")) {
|
|
600
639
|
const { runConfigDoctor, formatConfigDoctorReport } =
|