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.
Files changed (71) hide show
  1. package/.env.example +1 -1
  2. package/agent/agent-prompt-catalog.mjs +971 -0
  3. package/agent/agent-prompts.mjs +2 -970
  4. package/agent/agent-supervisor.mjs +6 -3
  5. package/agent/autofix-git.mjs +33 -0
  6. package/agent/autofix-prompts.mjs +151 -0
  7. package/agent/autofix.mjs +11 -175
  8. package/agent/bosun-skills.mjs +3 -2
  9. package/bosun.config.example.json +17 -0
  10. package/bosun.schema.json +87 -188
  11. package/cli.mjs +34 -1
  12. package/config/config-doctor.mjs +5 -250
  13. package/config/config-file-names.mjs +5 -0
  14. package/config/config.mjs +89 -493
  15. package/config/executor-config.mjs +493 -0
  16. package/config/repo-root.mjs +1 -2
  17. package/config/workspace-health.mjs +242 -0
  18. package/git/git-safety.mjs +15 -0
  19. package/github/github-oauth-portal.mjs +46 -0
  20. package/infra/library-manager-utils.mjs +22 -0
  21. package/infra/library-manager-well-known-sources.mjs +578 -0
  22. package/infra/library-manager.mjs +512 -1030
  23. package/infra/monitor.mjs +28 -9
  24. package/infra/session-tracker.mjs +10 -7
  25. package/kanban/kanban-adapter.mjs +17 -1
  26. package/lib/codebase-audit-manifests.mjs +117 -0
  27. package/lib/codebase-audit.mjs +18 -115
  28. package/package.json +18 -3
  29. package/server/ui-server.mjs +1194 -79
  30. package/shell/codex-config-file.mjs +178 -0
  31. package/shell/codex-config.mjs +538 -575
  32. package/task/task-cli.mjs +54 -3
  33. package/task/task-executor.mjs +143 -13
  34. package/task/task-store.mjs +409 -1
  35. package/telegram/telegram-bot.mjs +127 -0
  36. package/tools/apply-pr-suggestions.mjs +401 -0
  37. package/tools/syntax-check.mjs +21 -9
  38. package/ui/app.js +3 -14
  39. package/ui/components/kanban-board.js +227 -4
  40. package/ui/components/session-list.js +85 -5
  41. package/ui/demo-defaults.js +334 -80
  42. package/ui/demo.html +155 -0
  43. package/ui/modules/session-api.js +96 -0
  44. package/ui/modules/settings-schema.js +1 -2
  45. package/ui/modules/state.js +21 -3
  46. package/ui/setup.html +4 -5
  47. package/ui/styles/components.css +58 -4
  48. package/ui/tabs/agents.js +12 -15
  49. package/ui/tabs/control.js +1 -0
  50. package/ui/tabs/library.js +484 -22
  51. package/ui/tabs/manual-flows.js +105 -29
  52. package/ui/tabs/tasks.js +785 -140
  53. package/ui/tabs/telemetry.js +129 -11
  54. package/ui/tabs/workflow-canvas-utils.mjs +130 -0
  55. package/ui/tabs/workflows.js +293 -23
  56. package/voice/voice-tool-definitions.mjs +757 -0
  57. package/voice/voice-tools.mjs +34 -778
  58. package/workflow/manual-flow-audit.mjs +165 -0
  59. package/workflow/manual-flows.mjs +164 -259
  60. package/workflow/workflow-engine.mjs +147 -58
  61. package/workflow/workflow-nodes/definitions.mjs +1207 -0
  62. package/workflow/workflow-nodes/transforms.mjs +612 -0
  63. package/workflow/workflow-nodes.mjs +304 -52
  64. package/workflow/workflow-templates.mjs +313 -191
  65. package/workflow-templates/_helpers.mjs +154 -0
  66. package/workflow-templates/agents.mjs +61 -4
  67. package/workflow-templates/code-quality.mjs +7 -7
  68. package/workflow-templates/github.mjs +20 -10
  69. package/workflow-templates/task-batch.mjs +20 -9
  70. package/workflow-templates/task-lifecycle.mjs +31 -6
  71. 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
