bosun 0.35.2 → 0.35.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.
- package/README.md +14 -1
- package/agent-hooks.mjs +7 -1
- package/agent-pool.mjs +16 -0
- package/agent-prompts.mjs +190 -4
- package/agent-sdk.mjs +6 -1
- package/agent-work-analyzer.mjs +48 -9
- package/autofix.mjs +32 -18
- package/bosun.schema.json +1 -1
- package/kanban-adapter.mjs +62 -12
- package/monitor.mjs +25 -6
- package/opencode-shell.mjs +881 -0
- package/package.json +5 -2
- package/primary-agent.mjs +43 -0
- package/session-tracker.mjs +55 -1
- package/setup.mjs +33 -4
- package/task-executor.mjs +43 -14
- package/ui/app.js +10 -7
- package/ui/components/chat-view.js +31 -9
- package/ui/components/session-list.js +20 -4
- package/ui/demo.html +49 -0
- package/ui/modules/router.js +2 -0
- package/ui/tabs/agents.js +66 -8
- package/ui/tabs/workflows.js +83 -0
- package/ui-server.mjs +236 -5
- package/workflow-engine.mjs +664 -10
- package/workflow-nodes.mjs +250 -1
- package/workflow-templates/github.mjs +389 -71
- package/workflow-templates/planning.mjs +31 -11
- package/workflow-templates.mjs +219 -2
package/workflow-templates.mjs
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* listTemplates() — List all available templates
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
|
-
import { randomUUID } from "node:crypto";
|
|
32
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
33
33
|
|
|
34
34
|
// ── Re-export helpers for external consumers ────────────────────────────────
|
|
35
35
|
export { node, edge, resetLayout } from "./workflow-templates/_helpers.mjs";
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
PR_CONFLICT_RESOLVER_TEMPLATE,
|
|
44
44
|
STALE_PR_REAPER_TEMPLATE,
|
|
45
45
|
RELEASE_DRAFTER_TEMPLATE,
|
|
46
|
+
BOSUN_PR_WATCHDOG_TEMPLATE,
|
|
46
47
|
} from "./workflow-templates/github.mjs";
|
|
47
48
|
|
|
48
49
|
// Agents
|
|
@@ -95,6 +96,7 @@ export {
|
|
|
95
96
|
PR_CONFLICT_RESOLVER_TEMPLATE,
|
|
96
97
|
STALE_PR_REAPER_TEMPLATE,
|
|
97
98
|
RELEASE_DRAFTER_TEMPLATE,
|
|
99
|
+
BOSUN_PR_WATCHDOG_TEMPLATE,
|
|
98
100
|
FRONTEND_AGENT_TEMPLATE,
|
|
99
101
|
REVIEW_AGENT_TEMPLATE,
|
|
100
102
|
CUSTOM_AGENT_TEMPLATE,
|
|
@@ -141,6 +143,7 @@ export const WORKFLOW_TEMPLATES = Object.freeze([
|
|
|
141
143
|
PR_CONFLICT_RESOLVER_TEMPLATE,
|
|
142
144
|
STALE_PR_REAPER_TEMPLATE,
|
|
143
145
|
RELEASE_DRAFTER_TEMPLATE,
|
|
146
|
+
BOSUN_PR_WATCHDOG_TEMPLATE,
|
|
144
147
|
// ── Agents ──
|
|
145
148
|
REVIEW_AGENT_TEMPLATE,
|
|
146
149
|
FRONTEND_AGENT_TEMPLATE,
|
|
@@ -173,6 +176,216 @@ export const WORKFLOW_TEMPLATES = Object.freeze([
|
|
|
173
176
|
const _TEMPLATE_BY_ID = new Map(
|
|
174
177
|
WORKFLOW_TEMPLATES.map((template) => [template.id, template]),
|
|
175
178
|
);
|
|
179
|
+
const TEMPLATE_STATE_VERSION = 1;
|
|
180
|
+
|
|
181
|
+
function stableNormalize(value) {
|
|
182
|
+
if (Array.isArray(value)) {
|
|
183
|
+
return value.map((entry) => stableNormalize(entry));
|
|
184
|
+
}
|
|
185
|
+
if (value && typeof value === "object") {
|
|
186
|
+
const normalized = {};
|
|
187
|
+
for (const key of Object.keys(value).sort()) {
|
|
188
|
+
normalized[key] = stableNormalize(value[key]);
|
|
189
|
+
}
|
|
190
|
+
return normalized;
|
|
191
|
+
}
|
|
192
|
+
return value;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function stableStringify(value) {
|
|
196
|
+
return JSON.stringify(stableNormalize(value));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function hashContent(value) {
|
|
200
|
+
return createHash("sha256").update(stableStringify(value)).digest("hex");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function toWorkflowFingerprintPayload(def = {}) {
|
|
204
|
+
return {
|
|
205
|
+
name: def.name || "",
|
|
206
|
+
description: def.description || "",
|
|
207
|
+
category: def.category || "custom",
|
|
208
|
+
trigger: def.trigger || "",
|
|
209
|
+
variables: def.variables || {},
|
|
210
|
+
nodes: def.nodes || [],
|
|
211
|
+
edges: def.edges || [],
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function computeWorkflowFingerprint(def = {}) {
|
|
216
|
+
return hashContent(toWorkflowFingerprintPayload(def));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function cloneTemplateDefinition(template) {
|
|
220
|
+
return JSON.parse(JSON.stringify(template));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function getTemplateVersion(templateId) {
|
|
224
|
+
const template = getTemplate(templateId);
|
|
225
|
+
if (!template) return null;
|
|
226
|
+
return computeWorkflowFingerprint(template).slice(0, 12);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function deriveTemplateState(def, template) {
|
|
230
|
+
const nowIso = new Date().toISOString();
|
|
231
|
+
const currentFingerprint = computeWorkflowFingerprint(def);
|
|
232
|
+
const templateFingerprint = computeWorkflowFingerprint(template);
|
|
233
|
+
const previousState = def?.metadata?.templateState || {};
|
|
234
|
+
|
|
235
|
+
const installedTemplateFingerprint = typeof previousState.installedTemplateFingerprint === "string"
|
|
236
|
+
? previousState.installedTemplateFingerprint
|
|
237
|
+
: (currentFingerprint === templateFingerprint ? templateFingerprint : null);
|
|
238
|
+
|
|
239
|
+
const installedFingerprint = typeof previousState.installedFingerprint === "string"
|
|
240
|
+
? previousState.installedFingerprint
|
|
241
|
+
: currentFingerprint;
|
|
242
|
+
|
|
243
|
+
const isCustomized = currentFingerprint !== installedFingerprint;
|
|
244
|
+
const updateAvailable = installedTemplateFingerprint
|
|
245
|
+
? installedTemplateFingerprint !== templateFingerprint
|
|
246
|
+
: false;
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
stateVersion: TEMPLATE_STATE_VERSION,
|
|
250
|
+
templateId: template.id,
|
|
251
|
+
templateName: template.name,
|
|
252
|
+
templateVersion: templateFingerprint.slice(0, 12),
|
|
253
|
+
templateFingerprint,
|
|
254
|
+
installedTemplateFingerprint,
|
|
255
|
+
installedTemplateVersion: installedTemplateFingerprint
|
|
256
|
+
? installedTemplateFingerprint.slice(0, 12)
|
|
257
|
+
: null,
|
|
258
|
+
installedFingerprint,
|
|
259
|
+
currentFingerprint,
|
|
260
|
+
isCustomized,
|
|
261
|
+
updateAvailable,
|
|
262
|
+
refreshedAt: nowIso,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function applyWorkflowTemplateState(def = {}) {
|
|
267
|
+
if (!def || typeof def !== "object") return def;
|
|
268
|
+
const templateId = String(def?.metadata?.installedFrom || "").trim();
|
|
269
|
+
if (!templateId) return def;
|
|
270
|
+
const template = getTemplate(templateId);
|
|
271
|
+
if (!template) return def;
|
|
272
|
+
if (!def.metadata || typeof def.metadata !== "object") def.metadata = {};
|
|
273
|
+
def.metadata.templateState = deriveTemplateState(def, template);
|
|
274
|
+
return def;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function makeUpdatedWorkflowFromTemplate(existing, template, mode = "replace") {
|
|
278
|
+
const templateClone = cloneTemplateDefinition(template);
|
|
279
|
+
const nowIso = new Date().toISOString();
|
|
280
|
+
const mergedVariables = {
|
|
281
|
+
...(templateClone.variables || {}),
|
|
282
|
+
...(existing.variables || {}),
|
|
283
|
+
};
|
|
284
|
+
const next = {
|
|
285
|
+
...templateClone,
|
|
286
|
+
id: mode === "copy" ? randomUUID() : existing.id,
|
|
287
|
+
name: mode === "copy" ? `${existing.name} (Updated)` : existing.name,
|
|
288
|
+
enabled: existing.enabled !== false,
|
|
289
|
+
variables: mergedVariables,
|
|
290
|
+
metadata: {
|
|
291
|
+
...(existing.metadata || {}),
|
|
292
|
+
...(templateClone.metadata || {}),
|
|
293
|
+
installedFrom: template.id,
|
|
294
|
+
templateUpdatedAt: nowIso,
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
delete next.metadata.templateState;
|
|
298
|
+
if (mode === "copy") {
|
|
299
|
+
next.metadata.createdAt = nowIso;
|
|
300
|
+
next.metadata.updatedAt = nowIso;
|
|
301
|
+
}
|
|
302
|
+
return applyWorkflowTemplateState(next);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function updateWorkflowFromTemplate(engine, workflowId, opts = {}) {
|
|
306
|
+
const mode = String(opts.mode || "replace").toLowerCase();
|
|
307
|
+
if (!["replace", "copy"].includes(mode)) {
|
|
308
|
+
throw new Error(`Unsupported template update mode "${mode}"`);
|
|
309
|
+
}
|
|
310
|
+
const existing = engine.get(workflowId);
|
|
311
|
+
if (!existing) throw new Error(`Workflow "${workflowId}" not found`);
|
|
312
|
+
const templateId = String(existing?.metadata?.installedFrom || "").trim();
|
|
313
|
+
if (!templateId) throw new Error(`Workflow "${workflowId}" is not template-backed`);
|
|
314
|
+
const template = getTemplate(templateId);
|
|
315
|
+
if (!template) throw new Error(`Template "${templateId}" not found`);
|
|
316
|
+
|
|
317
|
+
const hydrated = applyWorkflowTemplateState(existing);
|
|
318
|
+
if (mode === "replace" && hydrated?.metadata?.templateState?.isCustomized && opts.force !== true) {
|
|
319
|
+
throw new Error("Workflow has custom changes; pass force=true to replace it");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const next = makeUpdatedWorkflowFromTemplate(hydrated, template, mode);
|
|
323
|
+
return engine.save(next);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function reconcileInstalledTemplates(engine, opts = {}) {
|
|
327
|
+
const autoUpdateUnmodified = opts.autoUpdateUnmodified !== false;
|
|
328
|
+
const workflows = engine.list();
|
|
329
|
+
const result = {
|
|
330
|
+
scanned: 0,
|
|
331
|
+
metadataUpdated: 0,
|
|
332
|
+
autoUpdated: 0,
|
|
333
|
+
updateAvailable: [],
|
|
334
|
+
customized: [],
|
|
335
|
+
updatedWorkflowIds: [],
|
|
336
|
+
errors: [],
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
for (const summary of workflows) {
|
|
340
|
+
const wfId = summary?.id;
|
|
341
|
+
if (!wfId) continue;
|
|
342
|
+
const def = engine.get(wfId);
|
|
343
|
+
if (!def?.metadata?.installedFrom) continue;
|
|
344
|
+
result.scanned += 1;
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
const before = stableStringify(def.metadata?.templateState || null);
|
|
348
|
+
applyWorkflowTemplateState(def);
|
|
349
|
+
const state = def.metadata?.templateState || null;
|
|
350
|
+
const after = stableStringify(state);
|
|
351
|
+
if (before !== after) {
|
|
352
|
+
engine.save(def);
|
|
353
|
+
result.metadataUpdated += 1;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (!state) continue;
|
|
357
|
+
if (state.isCustomized) {
|
|
358
|
+
result.customized.push({
|
|
359
|
+
workflowId: def.id,
|
|
360
|
+
name: def.name,
|
|
361
|
+
templateId: state.templateId,
|
|
362
|
+
updateAvailable: state.updateAvailable === true,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
if (state.updateAvailable === true) {
|
|
366
|
+
result.updateAvailable.push({
|
|
367
|
+
workflowId: def.id,
|
|
368
|
+
name: def.name,
|
|
369
|
+
templateId: state.templateId,
|
|
370
|
+
isCustomized: state.isCustomized === true,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (autoUpdateUnmodified && state.updateAvailable === true && state.isCustomized !== true) {
|
|
375
|
+
const saved = updateWorkflowFromTemplate(engine, def.id, { mode: "replace", force: true });
|
|
376
|
+
result.autoUpdated += 1;
|
|
377
|
+
result.updatedWorkflowIds.push(saved.id);
|
|
378
|
+
}
|
|
379
|
+
} catch (err) {
|
|
380
|
+
result.errors.push({
|
|
381
|
+
workflowId: wfId,
|
|
382
|
+
error: err.message,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return result;
|
|
388
|
+
}
|
|
176
389
|
|
|
177
390
|
/**
|
|
178
391
|
* Setup workflow profiles used by `bosun --setup`.
|
|
@@ -284,6 +497,7 @@ export function getTemplate(id) {
|
|
|
284
497
|
export function listTemplates() {
|
|
285
498
|
return WORKFLOW_TEMPLATES.map((t) => {
|
|
286
499
|
const cat = TEMPLATE_CATEGORIES[t.category] || TEMPLATE_CATEGORIES.custom;
|
|
500
|
+
const fingerprint = computeWorkflowFingerprint(t);
|
|
287
501
|
return {
|
|
288
502
|
id: t.id,
|
|
289
503
|
name: t.name,
|
|
@@ -295,6 +509,8 @@ export function listTemplates() {
|
|
|
295
509
|
tags: t.metadata?.tags || [],
|
|
296
510
|
nodeCount: t.nodes?.length || 0,
|
|
297
511
|
edgeCount: t.edges?.length || 0,
|
|
512
|
+
version: fingerprint.slice(0, 12),
|
|
513
|
+
fingerprint,
|
|
298
514
|
replaces: t.metadata?.replaces || null,
|
|
299
515
|
recommended: t.recommended === true,
|
|
300
516
|
enabled: t.enabled !== false,
|
|
@@ -381,7 +597,7 @@ export function installTemplate(templateId, engine, overrides = {}) {
|
|
|
381
597
|
}
|
|
382
598
|
|
|
383
599
|
// Deep clone
|
|
384
|
-
const def =
|
|
600
|
+
const def = cloneTemplateDefinition(template);
|
|
385
601
|
def.id = randomUUID(); // New unique ID
|
|
386
602
|
def.metadata = {
|
|
387
603
|
...def.metadata,
|
|
@@ -395,6 +611,7 @@ export function installTemplate(templateId, engine, overrides = {}) {
|
|
|
395
611
|
def.variables = { ...def.variables, ...overrides };
|
|
396
612
|
}
|
|
397
613
|
|
|
614
|
+
applyWorkflowTemplateState(def);
|
|
398
615
|
return engine.save(def);
|
|
399
616
|
}
|
|
400
617
|
|