bosun 0.41.0 → 0.41.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +8 -0
- package/README.md +20 -0
- package/agent/agent-event-bus.mjs +248 -6
- package/agent/agent-pool.mjs +125 -28
- package/agent/agent-work-analyzer.mjs +8 -16
- package/agent/retry-queue.mjs +164 -0
- package/bosun.config.example.json +25 -0
- package/bosun.schema.json +825 -183
- package/cli.mjs +59 -5
- package/config/config.mjs +130 -3
- package/infra/monitor.mjs +693 -67
- package/infra/runtime-accumulator.mjs +376 -84
- package/infra/session-tracker.mjs +82 -25
- package/lib/codebase-audit.mjs +133 -18
- package/package.json +23 -4
- package/server/setup-web-server.mjs +25 -0
- package/server/ui-server.mjs +248 -29
- package/setup.mjs +27 -24
- package/shell/codex-shell.mjs +34 -3
- package/shell/copilot-shell.mjs +50 -8
- package/task/msg-hub.mjs +193 -0
- package/task/pipeline.mjs +544 -0
- package/task/task-cli.mjs +38 -2
- package/task/task-executor-pipeline.mjs +143 -0
- package/task/task-executor.mjs +36 -27
- package/telegram/get-telegram-chat-id.mjs +57 -47
- package/ui/components/workspace-switcher.js +7 -7
- package/ui/demo-defaults.js +15694 -10573
- package/ui/modules/settings-schema.js +2 -0
- package/ui/modules/state.js +54 -57
- package/ui/modules/voice-client-sdk.js +375 -36
- package/ui/modules/voice-client.js +140 -31
- package/ui/setup.html +68 -2
- package/ui/styles/components.css +57 -0
- package/ui/styles.css +201 -1
- package/ui/tabs/dashboard.js +74 -0
- package/ui/tabs/logs.js +10 -0
- package/ui/tabs/settings.js +178 -99
- package/ui/tabs/tasks.js +31 -1
- package/ui/tabs/telemetry.js +34 -0
- package/ui/tabs/workflow-canvas-utils.mjs +8 -1
- package/ui/tabs/workflows.js +532 -275
- package/voice/voice-agents-sdk.mjs +1 -1
- package/voice/voice-relay.mjs +6 -6
- package/workflow/declarative-workflows.mjs +145 -0
- package/workflow/msg-hub.mjs +237 -0
- package/workflow/pipeline-workflows.mjs +287 -0
- package/workflow/pipeline.mjs +828 -315
- package/workflow/workflow-cli.mjs +128 -0
- package/workflow/workflow-engine.mjs +329 -17
- package/workflow/workflow-nodes/custom-loader.mjs +250 -0
- package/workflow/workflow-nodes.mjs +1955 -223
- package/workflow/workflow-templates.mjs +26 -8
- package/workflow-templates/agents.mjs +0 -1
- package/workflow-templates/bosun-native.mjs +212 -2
- package/workflow-templates/continuation-loop.mjs +339 -0
- package/workflow-templates/github.mjs +516 -40
- package/workflow-templates/planning.mjs +446 -17
- package/workflow-templates/reliability.mjs +65 -12
- package/workflow-templates/task-batch.mjs +24 -8
- package/workflow-templates/task-lifecycle.mjs +83 -6
- package/workspace/context-cache.mjs +66 -18
- package/workspace/workspace-manager.mjs +2 -1
- package/workflow-templates/issue-continuation.mjs +0 -243
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module workflow/pipeline-workflows
|
|
3
|
+
* @description Config-driven pipeline workflow definitions and Bosun stage runner.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
FanoutPipeline,
|
|
8
|
+
RacePipeline,
|
|
9
|
+
SequentialPipeline,
|
|
10
|
+
toMinimalDescriptor,
|
|
11
|
+
} from "../task/pipeline.mjs";
|
|
12
|
+
|
|
13
|
+
const VALID_PIPELINE_TYPES = new Set(["sequential", "fanout", "race"]);
|
|
14
|
+
|
|
15
|
+
export const BUILTIN_PIPELINE_WORKFLOWS = Object.freeze({
|
|
16
|
+
"code-review-chain": Object.freeze({
|
|
17
|
+
name: "code-review-chain",
|
|
18
|
+
type: "sequential",
|
|
19
|
+
description: "Implement, validate, then review with fresh context at every stage.",
|
|
20
|
+
stages: Object.freeze(["implement", "test", "review"]),
|
|
21
|
+
builtin: true,
|
|
22
|
+
}),
|
|
23
|
+
"parallel-search": Object.freeze({
|
|
24
|
+
name: "parallel-search",
|
|
25
|
+
type: "fanout",
|
|
26
|
+
description: "Broadcast the same search task to multiple agents and collect every result.",
|
|
27
|
+
stages: Object.freeze([
|
|
28
|
+
Object.freeze({ name: "search-codex", sdk: "codex" }),
|
|
29
|
+
Object.freeze({ name: "search-claude", sdk: "claude" }),
|
|
30
|
+
Object.freeze({ name: "search-copilot", sdk: "copilot" }),
|
|
31
|
+
]),
|
|
32
|
+
builtin: true,
|
|
33
|
+
}),
|
|
34
|
+
"consensus-vote": Object.freeze({
|
|
35
|
+
name: "consensus-vote",
|
|
36
|
+
type: "fanout",
|
|
37
|
+
description: "Fan out to multiple agents and compare their independent recommendations.",
|
|
38
|
+
stages: Object.freeze([
|
|
39
|
+
Object.freeze({ name: "vote-codex", sdk: "codex" }),
|
|
40
|
+
Object.freeze({ name: "vote-claude", sdk: "claude" }),
|
|
41
|
+
Object.freeze({ name: "vote-copilot", sdk: "copilot" }),
|
|
42
|
+
]),
|
|
43
|
+
resultStrategy: "consensus-vote",
|
|
44
|
+
builtin: true,
|
|
45
|
+
}),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
function normalizeStageDefinition(stage, index = 0) {
|
|
49
|
+
if (typeof stage === "string") {
|
|
50
|
+
return Object.freeze({
|
|
51
|
+
id: stage,
|
|
52
|
+
name: stage,
|
|
53
|
+
role: stage,
|
|
54
|
+
sdk: "auto",
|
|
55
|
+
model: "auto",
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!stage || typeof stage !== "object") {
|
|
60
|
+
throw new TypeError(`Invalid workflow stage at index ${index}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const name = String(stage.name || stage.id || `stage-${index + 1}`);
|
|
64
|
+
return Object.freeze({
|
|
65
|
+
...stage,
|
|
66
|
+
id: String(stage.id || name),
|
|
67
|
+
name,
|
|
68
|
+
role: String(stage.role || name),
|
|
69
|
+
sdk: String(stage.sdk || "auto"),
|
|
70
|
+
model: String(stage.model || "auto"),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function normalizePipelineWorkflowDefinition(name, workflow, options = {}) {
|
|
75
|
+
if (!workflow || typeof workflow !== "object") {
|
|
76
|
+
throw new TypeError(`Workflow "${name}" must be an object`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const type = String(workflow.type || "sequential").toLowerCase();
|
|
80
|
+
if (!VALID_PIPELINE_TYPES.has(type)) {
|
|
81
|
+
throw new TypeError(`Workflow "${name}" has unsupported type "${type}"`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const stages = Array.isArray(workflow.stages) && workflow.stages.length > 0
|
|
85
|
+
? workflow.stages.map((stage, index) => normalizeStageDefinition(stage, index))
|
|
86
|
+
: Array.isArray(workflow.agents) && workflow.agents.length > 0
|
|
87
|
+
? workflow.agents.map((stage, index) => normalizeStageDefinition(stage, index))
|
|
88
|
+
: [];
|
|
89
|
+
if (stages.length === 0) {
|
|
90
|
+
throw new TypeError(`Workflow "${name}" must declare at least one stage`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return Object.freeze({
|
|
94
|
+
...workflow,
|
|
95
|
+
name,
|
|
96
|
+
type,
|
|
97
|
+
description: String(workflow.description || "").trim(),
|
|
98
|
+
stages: Object.freeze(stages),
|
|
99
|
+
builtin: options.builtin === true || workflow.builtin === true,
|
|
100
|
+
resultStrategy: workflow.resultStrategy ? String(workflow.resultStrategy) : null,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function normalizePipelineWorkflows(rawWorkflows = {}) {
|
|
105
|
+
const normalized = {};
|
|
106
|
+
|
|
107
|
+
for (const [name, workflow] of Object.entries(BUILTIN_PIPELINE_WORKFLOWS)) {
|
|
108
|
+
normalized[name] = normalizePipelineWorkflowDefinition(name, workflow, { builtin: true });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (rawWorkflows && typeof rawWorkflows === "object") {
|
|
112
|
+
for (const [name, workflow] of Object.entries(rawWorkflows)) {
|
|
113
|
+
normalized[name] = normalizePipelineWorkflowDefinition(name, workflow, { builtin: false });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return Object.freeze(normalized);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function listPipelineWorkflows(workflows = {}) {
|
|
121
|
+
return Object.values(workflows)
|
|
122
|
+
.slice()
|
|
123
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function getPipelineWorkflow(name, workflows = {}) {
|
|
127
|
+
return workflows?.[name] || null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function stageInstructionFor(stage) {
|
|
131
|
+
const role = String(stage.role || stage.name || "agent").toLowerCase();
|
|
132
|
+
if (role.includes("implement")) {
|
|
133
|
+
return "Implement the requested change. Return a concise summary, changed files, and any follow-up risks.";
|
|
134
|
+
}
|
|
135
|
+
if (role.includes("test")) {
|
|
136
|
+
return "Validate the current solution. Focus on tests, verification, and defects. Return findings and any failing checks.";
|
|
137
|
+
}
|
|
138
|
+
if (role.includes("review")) {
|
|
139
|
+
return "Review the proposed change independently. Highlight correctness, regressions, and merge readiness.";
|
|
140
|
+
}
|
|
141
|
+
if (role.includes("search")) {
|
|
142
|
+
return "Research the task independently and return the strongest relevant findings with file or branch references.";
|
|
143
|
+
}
|
|
144
|
+
if (role.includes("vote")) {
|
|
145
|
+
return "Independently evaluate the options and return a recommendation, confidence, and short rationale.";
|
|
146
|
+
}
|
|
147
|
+
return "Execute your stage independently with fresh context and return a concise structured summary.";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function buildStagePrompt(workflow, stage, input, context) {
|
|
151
|
+
const descriptor = toMinimalDescriptor(input);
|
|
152
|
+
const priorOutput = context.previousOutput || null;
|
|
153
|
+
const lines = [
|
|
154
|
+
`Workflow: ${workflow.name}`,
|
|
155
|
+
`Stage: ${stage.name}`,
|
|
156
|
+
"",
|
|
157
|
+
stageInstructionFor(stage),
|
|
158
|
+
"",
|
|
159
|
+
"Task descriptor:",
|
|
160
|
+
JSON.stringify(descriptor, null, 2),
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
if (priorOutput) {
|
|
164
|
+
lines.push("", "Prior stage output reference:", JSON.stringify(priorOutput, null, 2));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (stage.prompt) {
|
|
168
|
+
lines.push("", "Stage override:", String(stage.prompt));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
lines.push(
|
|
172
|
+
"",
|
|
173
|
+
"Return JSON if practical with keys: summary, status, paths, branch, confidence.",
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
return lines.join("\n");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function summarizeOutputText(value) {
|
|
180
|
+
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
181
|
+
return String(text || "").trim().replace(/\s+/g, " ").slice(0, 400);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function buildResultDescriptor(input, result, stage) {
|
|
185
|
+
const inherited = toMinimalDescriptor(input);
|
|
186
|
+
const outputText = summarizeOutputText(result?.output ?? result);
|
|
187
|
+
const descriptor = {
|
|
188
|
+
...inherited,
|
|
189
|
+
summary: outputText || inherited.summary || `${stage.name} completed`,
|
|
190
|
+
stage: stage.name,
|
|
191
|
+
};
|
|
192
|
+
const resultPaths = result?.paths || result?.files || result?.filePaths;
|
|
193
|
+
if (Array.isArray(resultPaths) && resultPaths.length > 0) {
|
|
194
|
+
descriptor.paths = resultPaths.slice(0, 50).map((entry) => String(entry));
|
|
195
|
+
}
|
|
196
|
+
return descriptor;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function runBosunStage(workflow, stage, input, context, options = {}) {
|
|
200
|
+
if (typeof options.runStage === "function") {
|
|
201
|
+
return options.runStage(stage, input, context, workflow);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const { launchOrResumeThread } = await import("../agent/agent-pool.mjs");
|
|
205
|
+
const prompt = buildStagePrompt(workflow, stage, input, context);
|
|
206
|
+
const cwd = String(stage.cwd || input?.repoRoot || input?.cwd || options.repoRoot || process.cwd());
|
|
207
|
+
const timeoutMs = Number(stage.timeoutMs || options.timeoutMs || 60 * 60 * 1000);
|
|
208
|
+
const result = await launchOrResumeThread(prompt, cwd, timeoutMs, {
|
|
209
|
+
sdk: String(stage.sdk || options.sdk || "auto"),
|
|
210
|
+
model: String(stage.model || options.model || "auto"),
|
|
211
|
+
sessionType: "pipeline-stage",
|
|
212
|
+
});
|
|
213
|
+
return {
|
|
214
|
+
...result,
|
|
215
|
+
descriptor: buildResultDescriptor(input, result, stage),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function selectPipelineFactory(type) {
|
|
220
|
+
if (type === "fanout") return FanoutPipeline;
|
|
221
|
+
if (type === "race") return RacePipeline;
|
|
222
|
+
return SequentialPipeline;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function buildConsensusSummary(outputs = []) {
|
|
226
|
+
const buckets = new Map();
|
|
227
|
+
for (const output of outputs) {
|
|
228
|
+
if (!output || output.success === false) continue;
|
|
229
|
+
const key = summarizeOutputText(output.output).toLowerCase();
|
|
230
|
+
if (!key) continue;
|
|
231
|
+
const bucket = buckets.get(key) || {
|
|
232
|
+
count: 0,
|
|
233
|
+
stages: [],
|
|
234
|
+
summary: summarizeOutputText(output.output),
|
|
235
|
+
};
|
|
236
|
+
bucket.count += 1;
|
|
237
|
+
bucket.stages.push(output.stageName);
|
|
238
|
+
buckets.set(key, bucket);
|
|
239
|
+
}
|
|
240
|
+
return Array.from(buckets.values()).sort((left, right) => right.count - left.count)[0] || null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function createPipelineWorkflowRunner(workflow, options = {}) {
|
|
244
|
+
const stages = workflow.stages.map((stage) => ({
|
|
245
|
+
...stage,
|
|
246
|
+
async run(input, context) {
|
|
247
|
+
return runBosunStage(workflow, stage, input, context, options);
|
|
248
|
+
},
|
|
249
|
+
}));
|
|
250
|
+
const factory = selectPipelineFactory(workflow.type);
|
|
251
|
+
return factory(stages, {
|
|
252
|
+
createContext(context) {
|
|
253
|
+
return {
|
|
254
|
+
...context,
|
|
255
|
+
workflow: workflow.name,
|
|
256
|
+
};
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export async function runPipelineWorkflow(name, input, options = {}) {
|
|
262
|
+
const workflows = options.workflows || normalizePipelineWorkflows(options.config?.workflows || {});
|
|
263
|
+
const workflow = getPipelineWorkflow(name, workflows);
|
|
264
|
+
if (!workflow) {
|
|
265
|
+
throw new Error(`Unknown workflow "${name}"`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const runner = createPipelineWorkflowRunner(workflow, options);
|
|
269
|
+
const result = await runner.run(input, options.runtime || {});
|
|
270
|
+
if (workflow.resultStrategy === "consensus-vote") {
|
|
271
|
+
return {
|
|
272
|
+
...result,
|
|
273
|
+
consensus: buildConsensusSummary(result.outputs),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
return result;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export default {
|
|
280
|
+
BUILTIN_PIPELINE_WORKFLOWS,
|
|
281
|
+
normalizePipelineWorkflowDefinition,
|
|
282
|
+
normalizePipelineWorkflows,
|
|
283
|
+
listPipelineWorkflows,
|
|
284
|
+
getPipelineWorkflow,
|
|
285
|
+
createPipelineWorkflowRunner,
|
|
286
|
+
runPipelineWorkflow,
|
|
287
|
+
};
|