coder-config 0.50.3 → 0.50.4-beta

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/lib/constants.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Constants and tool path configurations
3
3
  */
4
4
 
5
- const VERSION = '0.50.3';
5
+ const VERSION = '0.50.4-beta';
6
6
 
7
7
  // Tool-specific path configurations
8
8
  const TOOL_PATHS = {
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Heartbeat module - monitors Ralph Loop health
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const { loadLoops, loadLoopState } = require('./loops.js');
8
+
9
+ /**
10
+ * Get default heartbeat configuration
11
+ */
12
+ function getDefaultHeartbeatConfig() {
13
+ return {
14
+ staleThresholdMinutes: 30,
15
+ iterationLimitPercent: 80,
16
+ cooldownMinutes: 15,
17
+ notifications: {
18
+ macos: { enabled: true },
19
+ slack: { enabled: false }
20
+ }
21
+ };
22
+ }
23
+
24
+ /**
25
+ * Evaluate heartbeat for all active loops
26
+ * @param {string} installDir - path to coder-config install dir
27
+ * @param {object} [config] - optional heartbeat config overrides
28
+ * @returns {object} report with alerts and healthy loops
29
+ */
30
+ function heartbeat(installDir, config) {
31
+ const hbConfig = Object.assign({}, getDefaultHeartbeatConfig(), config || {});
32
+
33
+ const registry = loadLoops(installDir);
34
+ const loops = registry.loops || [];
35
+
36
+ const alerts = [];
37
+ const healthy = [];
38
+ const terminalStates = new Set(['completed', 'cancelled']);
39
+
40
+ for (const entry of loops) {
41
+ const state = loadLoopState(installDir, entry.id);
42
+ if (!state) continue;
43
+
44
+ const status = state.status || 'unknown';
45
+ if (terminalStates.has(status)) continue;
46
+
47
+ const name = state.name || entry.name || entry.id;
48
+
49
+ // Check: failed
50
+ if (status === 'failed') {
51
+ alerts.push({
52
+ loopId: entry.id,
53
+ name,
54
+ type: 'failed',
55
+ message: `Loop "${name}" has failed`,
56
+ severity: 'critical'
57
+ });
58
+ continue;
59
+ }
60
+
61
+ let hasAlert = false;
62
+
63
+ // Check: stale
64
+ const perLoopThreshold = state.heartbeat && state.heartbeat.staleThresholdMinutes != null
65
+ ? state.heartbeat.staleThresholdMinutes
66
+ : hbConfig.staleThresholdMinutes;
67
+
68
+ if (state.updatedAt) {
69
+ const updatedAt = new Date(state.updatedAt);
70
+ const ageMinutes = (Date.now() - updatedAt.getTime()) / 60000;
71
+ if (ageMinutes >= perLoopThreshold) {
72
+ alerts.push({
73
+ loopId: entry.id,
74
+ name,
75
+ type: 'stale',
76
+ message: `Loop "${name}" has not updated in ${Math.floor(ageMinutes)} minutes`,
77
+ severity: 'warning'
78
+ });
79
+ hasAlert = true;
80
+ }
81
+ }
82
+
83
+ // Check: iteration_limit
84
+ const iterations = state.iterations || {};
85
+ const current = iterations.current != null ? iterations.current : 0;
86
+ const max = iterations.max != null ? iterations.max : 0;
87
+ if (max > 0) {
88
+ const pct = (current / max) * 100;
89
+ if (pct >= hbConfig.iterationLimitPercent) {
90
+ alerts.push({
91
+ loopId: entry.id,
92
+ name,
93
+ type: 'iteration_limit',
94
+ message: `Loop "${name}" is at ${Math.round(pct)}% of iteration limit (${current}/${max})`,
95
+ severity: 'warning'
96
+ });
97
+ hasAlert = true;
98
+ }
99
+ }
100
+
101
+ // Check: phase_gate (plan phase + running)
102
+ if (state.phase === 'plan' && status === 'running') {
103
+ alerts.push({
104
+ loopId: entry.id,
105
+ name,
106
+ type: 'phase_gate',
107
+ message: `Loop "${name}" is waiting at plan phase gate`,
108
+ severity: 'info'
109
+ });
110
+ hasAlert = true;
111
+ }
112
+
113
+ // Check: blocked (paused with pauseReason)
114
+ if (status === 'paused' && state.pauseReason) {
115
+ alerts.push({
116
+ loopId: entry.id,
117
+ name,
118
+ type: 'blocked',
119
+ message: `Loop "${name}" is blocked: ${state.pauseReason}`,
120
+ severity: 'info'
121
+ });
122
+ hasAlert = true;
123
+ }
124
+
125
+ if (!hasAlert) {
126
+ healthy.push({
127
+ loopId: entry.id,
128
+ name,
129
+ phase: state.phase || null,
130
+ iteration: current
131
+ });
132
+ }
133
+ }
134
+
135
+ const activeLoops = loops.filter(e => {
136
+ const state = loadLoopState(installDir, e.id);
137
+ if (!state) return false;
138
+ const status = state.status || 'unknown';
139
+ return !terminalStates.has(status);
140
+ }).length;
141
+
142
+ const criticalCount = alerts.filter(a => a.severity === 'critical').length;
143
+ const warningCount = alerts.filter(a => a.severity === 'warning').length;
144
+
145
+ let summary;
146
+ if (alerts.length === 0) {
147
+ summary = `${activeLoops} active loop${activeLoops !== 1 ? 's' : ''}, all healthy`;
148
+ } else {
149
+ const parts = [];
150
+ if (criticalCount > 0) parts.push(`${criticalCount} critical`);
151
+ if (warningCount > 0) parts.push(`${warningCount} warning${warningCount !== 1 ? 's' : ''}`);
152
+ const infoCount = alerts.filter(a => a.severity === 'info').length;
153
+ if (infoCount > 0) parts.push(`${infoCount} info`);
154
+ summary = `${activeLoops} active loop${activeLoops !== 1 ? 's' : ''}, ${parts.join(', ')}`;
155
+ }
156
+
157
+ return {
158
+ timestamp: new Date().toISOString(),
159
+ activeLoops,
160
+ alerts,
161
+ healthy,
162
+ summary
163
+ };
164
+ }
165
+
166
+ module.exports = {
167
+ getDefaultHeartbeatConfig,
168
+ heartbeat
169
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coder-config",
3
- "version": "0.50.3",
3
+ "version": "0.50.4-beta",
4
4
  "description": "Configuration manager for AI coding tools - Claude Code, Gemini CLI, Codex CLI, Antigravity. Manage MCPs, rules, permissions, memory, and workstreams.",
5
5
  "author": "regression.io",
6
6
  "main": "config-loader.js",