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.
Files changed (160) hide show
  1. package/README.md +377 -277
  2. package/configs/agent-health-rules.yaml +75 -0
  3. package/configs/pipeline.yaml +24 -7
  4. package/package.json +1 -1
  5. package/src/init.mjs +20 -3
  6. package/src/lib/agent-health-registry.mjs +245 -0
  7. package/src/lib/agent-spawner.mjs +47 -6
  8. package/src/lib/artifact-snapshot.mjs +233 -0
  9. package/src/lib/error-classifier.mjs +311 -0
  10. package/src/lib/test-error-classifier.mjs +60 -0
  11. package/src/lib/test-extends.mjs +58 -0
  12. package/src/lib/test-version.mjs +21 -0
  13. package/src/runner.mjs +215 -58
  14. package/src/scripts/move-to-review.js +5 -7
  15. package/src/scripts/reset-agent-health.js +62 -0
  16. package/src/skills/coach/SKILL.md +1 -0
  17. package/src/skills/coach/tests/cases/TC-COACH-001/current/meta.json +93 -94
  18. package/src/skills/coach/tests/cases/TC-COACH-002/current/meta.json +93 -94
  19. package/src/skills/create-plan/SKILL.md +1 -0
  20. package/src/skills/create-plan/knowledge/test-hygiene.md +47 -0
  21. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-001/current/claude-sonnet/trial-1.md +23 -31
  22. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-001/current/claude-sonnet/trial-2.md +20 -35
  23. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-001/current/claude-sonnet/trial-3.md +36 -19
  24. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-001/current/judge.json +1 -1
  25. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-001/current/kilo-deepseek/trial-2.md +11 -5
  26. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-001/current/kilo-deepseek/trial-3.md +12 -16
  27. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-001/current/kilo-glm/trial-1.md +15 -9
  28. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-001/current/kilo-glm/trial-3.md +15 -14
  29. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-001/current/kilo-minimax/trial-1.md +22 -18
  30. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-001/current/kilo-minimax/trial-2.md +24 -16
  31. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-001/current/kilo-minimax/trial-3.md +13 -20
  32. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-001/current/meta.json +2 -2
  33. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/claude-sonnet/trial-1.md +14 -19
  34. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/claude-sonnet/trial-2.md +24 -14
  35. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/claude-sonnet/trial-3.md +20 -19
  36. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/judge.json +16 -17
  37. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/kilo-deepseek/trial-1.md +0 -7
  38. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/kilo-deepseek/trial-2.md +9 -10
  39. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/kilo-deepseek/trial-3.md +5 -5
  40. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/kilo-glm/trial-1.md +20 -4
  41. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/kilo-glm/trial-2.md +36 -9
  42. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/kilo-glm/trial-3.md +9 -6
  43. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/kilo-minimax/trial-1.md +4 -12
  44. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/kilo-minimax/trial-2.md +6 -8
  45. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/kilo-minimax/trial-3.md +8 -4
  46. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/meta.json +10 -11
  47. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/claude-sonnet/trial-1.md +30 -0
  48. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/claude-sonnet/trial-2.md +30 -0
  49. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/claude-sonnet/trial-3.md +30 -0
  50. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/judge.json +165 -0
  51. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/kilo-deepseek/trial-1.md +5 -0
  52. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/kilo-deepseek/trial-2.md +26 -0
  53. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/kilo-deepseek/trial-3.md +5 -0
  54. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/kilo-glm/trial-1.md +39 -0
  55. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/kilo-glm/trial-2.md +37 -0
  56. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/kilo-glm/trial-3.md +45 -0
  57. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/kilo-minimax/trial-1.md +26 -0
  58. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/kilo-minimax/trial-2.md +27 -0
  59. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/kilo-minimax/trial-3.md +7 -0
  60. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/meta.json +117 -0
  61. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003-parent-plan-mandatory.yaml +41 -0
  62. package/src/skills/decompose-gaps/tests/index.yaml +5 -0
  63. package/src/skills/decompose-gaps/tests/rubrics/parent-plan-mandatory.md +22 -0
  64. package/src/skills/decompose-gaps/workflows/decompose.md +5 -2
  65. package/src/skills/decompose-plan/knowledge/atomicity-checklist.md +31 -5
  66. package/src/skills/decompose-plan/knowledge/capabilities.md +29 -5
  67. package/src/skills/decompose-plan/knowledge/human-task-rules.md +15 -0
  68. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/claude-sonnet/trial-1.md +55 -0
  69. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/claude-sonnet/trial-2.md +49 -0
  70. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/claude-sonnet/trial-3.md +49 -0
  71. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/judge.json +163 -0
  72. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/kilo-deepseek/trial-1.md +104 -0
  73. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/kilo-deepseek/trial-2.md +45 -0
  74. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/kilo-deepseek/trial-3.md +58 -0
  75. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/kilo-glm/trial-1.md +193 -0
  76. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/kilo-glm/trial-2.md +202 -0
  77. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/kilo-glm/trial-3.md +155 -0
  78. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/kilo-minimax/trial-1.md +52 -0
  79. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/kilo-minimax/trial-2.md +17 -0
  80. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/kilo-minimax/trial-3.md +0 -0
  81. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/meta.json +115 -0
  82. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004-executor-atomicity.yaml +64 -0
  83. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/claude-sonnet/trial-1.md +59 -0
  84. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/claude-sonnet/trial-2.md +204 -0
  85. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/claude-sonnet/trial-3.md +213 -0
  86. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/judge.json +163 -0
  87. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/kilo-deepseek/trial-1.md +0 -0
  88. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/kilo-deepseek/trial-2.md +57 -0
  89. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/kilo-deepseek/trial-3.md +54 -0
  90. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/kilo-glm/trial-1.md +147 -0
  91. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/kilo-glm/trial-2.md +165 -0
  92. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/kilo-glm/trial-3.md +133 -0
  93. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/kilo-minimax/trial-1.md +81 -0
  94. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/kilo-minimax/trial-2.md +108 -0
  95. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/kilo-minimax/trial-3.md +3 -0
  96. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/meta.json +114 -0
  97. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005-capabilities-registry.yaml +78 -0
  98. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/claude-sonnet/trial-1.md +225 -0
  99. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/claude-sonnet/trial-2.md +66 -0
  100. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/claude-sonnet/trial-3.md +36 -0
  101. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/judge.json +163 -0
  102. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/kilo-deepseek/trial-1.md +42 -0
  103. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/kilo-deepseek/trial-2.md +67 -0
  104. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/kilo-deepseek/trial-3.md +40 -0
  105. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/kilo-glm/trial-1.md +122 -0
  106. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/kilo-glm/trial-2.md +131 -0
  107. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/kilo-glm/trial-3.md +138 -0
  108. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/kilo-minimax/trial-1.md +41 -0
  109. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/kilo-minimax/trial-2.md +88 -0
  110. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/kilo-minimax/trial-3.md +0 -0
  111. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/meta.json +115 -0
  112. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006-dod-threshold.yaml +72 -0
  113. package/src/skills/decompose-plan/tests/index.yaml +15 -0
  114. package/src/skills/decompose-plan/tests/rubrics/capabilities-registry.md +21 -0
  115. package/src/skills/decompose-plan/tests/rubrics/dod-threshold.md +21 -0
  116. package/src/skills/decompose-plan/tests/rubrics/executor-atomicity.md +21 -0
  117. package/src/skills/decompose-plan/workflows/decompose.md +38 -5
  118. package/src/skills/execute-task/tests/cases/TC-EXECUTE-TASK-001/current/meta.json +87 -88
  119. package/src/skills/execute-task/tests/cases/TC-EXECUTE-TASK-005/current/meta.json +87 -88
  120. package/src/skills/manual-testing/SKILL.md +6 -4
  121. package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-001/current/claude-sonnet/trial-1.md +29 -16
  122. package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-001/current/claude-sonnet/trial-2.md +21 -54
  123. package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-001/current/claude-sonnet/trial-3.md +18 -23
  124. package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-001/current/judge.json +17 -17
  125. package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-001/current/meta.json +19 -19
  126. package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-002/current/claude-sonnet/trial-1.md +27 -30
  127. package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-002/current/claude-sonnet/trial-2.md +16 -23
  128. package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-002/current/claude-sonnet/trial-3.md +35 -28
  129. package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-002/current/judge.json +13 -13
  130. package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-002/current/meta.json +15 -15
  131. package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-003/current/claude-sonnet/trial-1.md +76 -0
  132. package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-003/current/claude-sonnet/trial-2.md +71 -0
  133. package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-003/current/claude-sonnet/trial-3.md +85 -0
  134. package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-003/current/judge.json +46 -0
  135. package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-003/current/meta.json +36 -0
  136. package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-003-qa-non-ui-assertion.yaml +65 -0
  137. package/src/skills/manual-testing/tests/index.yaml +5 -0
  138. package/src/skills/manual-testing/tests/rubrics/qa-non-ui-assertion.md +31 -0
  139. package/src/skills/review-result/SKILL.md +1 -0
  140. package/src/skills/review-result/knowledge/test-hygiene.md +44 -0
  141. package/src/skills/review-result/scripts/verify-artifacts.js +157 -14
  142. package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/claude-sonnet/trial-1.md +7 -0
  143. package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/claude-sonnet/trial-2.md +7 -0
  144. package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/claude-sonnet/trial-3.md +7 -0
  145. package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/judge.json +163 -0
  146. package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/kilo-deepseek/trial-1.md +5 -0
  147. package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/kilo-deepseek/trial-2.md +5 -0
  148. package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/kilo-deepseek/trial-3.md +11 -0
  149. package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/kilo-glm/trial-1.md +16 -0
  150. package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/kilo-glm/trial-2.md +18 -0
  151. package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/kilo-glm/trial-3.md +17 -0
  152. package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/kilo-minimax/trial-1.md +17 -0
  153. package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/kilo-minimax/trial-2.md +31 -0
  154. package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/kilo-minimax/trial-3.md +5 -0
  155. package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/meta.json +115 -0
  156. package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003-test-isolation.yaml +50 -0
  157. package/src/skills/review-result/tests/fixtures/QA-904-test-isolation-violation/QA-904.md +51 -0
  158. package/src/skills/review-result/tests/fixtures/QA-904-test-isolation-violation/example-test.mjs +36 -0
  159. package/src/skills/review-result/tests/index.yaml +5 -0
  160. 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
@@ -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, kilo-deepseek]
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "workflow-ai",
3
- "version": "1.0.64",
3
+ "version": "1.0.66",
4
4
  "description": "AI Agent Workflow Coordinator — kanban-based pipeline for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
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 (16 directories)
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('Created .workflow/ directory structure (16 directories)');
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 timeoutId = setTimeout(() => {
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
  }