bosun 0.41.0 → 0.41.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +8 -0
- package/README.md +20 -0
- package/agent/agent-event-bus.mjs +248 -6
- package/agent/agent-pool.mjs +125 -28
- package/agent/agent-work-analyzer.mjs +8 -16
- package/agent/retry-queue.mjs +164 -0
- package/bosun.config.example.json +25 -0
- package/bosun.schema.json +825 -183
- package/cli.mjs +59 -5
- package/config/config.mjs +130 -3
- package/infra/monitor.mjs +693 -67
- package/infra/runtime-accumulator.mjs +376 -84
- package/infra/session-tracker.mjs +82 -25
- package/lib/codebase-audit.mjs +133 -18
- package/package.json +23 -4
- package/server/setup-web-server.mjs +25 -0
- package/server/ui-server.mjs +248 -29
- package/setup.mjs +27 -24
- package/shell/codex-shell.mjs +34 -3
- package/shell/copilot-shell.mjs +50 -8
- package/task/msg-hub.mjs +193 -0
- package/task/pipeline.mjs +544 -0
- package/task/task-cli.mjs +38 -2
- package/task/task-executor-pipeline.mjs +143 -0
- package/task/task-executor.mjs +36 -27
- package/telegram/get-telegram-chat-id.mjs +57 -47
- package/ui/components/workspace-switcher.js +7 -7
- package/ui/demo-defaults.js +15694 -10573
- package/ui/modules/settings-schema.js +2 -0
- package/ui/modules/state.js +54 -57
- package/ui/modules/voice-client-sdk.js +375 -36
- package/ui/modules/voice-client.js +140 -31
- package/ui/setup.html +68 -2
- package/ui/styles/components.css +57 -0
- package/ui/styles.css +201 -1
- package/ui/tabs/dashboard.js +74 -0
- package/ui/tabs/logs.js +10 -0
- package/ui/tabs/settings.js +178 -99
- package/ui/tabs/tasks.js +31 -1
- package/ui/tabs/telemetry.js +34 -0
- package/ui/tabs/workflow-canvas-utils.mjs +8 -1
- package/ui/tabs/workflows.js +532 -275
- package/voice/voice-agents-sdk.mjs +1 -1
- package/voice/voice-relay.mjs +6 -6
- package/workflow/declarative-workflows.mjs +145 -0
- package/workflow/msg-hub.mjs +237 -0
- package/workflow/pipeline-workflows.mjs +287 -0
- package/workflow/pipeline.mjs +828 -315
- package/workflow/workflow-cli.mjs +128 -0
- package/workflow/workflow-engine.mjs +329 -17
- package/workflow/workflow-nodes/custom-loader.mjs +250 -0
- package/workflow/workflow-nodes.mjs +1955 -223
- package/workflow/workflow-templates.mjs +26 -8
- package/workflow-templates/agents.mjs +0 -1
- package/workflow-templates/bosun-native.mjs +212 -2
- package/workflow-templates/continuation-loop.mjs +339 -0
- package/workflow-templates/github.mjs +516 -40
- package/workflow-templates/planning.mjs +446 -17
- package/workflow-templates/reliability.mjs +65 -12
- package/workflow-templates/task-batch.mjs +24 -8
- package/workflow-templates/task-lifecycle.mjs +83 -6
- package/workspace/context-cache.mjs +66 -18
- package/workspace/workspace-manager.mjs +2 -1
- package/workflow-templates/issue-continuation.mjs +0 -243
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
|
|
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
|
|
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
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
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
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
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
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
const
|
|
928
|
-
|
|
929
|
-
|
|
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
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
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
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
)
|
|
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
|
-
|
|
1629
|
-
|
|
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
|
-
|
|
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:
|
|
1636
|
-
); //
|
|
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
|
-
|
|
1662
|
-
|
|
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
|
|
13295
|
-
//
|
|
13296
|
-
|
|
13297
|
-
|
|
13298
|
-
|
|
13299
|
-
|
|
13300
|
-
|
|
13301
|
-
|
|
13302
|
-
|
|
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
|
-
},
|
|
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
|
-
|