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.
Files changed (64) hide show
  1. package/.env.example +8 -0
  2. package/README.md +20 -0
  3. package/agent/agent-event-bus.mjs +248 -6
  4. package/agent/agent-pool.mjs +125 -28
  5. package/agent/agent-work-analyzer.mjs +8 -16
  6. package/agent/retry-queue.mjs +164 -0
  7. package/bosun.config.example.json +25 -0
  8. package/bosun.schema.json +825 -183
  9. package/cli.mjs +59 -5
  10. package/config/config.mjs +130 -3
  11. package/infra/monitor.mjs +693 -67
  12. package/infra/runtime-accumulator.mjs +376 -84
  13. package/infra/session-tracker.mjs +82 -25
  14. package/lib/codebase-audit.mjs +133 -18
  15. package/package.json +23 -4
  16. package/server/setup-web-server.mjs +25 -0
  17. package/server/ui-server.mjs +248 -29
  18. package/setup.mjs +27 -24
  19. package/shell/codex-shell.mjs +34 -3
  20. package/shell/copilot-shell.mjs +50 -8
  21. package/task/msg-hub.mjs +193 -0
  22. package/task/pipeline.mjs +544 -0
  23. package/task/task-cli.mjs +38 -2
  24. package/task/task-executor-pipeline.mjs +143 -0
  25. package/task/task-executor.mjs +36 -27
  26. package/telegram/get-telegram-chat-id.mjs +57 -47
  27. package/ui/components/workspace-switcher.js +7 -7
  28. package/ui/demo-defaults.js +15694 -10573
  29. package/ui/modules/settings-schema.js +2 -0
  30. package/ui/modules/state.js +54 -57
  31. package/ui/modules/voice-client-sdk.js +375 -36
  32. package/ui/modules/voice-client.js +140 -31
  33. package/ui/setup.html +68 -2
  34. package/ui/styles/components.css +57 -0
  35. package/ui/styles.css +201 -1
  36. package/ui/tabs/dashboard.js +74 -0
  37. package/ui/tabs/logs.js +10 -0
  38. package/ui/tabs/settings.js +178 -99
  39. package/ui/tabs/tasks.js +31 -1
  40. package/ui/tabs/telemetry.js +34 -0
  41. package/ui/tabs/workflow-canvas-utils.mjs +8 -1
  42. package/ui/tabs/workflows.js +532 -275
  43. package/voice/voice-agents-sdk.mjs +1 -1
  44. package/voice/voice-relay.mjs +6 -6
  45. package/workflow/declarative-workflows.mjs +145 -0
  46. package/workflow/msg-hub.mjs +237 -0
  47. package/workflow/pipeline-workflows.mjs +287 -0
  48. package/workflow/pipeline.mjs +828 -315
  49. package/workflow/workflow-cli.mjs +128 -0
  50. package/workflow/workflow-engine.mjs +329 -17
  51. package/workflow/workflow-nodes/custom-loader.mjs +250 -0
  52. package/workflow/workflow-nodes.mjs +1955 -223
  53. package/workflow/workflow-templates.mjs +26 -8
  54. package/workflow-templates/agents.mjs +0 -1
  55. package/workflow-templates/bosun-native.mjs +212 -2
  56. package/workflow-templates/continuation-loop.mjs +339 -0
  57. package/workflow-templates/github.mjs +516 -40
  58. package/workflow-templates/planning.mjs +446 -17
  59. package/workflow-templates/reliability.mjs +65 -12
  60. package/workflow-templates/task-batch.mjs +24 -8
  61. package/workflow-templates/task-lifecycle.mjs +83 -6
  62. package/workspace/context-cache.mjs +66 -18
  63. package/workspace/workspace-manager.mjs +2 -1
  64. package/workflow-templates/issue-continuation.mjs +0 -243
