workflow-ai 1.0.65 → 1.0.67
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/README.md +377 -371
- package/configs/agent-health-rules.yaml +12 -1
- package/configs/pipeline.yaml +6 -6
- package/package.json +1 -1
- package/src/lib/agent-spawner.mjs +47 -6
- package/src/lib/error-classifier.mjs +311 -274
- package/src/runner.mjs +241 -58
- package/src/skills/coach/tests/cases/TC-COACH-001/current/meta.json +93 -93
- package/src/skills/coach/tests/cases/TC-COACH-002/current/meta.json +93 -93
- package/src/skills/create-plan/SKILL.md +1 -0
- package/src/skills/create-plan/knowledge/test-hygiene.md +47 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/meta.json +113 -113
- package/src/skills/execute-task/tests/cases/TC-EXECUTE-TASK-001/current/meta.json +87 -87
- package/src/skills/execute-task/tests/cases/TC-EXECUTE-TASK-005/current/meta.json +87 -87
- package/src/skills/review-result/SKILL.md +1 -0
- package/src/skills/review-result/knowledge/test-hygiene.md +44 -0
- package/src/skills/review-result/scripts/verify-artifacts.js +115 -2
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/claude-sonnet/trial-1.md +7 -0
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/claude-sonnet/trial-2.md +7 -0
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/claude-sonnet/trial-3.md +7 -0
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/judge.json +163 -0
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/kilo-deepseek/trial-1.md +5 -0
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/kilo-deepseek/trial-2.md +5 -0
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/kilo-deepseek/trial-3.md +11 -0
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/kilo-glm/trial-1.md +16 -0
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/kilo-glm/trial-2.md +18 -0
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/kilo-glm/trial-3.md +17 -0
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/kilo-minimax/trial-1.md +17 -0
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/kilo-minimax/trial-2.md +31 -0
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/kilo-minimax/trial-3.md +5 -0
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/meta.json +115 -0
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003-test-isolation.yaml +50 -0
- package/src/skills/review-result/tests/fixtures/QA-904-test-isolation-violation/QA-904.md +51 -0
- package/src/skills/review-result/tests/fixtures/QA-904-test-isolation-violation/example-test.mjs +36 -0
- package/src/skills/review-result/tests/index.yaml +5 -0
- package/src/skills/review-result/tests/rubrics/test-isolation.md +20 -0
|
@@ -1,274 +1,311 @@
|
|
|
1
|
-
import { load as loadYaml } from './js-yaml.mjs';
|
|
2
|
-
import { readFileSync, existsSync } from 'fs';
|
|
3
|
-
import { resolve, join } from 'path';
|
|
4
|
-
|
|
5
|
-
const STDERR_MATCH_LIMIT = 64 * 1024;
|
|
6
|
-
const MATCH_TIMEOUT_MS = 100;
|
|
7
|
-
const SUPPORTED_VERSION = '1.0';
|
|
8
|
-
const MIN_UTC_MIDNIGHT_DELAY_MS = 30 * 60 * 1000;
|
|
9
|
-
|
|
10
|
-
export class InvalidRulesConfigError extends Error {
|
|
11
|
-
constructor(message) {
|
|
12
|
-
super(message);
|
|
13
|
-
this.name = 'InvalidRulesConfigError';
|
|
14
|
-
if (Error.captureStackTrace) {
|
|
15
|
-
Error.captureStackTrace(this, this.constructor);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function parseConfigFile(configPath) {
|
|
21
|
-
if (!existsSync(configPath)) {
|
|
22
|
-
return { common: [], agents: {} };
|
|
23
|
-
}
|
|
24
|
-
const content = readFileSync(configPath, 'utf-8');
|
|
25
|
-
const config = loadYaml(content);
|
|
26
|
-
if (!config || typeof config !== 'object') {
|
|
27
|
-
return { common: [], agents: {} };
|
|
28
|
-
}
|
|
29
|
-
return config;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function validateVersion(version) {
|
|
33
|
-
if (version !== SUPPORTED_VERSION) {
|
|
34
|
-
throw new InvalidRulesConfigError(
|
|
35
|
-
`Unsupported rules version: ${version}. Update runner or downgrade rules file.`
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function compilePattern(pattern) {
|
|
41
|
-
if (!pattern) {
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
try {
|
|
45
|
-
return new RegExp(pattern);
|
|
46
|
-
} catch (e) {
|
|
47
|
-
throw new InvalidRulesConfigError(`Invalid regex pattern: ${pattern}. ${e.message}`);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function resolveExtends(agentsConfig, agentId, visited = new Set()) {
|
|
52
|
-
if (!agentsConfig[agentId]) {
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
if (!agentsConfig[agentId].extends) {
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
const extendsTarget = agentsConfig[agentId].extends;
|
|
59
|
-
if (visited.has(agentId)) {
|
|
60
|
-
throw new InvalidRulesConfigError('chained extends not supported');
|
|
61
|
-
}
|
|
62
|
-
if (!agentsConfig[extendsTarget]) {
|
|
63
|
-
throw new InvalidRulesConfigError(`extends target '${extendsTarget}' not found`);
|
|
64
|
-
}
|
|
65
|
-
if (agentsConfig[extendsTarget].extends) {
|
|
66
|
-
throw new InvalidRulesConfigError('chained extends not supported');
|
|
67
|
-
}
|
|
68
|
-
visited.add(agentId);
|
|
69
|
-
return agentsConfig[extendsTarget].rules || [];
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function buildRules(rulesData, compiledRules = []) {
|
|
73
|
-
if (!Array.isArray(rulesData)) {
|
|
74
|
-
return compiledRules;
|
|
75
|
-
}
|
|
76
|
-
for (const rule of rulesData) {
|
|
77
|
-
if (!rule || !rule.id || !rule.class || !rule.ttl) {
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
compiledRules.push({
|
|
81
|
-
id: rule.id,
|
|
82
|
-
class: rule.class,
|
|
83
|
-
ttl: rule.ttl,
|
|
84
|
-
pattern: compilePattern(rule.pattern),
|
|
85
|
-
exitCodes: rule.exit_codes === 'any' ? 'any' : (
|
|
86
|
-
Array.isArray(rule.exit_codes)
|
|
87
|
-
? rule.exit_codes.filter(c => typeof c === 'number')
|
|
88
|
-
: []
|
|
89
|
-
),
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
return compiledRules;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export function loadRules(projectRoot, configPath) {
|
|
96
|
-
const defaultPath = join(projectRoot, '.workflow/config/agent-health-rules.yaml');
|
|
97
|
-
const fullPath = configPath || defaultPath;
|
|
98
|
-
let config;
|
|
99
|
-
try {
|
|
100
|
-
config = parseConfigFile(fullPath);
|
|
101
|
-
} catch (e) {
|
|
102
|
-
if (e instanceof InvalidRulesConfigError) {
|
|
103
|
-
throw e;
|
|
104
|
-
}
|
|
105
|
-
return { common: [], agents: new Map() };
|
|
106
|
-
}
|
|
107
|
-
if (!config || typeof config !== 'object') {
|
|
108
|
-
return { common: [], agents: new Map() };
|
|
109
|
-
}
|
|
110
|
-
if (config.version) {
|
|
111
|
-
validateVersion(config.version);
|
|
112
|
-
}
|
|
113
|
-
const commonRules = buildRules(config.common || []);
|
|
114
|
-
const agents = new Map();
|
|
115
|
-
const agentsConfig = config.agents || {};
|
|
116
|
-
for (const agentId of Object.keys(agentsConfig)) {
|
|
117
|
-
const agentConfig = agentsConfig[agentId];
|
|
118
|
-
if (!agentConfig) {
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
const inheritedRules = resolveExtends(agentsConfig, agentId);
|
|
122
|
-
const ownRules = buildRules(agentConfig.rules || []);
|
|
123
|
-
const finalRules = [...ownRules];
|
|
124
|
-
if (inheritedRules && inheritedRules.length > 0) {
|
|
125
|
-
finalRules.push(...buildRules(inheritedRules));
|
|
126
|
-
}
|
|
127
|
-
agents.set(agentId, finalRules);
|
|
128
|
-
}
|
|
129
|
-
return { common: commonRules, agents };
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function truncateStderr(stderr) {
|
|
133
|
-
if (!stderr || stderr.length <= STDERR_MATCH_LIMIT) {
|
|
134
|
-
return stderr;
|
|
135
|
-
}
|
|
136
|
-
const halfLimit = STDERR_MATCH_LIMIT / 2;
|
|
137
|
-
const head = stderr.slice(0, halfLimit);
|
|
138
|
-
const tail = stderr.slice(-halfLimit);
|
|
139
|
-
return head + '\n...[TRUNCATED]...\n' + tail;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function matchRule(rule, exitCode, truncatedStderr) {
|
|
143
|
-
const exitCodesMatch = rule.exitCodes === 'any' ||
|
|
144
|
-
rule.exitCodes.includes(exitCode);
|
|
145
|
-
if (!exitCodesMatch) {
|
|
146
|
-
return false;
|
|
147
|
-
}
|
|
148
|
-
if (!rule.pattern) {
|
|
149
|
-
return true;
|
|
150
|
-
}
|
|
151
|
-
try {
|
|
152
|
-
return rule.pattern.test(truncatedStderr);
|
|
153
|
-
} catch (e) {
|
|
154
|
-
return false;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function matchWithTimeout(regex, text) {
|
|
159
|
-
return new Promise((resolve) => {
|
|
160
|
-
const timeoutId = setTimeout(() => {
|
|
161
|
-
resolve(false);
|
|
162
|
-
}, MATCH_TIMEOUT_MS);
|
|
163
|
-
try {
|
|
164
|
-
const result = regex.test(text);
|
|
165
|
-
clearTimeout(timeoutId);
|
|
166
|
-
resolve(result);
|
|
167
|
-
} catch (e) {
|
|
168
|
-
clearTimeout(timeoutId);
|
|
169
|
-
resolve(false);
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export async function classify(rules, agentId, { exitCode, stderr }) {
|
|
175
|
-
const truncatedStderr = truncateStderr(stderr);
|
|
176
|
-
const agentRules = rules.agents.get(agentId) || [];
|
|
177
|
-
for (const rule of agentRules) {
|
|
178
|
-
const matched = rule.pattern
|
|
179
|
-
? await matchWithTimeout(rule.pattern, truncatedStderr)
|
|
180
|
-
: matchRule(rule, exitCode, truncatedStderr);
|
|
181
|
-
if (matched) {
|
|
182
|
-
return {
|
|
183
|
-
class: rule.class,
|
|
184
|
-
rule_id: rule.id,
|
|
185
|
-
ttl: rule.ttl,
|
|
186
|
-
reason: truncatedStderr,
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
for (const rule of rules.common) {
|
|
191
|
-
const matched = rule.pattern
|
|
192
|
-
? await matchWithTimeout(rule.pattern, truncatedStderr)
|
|
193
|
-
: matchRule(rule, exitCode, truncatedStderr);
|
|
194
|
-
if (matched) {
|
|
195
|
-
return {
|
|
196
|
-
class: rule.class,
|
|
197
|
-
rule_id: rule.id,
|
|
198
|
-
ttl: rule.ttl,
|
|
199
|
-
reason: truncatedStderr,
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
return null;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
1
|
+
import { load as loadYaml } from './js-yaml.mjs';
|
|
2
|
+
import { readFileSync, existsSync } from 'fs';
|
|
3
|
+
import { resolve, join } from 'path';
|
|
4
|
+
|
|
5
|
+
const STDERR_MATCH_LIMIT = 64 * 1024;
|
|
6
|
+
const MATCH_TIMEOUT_MS = 100;
|
|
7
|
+
const SUPPORTED_VERSION = '1.0';
|
|
8
|
+
const MIN_UTC_MIDNIGHT_DELAY_MS = 30 * 60 * 1000;
|
|
9
|
+
|
|
10
|
+
export class InvalidRulesConfigError extends Error {
|
|
11
|
+
constructor(message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'InvalidRulesConfigError';
|
|
14
|
+
if (Error.captureStackTrace) {
|
|
15
|
+
Error.captureStackTrace(this, this.constructor);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseConfigFile(configPath) {
|
|
21
|
+
if (!existsSync(configPath)) {
|
|
22
|
+
return { common: [], agents: {} };
|
|
23
|
+
}
|
|
24
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
25
|
+
const config = loadYaml(content);
|
|
26
|
+
if (!config || typeof config !== 'object') {
|
|
27
|
+
return { common: [], agents: {} };
|
|
28
|
+
}
|
|
29
|
+
return config;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function validateVersion(version) {
|
|
33
|
+
if (version !== SUPPORTED_VERSION) {
|
|
34
|
+
throw new InvalidRulesConfigError(
|
|
35
|
+
`Unsupported rules version: ${version}. Update runner or downgrade rules file.`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function compilePattern(pattern) {
|
|
41
|
+
if (!pattern) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
return new RegExp(pattern);
|
|
46
|
+
} catch (e) {
|
|
47
|
+
throw new InvalidRulesConfigError(`Invalid regex pattern: ${pattern}. ${e.message}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function resolveExtends(agentsConfig, agentId, visited = new Set()) {
|
|
52
|
+
if (!agentsConfig[agentId]) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
if (!agentsConfig[agentId].extends) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const extendsTarget = agentsConfig[agentId].extends;
|
|
59
|
+
if (visited.has(agentId)) {
|
|
60
|
+
throw new InvalidRulesConfigError('chained extends not supported');
|
|
61
|
+
}
|
|
62
|
+
if (!agentsConfig[extendsTarget]) {
|
|
63
|
+
throw new InvalidRulesConfigError(`extends target '${extendsTarget}' not found`);
|
|
64
|
+
}
|
|
65
|
+
if (agentsConfig[extendsTarget].extends) {
|
|
66
|
+
throw new InvalidRulesConfigError('chained extends not supported');
|
|
67
|
+
}
|
|
68
|
+
visited.add(agentId);
|
|
69
|
+
return agentsConfig[extendsTarget].rules || [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function buildRules(rulesData, compiledRules = []) {
|
|
73
|
+
if (!Array.isArray(rulesData)) {
|
|
74
|
+
return compiledRules;
|
|
75
|
+
}
|
|
76
|
+
for (const rule of rulesData) {
|
|
77
|
+
if (!rule || !rule.id || !rule.class || !rule.ttl) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
compiledRules.push({
|
|
81
|
+
id: rule.id,
|
|
82
|
+
class: rule.class,
|
|
83
|
+
ttl: rule.ttl,
|
|
84
|
+
pattern: compilePattern(rule.pattern),
|
|
85
|
+
exitCodes: rule.exit_codes === 'any' ? 'any' : (
|
|
86
|
+
Array.isArray(rule.exit_codes)
|
|
87
|
+
? rule.exit_codes.filter(c => typeof c === 'number')
|
|
88
|
+
: []
|
|
89
|
+
),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return compiledRules;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function loadRules(projectRoot, configPath) {
|
|
96
|
+
const defaultPath = join(projectRoot, '.workflow/config/agent-health-rules.yaml');
|
|
97
|
+
const fullPath = configPath || defaultPath;
|
|
98
|
+
let config;
|
|
99
|
+
try {
|
|
100
|
+
config = parseConfigFile(fullPath);
|
|
101
|
+
} catch (e) {
|
|
102
|
+
if (e instanceof InvalidRulesConfigError) {
|
|
103
|
+
throw e;
|
|
104
|
+
}
|
|
105
|
+
return { common: [], agents: new Map() };
|
|
106
|
+
}
|
|
107
|
+
if (!config || typeof config !== 'object') {
|
|
108
|
+
return { common: [], agents: new Map() };
|
|
109
|
+
}
|
|
110
|
+
if (config.version) {
|
|
111
|
+
validateVersion(config.version);
|
|
112
|
+
}
|
|
113
|
+
const commonRules = buildRules(config.common || []);
|
|
114
|
+
const agents = new Map();
|
|
115
|
+
const agentsConfig = config.agents || {};
|
|
116
|
+
for (const agentId of Object.keys(agentsConfig)) {
|
|
117
|
+
const agentConfig = agentsConfig[agentId];
|
|
118
|
+
if (!agentConfig) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const inheritedRules = resolveExtends(agentsConfig, agentId);
|
|
122
|
+
const ownRules = buildRules(agentConfig.rules || []);
|
|
123
|
+
const finalRules = [...ownRules];
|
|
124
|
+
if (inheritedRules && inheritedRules.length > 0) {
|
|
125
|
+
finalRules.push(...buildRules(inheritedRules));
|
|
126
|
+
}
|
|
127
|
+
agents.set(agentId, finalRules);
|
|
128
|
+
}
|
|
129
|
+
return { common: commonRules, agents };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function truncateStderr(stderr) {
|
|
133
|
+
if (!stderr || stderr.length <= STDERR_MATCH_LIMIT) {
|
|
134
|
+
return stderr;
|
|
135
|
+
}
|
|
136
|
+
const halfLimit = STDERR_MATCH_LIMIT / 2;
|
|
137
|
+
const head = stderr.slice(0, halfLimit);
|
|
138
|
+
const tail = stderr.slice(-halfLimit);
|
|
139
|
+
return head + '\n...[TRUNCATED]...\n' + tail;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function matchRule(rule, exitCode, truncatedStderr) {
|
|
143
|
+
const exitCodesMatch = rule.exitCodes === 'any' ||
|
|
144
|
+
rule.exitCodes.includes(exitCode);
|
|
145
|
+
if (!exitCodesMatch) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
if (!rule.pattern) {
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
return rule.pattern.test(truncatedStderr);
|
|
153
|
+
} catch (e) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function matchWithTimeout(regex, text) {
|
|
159
|
+
return new Promise((resolve) => {
|
|
160
|
+
const timeoutId = setTimeout(() => {
|
|
161
|
+
resolve(false);
|
|
162
|
+
}, MATCH_TIMEOUT_MS);
|
|
163
|
+
try {
|
|
164
|
+
const result = regex.test(text);
|
|
165
|
+
clearTimeout(timeoutId);
|
|
166
|
+
resolve(result);
|
|
167
|
+
} catch (e) {
|
|
168
|
+
clearTimeout(timeoutId);
|
|
169
|
+
resolve(false);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function classify(rules, agentId, { exitCode, stderr }) {
|
|
175
|
+
const truncatedStderr = truncateStderr(stderr);
|
|
176
|
+
const agentRules = rules.agents.get(agentId) || [];
|
|
177
|
+
for (const rule of agentRules) {
|
|
178
|
+
const matched = rule.pattern
|
|
179
|
+
? await matchWithTimeout(rule.pattern, truncatedStderr)
|
|
180
|
+
: matchRule(rule, exitCode, truncatedStderr);
|
|
181
|
+
if (matched) {
|
|
182
|
+
return {
|
|
183
|
+
class: rule.class,
|
|
184
|
+
rule_id: rule.id,
|
|
185
|
+
ttl: rule.ttl,
|
|
186
|
+
reason: truncatedStderr,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
for (const rule of rules.common) {
|
|
191
|
+
const matched = rule.pattern
|
|
192
|
+
? await matchWithTimeout(rule.pattern, truncatedStderr)
|
|
193
|
+
: matchRule(rule, exitCode, truncatedStderr);
|
|
194
|
+
if (matched) {
|
|
195
|
+
return {
|
|
196
|
+
class: rule.class,
|
|
197
|
+
rule_id: rule.id,
|
|
198
|
+
ttl: rule.ttl,
|
|
199
|
+
reason: truncatedStderr,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Онлайн-проверка stderr на "фатальные" паттерны для агента.
|
|
208
|
+
* Используется spawn-хендлером для раннего kill зависшего процесса,
|
|
209
|
+
* когда дочерний агент уходит в retry-цикл на HTTP 429 / quota без завершения.
|
|
210
|
+
*
|
|
211
|
+
* Проверяет только правила самого агента (не common), и только те,
|
|
212
|
+
* у которых class ∈ {unavailable, misconfigured} — это сигналы, после которых
|
|
213
|
+
* продолжать вызов бессмысленно.
|
|
214
|
+
*
|
|
215
|
+
* @param {{common: Array, agents: Map}} rules — результат loadRules()
|
|
216
|
+
* @param {string} agentId
|
|
217
|
+
* @param {string} stderrText — весь накопленный stderr (до STDERR_MATCH_LIMIT)
|
|
218
|
+
* @returns {{rule_id: string, class: string, ttl: string, reason: string} | null}
|
|
219
|
+
*/
|
|
220
|
+
export function scanStderrForFatalRule(rules, agentId, stderrText) {
|
|
221
|
+
if (!stderrText || !agentId) return null;
|
|
222
|
+
const truncated = truncateStderr(stderrText);
|
|
223
|
+
const agentRules = rules.agents.get(agentId) || [];
|
|
224
|
+
for (const rule of agentRules) {
|
|
225
|
+
if (rule.class !== 'unavailable' && rule.class !== 'misconfigured') continue;
|
|
226
|
+
if (!rule.pattern) continue;
|
|
227
|
+
try {
|
|
228
|
+
if (rule.pattern.test(truncated)) {
|
|
229
|
+
return {
|
|
230
|
+
rule_id: rule.id,
|
|
231
|
+
class: rule.class,
|
|
232
|
+
ttl: rule.ttl,
|
|
233
|
+
reason: truncated,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
} catch {
|
|
237
|
+
// broken regex — пропускаем
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function parseTtl(ttl, now = Date.now()) {
|
|
244
|
+
if (ttl === 'infinite') {
|
|
245
|
+
return Number.MAX_SAFE_INTEGER;
|
|
246
|
+
}
|
|
247
|
+
const untilMatch = ttl.match(/^(\d+)d$/);
|
|
248
|
+
if (untilMatch) {
|
|
249
|
+
return now + parseInt(untilMatch[1], 10) * 24 * 60 * 60 * 1000;
|
|
250
|
+
}
|
|
251
|
+
const hourMatch = ttl.match(/^(\d+)h$/);
|
|
252
|
+
if (hourMatch) {
|
|
253
|
+
return now + parseInt(hourMatch[1], 10) * 60 * 60 * 1000;
|
|
254
|
+
}
|
|
255
|
+
const minMatch = ttl.match(/^(\d+)m$/);
|
|
256
|
+
if (minMatch) {
|
|
257
|
+
return now + parseInt(minMatch[1], 10) * 60 * 1000;
|
|
258
|
+
}
|
|
259
|
+
if (ttl === 'until_utc_midnight') {
|
|
260
|
+
const nextMidnight = new Date(now);
|
|
261
|
+
nextMidnight.setUTCHours(24, 0, 0, 0);
|
|
262
|
+
const minDelay = now + MIN_UTC_MIDNIGHT_DELAY_MS;
|
|
263
|
+
return Math.max(nextMidnight.getTime(), minDelay);
|
|
264
|
+
}
|
|
265
|
+
throw new Error(`Invalid TTL format: ${ttl}. Expected Nm, Nh, Nd, until_utc_midnight, or infinite.`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function classifySync(rules, agentId, { exitCode, stderr }) {
|
|
269
|
+
const truncatedStderr = truncateStderr(stderr);
|
|
270
|
+
const agentRules = rules.agents.get(agentId) || [];
|
|
271
|
+
for (const rule of agentRules) {
|
|
272
|
+
const matched = rule.pattern
|
|
273
|
+
? (() => {
|
|
274
|
+
try {
|
|
275
|
+
return rule.pattern.test(truncatedStderr);
|
|
276
|
+
} catch (e) {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
})()
|
|
280
|
+
: matchRule(rule, exitCode, truncatedStderr);
|
|
281
|
+
if (matched) {
|
|
282
|
+
return {
|
|
283
|
+
class: rule.class,
|
|
284
|
+
rule_id: rule.id,
|
|
285
|
+
ttl: rule.ttl,
|
|
286
|
+
reason: truncatedStderr,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
for (const rule of rules.common) {
|
|
291
|
+
const matched = rule.pattern
|
|
292
|
+
? (() => {
|
|
293
|
+
try {
|
|
294
|
+
return rule.pattern.test(truncatedStderr);
|
|
295
|
+
} catch (e) {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
})()
|
|
299
|
+
: matchRule(rule, exitCode, truncatedStderr);
|
|
300
|
+
if (matched) {
|
|
301
|
+
return {
|
|
302
|
+
class: rule.class,
|
|
303
|
+
rule_id: rule.id,
|
|
304
|
+
ttl: rule.ttl,
|
|
305
|
+
reason: truncatedStderr,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
|