bosun 0.41.2 → 0.41.3
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 +1 -1
- package/agent/agent-prompt-catalog.mjs +971 -0
- package/agent/agent-prompts.mjs +2 -970
- package/agent/agent-supervisor.mjs +6 -3
- package/agent/autofix-git.mjs +33 -0
- package/agent/autofix-prompts.mjs +151 -0
- package/agent/autofix.mjs +11 -175
- package/agent/bosun-skills.mjs +3 -2
- package/bosun.config.example.json +17 -0
- package/bosun.schema.json +87 -188
- package/cli.mjs +34 -1
- package/config/config-doctor.mjs +5 -250
- package/config/config-file-names.mjs +5 -0
- package/config/config.mjs +89 -493
- package/config/executor-config.mjs +493 -0
- package/config/repo-root.mjs +1 -2
- package/config/workspace-health.mjs +242 -0
- package/git/git-safety.mjs +15 -0
- package/github/github-oauth-portal.mjs +46 -0
- package/infra/library-manager-utils.mjs +22 -0
- package/infra/library-manager-well-known-sources.mjs +578 -0
- package/infra/library-manager.mjs +512 -1030
- package/infra/monitor.mjs +28 -9
- package/infra/session-tracker.mjs +10 -7
- package/kanban/kanban-adapter.mjs +17 -1
- package/lib/codebase-audit-manifests.mjs +117 -0
- package/lib/codebase-audit.mjs +18 -115
- package/package.json +18 -3
- package/server/ui-server.mjs +1194 -79
- package/shell/codex-config-file.mjs +178 -0
- package/shell/codex-config.mjs +538 -575
- package/task/task-cli.mjs +54 -3
- package/task/task-executor.mjs +143 -13
- package/task/task-store.mjs +409 -1
- package/telegram/telegram-bot.mjs +127 -0
- package/tools/apply-pr-suggestions.mjs +401 -0
- package/tools/syntax-check.mjs +21 -9
- package/ui/app.js +3 -14
- package/ui/components/kanban-board.js +227 -4
- package/ui/components/session-list.js +85 -5
- package/ui/demo-defaults.js +334 -80
- package/ui/demo.html +155 -0
- package/ui/modules/session-api.js +96 -0
- package/ui/modules/settings-schema.js +1 -2
- package/ui/modules/state.js +21 -3
- package/ui/setup.html +4 -5
- package/ui/styles/components.css +58 -4
- package/ui/tabs/agents.js +12 -15
- package/ui/tabs/control.js +1 -0
- package/ui/tabs/library.js +484 -22
- package/ui/tabs/manual-flows.js +105 -29
- package/ui/tabs/tasks.js +785 -140
- package/ui/tabs/telemetry.js +129 -11
- package/ui/tabs/workflow-canvas-utils.mjs +130 -0
- package/ui/tabs/workflows.js +293 -23
- package/voice/voice-tool-definitions.mjs +757 -0
- package/voice/voice-tools.mjs +34 -778
- package/workflow/manual-flow-audit.mjs +165 -0
- package/workflow/manual-flows.mjs +164 -259
- package/workflow/workflow-engine.mjs +147 -58
- package/workflow/workflow-nodes/definitions.mjs +1207 -0
- package/workflow/workflow-nodes/transforms.mjs +612 -0
- package/workflow/workflow-nodes.mjs +304 -52
- package/workflow/workflow-templates.mjs +313 -191
- package/workflow-templates/_helpers.mjs +154 -0
- package/workflow-templates/agents.mjs +61 -4
- package/workflow-templates/code-quality.mjs +7 -7
- package/workflow-templates/github.mjs +20 -10
- package/workflow-templates/task-batch.mjs +20 -9
- package/workflow-templates/task-lifecycle.mjs +31 -6
- package/workspace/worktree-manager.mjs +277 -3
|
@@ -32,9 +32,10 @@
|
|
|
32
32
|
|
|
33
33
|
import { createHash, randomUUID } from "node:crypto";
|
|
34
34
|
import { detectProjectStack, getCommandPresets } from "./project-detection.mjs";
|
|
35
|
+
import { normalizeTemplateLayoutInPlace } from "../workflow-templates/_helpers.mjs";
|
|
35
36
|
|
|
36
37
|
// ── Re-export helpers for external consumers ────────────────────────────────
|
|
37
|
-
export { node, edge, resetLayout } from "../workflow-templates/_helpers.mjs";
|
|
38
|
+
export { node, edge, resetLayout, normalizeTemplateLayoutInPlace } from "../workflow-templates/_helpers.mjs";
|
|
38
39
|
|
|
39
40
|
// ── Import templates from category modules ──────────────────────────────────
|
|
40
41
|
|
|
@@ -252,7 +253,7 @@ export const TEMPLATE_CATEGORIES = Object.freeze({
|
|
|
252
253
|
custom: { label: "Custom", icon: ":settings:", order: 13 },
|
|
253
254
|
});
|
|
254
255
|
|
|
255
|
-
|
|
256
|
+
const BUILTIN_WORKFLOW_TEMPLATES = [
|
|
256
257
|
// ── GitHub ──
|
|
257
258
|
PR_MERGE_STRATEGY_TEMPLATE,
|
|
258
259
|
PR_TRIAGE_TEMPLATE,
|
|
@@ -332,248 +333,347 @@ export const WORKFLOW_TEMPLATES = Object.freeze([
|
|
|
332
333
|
INLINE_WORKFLOW_COMPOSITION_TEMPLATE,
|
|
333
334
|
MCP_TO_BOSUN_BRIDGE_TEMPLATE,
|
|
334
335
|
GIT_HEALTH_PIPELINE_TEMPLATE,
|
|
335
|
-
]
|
|
336
|
+
];
|
|
337
|
+
|
|
338
|
+
for (const template of BUILTIN_WORKFLOW_TEMPLATES) {
|
|
339
|
+
normalizeTemplateLayoutInPlace(template);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export const WORKFLOW_TEMPLATES = Object.freeze(BUILTIN_WORKFLOW_TEMPLATES);
|
|
336
343
|
|
|
337
344
|
const _TEMPLATE_BY_ID = new Map(
|
|
338
345
|
WORKFLOW_TEMPLATES.map((template) => [template.id, template]),
|
|
339
346
|
);
|
|
340
|
-
|
|
347
|
+
function createWorkflowTemplateState({ getTemplate, cloneTemplateDefinition }) {
|
|
348
|
+
const templateStateVersion = 1;
|
|
349
|
+
|
|
350
|
+
function toFingerprintNode(node = {}) {
|
|
351
|
+
if (!node || typeof node !== "object") return node;
|
|
352
|
+
const next = JSON.parse(JSON.stringify(node));
|
|
353
|
+
delete next.position;
|
|
354
|
+
delete next.inputPorts;
|
|
355
|
+
delete next.outputPorts;
|
|
356
|
+
delete next.width;
|
|
357
|
+
delete next.height;
|
|
358
|
+
return next;
|
|
359
|
+
}
|
|
341
360
|
|
|
342
|
-
function
|
|
343
|
-
|
|
344
|
-
|
|
361
|
+
function toFingerprintEdge(edgeDef = {}) {
|
|
362
|
+
if (!edgeDef || typeof edgeDef !== "object") return edgeDef;
|
|
363
|
+
const next = JSON.parse(JSON.stringify(edgeDef));
|
|
364
|
+
next.sourcePort = String(next.sourcePort || "default").trim() || "default";
|
|
365
|
+
next.targetPort = String(next.targetPort || "default").trim() || "default";
|
|
366
|
+
delete next.sourcePortType;
|
|
367
|
+
delete next.targetPortType;
|
|
368
|
+
return next;
|
|
345
369
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
370
|
+
|
|
371
|
+
function stableNormalize(value) {
|
|
372
|
+
if (Array.isArray(value)) {
|
|
373
|
+
return value.map((entry) => stableNormalize(entry));
|
|
350
374
|
}
|
|
351
|
-
|
|
375
|
+
if (value && typeof value === "object") {
|
|
376
|
+
const normalized = {};
|
|
377
|
+
for (const key of Object.keys(value).sort()) {
|
|
378
|
+
normalized[key] = stableNormalize(value[key]);
|
|
379
|
+
}
|
|
380
|
+
return normalized;
|
|
381
|
+
}
|
|
382
|
+
return value;
|
|
352
383
|
}
|
|
353
|
-
return value;
|
|
354
|
-
}
|
|
355
384
|
|
|
356
|
-
function stableStringify(value) {
|
|
357
|
-
|
|
358
|
-
}
|
|
385
|
+
function stableStringify(value) {
|
|
386
|
+
return JSON.stringify(stableNormalize(value));
|
|
387
|
+
}
|
|
359
388
|
|
|
360
|
-
function hashContent(value) {
|
|
361
|
-
|
|
362
|
-
}
|
|
389
|
+
function hashContent(value) {
|
|
390
|
+
return createHash("sha256").update(stableStringify(value)).digest("hex");
|
|
391
|
+
}
|
|
363
392
|
|
|
364
|
-
function toWorkflowFingerprintPayload(def = {}) {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
}
|
|
393
|
+
function toWorkflowFingerprintPayload(def = {}) {
|
|
394
|
+
return {
|
|
395
|
+
name: def.name || "",
|
|
396
|
+
description: def.description || "",
|
|
397
|
+
category: def.category || "custom",
|
|
398
|
+
trigger: def.trigger || "",
|
|
399
|
+
variables: def.variables || {},
|
|
400
|
+
nodes: Array.isArray(def.nodes) ? def.nodes.map((node) => toFingerprintNode(node)) : [],
|
|
401
|
+
edges: Array.isArray(def.edges) ? def.edges.map((edgeDef) => toFingerprintEdge(edgeDef)) : [],
|
|
402
|
+
};
|
|
403
|
+
}
|
|
375
404
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
}
|
|
405
|
+
function computeWorkflowFingerprint(def = {}) {
|
|
406
|
+
return hashContent(toWorkflowFingerprintPayload(def));
|
|
407
|
+
}
|
|
379
408
|
|
|
380
|
-
function
|
|
381
|
-
|
|
382
|
-
|
|
409
|
+
function deriveTemplateState(def, template) {
|
|
410
|
+
const nowIso = new Date().toISOString();
|
|
411
|
+
const currentFingerprint = computeWorkflowFingerprint(def);
|
|
412
|
+
const templateFingerprint = computeWorkflowFingerprint(template);
|
|
413
|
+
const previousState = def?.metadata?.templateState || {};
|
|
383
414
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
415
|
+
const installedTemplateFingerprint = typeof previousState.installedTemplateFingerprint === "string"
|
|
416
|
+
? previousState.installedTemplateFingerprint
|
|
417
|
+
: (currentFingerprint === templateFingerprint ? templateFingerprint : null);
|
|
418
|
+
|
|
419
|
+
const installedFingerprint = typeof previousState.installedFingerprint === "string"
|
|
420
|
+
? previousState.installedFingerprint
|
|
421
|
+
: currentFingerprint;
|
|
422
|
+
|
|
423
|
+
const isCustomized = currentFingerprint !== installedFingerprint;
|
|
424
|
+
const updateAvailable = installedTemplateFingerprint
|
|
425
|
+
? installedTemplateFingerprint !== templateFingerprint
|
|
426
|
+
: false;
|
|
427
|
+
|
|
428
|
+
return {
|
|
429
|
+
stateVersion: templateStateVersion,
|
|
430
|
+
templateId: template.id,
|
|
431
|
+
templateName: template.name,
|
|
432
|
+
templateVersion: templateFingerprint.slice(0, 12),
|
|
433
|
+
templateFingerprint,
|
|
434
|
+
installedTemplateFingerprint,
|
|
435
|
+
installedTemplateVersion: installedTemplateFingerprint
|
|
436
|
+
? installedTemplateFingerprint.slice(0, 12)
|
|
437
|
+
: null,
|
|
438
|
+
installedFingerprint,
|
|
439
|
+
currentFingerprint,
|
|
440
|
+
isCustomized,
|
|
441
|
+
updateAvailable,
|
|
442
|
+
refreshedAt: nowIso,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function applyWorkflowTemplateState(def = {}) {
|
|
447
|
+
if (!def || typeof def !== "object") return def;
|
|
448
|
+
const templateId = String(def?.metadata?.installedFrom || "").trim();
|
|
449
|
+
if (!templateId) return def;
|
|
450
|
+
const template = getTemplate(templateId);
|
|
451
|
+
if (!template) return def;
|
|
452
|
+
if (!def.metadata || typeof def.metadata !== "object") def.metadata = {};
|
|
453
|
+
def.metadata.templateState = deriveTemplateState(def, template);
|
|
454
|
+
return def;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function makeUpdatedWorkflowFromTemplate(existing, template, mode = "replace") {
|
|
458
|
+
const templateClone = cloneTemplateDefinition(template);
|
|
459
|
+
const nowIso = new Date().toISOString();
|
|
460
|
+
const mergedVariables = {
|
|
461
|
+
...(templateClone.variables || {}),
|
|
462
|
+
...(existing.variables || {}),
|
|
463
|
+
};
|
|
464
|
+
const next = {
|
|
465
|
+
...templateClone,
|
|
466
|
+
id: mode === "copy" ? randomUUID() : existing.id,
|
|
467
|
+
name: mode === "copy" ? `${existing.name} (Updated)` : existing.name,
|
|
468
|
+
enabled: existing.enabled !== false,
|
|
469
|
+
variables: mergedVariables,
|
|
470
|
+
metadata: {
|
|
471
|
+
...(existing.metadata || {}),
|
|
472
|
+
...(templateClone.metadata || {}),
|
|
473
|
+
installedFrom: template.id,
|
|
474
|
+
templateUpdatedAt: nowIso,
|
|
475
|
+
},
|
|
476
|
+
};
|
|
477
|
+
delete next.metadata.templateState;
|
|
478
|
+
if (mode === "copy") {
|
|
479
|
+
next.metadata.createdAt = nowIso;
|
|
480
|
+
next.metadata.updatedAt = nowIso;
|
|
481
|
+
}
|
|
482
|
+
return applyWorkflowTemplateState(next);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function updateWorkflowFromTemplate(engine, workflowId, opts = {}) {
|
|
486
|
+
const mode = String(opts.mode || "replace").toLowerCase();
|
|
487
|
+
if (!["replace", "copy"].includes(mode)) {
|
|
488
|
+
throw new Error(`Unsupported template update mode "${mode}"`);
|
|
489
|
+
}
|
|
389
490
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
491
|
+
const existing = engine.get(workflowId);
|
|
492
|
+
if (!existing) throw new Error(`Workflow "${workflowId}" not found`);
|
|
493
|
+
const templateId = String(existing?.metadata?.installedFrom || "").trim();
|
|
494
|
+
if (!templateId) throw new Error(`Workflow "${workflowId}" is not template-backed`);
|
|
495
|
+
const template = getTemplate(templateId);
|
|
496
|
+
if (!template) throw new Error(`Template "${templateId}" not found`);
|
|
497
|
+
|
|
498
|
+
const hydrated = applyWorkflowTemplateState(existing);
|
|
499
|
+
if (mode === "replace" && hydrated?.metadata?.templateState?.isCustomized && opts.force !== true) {
|
|
500
|
+
throw new Error("Workflow has custom changes; pass force=true to replace it");
|
|
501
|
+
}
|
|
395
502
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
503
|
+
const next = makeUpdatedWorkflowFromTemplate(hydrated, template, mode);
|
|
504
|
+
return engine.save(next);
|
|
505
|
+
}
|
|
399
506
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
507
|
+
function reconcileInstalledTemplates(engine, opts = {}) {
|
|
508
|
+
const autoUpdateUnmodified = opts.autoUpdateUnmodified !== false;
|
|
509
|
+
const forceUpdateTemplateIds = new Set(
|
|
510
|
+
(Array.isArray(opts.forceUpdateTemplateIds)
|
|
511
|
+
? opts.forceUpdateTemplateIds
|
|
512
|
+
: [opts.forceUpdateTemplateIds])
|
|
513
|
+
.map((value) => String(value || "").trim())
|
|
514
|
+
.filter(Boolean),
|
|
515
|
+
);
|
|
516
|
+
const workflows = engine.list();
|
|
517
|
+
const result = {
|
|
518
|
+
scanned: 0,
|
|
519
|
+
metadataUpdated: 0,
|
|
520
|
+
autoUpdated: 0,
|
|
521
|
+
forceUpdated: [],
|
|
522
|
+
updateAvailable: [],
|
|
523
|
+
customized: [],
|
|
524
|
+
updatedWorkflowIds: [],
|
|
525
|
+
errors: [],
|
|
526
|
+
};
|
|
403
527
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
528
|
+
for (const summary of workflows) {
|
|
529
|
+
const wfId = summary?.id;
|
|
530
|
+
if (!wfId) continue;
|
|
531
|
+
const def = engine.get(wfId);
|
|
532
|
+
if (!def?.metadata?.installedFrom) continue;
|
|
533
|
+
result.scanned += 1;
|
|
534
|
+
|
|
535
|
+
try {
|
|
536
|
+
const previousState = def.metadata?.templateState || null;
|
|
537
|
+
const before = stableStringify(previousState);
|
|
538
|
+
applyWorkflowTemplateState(def);
|
|
539
|
+
const state = def.metadata?.templateState || null;
|
|
540
|
+
const after = stableStringify(state);
|
|
541
|
+
if (before !== after) {
|
|
542
|
+
engine.save(def);
|
|
543
|
+
result.metadataUpdated += 1;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (!state) continue;
|
|
547
|
+
if (state.isCustomized) {
|
|
548
|
+
result.customized.push({
|
|
549
|
+
workflowId: def.id,
|
|
550
|
+
name: def.name,
|
|
551
|
+
templateId: state.templateId,
|
|
552
|
+
updateAvailable: state.updateAvailable === true,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
if (state.updateAvailable === true) {
|
|
556
|
+
result.updateAvailable.push({
|
|
557
|
+
workflowId: def.id,
|
|
558
|
+
name: def.name,
|
|
559
|
+
templateId: state.templateId,
|
|
560
|
+
isCustomized: state.isCustomized === true,
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const templateId = String(state.templateId || "").trim();
|
|
565
|
+
const shouldForceUpdate = templateId && forceUpdateTemplateIds.has(templateId);
|
|
566
|
+
if (shouldForceUpdate) {
|
|
567
|
+
const saved = updateWorkflowFromTemplate(engine, def.id, { mode: "replace", force: true });
|
|
568
|
+
result.autoUpdated += 1;
|
|
569
|
+
result.updatedWorkflowIds.push(saved.id);
|
|
570
|
+
result.forceUpdated.push(saved.id);
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const wasCustomized = previousState?.isCustomized === true;
|
|
575
|
+
if (autoUpdateUnmodified && state.updateAvailable === true && !wasCustomized) {
|
|
576
|
+
const saved = updateWorkflowFromTemplate(engine, def.id, { mode: "replace", force: true });
|
|
577
|
+
result.autoUpdated += 1;
|
|
578
|
+
result.updatedWorkflowIds.push(saved.id);
|
|
579
|
+
}
|
|
580
|
+
} catch (err) {
|
|
581
|
+
result.errors.push({
|
|
582
|
+
workflowId: wfId,
|
|
583
|
+
error: err.message,
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return result;
|
|
589
|
+
}
|
|
408
590
|
|
|
409
591
|
return {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
templateFingerprint,
|
|
415
|
-
installedTemplateFingerprint,
|
|
416
|
-
installedTemplateVersion: installedTemplateFingerprint
|
|
417
|
-
? installedTemplateFingerprint.slice(0, 12)
|
|
418
|
-
: null,
|
|
419
|
-
installedFingerprint,
|
|
420
|
-
currentFingerprint,
|
|
421
|
-
isCustomized,
|
|
422
|
-
updateAvailable,
|
|
423
|
-
refreshedAt: nowIso,
|
|
592
|
+
applyWorkflowTemplateState,
|
|
593
|
+
computeWorkflowFingerprint,
|
|
594
|
+
reconcileInstalledTemplates,
|
|
595
|
+
updateWorkflowFromTemplate,
|
|
424
596
|
};
|
|
425
597
|
}
|
|
426
598
|
|
|
427
|
-
|
|
599
|
+
|
|
600
|
+
function cloneTemplateDefinition(template) {
|
|
601
|
+
return JSON.parse(JSON.stringify(template));
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function relayoutWorkflowDefinition(def = {}) {
|
|
428
605
|
if (!def || typeof def !== "object") return def;
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
const template = getTemplate(templateId);
|
|
432
|
-
if (!template) return def;
|
|
433
|
-
if (!def.metadata || typeof def.metadata !== "object") def.metadata = {};
|
|
434
|
-
def.metadata.templateState = deriveTemplateState(def, template);
|
|
606
|
+
normalizeTemplateLayoutInPlace(def);
|
|
607
|
+
applyWorkflowTemplateState(def);
|
|
435
608
|
return def;
|
|
436
609
|
}
|
|
437
610
|
|
|
438
|
-
function
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const mergedVariables = {
|
|
442
|
-
...(templateClone.variables || {}),
|
|
443
|
-
...(existing.variables || {}),
|
|
444
|
-
};
|
|
445
|
-
const next = {
|
|
446
|
-
...templateClone,
|
|
447
|
-
id: mode === "copy" ? randomUUID() : existing.id,
|
|
448
|
-
name: mode === "copy" ? `${existing.name} (Updated)` : existing.name,
|
|
449
|
-
enabled: existing.enabled !== false,
|
|
450
|
-
variables: mergedVariables,
|
|
451
|
-
metadata: {
|
|
452
|
-
...(existing.metadata || {}),
|
|
453
|
-
...(templateClone.metadata || {}),
|
|
454
|
-
installedFrom: template.id,
|
|
455
|
-
templateUpdatedAt: nowIso,
|
|
456
|
-
},
|
|
457
|
-
};
|
|
458
|
-
delete next.metadata.templateState;
|
|
459
|
-
if (mode === "copy") {
|
|
460
|
-
next.metadata.createdAt = nowIso;
|
|
461
|
-
next.metadata.updatedAt = nowIso;
|
|
611
|
+
function normalizeRelayoutWorkflowIdInput(value) {
|
|
612
|
+
if (Array.isArray(value)) {
|
|
613
|
+
return value.map((entry) => String(entry || "").trim()).filter(Boolean);
|
|
462
614
|
}
|
|
463
|
-
|
|
615
|
+
const id = String(value || "").trim();
|
|
616
|
+
return id ? [id] : [];
|
|
464
617
|
}
|
|
465
618
|
|
|
466
|
-
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
619
|
+
function resolveRelayoutWorkflowTargets(engine, requestedIds = []) {
|
|
620
|
+
const targets = new Set();
|
|
621
|
+
for (const requestedId of requestedIds) {
|
|
622
|
+
const normalizedId = String(requestedId || "").trim();
|
|
623
|
+
if (!normalizedId) continue;
|
|
624
|
+
const resolved = engine.get(normalizedId);
|
|
625
|
+
if (resolved?.id) {
|
|
626
|
+
targets.add(String(resolved.id).trim());
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
targets.add(normalizedId);
|
|
470
630
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const templateId = String(existing?.metadata?.installedFrom || "").trim();
|
|
474
|
-
if (!templateId) throw new Error(`Workflow "${workflowId}" is not template-backed`);
|
|
475
|
-
const template = getTemplate(templateId);
|
|
476
|
-
if (!template) throw new Error(`Template "${templateId}" not found`);
|
|
631
|
+
return targets;
|
|
632
|
+
}
|
|
477
633
|
|
|
478
|
-
|
|
479
|
-
if (
|
|
480
|
-
throw new Error("
|
|
634
|
+
export function relayoutInstalledTemplateWorkflows(engine, opts = {}) {
|
|
635
|
+
if (!engine || typeof engine.list !== "function" || typeof engine.get !== "function" || typeof engine.save !== "function") {
|
|
636
|
+
throw new Error("A workflow engine with list/get/save is required");
|
|
481
637
|
}
|
|
482
638
|
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
export function reconcileInstalledTemplates(engine, opts = {}) {
|
|
488
|
-
const autoUpdateUnmodified = opts.autoUpdateUnmodified !== false;
|
|
489
|
-
const forceUpdateTemplateIds = new Set(
|
|
490
|
-
(Array.isArray(opts.forceUpdateTemplateIds)
|
|
491
|
-
? opts.forceUpdateTemplateIds
|
|
492
|
-
: [opts.forceUpdateTemplateIds])
|
|
493
|
-
.map((value) => String(value || "").trim())
|
|
494
|
-
.filter(Boolean),
|
|
639
|
+
const targetWorkflowIds = resolveRelayoutWorkflowTargets(
|
|
640
|
+
engine,
|
|
641
|
+
normalizeRelayoutWorkflowIdInput(opts.workflowIds || opts.workflowId),
|
|
495
642
|
);
|
|
496
|
-
const workflows = engine.list();
|
|
497
643
|
const result = {
|
|
498
644
|
scanned: 0,
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
forceUpdated: [],
|
|
502
|
-
updateAvailable: [],
|
|
503
|
-
customized: [],
|
|
645
|
+
updated: 0,
|
|
646
|
+
skipped: 0,
|
|
504
647
|
updatedWorkflowIds: [],
|
|
648
|
+
skippedWorkflowIds: [],
|
|
505
649
|
errors: [],
|
|
506
650
|
};
|
|
507
651
|
|
|
508
|
-
for (const summary of
|
|
509
|
-
const
|
|
510
|
-
if (!
|
|
511
|
-
|
|
512
|
-
if (!def?.metadata?.installedFrom) continue;
|
|
652
|
+
for (const summary of engine.list()) {
|
|
653
|
+
const workflowId = String(summary?.id || "").trim();
|
|
654
|
+
if (!workflowId) continue;
|
|
655
|
+
if (targetWorkflowIds.size > 0 && !targetWorkflowIds.has(workflowId)) continue;
|
|
513
656
|
result.scanned += 1;
|
|
514
657
|
|
|
515
658
|
try {
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
const after = stableStringify(state);
|
|
521
|
-
if (before !== after) {
|
|
522
|
-
engine.save(def);
|
|
523
|
-
result.metadataUpdated += 1;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
if (!state) continue;
|
|
527
|
-
if (state.isCustomized) {
|
|
528
|
-
result.customized.push({
|
|
529
|
-
workflowId: def.id,
|
|
530
|
-
name: def.name,
|
|
531
|
-
templateId: state.templateId,
|
|
532
|
-
updateAvailable: state.updateAvailable === true,
|
|
533
|
-
});
|
|
534
|
-
}
|
|
535
|
-
if (state.updateAvailable === true) {
|
|
536
|
-
result.updateAvailable.push({
|
|
537
|
-
workflowId: def.id,
|
|
538
|
-
name: def.name,
|
|
539
|
-
templateId: state.templateId,
|
|
540
|
-
isCustomized: state.isCustomized === true,
|
|
541
|
-
});
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
const templateId = String(state.templateId || "").trim();
|
|
545
|
-
const shouldForceUpdate = templateId && forceUpdateTemplateIds.has(templateId);
|
|
546
|
-
if (shouldForceUpdate) {
|
|
547
|
-
const saved = updateWorkflowFromTemplate(engine, def.id, { mode: "replace", force: true });
|
|
548
|
-
result.autoUpdated += 1;
|
|
549
|
-
result.updatedWorkflowIds.push(saved.id);
|
|
550
|
-
result.forceUpdated.push(saved.id);
|
|
659
|
+
const def = engine.get(workflowId);
|
|
660
|
+
if (!def?.metadata?.installedFrom) {
|
|
661
|
+
result.skipped += 1;
|
|
662
|
+
result.skippedWorkflowIds.push(workflowId);
|
|
551
663
|
continue;
|
|
552
664
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
result.autoUpdated += 1;
|
|
558
|
-
result.updatedWorkflowIds.push(saved.id);
|
|
559
|
-
}
|
|
665
|
+
relayoutWorkflowDefinition(def);
|
|
666
|
+
engine.save(def);
|
|
667
|
+
result.updated += 1;
|
|
668
|
+
result.updatedWorkflowIds.push(workflowId);
|
|
560
669
|
} catch (err) {
|
|
561
|
-
result.errors.push({
|
|
562
|
-
workflowId: wfId,
|
|
563
|
-
error: err.message,
|
|
564
|
-
});
|
|
670
|
+
result.errors.push({ workflowId, error: err.message });
|
|
565
671
|
}
|
|
566
672
|
}
|
|
567
673
|
|
|
568
674
|
return result;
|
|
569
675
|
}
|
|
570
676
|
|
|
571
|
-
/**
|
|
572
|
-
* Setup workflow profiles used by `bosun --setup`.
|
|
573
|
-
* - `manual`: human-driven dispatch with reliability safety nets.
|
|
574
|
-
* - `balanced`: recommended default for most teams.
|
|
575
|
-
* - `autonomous`: higher automation with planning + maintenance workflows.
|
|
576
|
-
*/
|
|
577
677
|
export const WORKFLOW_SETUP_PROFILES = Object.freeze({
|
|
578
678
|
manual: Object.freeze({
|
|
579
679
|
id: "manual",
|
|
@@ -788,6 +888,21 @@ export function getTemplate(id) {
|
|
|
788
888
|
return _TEMPLATE_BY_ID.get(id) || null;
|
|
789
889
|
}
|
|
790
890
|
|
|
891
|
+
const {
|
|
892
|
+
applyWorkflowTemplateState,
|
|
893
|
+
computeWorkflowFingerprint,
|
|
894
|
+
reconcileInstalledTemplates,
|
|
895
|
+
updateWorkflowFromTemplate,
|
|
896
|
+
} = createWorkflowTemplateState({ getTemplate, cloneTemplateDefinition });
|
|
897
|
+
|
|
898
|
+
export {
|
|
899
|
+
applyWorkflowTemplateState,
|
|
900
|
+
computeWorkflowFingerprint,
|
|
901
|
+
reconcileInstalledTemplates,
|
|
902
|
+
updateWorkflowFromTemplate,
|
|
903
|
+
};
|
|
904
|
+
|
|
905
|
+
|
|
791
906
|
// ── Grouped Flows ──────────────────────────────────────────────────────────
|
|
792
907
|
// Templates that use action.execute_workflow to chain into other templates
|
|
793
908
|
// declare metadata.requiredTemplates. When one template in a group is
|
|
@@ -1275,3 +1390,10 @@ export function installRecommendedTemplates(engine, overridesById = {}) {
|
|
|
1275
1390
|
.map((template) => template.id);
|
|
1276
1391
|
return installTemplateSet(engine, recommendedIds, overridesById);
|
|
1277
1392
|
}
|
|
1393
|
+
|
|
1394
|
+
|
|
1395
|
+
|
|
1396
|
+
|
|
1397
|
+
|
|
1398
|
+
|
|
1399
|
+
|