package/infra/monitor.mjs CHANGED
@@ -499,7 +499,7 @@ async function ensureWorkflowAutomationEngine() {
499
499
 
500
500
  workflowAutomationInitPromise = (async () => {
501
501
  try {
502
- const [{ getWorkflowEngine }, { createTask }, wfNodes, workflowTemplates] = await Promise.all([
502
+ const [{ getWorkflowEngine }, { createTask, getTask }, wfNodes, workflowTemplates] = await Promise.all([
503
503
  import("../workflow/workflow-engine.mjs"),
504
504
  import("../kanban/kanban-adapter.mjs"),
505
505
  import("../workflow/workflow-nodes.mjs"),
@@ -532,6 +532,8 @@ async function ensureWorkflowAutomationEngine() {
532
532
  ),
533
533
  listTasks: async (projectId, filters = {}) =>
534
534
  listKanbanTasks(String(projectId || ""), filters || {}),
535
+ getTask: async (taskId) =>
536
+ getTask(String(taskId || "").trim()),
535
537
  };
536
538
 
537
539
  const agentPoolService = {
@@ -613,15 +615,47 @@ async function ensureWorkflowAutomationEngine() {
613
615
  config?.workflowDefaults && typeof config.workflowDefaults === "object"
614
616
  ? config.workflowDefaults.templates || []
615
617
  : [];
618
+ const typedWorkflowTemplateConfig =
619
+ typeof workflowTemplates?.resolveWorkflowTemplateConfig === "function"
620
+ ? workflowTemplates.resolveWorkflowTemplateConfig(config?.workflows || [])
621
+ : { templateIds: [], overridesById: {} };
622
+
616
623
  const requestedTemplateIds = new Set(
617
624
  typeof workflowTemplates?.resolveWorkflowTemplateIds === "function"
618
625
  ? workflowTemplates.resolveWorkflowTemplateIds({
619
626
  profileId: configuredWorkflowProfile,
620
627
  templateIds: configuredWorkflowTemplates,
628
+ workflows: config?.workflows || [],
621
629
  })
622
630
  : [],
623
631
  );
624
- const staleWorkflowTemplateIds = ["template-task-batch-pr"];
632
+ for (const templateId of typedWorkflowTemplateConfig.templateIds || []) {
633
+ const overrides = typedWorkflowTemplateConfig.overridesById?.[templateId] || {};
634
+ let installed = (engine.list?.() || []).find(
635
+ (wf) => String(wf?.metadata?.installedFrom || "").trim() === templateId,
636
+ );
637
+
638
+ if (!installed && typeof workflowTemplates?.installTemplate === "function") {
639
+ installed = workflowTemplates.installTemplate(templateId, engine, overrides);
640
+ }
641
+
642
+ if (!installed) continue;
643
+ const def = engine.get?.(installed.id);
644
+ if (!def) continue;
645
+
646
+ def.enabled = true;
647
+ def.variables = {
648
+ ...(def.variables || {}),
649
+ ...overrides,
650
+ };
651
+ def.metadata = {
652
+ ...(def.metadata || {}),
653
+ configuredFrom: "workflows.config",
654
+ };
655
+ engine.save(def);
656
+ }
657
+
658
+ const staleWorkflowTemplateIds = ["template-task-batch-pr", "template-continuation-loop"];
625
659
  if (typeof workflowTemplates?.reconcileInstalledTemplates === "function") {
626
660
  const reconcile = workflowTemplates.reconcileInstalledTemplates(engine, {
627
661
  autoUpdateUnmodified: true,
@@ -647,7 +681,7 @@ async function ensureWorkflowAutomationEngine() {
647
681
  def.metadata = {
648
682
  ...(def.metadata || {}),
649
683
  autoDisabledReason:
650
- "disabled on startup because the template is no longer requested by workflowDefaults",
684
+ "disabled on startup because the template is no longer requested by workflowDefaults/workflows config",
651
685
  };
652
686
  engine.save(def);
653
687
  console.log(
@@ -846,6 +880,133 @@ function resolvePlannerPromptFallback() {
846
880
  };
847
881
  }
848
882
 
883
+ const PLANNER_PATTERN_COUNTER_RELATIVE_PATH =
884
+ ".bosun/workflow-runs/planner-pattern-feedback.json";
885
+ const PLANNER_FAILURE_SIGNAL_WEIGHTS = Object.freeze({
886
+ agentAttempts: 0.35,
887
+ consecutiveNoCommits: 1.25,
888
+ blockedReason: 1.5,
889
+ debtTrend: 0.4,
890
+ taskDebt: 0.5,
891
+ });
892
+ const PLANNER_FAILURE_COUNTER_DECAY = 0.8;
893
+ const PLANNER_FAILURE_COUNTER_IDLE_DECAY = 0.95;
894
+ const PLANNER_FAILURE_THRESHOLD = 2.5;
895
+
896
+ function normalizePlannerAreaKey(value) {
897
+ return String(value || "").trim().toLowerCase();
898
+ }
899
+
900
+ function normalizePlannerArchetypeKey(value) {
901
+ const normalized = String(value || "")
902
+ .trim()
903
+ .toLowerCase()
904
+ .replace(/[^a-z0-9]+/g, "_")
905
+ .replace(/^_+|_+$/g, "");
906
+ return normalized || "general";
907
+ }
908
+
909
+ function inferPlannerTaskArchetype(task) {
910
+ const explicit =
911
+ task?.meta?.planner?.archetype ||
912
+ task?.meta?.archetype ||
913
+ task?.archetype ||
914
+ "";
915
+ if (String(explicit || "").trim()) {
916
+ return normalizePlannerArchetypeKey(explicit);
917
+ }
918
+ const title = String(task?.title || "").trim().toLowerCase();
919
+ const conventional = title.match(
920
+ /^(?:\[[^\]]+\]\s*)?([a-z][a-z0-9_-]*)(?:\([^)]*\))?:/,
921
+ );
922
+ if (conventional?.[1]) return normalizePlannerArchetypeKey(conventional[1]);
923
+ if (title.includes("test")) return "test";
924
+ if (title.includes("doc")) return "docs";
925
+ if (title.includes("refactor")) return "refactor";
926
+ return "general";
927
+ }
928
+
929
+ function resolvePlannerTaskPatternAreas(task) {
930
+ const candidates = []
931
+ .concat(Array.isArray(task?.repo_areas) ? task.repo_areas : [])
932
+ .concat(Array.isArray(task?.repoAreas) ? task.repoAreas : [])
933
+ .concat(Array.isArray(task?.meta?.repo_areas) ? task.meta.repo_areas : [])
934
+ .concat(Array.isArray(task?.meta?.repoAreas) ? task.meta.repoAreas : [])
935
+ .concat(
936
+ Array.isArray(task?.meta?.planner?.repo_areas)
937
+ ? task.meta.planner.repo_areas
938
+ : [],
939
+ )
940
+ .concat(
941
+ Array.isArray(task?.meta?.planner?.repoAreas)
942
+ ? task.meta.planner.repoAreas
943
+ : [],
944
+ );
945
+ if (!candidates.length) return ["global"];
946
+ const dedup = new Set();
947
+ const areas = [];
948
+ for (const candidate of candidates) {
949
+ const areaKey = normalizePlannerAreaKey(candidate);
950
+ if (!areaKey || dedup.has(areaKey)) continue;
951
+ dedup.add(areaKey);
952
+ areas.push(areaKey);
953
+ }
954
+ return areas.length > 0 ? areas : ["global"];
955
+ }
956
+
957
+ function buildPlannerPatternKey(repoArea, archetype) {
958
+ return `${normalizePlannerAreaKey(repoArea) || "global"}::${normalizePlannerArchetypeKey(archetype)}`;
959
+ }
960
+
961
+ function readPlannerPatternCounterState(baseDir) {
962
+ const statePath = resolve(
963
+ baseDir || process.cwd(),
964
+ PLANNER_PATTERN_COUNTER_RELATIVE_PATH,
965
+ );
966
+ if (!existsSync(statePath)) {
967
+ return { statePath, state: { patterns: {} } };
968
+ }
969
+ try {
970
+ const parsed = JSON.parse(readFileSync(statePath, "utf8"));
971
+ const patterns =
972
+ parsed?.patterns && typeof parsed.patterns === "object"
973
+ ? parsed.patterns
974
+ : {};
975
+ return {
976
+ statePath,
977
+ state: {
978
+ updatedAt: String(parsed?.updatedAt || "").trim() || null,
979
+ patterns,
980
+ },
981
+ };
982
+ } catch {
983
+ return { statePath, state: { patterns: {} } };
984
+ }
985
+ }
986
+
987
+ function writePlannerPatternCounterState(statePath, state) {
988
+ try {
989
+ mkdirSync(dirname(statePath), { recursive: true });
990
+ writeFileSync(
991
+ statePath,
992
+ JSON.stringify(
993
+ {
994
+ updatedAt: new Date().toISOString(),
995
+ patterns:
996
+ state?.patterns && typeof state.patterns === "object"
997
+ ? state.patterns
998
+ : {},
999
+ },
1000
+ null,
1001
+ 2,
1002
+ ),
1003
+ "utf8",
1004
+ );
1005
+ } catch {
1006
+ // best effort
1007
+ }
1008
+ }
1009
+
849
1010
  function buildPlannerFeedback() {
850
1011
  const tasks = Array.isArray(getAllInternalTasks?.()) ? getAllInternalTasks() : [];
851
1012
  const statusCounts = {
@@ -858,9 +1019,71 @@ function buildPlannerFeedback() {
858
1019
  };
859
1020
  const blockedReasonCounts = new Map();
860
1021
  const hotTasks = [];
1022
+ const patternObservations = new Map();
1023
+ const taskDebtWeightById = new Map();
861
1024
  let attemptedCount = 0;
862
1025
  let noCommitCount = 0;
863
1026
 
1027
+ let debtEntries = [];
1028
+ try {
1029
+ debtEntries = readTaskDebtEntries({ baseDir: repoRoot, limit: 300 });
1030
+ } catch {
1031
+ debtEntries = [];
1032
+ }
1033
+
1034
+ const now = Date.now();
1035
+ const recentWindowMs = 7 * 24 * 60 * 60 * 1000;
1036
+ const previousWindowMs = 14 * 24 * 60 * 60 * 1000;
1037
+ const recentDebtEntries = debtEntries.filter((entry) => {
1038
+ const ts = Date.parse(String(entry?.recordedAt || ""));
1039
+ return Number.isFinite(ts) && now - ts <= recentWindowMs;
1040
+ });
1041
+ const previousDebtEntries = debtEntries.filter((entry) => {
1042
+ const ts = Date.parse(String(entry?.recordedAt || ""));
1043
+ return Number.isFinite(ts) && now - ts > recentWindowMs && now - ts <= previousWindowMs;
1044
+ });
1045
+ const debtSeverityCounts = { critical: 0, high: 0, medium: 0, low: 0 };
1046
+ const previousDebtSeverityCounts = { critical: 0, high: 0, medium: 0, low: 0 };
1047
+ for (const entry of recentDebtEntries) {
1048
+ for (const item of Array.isArray(entry?.debtItems) ? entry.debtItems : []) {
1049
+ const severity = String(item?.severity || "").trim().toLowerCase();
1050
+ if (Object.prototype.hasOwnProperty.call(debtSeverityCounts, severity)) {
1051
+ debtSeverityCounts[severity] += 1;
1052
+ const taskId = String(entry?.taskId || "").trim();
1053
+ if (taskId) {
1054
+ const severityWeight = severity === "critical"
1055
+ ? 2
1056
+ : severity === "high"
1057
+ ? 1.5
1058
+ : severity === "medium"
1059
+ ? 1
1060
+ : 0.5;
1061
+ taskDebtWeightById.set(
1062
+ taskId,
1063
+ (taskDebtWeightById.get(taskId) || 0) + severityWeight,
1064
+ );
1065
+ }
1066
+ }
1067
+ }
1068
+ }
1069
+ for (const entry of previousDebtEntries) {
1070
+ for (const item of Array.isArray(entry?.debtItems) ? entry.debtItems : []) {
1071
+ const severity = String(item?.severity || "").trim().toLowerCase();
1072
+ if (Object.prototype.hasOwnProperty.call(previousDebtSeverityCounts, severity)) {
1073
+ previousDebtSeverityCounts[severity] += 1;
1074
+ }
1075
+ }
1076
+ }
1077
+
1078
+ const weightedDebtSeverity = (counts) =>
1079
+ (counts.critical * 2) + (counts.high * 1.5) + counts.medium + (counts.low * 0.5);
1080
+ const recentDebtWeighted = weightedDebtSeverity(debtSeverityCounts);
1081
+ const previousDebtWeighted = weightedDebtSeverity(previousDebtSeverityCounts);
1082
+ const debtTrendDelta = recentDebtWeighted - previousDebtWeighted;
1083
+ const debtTrendPenalty = debtTrendDelta > 0
1084
+ ? Math.min(2, (debtTrendDelta / 8) * PLANNER_FAILURE_SIGNAL_WEIGHTS.debtTrend)
1085
+ : 0;
1086
+
864
1087
  for (const task of tasks) {
865
1088
  const status = String(task?.status || "").trim().toLowerCase();
866
1089
  if (Object.prototype.hasOwnProperty.call(statusCounts, status)) {
@@ -869,6 +1092,7 @@ function buildPlannerFeedback() {
869
1092
  statusCounts.other += 1;
870
1093
  }
871
1094
 
1095
+ const taskId = String(task?.id || "").trim();
872
1096
  const agentAttempts = Number(task?.agentAttempts || 0);
873
1097
  const consecutiveNoCommits = Number(task?.consecutiveNoCommits || 0);
874
1098
  const blockedReason = String(task?.blockedReason || "").trim();
@@ -881,6 +1105,57 @@ function buildPlannerFeedback() {
881
1105
  );
882
1106
  }
883
1107
 
1108
+ const attemptPenalty =
1109
+ Math.min(Math.max(0, agentAttempts), 6) * PLANNER_FAILURE_SIGNAL_WEIGHTS.agentAttempts;
1110
+ const noCommitPenalty =
1111
+ Math.min(Math.max(0, consecutiveNoCommits), 4) * PLANNER_FAILURE_SIGNAL_WEIGHTS.consecutiveNoCommits;
1112
+ const blockedPenalty = blockedReason ? PLANNER_FAILURE_SIGNAL_WEIGHTS.blockedReason : 0;
1113
+ const taskDebtPenalty =
1114
+ Math.min(2, (taskDebtWeightById.get(taskId) || 0) * PLANNER_FAILURE_SIGNAL_WEIGHTS.taskDebt);
1115
+ const failureSignal =
1116
+ attemptPenalty +
1117
+ noCommitPenalty +
1118
+ blockedPenalty +
1119
+ taskDebtPenalty +
1120
+ debtTrendPenalty;
1121
+ const terminalSuccess = ["done", "completed", "closed"].includes(status);
1122
+ const successSignal =
1123
+ terminalSuccess &&
1124
+ agentAttempts > 0 &&
1125
+ consecutiveNoCommits <= 0 &&
1126
+ !blockedReason
1127
+ ? 1
1128
+ : 0;
1129
+ const archetype = inferPlannerTaskArchetype(task);
1130
+ const areas = resolvePlannerTaskPatternAreas(task);
1131
+
1132
+ for (const area of areas) {
1133
+ const key = buildPlannerPatternKey(area, archetype);
1134
+ let entry = patternObservations.get(key);
1135
+ if (!entry) {
1136
+ entry = {
1137
+ key,
1138
+ repoArea: area,
1139
+ archetype,
1140
+ failures: 0,
1141
+ successes: 0,
1142
+ failureSignalTotal: 0,
1143
+ successSignalTotal: 0,
1144
+ };
1145
+ patternObservations.set(key, entry);
1146
+ }
1147
+ if (failureSignal > 0) {
1148
+ entry.failureSignalTotal += failureSignal;
1149
+ }
1150
+ if (failureSignal >= 0.75) {
1151
+ entry.failures += 1;
1152
+ }
1153
+ if (successSignal > 0) {
1154
+ entry.successSignalTotal += successSignal;
1155
+ entry.successes += 1;
1156
+ }
1157
+ }
1158
+
884
1159
  if (
885
1160
  agentAttempts > 0 ||
886
1161
  consecutiveNoCommits > 0 ||
@@ -888,17 +1163,23 @@ function buildPlannerFeedback() {
888
1163
  status === "blocked"
889
1164
  ) {
890
1165
  hotTasks.push({
891
- taskId: String(task?.id || "").trim(),
1166
+ taskId,
892
1167
  title: String(task?.title || "").trim(),
893
1168
  status: status || null,
894
1169
  agentAttempts: agentAttempts || 0,
895
1170
  consecutiveNoCommits: consecutiveNoCommits || 0,
896
1171
  blockedReason: blockedReason || null,
1172
+ failureSignal: Number(failureSignal.toFixed(2)),
1173
+ archetype,
1174
+ repoAreas: areas,
897
1175
  });
898
1176
  }
899
1177
  }
900
1178
 
901
1179
  hotTasks.sort((a, b) => {
1180
+ if ((b.failureSignal || 0) !== (a.failureSignal || 0)) {
1181
+ return (b.failureSignal || 0) - (a.failureSignal || 0);
1182
+ }
902
1183
  if ((b.consecutiveNoCommits || 0) !== (a.consecutiveNoCommits || 0)) {
903
1184
  return (b.consecutiveNoCommits || 0) - (a.consecutiveNoCommits || 0);
904
1185
  }
@@ -908,28 +1189,80 @@ function buildPlannerFeedback() {
908
1189
  return String(a.taskId || "").localeCompare(String(b.taskId || ""));
909
1190
  });
910
1191
 
911
- let debtEntries = [];
912
- try {
913
- debtEntries = readTaskDebtEntries({ baseDir: repoRoot, limit: 300 });
914
- } catch {
915
- debtEntries = [];
1192
+ const persistedCounter = readPlannerPatternCounterState(repoRoot);
1193
+ const previousPatternState =
1194
+ persistedCounter?.state?.patterns && typeof persistedCounter.state.patterns === "object"
1195
+ ? persistedCounter.state.patterns
1196
+ : {};
1197
+ const nextPatternState = {};
1198
+ const observedKeys = new Set(patternObservations.keys());
1199
+
1200
+ for (const [key, observation] of patternObservations.entries()) {
1201
+ const previous = previousPatternState[key] || {};
1202
+ const previousCounter = Number(previous.failureCounter || 0);
1203
+ const nextCounter = Math.max(
1204
+ 0,
1205
+ (previousCounter * PLANNER_FAILURE_COUNTER_DECAY) +
1206
+ observation.failureSignalTotal -
1207
+ (observation.successSignalTotal * 1.1),
1208
+ );
1209
+ nextPatternState[key] = {
1210
+ key,
1211
+ repoArea: observation.repoArea,
1212
+ archetype: observation.archetype,
1213
+ observations: Number(previous.observations || 0) + 1,
1214
+ failures: Number(previous.failures || 0) + observation.failures,
1215
+ successes: Number(previous.successes || 0) + observation.successes,
1216
+ failureSignal: Number(observation.failureSignalTotal.toFixed(2)),
1217
+ successSignal: Number(observation.successSignalTotal.toFixed(2)),
1218
+ failureCounter: Number(nextCounter.toFixed(3)),
1219
+ lastUpdatedAt: new Date().toISOString(),
1220
+ };
916
1221
  }
917
1222
 
918
- const now = Date.now();
919
- const recentWindowMs = 7 * 24 * 60 * 60 * 1000;
920
- const recentDebtEntries = debtEntries.filter((entry) => {
921
- const ts = Date.parse(String(entry?.recordedAt || ""));
922
- return Number.isFinite(ts) && now - ts <= recentWindowMs;
1223
+ for (const [key, previous] of Object.entries(previousPatternState)) {
1224
+ if (observedKeys.has(key)) continue;
1225
+ const previousCounter = Number(previous?.failureCounter || 0);
1226
+ if (previousCounter <= 0) continue;
1227
+ nextPatternState[key] = {
1228
+ ...previous,
1229
+ failureCounter: Number(
1230
+ Math.max(0, previousCounter * PLANNER_FAILURE_COUNTER_IDLE_DECAY).toFixed(3),
1231
+ ),
1232
+ lastUpdatedAt: new Date().toISOString(),
1233
+ };
1234
+ }
1235
+
1236
+ writePlannerPatternCounterState(persistedCounter.statePath, {
1237
+ patterns: nextPatternState,
923
1238
  });
924
- const debtSeverityCounts = { critical: 0, high: 0, medium: 0, low: 0 };
925
- for (const entry of recentDebtEntries) {
926
- for (const item of Array.isArray(entry?.debtItems) ? entry.debtItems : []) {
927
- const severity = String(item?.severity || "").trim().toLowerCase();
928
- if (Object.prototype.hasOwnProperty.call(debtSeverityCounts, severity)) {
929
- debtSeverityCounts[severity] += 1;
1239
+
1240
+ const rankedPatternCounters = Object.values(nextPatternState)
1241
+ .map((entry) => {
1242
+ const failureCounter = Number(entry?.failureCounter || 0);
1243
+ const negativePrior = failureCounter > PLANNER_FAILURE_THRESHOLD
1244
+ ? Number(((failureCounter - PLANNER_FAILURE_THRESHOLD) * 0.6).toFixed(3))
1245
+ : 0;
1246
+ return {
1247
+ key: String(entry?.key || "").trim(),
1248
+ repoArea: String(entry?.repoArea || "").trim() || "global",
1249
+ archetype: normalizePlannerArchetypeKey(entry?.archetype),
1250
+ failureCounter,
1251
+ failures: Number(entry?.failures || 0),
1252
+ successes: Number(entry?.successes || 0),
1253
+ negativePrior,
1254
+ };
1255
+ })
1256
+ .sort((a, b) => {
1257
+ if ((b.negativePrior || 0) !== (a.negativePrior || 0)) {
1258
+ return (b.negativePrior || 0) - (a.negativePrior || 0);
930
1259
  }
931
- }
932
- }
1260
+ if ((b.failureCounter || 0) !== (a.failureCounter || 0)) {
1261
+ return (b.failureCounter || 0) - (a.failureCounter || 0);
1262
+ }
1263
+ return String(a.key || "").localeCompare(String(b.key || ""));
1264
+ })
1265
+ .slice(0, 50);
933
1266
 
934
1267
  const blockedTop = [...blockedReasonCounts.entries()]
935
1268
  .sort((a, b) => b[1] - a[1])
@@ -947,6 +1280,8 @@ function buildPlannerFeedback() {
947
1280
  `noCommitHot=${noCommitCount}`,
948
1281
  `debtRecent7d=${recentDebtEntries.length}`,
949
1282
  `debtCriticalHigh=${debtSeverityCounts.critical + debtSeverityCounts.high}`,
1283
+ `debtTrendDelta=${Number(debtTrendDelta.toFixed(2))}`,
1284
+ `penalizedPatterns=${rankedPatternCounters.filter((entry) => entry.negativePrior > 0).length}`,
950
1285
  ];
951
1286
  const blockedSummary = blockedTop.length > 0
952
1287
  ? blockedTop.map((item) => `${item.reason}(${item.count})`).join(", ")
@@ -970,6 +1305,15 @@ function buildPlannerFeedback() {
970
1305
  totalEntries: debtEntries.length,
971
1306
  recentEntries7d: recentDebtEntries.length,
972
1307
  severityCounts7d: debtSeverityCounts,
1308
+ previousSeverityCounts7d: previousDebtSeverityCounts,
1309
+ trendDelta: Number(debtTrendDelta.toFixed(2)),
1310
+ },
1311
+ rankingSignals: {
1312
+ failureThreshold: PLANNER_FAILURE_THRESHOLD,
1313
+ debtTrendDelta: Number(debtTrendDelta.toFixed(2)),
1314
+ debtTrendPenalty: Number(debtTrendPenalty.toFixed(3)),
1315
+ weights: PLANNER_FAILURE_SIGNAL_WEIGHTS,
1316
+ patterns: rankedPatternCounters,
973
1317
  },
974
1318
  };
975
1319
  }
@@ -1610,34 +1954,243 @@ const WORKSPACE_SYNC_INTERVAL_MS = parseEnvInteger(
1610
1954
  { min: 60 * 1000, max: 120 * 60 * 1000 },
1611
1955
  ); // 1m..120m (default 30m)
1612
1956
  const WORKSPACE_SYNC_INITIAL_DELAY_MS = parseEnvInteger(
1613
- process.env.BOSUN_WORKSPACE_SYNC_INITIAL_DELAY_MS,
1614
- 20 * 1000,
1615
- { min: 0, max: 5 * 60 * 1000 },
1616
- ); // 0s..5m (default 20s)
1957
+ isDevMode()
1958
+ ? process.env.DEVMODE_WORKSPACE_SYNC_INITIAL_DELAY_MS
1959
+ : process.env.BOSUN_WORKSPACE_SYNC_INITIAL_DELAY_MS,
1960
+ 60 * 1000,
1961
+ { min: 0, max: 10 * 60 * 1000 },
1962
+ ); // 0s..5m (default 60s)
1617
1963
  const WORKSPACE_SYNC_INITIAL_JITTER_MS = parseEnvInteger(
1618
1964
  process.env.BOSUN_WORKSPACE_SYNC_INITIAL_JITTER_MS,
1619
1965
  5 * 1000,
1620
1966
  { min: 0, max: 60 * 1000 },
1621
1967
  ); // 0s..60s (default 5s)
1622
1968
  const WORKSPACE_SYNC_WARN_THROTTLE_MS = parseEnvInteger(
1623
- process.env.BOSUN_WORKSPACE_SYNC_WARN_THROTTLE_MS,
1624
- 6 * 60 * 60 * 1000,
1625
- { min: 60 * 1000, max: 24 * 60 * 60 * 1000 },
1626
- ); // 1m..24h (default 6h)
1969
+ isDevMode()
1970
+ ? process.env.DEVMODE_WORKSPACE_SYNC_WARN_THROTTLE_MS
1971
+ : process.env.BOSUN_WORKSPACE_SYNC_WARN_THROTTLE_MS,
1972
+ isDevMode() ? 30 * 60 * 1000 : 6 * 60 * 60 * 1000,
1973
+ { min: 10 * 1000, max: 24 * 60 * 60 * 1000 },
1974
+ ); // 10s..24h (default dev 30m, prod 6h)
1627
1975
  const WORKSPACE_SYNC_SLOW_WARN_MS = parseEnvInteger(
1628
- process.env.BOSUN_WORKSPACE_SYNC_SLOW_WARN_MS,
1629
- 90 * 1000,
1976
+ isDevMode()
1977
+ ? process.env.DEVMODE_WORKSPACE_SYNC_SLOW_WARN_MS
1978
+ : process.env.BOSUN_WORKSPACE_SYNC_SLOW_WARN_MS,
1979
+ isDevMode() ? 30 * 1000 : 90 * 1000,
1630
1980
  { min: 5 * 1000, max: 10 * 60 * 1000 },
1631
- ); // 5s..10m (default 90s)
1981
+ ); // 5s..10m (default dev 30s, prod 90s)
1632
1982
  const WORKSPACE_SYNC_WARN_MAX_KEYS = parseEnvInteger(
1633
- process.env.BOSUN_WORKSPACE_SYNC_WARN_MAX_KEYS,
1983
+ isDevMode()
1984
+ ? process.env.DEVMODE_WORKSPACE_SYNC_WARN_MAX_KEYS
1985
+ : process.env.BOSUN_WORKSPACE_SYNC_WARN_MAX_KEYS,
1634
1986
  500,
1635
- { min: 50, max: 5000 },
1636
- ); // 50..5000 (default 500)
1987
+ { min: 10, max: 5000 },
1988
+ ); // 10..5000 (default 500)
1989
+ const WORKSPACE_SYNC_WARN_STATE_FILE = "ve-workspace-sync-warn-state.json";
1990
+ const WORKSPACE_SYNC_WARN_STATE_TMP_FILE = "ve-workspace-sync-warn-state.json.tmp";
1991
+ const WORKSPACE_SYNC_WARN_STATE_WRITE_GAP_MS = isDevMode()
1992
+ ? parseEnvInteger(
1993
+ process.env.DEVMODE_WORKSPACE_SYNC_WARN_STATE_WRITE_GAP_MS,
1994
+ 2 * 1000,
1995
+ { min: 200, max: 60 * 1000 },
1996
+ )
1997
+ : parseEnvInteger(
1998
+ process.env.BOSUN_WORKSPACE_SYNC_WARN_STATE_WRITE_GAP_MS,
1999
+ 5 * 1000,
2000
+ { min: 200, max: 60 * 1000 },
2001
+ );
2002
+ const WORKSPACE_SYNC_WARN_STALE_EXPIRY_MS = 24 * 60 * 60 * 1000;
2003
+ const WORKSPACE_SYNC_WARN_ARCHIVE_MAX_FILES = 5;
2004
+ const WORKSPACE_SYNC_WARN_ARCHIVE_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1000;
1637
2005
  let workspaceSyncTimer = null;
1638
2006
  let workspaceSyncInitialTimer = null;
1639
2007
  let workspaceSyncInFlight = false;
2008
+ let workspaceSyncWarnStateWriteTimer = null;
2009
+ let workspaceSyncWarnStateWriteConfigDir = null;
1640
2010
  const workspaceSyncWarnSeen = new Map();
2011
+ function resolveWorkspaceSyncWarnStatePaths(configDir) {
2012
+ const cacheDir = resolve(configDir || process.cwd(), ".cache");
2013
+ const statePath = resolve(cacheDir, WORKSPACE_SYNC_WARN_STATE_FILE);
2014
+ const tmpPath = resolve(cacheDir, WORKSPACE_SYNC_WARN_STATE_TMP_FILE);
2015
+ return { cacheDir, statePath, tmpPath };
2016
+ }
2017
+ function pruneWorkspaceSyncWarnSeen(now = Date.now()) {
2018
+ let changed = false;
2019
+ for (const [seenKey, seenAt] of workspaceSyncWarnSeen.entries()) {
2020
+ const updatedAt = Number(seenAt || 0);
2021
+ if (now - updatedAt >= WORKSPACE_SYNC_WARN_STALE_EXPIRY_MS) {
2022
+ workspaceSyncWarnSeen.delete(seenKey);
2023
+ changed = true;
2024
+ }
2025
+ }
2026
+ return changed;
2027
+ }
2028
+ function evictWorkspaceSyncWarnSeenOverflow() {
2029
+ let changed = false;
2030
+ // Evict oldest entries so warn state remains bounded.
2031
+ while (workspaceSyncWarnSeen.size > WORKSPACE_SYNC_WARN_MAX_KEYS) {
2032
+ const oldestKey = workspaceSyncWarnSeen.keys().next().value;
2033
+ if (!oldestKey) break;
2034
+ workspaceSyncWarnSeen.delete(oldestKey);
2035
+ changed = true;
2036
+ }
2037
+ return changed;
2038
+ }
2039
+ function cleanupWorkspaceSyncWarnStateArchives(configDir, now = Date.now()) {
2040
+ const { cacheDir, statePath } = resolveWorkspaceSyncWarnStatePaths(configDir);
2041
+ const archivePrefix = `${WORKSPACE_SYNC_WARN_STATE_FILE}.archive.`;
2042
+ let entries = [];
2043
+ try {
2044
+ entries = readdirSync(cacheDir)
2045
+ .filter((name) => name.startsWith(archivePrefix))
2046
+ .map((name) => {
2047
+ const path = resolve(cacheDir, name);
2048
+ let mtimeMs = 0;
2049
+ try {
2050
+ mtimeMs = Number(statSync(path).mtimeMs || 0);
2051
+ } catch {
2052
+ // Ignore files that disappear during cleanup.
2053
+ }
2054
+ return { name, path, mtimeMs };
2055
+ });
2056
+ } catch {
2057
+ return;
2058
+ }
2059
+ const keep = [];
2060
+ for (const entry of entries) {
2061
+ if (!entry.mtimeMs || now - entry.mtimeMs > WORKSPACE_SYNC_WARN_ARCHIVE_MAX_AGE_MS) {
2062
+ try {
2063
+ unlinkSync(entry.path);
2064
+ } catch {
2065
+ // Ignore cleanup failures; best effort.
2066
+ }
2067
+ continue;
2068
+ }
2069
+ keep.push(entry);
2070
+ }
2071
+ keep.sort((a, b) => b.mtimeMs - a.mtimeMs);
2072
+ for (const entry of keep.slice(WORKSPACE_SYNC_WARN_ARCHIVE_MAX_FILES)) {
2073
+ try {
2074
+ unlinkSync(entry.path);
2075
+ } catch {
2076
+ // Ignore cleanup failures; best effort.
2077
+ }
2078
+ }
2079
+ if (keep.length > WORKSPACE_SYNC_WARN_ARCHIVE_MAX_FILES) {
2080
+ console.log(
2081
+ `[monitor] workspace sync warn archive cleanup: kept ${WORKSPACE_SYNC_WARN_ARCHIVE_MAX_FILES} newest archives for ${basename(statePath)}`,
2082
+ );
2083
+ }
2084
+ }
2085
+ function archiveWorkspaceSyncWarnStateFile(configDir, reason = "corrupt") {
2086
+ const { cacheDir, statePath } = resolveWorkspaceSyncWarnStatePaths(configDir);
2087
+ if (!existsSync(statePath)) return;
2088
+ const timestamp = new Date().toISOString().replaceAll(":", "-");
2089
+ const archiveName = `${WORKSPACE_SYNC_WARN_STATE_FILE}.archive.${timestamp}.${reason}.json`;
2090
+ const archivePath = resolve(cacheDir, archiveName);
2091
+ try {
2092
+ renameSync(statePath, archivePath);
2093
+ console.warn(
2094
+ `[monitor] workspace sync warn state archived (${reason}): ${basename(archivePath)}`,
2095
+ );
2096
+ } catch (err) {
2097
+ console.warn(
2098
+ `[monitor] workspace sync warn state archive failed (${reason}): ${err?.message || err}`,
2099
+ );
2100
+ }
2101
+ }
2102
+ function persistWorkspaceSyncWarnState(configDir) {
2103
+ const { cacheDir, statePath, tmpPath } = resolveWorkspaceSyncWarnStatePaths(configDir);
2104
+ try {
2105
+ mkdirSync(cacheDir, { recursive: true });
2106
+ } catch (err) {
2107
+ console.warn(
2108
+ `[monitor] workspace sync warn state: unable to create cache dir: ${err?.message || err}`,
2109
+ );
2110
+ return;
2111
+ }
2112
+ const now = Date.now();
2113
+ pruneWorkspaceSyncWarnSeen(now);
2114
+ evictWorkspaceSyncWarnSeenOverflow();
2115
+ const entries = [];
2116
+ for (const [key, value] of workspaceSyncWarnSeen.entries()) {
2117
+ entries.push({ key, updatedAt: Number(value || 0) });
2118
+ }
2119
+ const payload = JSON.stringify(
2120
+ {
2121
+ version: 1,
2122
+ updatedAt: now,
2123
+ entries,
2124
+ },
2125
+ null,
2126
+ 2,
2127
+ );
2128
+ let tmpWritten = false;
2129
+ try {
2130
+ writeFileSync(tmpPath, payload, "utf8");
2131
+ tmpWritten = true;
2132
+ renameSync(tmpPath, statePath);
2133
+ } catch (err) {
2134
+ console.warn(
2135
+ `[monitor] workspace sync warn state persist failed: ${err?.message || err}`,
2136
+ );
2137
+ if (tmpWritten || existsSync(tmpPath)) {
2138
+ try {
2139
+ unlinkSync(tmpPath);
2140
+ } catch {
2141
+ // Ignore temp cleanup failures.
2142
+ }
2143
+ }
2144
+ }
2145
+ }
2146
+ function scheduleWorkspaceSyncWarnStatePersist(configDir, { immediate = false } = {}) {
2147
+ workspaceSyncWarnStateWriteConfigDir = configDir || workspaceSyncWarnStateWriteConfigDir || process.cwd();
2148
+ if (immediate || WORKSPACE_SYNC_WARN_STATE_WRITE_GAP_MS <= 0) {
2149
+ if (workspaceSyncWarnStateWriteTimer) {
2150
+ clearTimeout(workspaceSyncWarnStateWriteTimer);
2151
+ workspaceSyncWarnStateWriteTimer = null;
2152
+ }
2153
+ persistWorkspaceSyncWarnState(workspaceSyncWarnStateWriteConfigDir);
2154
+ return;
2155
+ }
2156
+ if (workspaceSyncWarnStateWriteTimer) {
2157
+ clearTimeout(workspaceSyncWarnStateWriteTimer);
2158
+ workspaceSyncWarnStateWriteTimer = null;
2159
+ }
2160
+ workspaceSyncWarnStateWriteTimer = safeSetTimeout("workspace-sync-warn-state-persist", () => {
2161
+ workspaceSyncWarnStateWriteTimer = null;
2162
+ persistWorkspaceSyncWarnState(workspaceSyncWarnStateWriteConfigDir);
2163
+ }, WORKSPACE_SYNC_WARN_STATE_WRITE_GAP_MS);
2164
+ if (workspaceSyncWarnStateWriteTimer?.unref) workspaceSyncWarnStateWriteTimer.unref();
2165
+ }
2166
+ function loadWorkspaceSyncWarnState(configDir) {
2167
+ const { statePath } = resolveWorkspaceSyncWarnStatePaths(configDir);
2168
+ // Run archive cleanup during monitor startup initialization.
2169
+ cleanupWorkspaceSyncWarnStateArchives(configDir);
2170
+ if (!existsSync(statePath)) return;
2171
+ try {
2172
+ const raw = readFileSync(statePath, "utf8");
2173
+ const parsed = JSON.parse(raw);
2174
+ const entries = Array.isArray(parsed?.entries)
2175
+ ? parsed.entries
2176
+ : Object.entries(parsed || {}).map(([key, updatedAt]) => ({ key, updatedAt }));
2177
+ workspaceSyncWarnSeen.clear();
2178
+ for (const entry of entries) {
2179
+ const key = String(entry?.key || "").trim();
2180
+ const updatedAt = Number(entry?.updatedAt || entry?.timestamp || 0);
2181
+ if (!key || !Number.isFinite(updatedAt) || updatedAt <= 0) continue;
2182
+ workspaceSyncWarnSeen.set(key, updatedAt);
2183
+ }
2184
+ const now = Date.now();
2185
+ const changed = pruneWorkspaceSyncWarnSeen(now) || evictWorkspaceSyncWarnSeenOverflow();
2186
+ if (changed) scheduleWorkspaceSyncWarnStatePersist(configDir, { immediate: true });
2187
+ } catch (err) {
2188
+ console.warn(
2189
+ `[monitor] workspace sync warn state load failed; archiving corrupt file: ${err?.message || err}`,
2190
+ );
2191
+ archiveWorkspaceSyncWarnStateFile(configDir, "corrupt");
2192
+ }
2193
+ }
1641
2194
  function stopWorkspaceSyncTimers() {
1642
2195
  if (workspaceSyncInitialTimer) {
1643
2196
  clearTimeout(workspaceSyncInitialTimer);
@@ -1647,30 +2200,48 @@ function stopWorkspaceSyncTimers() {
1647
2200
  clearInterval(workspaceSyncTimer);
1648
2201
  workspaceSyncTimer = null;
1649
2202
  }
2203
+ if (workspaceSyncWarnStateWriteTimer) {
2204
+ clearTimeout(workspaceSyncWarnStateWriteTimer);
2205
+ workspaceSyncWarnStateWriteTimer = null;
2206
+ scheduleWorkspaceSyncWarnStatePersist(
2207
+ workspaceSyncWarnStateWriteConfigDir || config?.configDir || process.cwd(),
2208
+ { immediate: true },
2209
+ );
2210
+ }
1650
2211
  }
1651
2212
  function shouldEmitWorkspaceSyncWarn(key, now = Date.now()) {
2213
+ const stateConfigDir = config?.configDir || process.cwd();
2214
+ let changed = false;
1652
2215
  for (const [seenKey, seenAt] of workspaceSyncWarnSeen.entries()) {
1653
2216
  if (now - Number(seenAt || 0) >= WORKSPACE_SYNC_WARN_THROTTLE_MS) {
1654
2217
  workspaceSyncWarnSeen.delete(seenKey);
2218
+ changed = true;
1655
2219
  }
1656
2220
  }
2221
+ changed = pruneWorkspaceSyncWarnSeen(now) || changed;
1657
2222
  const last = Number(workspaceSyncWarnSeen.get(key) || 0);
1658
2223
  if (last > 0 && now - last < WORKSPACE_SYNC_WARN_THROTTLE_MS) return false;
2224
+ // Refresh key insertion order so oldest eviction works predictably.
2225
+ workspaceSyncWarnSeen.delete(key);
1659
2226
  workspaceSyncWarnSeen.set(key, now);
2227
+ changed = true;
1660
2228
  // keep memory bounded
1661
- if (workspaceSyncWarnSeen.size > WORKSPACE_SYNC_WARN_MAX_KEYS) {
1662
- const oldestKey = workspaceSyncWarnSeen.keys().next().value;
1663
- if (oldestKey) workspaceSyncWarnSeen.delete(oldestKey);
1664
- }
2229
+ changed = evictWorkspaceSyncWarnSeenOverflow() || changed;
2230
+ if (changed) scheduleWorkspaceSyncWarnStatePersist(stateConfigDir);
1665
2231
  return true;
1666
2232
  }
1667
2233
  function clearWorkspaceSyncWarnForWorkspace(workspaceId) {
2234
+ let changed = false;
1668
2235
  const prefix = `${workspaceId}:`;
1669
2236
  for (const key of workspaceSyncWarnSeen.keys()) {
1670
2237
  if (String(key).startsWith(prefix)) {
1671
2238
  workspaceSyncWarnSeen.delete(key);
2239
+ changed = true;
1672
2240
  }
1673
2241
  }
2242
+ if (changed) {
2243
+ scheduleWorkspaceSyncWarnStatePersist(config?.configDir || process.cwd());
2244
+ }
1674
2245
  }
1675
2246
  function isBenignWorkspaceSyncFailure(errorText) {
1676
2247
  const text = String(errorText || "").toLowerCase();
@@ -1695,6 +2266,7 @@ function isBenignWorkspaceSyncFailure(errorText) {
1695
2266
  {
1696
2267
  const wsArray = config.repositories?.filter((r) => r.workspace) || [];
1697
2268
  if (wsArray.length > 0) {
2269
+ loadWorkspaceSyncWarnState(config?.configDir || process.cwd());
1698
2270
  const workspaceIds = [...new Set(wsArray.map((r) => r.workspace).filter(Boolean))];
1699
2271
  const doWorkspaceSync = () => {
1700
2272
  if (shuttingDown) return;
@@ -2147,6 +2719,10 @@ const SELF_RESTART_QUIET_MS = Math.max(
2147
2719
  90_000,
2148
2720
  Number(process.env.SELF_RESTART_QUIET_MS || "90000"),
2149
2721
  );
2722
+ const ENV_RELOAD_DELAY_MS = Math.max(
2723
+ 500,
2724
+ Number(process.env.ENV_RELOAD_DELAY_MS || "5000") || 5000,
2725
+ );
2150
2726
  const SELF_RESTART_RETRY_MS = Math.max(
2151
2727
  15_000,
2152
2728
  Number(process.env.SELF_RESTART_RETRY_MS || "30000"),
@@ -13283,7 +13859,6 @@ async function startWatcher(force = false) {
13283
13859
  stopWatcher();
13284
13860
  }
13285
13861
  let targetPath = watchPath;
13286
- let missingWatchPath = false;
13287
13862
  try {
13288
13863
  const stats = await (await import("node:fs/promises")).stat(watchPath);
13289
13864
  if (stats.isFile()) {
@@ -13291,21 +13866,15 @@ async function startWatcher(force = false) {
13291
13866
  targetPath = watchPath.split(/[\\/]/).slice(0, -1).join("/") || ".";
13292
13867
  }
13293
13868
  } catch {
13294
- // The configured path may not exist yet (common for stale ORCHESTRATOR_SCRIPT paths).
13295
- // Fall back to watching its parent directory if present; otherwise watch repoRoot.
13296
- missingWatchPath = true;
13297
- const candidateFile = watchPath.split(/[\\/]/).pop() || null;
13298
- const candidateDir = watchPath.split(/[\\/]/).slice(0, -1).join("/") || ".";
13299
- if (existsSync(candidateDir)) {
13300
- targetPath = candidateDir;
13301
- watchFileName = candidateFile;
13302
- } else if (existsSync(repoRoot)) {
13303
- targetPath = repoRoot;
13304
- watchFileName = null;
13305
- } else {
13306
- targetPath = process.cwd();
13307
- watchFileName = null;
13308
- }
13869
+ // The configured path does not exist. Previous behaviour fell back to
13870
+ // watching the parent directory, which could be extremely broad (e.g. the
13871
+ // entire AppData/Roaming tree) and trigger spurious restarts. Disable the
13872
+ // watcher entirely instead the auto-update loop handles updates in
13873
+ // npm/prod mode, and in dev mode the source-dir watcher covers restarts.
13874
+ console.warn(
13875
+ `[monitor] watcher disabled — configured watch path does not exist: ${watchPath}`,
13876
+ );
13877
+ return;
13309
13878
  }
13310
13879
 
13311
13880
  if (!existsSync(targetPath)) {
@@ -13314,11 +13883,6 @@ async function startWatcher(force = false) {
13314
13883
  );
13315
13884
  return;
13316
13885
  }
13317
- if (missingWatchPath) {
13318
- console.warn(
13319
- `[monitor] watch path not found: ${watchPath} — watching ${targetPath} instead`,
13320
- );
13321
- }
13322
13886
 
13323
13887
  try {
13324
13888
  watcher = watch(targetPath, { persistent: true }, (_event, filename) => {
@@ -13360,7 +13924,7 @@ function scheduleEnvReload(reason) {
13360
13924
  runDetached("config-reload:env-change", () =>
13361
13925
  reloadConfig(reason || "env-change"),
13362
13926
  );
13363
- }, 400);
13927
+ }, ENV_RELOAD_DELAY_MS);
13364
13928
  }
13365
13929
 
13366
13930
  function startEnvWatchers() {
@@ -14943,6 +15507,73 @@ if (isExecutorDisabled()) {
14943
15507
  console.warn(`[monitor] supervisor continue: no active session for ${taskId}`);
14944
15508
  }
14945
15509
  },
15510
+ dispatchFixTask: (taskId, issues) => {
15511
+ const normalizedTaskId = String(taskId || "").trim();
15512
+ if (!normalizedTaskId) return;
15513
+ const task = getInternalTask(normalizedTaskId);
15514
+ const issueList = Array.isArray(issues) ? issues : [];
15515
+ const issueCount = issueList.length;
15516
+ const status = String(task?.status || "").trim().toLowerCase();
15517
+
15518
+ if (status && status !== "inreview") {
15519
+ console.warn(
15520
+ `[monitor] supervisor dispatch-fix skipped for ${normalizedTaskId}: status=${status}`,
15521
+ );
15522
+ return;
15523
+ }
15524
+
15525
+ if (hasActiveSession(normalizedTaskId)) {
15526
+ const issueSummary = issueList
15527
+ .slice(0, 5)
15528
+ .map((issue) => {
15529
+ const severity = String(issue?.severity || "major");
15530
+ const category = String(issue?.category || "review");
15531
+ const file = String(issue?.file || "(unknown)");
15532
+ const line = Number.isFinite(Number(issue?.line))
15533
+ ? `:${Number(issue.line)}`
15534
+ : "";
15535
+ const description = String(issue?.description || "").trim();
15536
+ return `- [${severity}/${category}] ${file}${line}${description ? ` - ${description}` : ""}`;
15537
+ })
15538
+ .join("\n");
15539
+ const prompt = [
15540
+ `Review requested changes for task "${task?.title || normalizedTaskId}".`,
15541
+ issueSummary
15542
+ ? `Fix these review issues first:\n${issueSummary}`
15543
+ : "Fix the reported review issues first.",
15544
+ "Stay on the current branch, run relevant tests, commit the fixes, and continue toward PR update.",
15545
+ ].join("\n\n");
15546
+ console.log(
15547
+ `[monitor] supervisor dispatch-fix steering active session for ${normalizedTaskId}`,
15548
+ );
15549
+ steerActiveThread(normalizedTaskId, prompt);
15550
+ return;
15551
+ }
15552
+
15553
+ console.warn(
15554
+ `[monitor] supervisor dispatch-fix: no active session for ${normalizedTaskId}; resetting task to todo`,
15555
+ );
15556
+ try {
15557
+ setInternalTaskStatus(
15558
+ normalizedTaskId,
15559
+ "todo",
15560
+ "review-fix-redispatch",
15561
+ );
15562
+ } catch {
15563
+ /* best-effort */
15564
+ }
15565
+ void updateTaskStatus(normalizedTaskId, "todo", {
15566
+ source: "review-fix-redispatch",
15567
+ workflowEvent: "task.review_fix_requested",
15568
+ workflowData: {
15569
+ reviewIssueCount: issueCount,
15570
+ },
15571
+ }).catch((err) => {
15572
+ console.warn(
15573
+ `[monitor] supervisor dispatch-fix transition failed for ${normalizedTaskId}: ${err?.message || err}`,
15574
+ );
15575
+ });
15576
+ },
14946
15577
  });
14947
15578
  agentSupervisor.start();
14948
15579
 
@@ -15383,8 +16014,3 @@ export {
15383
16014
  // Workflow event bridge — for fleet/kanban modules to emit events
15384
16015
  queueWorkflowEvent,
15385
16016
  };
15386
-
15387
-
15388
-
15389
-
15390
-