workflow-ai 1.0.64 → 1.0.66
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 -277
- package/configs/agent-health-rules.yaml +75 -0
- package/configs/pipeline.yaml +24 -7
- package/package.json +1 -1
- package/src/init.mjs +20 -3
- package/src/lib/agent-health-registry.mjs +245 -0
- package/src/lib/agent-spawner.mjs +47 -6
- package/src/lib/artifact-snapshot.mjs +233 -0
- package/src/lib/error-classifier.mjs +311 -0
- package/src/lib/test-error-classifier.mjs +60 -0
- package/src/lib/test-extends.mjs +58 -0
- package/src/lib/test-version.mjs +21 -0
- package/src/runner.mjs +215 -58
- package/src/scripts/move-to-review.js +5 -7
- package/src/scripts/reset-agent-health.js +62 -0
- package/src/skills/coach/SKILL.md +1 -0
- package/src/skills/coach/tests/cases/TC-COACH-001/current/meta.json +93 -94
- package/src/skills/coach/tests/cases/TC-COACH-002/current/meta.json +93 -94
- package/src/skills/create-plan/SKILL.md +1 -0
- package/src/skills/create-plan/knowledge/test-hygiene.md +47 -0
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-001/current/claude-sonnet/trial-1.md +23 -31
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-001/current/claude-sonnet/trial-2.md +20 -35
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-001/current/claude-sonnet/trial-3.md +36 -19
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-001/current/judge.json +1 -1
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-001/current/kilo-deepseek/trial-2.md +11 -5
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-001/current/kilo-deepseek/trial-3.md +12 -16
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-001/current/kilo-glm/trial-1.md +15 -9
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-001/current/kilo-glm/trial-3.md +15 -14
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-001/current/kilo-minimax/trial-1.md +22 -18
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-001/current/kilo-minimax/trial-2.md +24 -16
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-001/current/kilo-minimax/trial-3.md +13 -20
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-001/current/meta.json +2 -2
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/claude-sonnet/trial-1.md +14 -19
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/claude-sonnet/trial-2.md +24 -14
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/claude-sonnet/trial-3.md +20 -19
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/judge.json +16 -17
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/kilo-deepseek/trial-1.md +0 -7
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/kilo-deepseek/trial-2.md +9 -10
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/kilo-deepseek/trial-3.md +5 -5
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/kilo-glm/trial-1.md +20 -4
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/kilo-glm/trial-2.md +36 -9
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/kilo-glm/trial-3.md +9 -6
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/kilo-minimax/trial-1.md +4 -12
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/kilo-minimax/trial-2.md +6 -8
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/kilo-minimax/trial-3.md +8 -4
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/meta.json +10 -11
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/claude-sonnet/trial-1.md +30 -0
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/claude-sonnet/trial-2.md +30 -0
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/claude-sonnet/trial-3.md +30 -0
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/judge.json +165 -0
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/kilo-deepseek/trial-1.md +5 -0
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/kilo-deepseek/trial-2.md +26 -0
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/kilo-deepseek/trial-3.md +5 -0
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/kilo-glm/trial-1.md +39 -0
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/kilo-glm/trial-2.md +37 -0
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/kilo-glm/trial-3.md +45 -0
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/kilo-minimax/trial-1.md +26 -0
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/kilo-minimax/trial-2.md +27 -0
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/kilo-minimax/trial-3.md +7 -0
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/meta.json +117 -0
- package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003-parent-plan-mandatory.yaml +41 -0
- package/src/skills/decompose-gaps/tests/index.yaml +5 -0
- package/src/skills/decompose-gaps/tests/rubrics/parent-plan-mandatory.md +22 -0
- package/src/skills/decompose-gaps/workflows/decompose.md +5 -2
- package/src/skills/decompose-plan/knowledge/atomicity-checklist.md +31 -5
- package/src/skills/decompose-plan/knowledge/capabilities.md +29 -5
- package/src/skills/decompose-plan/knowledge/human-task-rules.md +15 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/claude-sonnet/trial-1.md +55 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/claude-sonnet/trial-2.md +49 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/claude-sonnet/trial-3.md +49 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/judge.json +163 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/kilo-deepseek/trial-1.md +104 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/kilo-deepseek/trial-2.md +45 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/kilo-deepseek/trial-3.md +58 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/kilo-glm/trial-1.md +193 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/kilo-glm/trial-2.md +202 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/kilo-glm/trial-3.md +155 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/kilo-minimax/trial-1.md +52 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/kilo-minimax/trial-2.md +17 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/kilo-minimax/trial-3.md +0 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/meta.json +115 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004-executor-atomicity.yaml +64 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/claude-sonnet/trial-1.md +59 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/claude-sonnet/trial-2.md +204 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/claude-sonnet/trial-3.md +213 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/judge.json +163 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/kilo-deepseek/trial-1.md +0 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/kilo-deepseek/trial-2.md +57 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/kilo-deepseek/trial-3.md +54 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/kilo-glm/trial-1.md +147 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/kilo-glm/trial-2.md +165 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/kilo-glm/trial-3.md +133 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/kilo-minimax/trial-1.md +81 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/kilo-minimax/trial-2.md +108 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/kilo-minimax/trial-3.md +3 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/meta.json +114 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005-capabilities-registry.yaml +78 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/claude-sonnet/trial-1.md +225 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/claude-sonnet/trial-2.md +66 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/claude-sonnet/trial-3.md +36 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/judge.json +163 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/kilo-deepseek/trial-1.md +42 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/kilo-deepseek/trial-2.md +67 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/kilo-deepseek/trial-3.md +40 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/kilo-glm/trial-1.md +122 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/kilo-glm/trial-2.md +131 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/kilo-glm/trial-3.md +138 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/kilo-minimax/trial-1.md +41 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/kilo-minimax/trial-2.md +88 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/kilo-minimax/trial-3.md +0 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/meta.json +115 -0
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006-dod-threshold.yaml +72 -0
- package/src/skills/decompose-plan/tests/index.yaml +15 -0
- package/src/skills/decompose-plan/tests/rubrics/capabilities-registry.md +21 -0
- package/src/skills/decompose-plan/tests/rubrics/dod-threshold.md +21 -0
- package/src/skills/decompose-plan/tests/rubrics/executor-atomicity.md +21 -0
- package/src/skills/decompose-plan/workflows/decompose.md +38 -5
- package/src/skills/execute-task/tests/cases/TC-EXECUTE-TASK-001/current/meta.json +87 -88
- package/src/skills/execute-task/tests/cases/TC-EXECUTE-TASK-005/current/meta.json +87 -88
- package/src/skills/manual-testing/SKILL.md +6 -4
- package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-001/current/claude-sonnet/trial-1.md +29 -16
- package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-001/current/claude-sonnet/trial-2.md +21 -54
- package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-001/current/claude-sonnet/trial-3.md +18 -23
- package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-001/current/judge.json +17 -17
- package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-001/current/meta.json +19 -19
- package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-002/current/claude-sonnet/trial-1.md +27 -30
- package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-002/current/claude-sonnet/trial-2.md +16 -23
- package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-002/current/claude-sonnet/trial-3.md +35 -28
- package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-002/current/judge.json +13 -13
- package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-002/current/meta.json +15 -15
- package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-003/current/claude-sonnet/trial-1.md +76 -0
- package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-003/current/claude-sonnet/trial-2.md +71 -0
- package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-003/current/claude-sonnet/trial-3.md +85 -0
- package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-003/current/judge.json +46 -0
- package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-003/current/meta.json +36 -0
- package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-003-qa-non-ui-assertion.yaml +65 -0
- package/src/skills/manual-testing/tests/index.yaml +5 -0
- package/src/skills/manual-testing/tests/rubrics/qa-non-ui-assertion.md +31 -0
- 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 +157 -14
- 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
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
version: "1.0"
|
|
2
|
+
|
|
3
|
+
common:
|
|
4
|
+
- id: "net-econnreset"
|
|
5
|
+
class: "transient"
|
|
6
|
+
ttl: "5m"
|
|
7
|
+
pattern: "ECONNRESET|ETIMEDOUT|EAI_AGAIN|connection reset by peer|socket hang up"
|
|
8
|
+
exit_codes: "any"
|
|
9
|
+
- id: "http-5xx-transient"
|
|
10
|
+
class: "transient"
|
|
11
|
+
ttl: "5m"
|
|
12
|
+
pattern: "\\b(502|503|504)\\b.*(Bad Gateway|Service Unavailable|Gateway Timeout)"
|
|
13
|
+
exit_codes: "any"
|
|
14
|
+
- id: "http-auth"
|
|
15
|
+
class: "misconfigured"
|
|
16
|
+
ttl: "1h"
|
|
17
|
+
pattern: "\\b(401|403)\\b|Unauthorized|Forbidden|API key (not found|invalid|missing)"
|
|
18
|
+
exit_codes: "any"
|
|
19
|
+
|
|
20
|
+
agents:
|
|
21
|
+
qwen-code:
|
|
22
|
+
rules:
|
|
23
|
+
- id: "qwen-quota"
|
|
24
|
+
class: "unavailable"
|
|
25
|
+
ttl: "until_utc_midnight"
|
|
26
|
+
pattern: "Qwen OAuth quota exceeded|qwen.*quota.*exceed|Daily limit.*reached"
|
|
27
|
+
exit_codes: "any"
|
|
28
|
+
- id: "qwen-oauth-required"
|
|
29
|
+
class: "misconfigured"
|
|
30
|
+
ttl: "1h"
|
|
31
|
+
pattern: "Please run `qwen login`|qwen OAuth flow"
|
|
32
|
+
exit_codes: "any"
|
|
33
|
+
|
|
34
|
+
claude-sonnet:
|
|
35
|
+
rules:
|
|
36
|
+
- id: "claude-overloaded"
|
|
37
|
+
class: "unavailable"
|
|
38
|
+
ttl: "10m"
|
|
39
|
+
pattern: "overloaded_error|\\b529\\b|Overloaded"
|
|
40
|
+
exit_codes: "any"
|
|
41
|
+
- id: "claude-rate-limit"
|
|
42
|
+
class: "unavailable"
|
|
43
|
+
ttl: "1h"
|
|
44
|
+
pattern: "rate_limit_error|\\b429\\b.*rate|usage limit"
|
|
45
|
+
exit_codes: "any"
|
|
46
|
+
|
|
47
|
+
claude-opus:
|
|
48
|
+
extends: claude-sonnet
|
|
49
|
+
|
|
50
|
+
kilo-free:
|
|
51
|
+
rules:
|
|
52
|
+
- id: "kilo-free-tier-exhausted"
|
|
53
|
+
class: "unavailable"
|
|
54
|
+
ttl: "until_utc_midnight"
|
|
55
|
+
pattern: "free tier.*exhaust|daily limit|too many requests"
|
|
56
|
+
exit_codes: "any"
|
|
57
|
+
|
|
58
|
+
kilo-deepseek:
|
|
59
|
+
rules:
|
|
60
|
+
- id: "kilo-deepseek-provider-down"
|
|
61
|
+
class: "transient"
|
|
62
|
+
ttl: "15m"
|
|
63
|
+
pattern: "deepseek.*unavailable|upstream error"
|
|
64
|
+
exit_codes: "any"
|
|
65
|
+
|
|
66
|
+
kilo-glm:
|
|
67
|
+
rules:
|
|
68
|
+
- id: "zai-usage-limit"
|
|
69
|
+
class: "unavailable"
|
|
70
|
+
ttl: "5h"
|
|
71
|
+
pattern: "Usage limit reached for \\d+ hour|api\\.z\\.ai.*\"statusCode\":429|AI_APICallError.*z\\.ai"
|
|
72
|
+
exit_codes: "any"
|
|
73
|
+
|
|
74
|
+
kilo-glm-air:
|
|
75
|
+
extends: kilo-glm
|
package/configs/pipeline.yaml
CHANGED
|
@@ -64,42 +64,42 @@ pipeline:
|
|
|
64
64
|
|
|
65
65
|
kilo-code:
|
|
66
66
|
command: "kilo"
|
|
67
|
-
args: ["-m", "kilo/kilo-auto/free", "--agent", "orchestrator", "run"]
|
|
67
|
+
args: ["-m", "kilo/kilo-auto/free", "--agent", "orchestrator", "--print-logs", "--log-level", "ERROR", "run"]
|
|
68
68
|
workdir: "."
|
|
69
69
|
capabilities: [text]
|
|
70
70
|
description: "Kilo мульти-режимный (architect, code, debug)"
|
|
71
71
|
|
|
72
72
|
kilo-glm:
|
|
73
73
|
command: "kilo"
|
|
74
|
-
args: ["-m", "zai/glm-5.1", "--agent", "code", "run"]
|
|
74
|
+
args: ["-m", "zai/glm-5.1", "--agent", "code", "--print-logs", "--log-level", "ERROR", "run"]
|
|
75
75
|
workdir: "."
|
|
76
76
|
capabilities: [text]
|
|
77
77
|
description: "Kilo GLM"
|
|
78
78
|
|
|
79
79
|
kilo-glm-air:
|
|
80
80
|
command: "kilo"
|
|
81
|
-
args: ["-m", "zai/glm-4.5-air", "--agent", "code", "run"]
|
|
81
|
+
args: ["-m", "zai/glm-4.5-air", "--agent", "code", "--print-logs", "--log-level", "ERROR", "run"]
|
|
82
82
|
workdir: "."
|
|
83
83
|
capabilities: [text]
|
|
84
84
|
description: "Kilo GLM air"
|
|
85
85
|
|
|
86
86
|
kilo-deepseek:
|
|
87
87
|
command: "kilo"
|
|
88
|
-
args: ["-m", "deepseek/deepseek-reasoner", "--agent", "code", "run"]
|
|
88
|
+
args: ["-m", "deepseek/deepseek-reasoner", "--agent", "code", "--print-logs", "--log-level", "ERROR", "run"]
|
|
89
89
|
workdir: "."
|
|
90
90
|
capabilities: [text]
|
|
91
91
|
description: "Kilo deepseek"
|
|
92
92
|
|
|
93
93
|
kilo-minimax:
|
|
94
94
|
command: "kilo"
|
|
95
|
-
args: ["-m", "kilo/minimax/minimax-m2.7", "--agent", "code", "run"]
|
|
95
|
+
args: ["-m", "kilo/minimax/minimax-m2.7", "--agent", "code", "--print-logs", "--log-level", "ERROR", "run"]
|
|
96
96
|
workdir: "."
|
|
97
97
|
capabilities: [text]
|
|
98
98
|
description: "Kilo minimax"
|
|
99
99
|
|
|
100
100
|
kilo-free:
|
|
101
101
|
command: "kilo"
|
|
102
|
-
args: ["-m", "kilo/kilo-auto/free", "--agent", "code", "run"]
|
|
102
|
+
args: ["-m", "kilo/kilo-auto/free", "--agent", "code", "--print-logs", "--log-level", "ERROR", "run"]
|
|
103
103
|
workdir: "."
|
|
104
104
|
capabilities: [text]
|
|
105
105
|
description: "Kilo free"
|
|
@@ -350,7 +350,7 @@ pipeline:
|
|
|
350
350
|
# -------------------------------------------------------------------------
|
|
351
351
|
decompose-plan:
|
|
352
352
|
description: "Декомпозировать план на тикеты"
|
|
353
|
-
agents: [claude-sonnet, kilo-glm
|
|
353
|
+
agents: [claude-sonnet, kilo-glm]
|
|
354
354
|
skill: decompose-plan
|
|
355
355
|
instructions: "Декомпозируй план .workflow/$context.plan_file на тикеты. ID тикетов бери ТОЛЬКО из JSON-карты id_ranges_json=$context.id_ranges_json — это стартовые номера по префиксам в формате {\"PREFIX\":N_start,...}, уже выделенные стадией allocate-ticket-ids. Распарси JSON, используй значения как есть. НЕ вызывай get-next-id.js и НЕ изобретай номера самостоятельно. Подробности: см. шаг 9.0 в workflows/decompose.md. При наличии $context.atomicity_failures — учти их при декомпозиции: исправь неатомарные тикеты."
|
|
356
356
|
goto:
|
|
@@ -593,6 +593,17 @@ pipeline:
|
|
|
593
593
|
ticket_id: "$context.ticket_id"
|
|
594
594
|
attempt: "$counter.task_attempts"
|
|
595
595
|
target: review
|
|
596
|
+
# Runner возвращает status=blocked при no_capable_agent / attempts_exhausted
|
|
597
|
+
# (resolveAgent в runner.mjs, строка ~926). Направляем тикет сразу в blocked/,
|
|
598
|
+
# минуя move-to-review → verify-artifacts → increment-task-attempts: иначе
|
|
599
|
+
# тикет крутится 6 итераций вхолостую (execute-task не может его взять,
|
|
600
|
+
# verify-artifacts фейлится с пустым result, счётчик достигает max → blocked
|
|
601
|
+
# всё равно). Прямой маршрут экономит ~6 пустых итераций на тикет.
|
|
602
|
+
blocked:
|
|
603
|
+
stage: move-ticket
|
|
604
|
+
params:
|
|
605
|
+
ticket_id: "$context.ticket_id"
|
|
606
|
+
target: blocked
|
|
596
607
|
error:
|
|
597
608
|
stage: move-to-review
|
|
598
609
|
params:
|
|
@@ -728,6 +739,10 @@ pipeline:
|
|
|
728
739
|
stage: analyze-report
|
|
729
740
|
params:
|
|
730
741
|
report_id: "$result.report_id"
|
|
742
|
+
# blocked означает, что required_capabilities из контекста (от предыдущего
|
|
743
|
+
# тикета) не покрыты ни одним агентом create-report. Это не фатально —
|
|
744
|
+
# завершаем пайплайн, отчёт не создан, но данные не теряются.
|
|
745
|
+
blocked: end
|
|
731
746
|
error:
|
|
732
747
|
stage: increment-create-report-attempts
|
|
733
748
|
|
|
@@ -758,6 +773,8 @@ pipeline:
|
|
|
758
773
|
params:
|
|
759
774
|
gaps: "$result.gaps"
|
|
760
775
|
report_id: "$context.report_id"
|
|
776
|
+
# blocked по тем же причинам, что и в create-report — завершаем пайплайн.
|
|
777
|
+
blocked: end
|
|
761
778
|
error:
|
|
762
779
|
stage: increment-analyze-report-attempts
|
|
763
780
|
|
package/package.json
CHANGED
package/src/init.mjs
CHANGED
|
@@ -357,7 +357,7 @@ export function initProject(targetPath = process.cwd(), options = {}) {
|
|
|
357
357
|
errors: []
|
|
358
358
|
};
|
|
359
359
|
|
|
360
|
-
// Step 1: Create .workflow/ structure (
|
|
360
|
+
// Step 1: Create .workflow/ structure (directories)
|
|
361
361
|
const directories = [
|
|
362
362
|
'tickets/backlog',
|
|
363
363
|
'tickets/ready',
|
|
@@ -371,13 +371,21 @@ export function initProject(targetPath = process.cwd(), options = {}) {
|
|
|
371
371
|
'logs',
|
|
372
372
|
'templates',
|
|
373
373
|
'src/skills',
|
|
374
|
-
'tests/skills'
|
|
374
|
+
'tests/skills',
|
|
375
|
+
'state'
|
|
375
376
|
];
|
|
376
377
|
|
|
377
378
|
for (const dir of directories) {
|
|
378
379
|
ensureDir(join(workflowRoot, dir));
|
|
379
380
|
}
|
|
380
|
-
result.steps.push(
|
|
381
|
+
result.steps.push(`Created .workflow/ directory structure (${directories.length} directories)`);
|
|
382
|
+
|
|
383
|
+
// Create .gitkeep in .workflow/tests/skills/
|
|
384
|
+
// FIX-9: Ensure .gitkeep exists for tests/skills directory
|
|
385
|
+
const testsSkillsGitkeep = join(workflowRoot, 'tests', 'skills', '.gitkeep');
|
|
386
|
+
if (!existsSync(testsSkillsGitkeep)) {
|
|
387
|
+
writeFileSync(testsSkillsGitkeep, '');
|
|
388
|
+
}
|
|
381
389
|
|
|
382
390
|
// Step 2: Ensure global dir and create skill junctions
|
|
383
391
|
const globalDir = getGlobalDir();
|
|
@@ -432,6 +440,15 @@ export function initProject(targetPath = process.cwd(), options = {}) {
|
|
|
432
440
|
updateGitignore(projectRoot);
|
|
433
441
|
result.steps.push('Updated .gitignore with .workflow/logs/');
|
|
434
442
|
|
|
443
|
+
// Step 10: Copy agent-health-rules.yaml to .workflow/config/
|
|
444
|
+
const agentHealthRulesSrc = join(packageRoot, 'configs', 'agent-health-rules.yaml');
|
|
445
|
+
const agentHealthRulesDest = join(workflowRoot, 'config', 'agent-health-rules.yaml');
|
|
446
|
+
if (existsSync(agentHealthRulesSrc)) {
|
|
447
|
+
ensureDir(dirname(agentHealthRulesDest));
|
|
448
|
+
copyFileSync(agentHealthRulesSrc, agentHealthRulesDest);
|
|
449
|
+
result.steps.push('Copied agent-health-rules.yaml → .workflow/config/');
|
|
450
|
+
}
|
|
451
|
+
|
|
435
452
|
return result;
|
|
436
453
|
}
|
|
437
454
|
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { createLogger } from './logger.mjs';
|
|
4
|
+
import { parseTtl } from './error-classifier.mjs';
|
|
5
|
+
|
|
6
|
+
const HEALTH_FILE = '.workflow/state/agent-health.json';
|
|
7
|
+
const LOCK_FILE = '.workflow/state/agent-health.json.lock';
|
|
8
|
+
const SUPPORTED_VERSION = '1.0';
|
|
9
|
+
const MAX_LOCK_RETRIES = 5;
|
|
10
|
+
const LOCK_TIMEOUT_MS = 2000;
|
|
11
|
+
const LOCK_BACKOFFS = [100, 200, 400, 800, 1600];
|
|
12
|
+
|
|
13
|
+
const logger = createLogger();
|
|
14
|
+
|
|
15
|
+
export class AgentHealthLockError extends Error {
|
|
16
|
+
constructor(message) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = 'AgentHealthLockError';
|
|
19
|
+
if (Error.captureStackTrace) {
|
|
20
|
+
Error.captureStackTrace(this, this.constructor);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getHealthFilePath(projectRoot) {
|
|
26
|
+
return path.join(projectRoot, HEALTH_FILE);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getLockFilePath(projectRoot) {
|
|
30
|
+
return path.join(projectRoot, LOCK_FILE);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function ensureDir(dirPath) {
|
|
34
|
+
if (!fs.existsSync(dirPath)) {
|
|
35
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseTimestamp(isoString) {
|
|
40
|
+
if (!isoString) return null;
|
|
41
|
+
const date = new Date(isoString);
|
|
42
|
+
return isNaN(date.getTime()) ? null : date.getTime();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function readHealthFile(filePath) {
|
|
46
|
+
if (!fs.existsSync(filePath)) {
|
|
47
|
+
return { version: SUPPORTED_VERSION, updated_at: null, agents: {} };
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
51
|
+
const data = JSON.parse(content);
|
|
52
|
+
if (!data || typeof data !== 'object') {
|
|
53
|
+
logger.warn(`agent-health-registry: corrupted JSON in ${filePath}, returning empty state`);
|
|
54
|
+
return { version: SUPPORTED_VERSION, updated_at: null, agents: {} };
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
version: data.version || SUPPORTED_VERSION,
|
|
58
|
+
updated_at: data.updated_at || null,
|
|
59
|
+
agents: data.agents || {}
|
|
60
|
+
};
|
|
61
|
+
} catch (e) {
|
|
62
|
+
logger.warn(`agent-health-registry: corrupted JSON in ${filePath}, returning empty state`);
|
|
63
|
+
return { version: SUPPORTED_VERSION, updated_at: null, agents: {} };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function writeHealthFileAtomic(filePath, data, lockFilePath, projectRoot) {
|
|
68
|
+
const tmpPath = filePath + '.tmp';
|
|
69
|
+
const dirPath = path.dirname(filePath);
|
|
70
|
+
ensureDir(dirPath);
|
|
71
|
+
|
|
72
|
+
const startTime = Date.now();
|
|
73
|
+
for (let attempt = 0; attempt < MAX_LOCK_RETRIES; attempt++) {
|
|
74
|
+
const elapsed = Date.now() - startTime;
|
|
75
|
+
if (elapsed >= LOCK_TIMEOUT_MS) {
|
|
76
|
+
throw new AgentHealthLockError(`Failed to acquire lock within ${LOCK_TIMEOUT_MS}ms`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const backoff = LOCK_BACKOFFS[attempt];
|
|
80
|
+
const remaining = LOCK_TIMEOUT_MS - elapsed;
|
|
81
|
+
const sleepTime = Math.min(backoff, remaining);
|
|
82
|
+
|
|
83
|
+
if (attempt > 0) {
|
|
84
|
+
if (sleepTime > 0) {
|
|
85
|
+
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
86
|
+
const start = Date.now();
|
|
87
|
+
while (Date.now() - start < sleepTime) {
|
|
88
|
+
// busy wait
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
fs.openSync(lockFilePath, 'wx');
|
|
95
|
+
} catch (e) {
|
|
96
|
+
if (e.code === 'EEXIST') {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
throw e;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const content = JSON.stringify(data, null, 2);
|
|
104
|
+
fs.writeFileSync(tmpPath, content, 'utf-8');
|
|
105
|
+
fs.renameSync(tmpPath, filePath);
|
|
106
|
+
return;
|
|
107
|
+
} finally {
|
|
108
|
+
try {
|
|
109
|
+
fs.unlinkSync(lockFilePath);
|
|
110
|
+
} catch (e) {
|
|
111
|
+
// ignore lock cleanup errors
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
throw new AgentHealthLockError(`Failed to acquire lock after ${MAX_LOCK_RETRIES} attempts`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function loadHealth(projectRoot, now = Date.now()) {
|
|
120
|
+
const filePath = getHealthFilePath(projectRoot);
|
|
121
|
+
const data = readHealthFile(filePath);
|
|
122
|
+
pruneExpired(projectRoot, now);
|
|
123
|
+
return { agents: data.agents };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function markUnhealthy(projectRoot, agentId, options) {
|
|
127
|
+
const { class: agentClass, rule_id, ttl, reason } = options;
|
|
128
|
+
const filePath = getHealthFilePath(projectRoot);
|
|
129
|
+
const lockPath = getLockFilePath(projectRoot);
|
|
130
|
+
|
|
131
|
+
const existing = readHealthFile(filePath);
|
|
132
|
+
const currentTime = new Date().toISOString();
|
|
133
|
+
|
|
134
|
+
let untilMs;
|
|
135
|
+
const now = Date.now();
|
|
136
|
+
if (typeof ttl === 'number') {
|
|
137
|
+
// Legacy: numeric milliseconds offset
|
|
138
|
+
untilMs = now + ttl;
|
|
139
|
+
} else if (typeof ttl === 'string') {
|
|
140
|
+
// String TTL: 'until_utc_midnight', '1h', '5m', '1d', 'infinite', etc.
|
|
141
|
+
try {
|
|
142
|
+
untilMs = parseTtl(ttl, now);
|
|
143
|
+
} catch {
|
|
144
|
+
untilMs = now + 5 * 60 * 1000;
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
untilMs = now + 5 * 60 * 1000;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const untilIso = new Date(untilMs).toISOString();
|
|
151
|
+
|
|
152
|
+
existing.agents[agentId] = {
|
|
153
|
+
status: 'unhealthy',
|
|
154
|
+
class: agentClass,
|
|
155
|
+
rule_id: rule_id || null,
|
|
156
|
+
reason: reason || null,
|
|
157
|
+
marked_at: currentTime,
|
|
158
|
+
until: untilIso
|
|
159
|
+
};
|
|
160
|
+
existing.updated_at = currentTime;
|
|
161
|
+
|
|
162
|
+
writeHealthFileAtomic(filePath, existing, lockPath, projectRoot);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function markHealthy(projectRoot, agentId) {
|
|
166
|
+
const filePath = getHealthFilePath(projectRoot);
|
|
167
|
+
const lockPath = getLockFilePath(projectRoot);
|
|
168
|
+
|
|
169
|
+
const existing = readHealthFile(filePath);
|
|
170
|
+
|
|
171
|
+
if (existing.agents[agentId]) {
|
|
172
|
+
delete existing.agents[agentId];
|
|
173
|
+
existing.updated_at = new Date().toISOString();
|
|
174
|
+
writeHealthFileAtomic(filePath, existing, lockPath, projectRoot);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function isHealthy(projectRoot, agentId, now = Date.now()) {
|
|
179
|
+
const filePath = getHealthFilePath(projectRoot);
|
|
180
|
+
const data = readHealthFile(filePath);
|
|
181
|
+
const agent = data.agents[agentId];
|
|
182
|
+
|
|
183
|
+
if (!agent) {
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (agent.status !== 'unhealthy') {
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const untilMs = parseTimestamp(agent.until);
|
|
192
|
+
if (untilMs === null) {
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return now >= untilMs;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function unhealthy(projectRoot, now = Date.now()) {
|
|
200
|
+
const filePath = getHealthFilePath(projectRoot);
|
|
201
|
+
const data = readHealthFile(filePath);
|
|
202
|
+
const result = [];
|
|
203
|
+
|
|
204
|
+
for (const [agentId, agent] of Object.entries(data.agents)) {
|
|
205
|
+
if (agent.status !== 'unhealthy') {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const untilMs = parseTimestamp(agent.until);
|
|
209
|
+
if (untilMs !== null && now < untilMs) {
|
|
210
|
+
result.push({
|
|
211
|
+
agentId,
|
|
212
|
+
class: agent.class,
|
|
213
|
+
rule_id: agent.rule_id,
|
|
214
|
+
reason: agent.reason,
|
|
215
|
+
until: agent.until
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function pruneExpired(projectRoot, now = Date.now()) {
|
|
224
|
+
const filePath = getHealthFilePath(projectRoot);
|
|
225
|
+
const lockPath = getLockFilePath(projectRoot);
|
|
226
|
+
|
|
227
|
+
const existing = readHealthFile(filePath);
|
|
228
|
+
let changed = false;
|
|
229
|
+
|
|
230
|
+
for (const [agentId, agent] of Object.entries(existing.agents)) {
|
|
231
|
+
if (agent.status !== 'unhealthy') {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const untilMs = parseTimestamp(agent.until);
|
|
235
|
+
if (untilMs !== null && now >= untilMs) {
|
|
236
|
+
delete existing.agents[agentId];
|
|
237
|
+
changed = true;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (changed) {
|
|
242
|
+
existing.updated_at = new Date().toISOString();
|
|
243
|
+
writeHealthFileAtomic(filePath, existing, lockPath, projectRoot);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { spawn, execSync } from 'child_process';
|
|
4
4
|
import path from 'path';
|
|
5
|
+
import { loadRules, scanStderrForFatalRule } from './error-classifier.mjs';
|
|
5
6
|
|
|
6
7
|
const ResultParser = {
|
|
7
8
|
STATUS_ALIASES: {
|
|
@@ -149,13 +150,21 @@ export async function spawnAgent(agentConfig, prompt, options = {}) {
|
|
|
149
150
|
stageId = 'unknown',
|
|
150
151
|
skillId = null,
|
|
151
152
|
projectRoot = process.cwd(),
|
|
152
|
-
currentChildRef = null
|
|
153
|
+
currentChildRef = null,
|
|
154
|
+
agentId = null,
|
|
155
|
+
healthRules = null
|
|
153
156
|
} = options;
|
|
154
157
|
|
|
155
158
|
return new Promise((resolve, reject) => {
|
|
156
159
|
const args = [...agentConfig.args];
|
|
157
160
|
const finalPrompt = prompt;
|
|
158
161
|
|
|
162
|
+
// Для онлайн-детекции фатальных stderr-паттернов
|
|
163
|
+
const rules = agentId
|
|
164
|
+
? (healthRules || (() => { try { return loadRules(projectRoot); } catch { return null; } })())
|
|
165
|
+
: null;
|
|
166
|
+
const hasAgentRules = Boolean(rules && agentId && rules.agents.get(agentId)?.length);
|
|
167
|
+
|
|
159
168
|
const useShell = process.platform === 'win32' && agentConfig.command !== 'node';
|
|
160
169
|
const useStdin = useShell && finalPrompt.includes('\n');
|
|
161
170
|
|
|
@@ -196,14 +205,20 @@ export async function spawnAgent(agentConfig, prompt, options = {}) {
|
|
|
196
205
|
let stdout = '';
|
|
197
206
|
let stderr = '';
|
|
198
207
|
let timedOut = false;
|
|
208
|
+
let earlyKilled = false;
|
|
209
|
+
let lastScanSize = 0;
|
|
199
210
|
|
|
200
|
-
const
|
|
201
|
-
timedOut = true;
|
|
211
|
+
const killChild = () => {
|
|
202
212
|
if (process.platform === 'win32' && child.pid) {
|
|
203
213
|
try { execSync(`taskkill /pid ${child.pid} /T /F`, { stdio: 'pipe' }); } catch {}
|
|
204
214
|
} else {
|
|
205
|
-
child.kill('SIGTERM');
|
|
215
|
+
try { child.kill('SIGTERM'); } catch {}
|
|
206
216
|
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const timeoutId = setTimeout(() => {
|
|
220
|
+
timedOut = true;
|
|
221
|
+
killChild();
|
|
207
222
|
if (logger) {
|
|
208
223
|
logger.timeout(stageId, timeout);
|
|
209
224
|
}
|
|
@@ -243,6 +258,32 @@ export async function spawnAgent(agentConfig, prompt, options = {}) {
|
|
|
243
258
|
child.stderr.on('data', (data) => {
|
|
244
259
|
stderr += data.toString();
|
|
245
260
|
process.stderr.write(data);
|
|
261
|
+
|
|
262
|
+
if (!hasAgentRules || earlyKilled || timedOut) return;
|
|
263
|
+
// Throttle: первый скан всегда, последующие — только после 200+ новых байт.
|
|
264
|
+
if (lastScanSize > 0 && stderr.length - lastScanSize < 200) return;
|
|
265
|
+
lastScanSize = stderr.length;
|
|
266
|
+
const match = scanStderrForFatalRule(rules, agentId, stderr);
|
|
267
|
+
if (!match) return;
|
|
268
|
+
|
|
269
|
+
earlyKilled = true;
|
|
270
|
+
clearTimeout(timeoutId);
|
|
271
|
+
if (logger) {
|
|
272
|
+
logger.error?.(
|
|
273
|
+
`Fatal stderr pattern matched for ${agentId} (rule=${match.rule_id}, class=${match.class}). Killing process.`,
|
|
274
|
+
stageId
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
killChild();
|
|
278
|
+
const err = new Error(
|
|
279
|
+
`Agent "${agentId}" killed early: ${match.rule_id} (class=${match.class})`
|
|
280
|
+
);
|
|
281
|
+
err.code = 'EARLY_KILL';
|
|
282
|
+
err.exitCode = -1;
|
|
283
|
+
err.stderr = stderr;
|
|
284
|
+
err.earlyKill = true;
|
|
285
|
+
err.rule = match;
|
|
286
|
+
reject(err);
|
|
246
287
|
});
|
|
247
288
|
|
|
248
289
|
child.on('close', (code) => {
|
|
@@ -264,7 +305,7 @@ export async function spawnAgent(agentConfig, prompt, options = {}) {
|
|
|
264
305
|
}
|
|
265
306
|
process.stdout.write('\n');
|
|
266
307
|
|
|
267
|
-
if (timedOut) return;
|
|
308
|
+
if (timedOut || earlyKilled) return;
|
|
268
309
|
|
|
269
310
|
if (logger) {
|
|
270
311
|
logger.cliCall(agentConfig.command, args, code);
|
|
@@ -324,7 +365,7 @@ export async function spawnAgent(agentConfig, prompt, options = {}) {
|
|
|
324
365
|
|
|
325
366
|
child.on('error', (err) => {
|
|
326
367
|
clearTimeout(timeoutId);
|
|
327
|
-
if (!timedOut) {
|
|
368
|
+
if (!timedOut && !earlyKilled) {
|
|
328
369
|
if (logger) {
|
|
329
370
|
logger.error(`CLI error: ${err.message}`, stageId);
|
|
330
371
|
}
|