- export const WORKFLOW_TEMPLATES = Object.freeze([
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
- const TEMPLATE_STATE_VERSION = 1;
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 stableNormalize(value) {
343
- if (Array.isArray(value)) {
344
- return value.map((entry) => stableNormalize(entry));
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
- if (value && typeof value === "object") {
347
- const normalized = {};
348
- for (const key of Object.keys(value).sort()) {
349
- normalized[key] = stableNormalize(value[key]);
370
+
371
+ function stableNormalize(value) {
372
+ if (Array.isArray(value)) {
373
+ return value.map((entry) => stableNormalize(entry));
350
374
  }
351
- return normalized;
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
- return JSON.stringify(stableNormalize(value));
358
- }
385
+ function stableStringify(value) {
386
+ return JSON.stringify(stableNormalize(value));
387
+ }
359
388
 
360
- function hashContent(value) {
361
- return createHash("sha256").update(stableStringify(value)).digest("hex");
362
- }
389
+ function hashContent(value) {
390
+ return createHash("sha256").update(stableStringify(value)).digest("hex");
391
+ }
363
392
 
364
- function toWorkflowFingerprintPayload(def = {}) {
365
- return {
366
- name: def.name || "",
367
- description: def.description || "",
368
- category: def.category || "custom",
369
- trigger: def.trigger || "",
370
- variables: def.variables || {},
371
- nodes: def.nodes || [],
372
- edges: def.edges || [],
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
- export function computeWorkflowFingerprint(def = {}) {
377
- return hashContent(toWorkflowFingerprintPayload(def));
378
- }
405
+ function computeWorkflowFingerprint(def = {}) {
406
+ return hashContent(toWorkflowFingerprintPayload(def));
407
+ }
379
408
 
380
- function cloneTemplateDefinition(template) {
381
- return JSON.parse(JSON.stringify(template));
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
- function getTemplateVersion(templateId) {
385
- const template = getTemplate(templateId);
386
- if (!template) return null;
387
- return computeWorkflowFingerprint(template).slice(0, 12);
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
- function deriveTemplateState(def, template) {
391
- const nowIso = new Date().toISOString();
392
- const currentFingerprint = computeWorkflowFingerprint(def);
393
- const templateFingerprint = computeWorkflowFingerprint(template);
394
- const previousState = def?.metadata?.templateState || {};
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
- const installedTemplateFingerprint = typeof previousState.installedTemplateFingerprint === "string"
397
- ? previousState.installedTemplateFingerprint
398
- : (currentFingerprint === templateFingerprint ? templateFingerprint : null);
503
+ const next = makeUpdatedWorkflowFromTemplate(hydrated, template, mode);
504
+ return engine.save(next);
505
+ }
399
506
 
400
- const installedFingerprint = typeof previousState.installedFingerprint === "string"
401
- ? previousState.installedFingerprint
402
- : currentFingerprint;
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
- const isCustomized = currentFingerprint !== installedFingerprint;
405
- const updateAvailable = installedTemplateFingerprint
406
- ? installedTemplateFingerprint !== templateFingerprint
407
- : false;
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
- stateVersion: TEMPLATE_STATE_VERSION,
411
- templateId: template.id,
412
- templateName: template.name,
413
- templateVersion: templateFingerprint.slice(0, 12),
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
- export function applyWorkflowTemplateState(def = {}) {
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
- const templateId = String(def?.metadata?.installedFrom || "").trim();
430
- if (!templateId) return def;
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 makeUpdatedWorkflowFromTemplate(existing, template, mode = "replace") {
439
- const templateClone = cloneTemplateDefinition(template);
440
- const nowIso = new Date().toISOString();
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
- return applyWorkflowTemplateState(next);
615
+ const id = String(value || "").trim();
616
+ return id ? [id] : [];
464
617
  }
465
618
 
466
- export function updateWorkflowFromTemplate(engine, workflowId, opts = {}) {
467
- const mode = String(opts.mode || "replace").toLowerCase();
468
- if (!["replace", "copy"].includes(mode)) {
469
- throw new Error(`Unsupported template update mode "${mode}"`);
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
- const existing = engine.get(workflowId);
472
- if (!existing) throw new Error(`Workflow "${workflowId}" not found`);
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
- const hydrated = applyWorkflowTemplateState(existing);
479
- if (mode === "replace" && hydrated?.metadata?.templateState?.isCustomized && opts.force !== true) {
480
- throw new Error("Workflow has custom changes; pass force=true to replace it");
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 next = makeUpdatedWorkflowFromTemplate(hydrated, template, mode);
484
- return engine.save(next);
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
- metadataUpdated: 0,
500
- autoUpdated: 0,
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 workflows) {
509
- const wfId = summary?.id;
510
- if (!wfId) continue;
511
- const def = engine.get(wfId);
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 previousState = def.metadata?.templateState || null;
517
- const before = stableStringify(previousState);
518
- applyWorkflowTemplateState(def);
519
- const state = def.metadata?.templateState || null;
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
- const wasCustomized = previousState?.isCustomized === true;
555
- if (autoUpdateUnmodified && state.updateAvailable === true && !wasCustomized) {
556
- const saved = updateWorkflowFromTemplate(engine, def.id, { mode: "replace", force: true });
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
+