workflow-ai 1.0.64 → 1.0.65

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 (135) hide show
  1. package/README.md +239 -145
  2. package/configs/agent-health-rules.yaml +64 -0
  3. package/configs/pipeline.yaml +18 -1
  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/artifact-snapshot.mjs +233 -0
  8. package/src/lib/error-classifier.mjs +274 -0
  9. package/src/lib/test-error-classifier.mjs +60 -0
  10. package/src/lib/test-extends.mjs +58 -0
  11. package/src/lib/test-version.mjs +21 -0
  12. package/src/scripts/move-to-review.js +5 -7
  13. package/src/scripts/reset-agent-health.js +62 -0
  14. package/src/skills/coach/SKILL.md +1 -0
  15. package/src/skills/coach/tests/cases/TC-COACH-001/current/meta.json +2 -3
  16. package/src/skills/coach/tests/cases/TC-COACH-002/current/meta.json +2 -3
  17. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-001/current/claude-sonnet/trial-1.md +23 -31
  18. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-001/current/claude-sonnet/trial-2.md +20 -35
  19. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-001/current/claude-sonnet/trial-3.md +36 -19
  20. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-001/current/judge.json +1 -1
  21. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-001/current/kilo-deepseek/trial-2.md +11 -5
  22. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-001/current/kilo-deepseek/trial-3.md +12 -16
  23. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-001/current/kilo-glm/trial-1.md +15 -9
  24. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-001/current/kilo-glm/trial-3.md +15 -14
  25. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-001/current/kilo-minimax/trial-1.md +22 -18
  26. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-001/current/kilo-minimax/trial-2.md +24 -16
  27. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-001/current/kilo-minimax/trial-3.md +13 -20
  28. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-001/current/meta.json +2 -2
  29. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/claude-sonnet/trial-1.md +14 -19
  30. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/claude-sonnet/trial-2.md +24 -14
  31. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/claude-sonnet/trial-3.md +20 -19
  32. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/judge.json +16 -17
  33. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/kilo-deepseek/trial-1.md +0 -7
  34. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/kilo-deepseek/trial-2.md +9 -10
  35. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/kilo-deepseek/trial-3.md +5 -5
  36. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/kilo-glm/trial-1.md +20 -4
  37. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/kilo-glm/trial-2.md +36 -9
  38. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/kilo-glm/trial-3.md +9 -6
  39. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/kilo-minimax/trial-1.md +4 -12
  40. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/kilo-minimax/trial-2.md +6 -8
  41. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/kilo-minimax/trial-3.md +8 -4
  42. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-002/current/meta.json +10 -11
  43. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/claude-sonnet/trial-1.md +30 -0
  44. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/claude-sonnet/trial-2.md +30 -0
  45. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/claude-sonnet/trial-3.md +30 -0
  46. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/judge.json +165 -0
  47. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/kilo-deepseek/trial-1.md +5 -0
  48. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/kilo-deepseek/trial-2.md +26 -0
  49. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/kilo-deepseek/trial-3.md +5 -0
  50. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/kilo-glm/trial-1.md +39 -0
  51. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/kilo-glm/trial-2.md +37 -0
  52. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/kilo-glm/trial-3.md +45 -0
  53. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/kilo-minimax/trial-1.md +26 -0
  54. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/kilo-minimax/trial-2.md +27 -0
  55. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/kilo-minimax/trial-3.md +7 -0
  56. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003/current/meta.json +117 -0
  57. package/src/skills/decompose-gaps/tests/cases/TC-DECOMPOSE-GAPS-003-parent-plan-mandatory.yaml +41 -0
  58. package/src/skills/decompose-gaps/tests/index.yaml +5 -0
  59. package/src/skills/decompose-gaps/tests/rubrics/parent-plan-mandatory.md +22 -0
  60. package/src/skills/decompose-gaps/workflows/decompose.md +5 -2
  61. package/src/skills/decompose-plan/knowledge/atomicity-checklist.md +31 -5
  62. package/src/skills/decompose-plan/knowledge/capabilities.md +29 -5
  63. package/src/skills/decompose-plan/knowledge/human-task-rules.md +15 -0
  64. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/claude-sonnet/trial-1.md +55 -0
  65. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/claude-sonnet/trial-2.md +49 -0
  66. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/claude-sonnet/trial-3.md +49 -0
  67. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/judge.json +163 -0
  68. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/kilo-deepseek/trial-1.md +104 -0
  69. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/kilo-deepseek/trial-2.md +45 -0
  70. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/kilo-deepseek/trial-3.md +58 -0
  71. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/kilo-glm/trial-1.md +193 -0
  72. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/kilo-glm/trial-2.md +202 -0
  73. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/kilo-glm/trial-3.md +155 -0
  74. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/kilo-minimax/trial-1.md +52 -0
  75. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/kilo-minimax/trial-2.md +17 -0
  76. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/kilo-minimax/trial-3.md +0 -0
  77. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004/current/meta.json +115 -0
  78. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-004-executor-atomicity.yaml +64 -0
  79. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/claude-sonnet/trial-1.md +59 -0
  80. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/claude-sonnet/trial-2.md +204 -0
  81. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/claude-sonnet/trial-3.md +213 -0
  82. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/judge.json +163 -0
  83. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/kilo-deepseek/trial-1.md +0 -0
  84. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/kilo-deepseek/trial-2.md +57 -0
  85. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/kilo-deepseek/trial-3.md +54 -0
  86. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/kilo-glm/trial-1.md +147 -0
  87. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/kilo-glm/trial-2.md +165 -0
  88. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/kilo-glm/trial-3.md +133 -0
  89. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/kilo-minimax/trial-1.md +81 -0
  90. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/kilo-minimax/trial-2.md +108 -0
  91. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/kilo-minimax/trial-3.md +3 -0
  92. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/meta.json +114 -0
  93. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005-capabilities-registry.yaml +78 -0
  94. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/claude-sonnet/trial-1.md +225 -0
  95. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/claude-sonnet/trial-2.md +66 -0
  96. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/claude-sonnet/trial-3.md +36 -0
  97. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/judge.json +163 -0
  98. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/kilo-deepseek/trial-1.md +42 -0
  99. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/kilo-deepseek/trial-2.md +67 -0
  100. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/kilo-deepseek/trial-3.md +40 -0
  101. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/kilo-glm/trial-1.md +122 -0
  102. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/kilo-glm/trial-2.md +131 -0
  103. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/kilo-glm/trial-3.md +138 -0
  104. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/kilo-minimax/trial-1.md +41 -0
  105. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/kilo-minimax/trial-2.md +88 -0
  106. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/kilo-minimax/trial-3.md +0 -0
  107. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006/current/meta.json +115 -0
  108. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-006-dod-threshold.yaml +72 -0
  109. package/src/skills/decompose-plan/tests/index.yaml +15 -0
  110. package/src/skills/decompose-plan/tests/rubrics/capabilities-registry.md +21 -0
  111. package/src/skills/decompose-plan/tests/rubrics/dod-threshold.md +21 -0
  112. package/src/skills/decompose-plan/tests/rubrics/executor-atomicity.md +21 -0
  113. package/src/skills/decompose-plan/workflows/decompose.md +38 -5
  114. package/src/skills/execute-task/tests/cases/TC-EXECUTE-TASK-001/current/meta.json +3 -4
  115. package/src/skills/execute-task/tests/cases/TC-EXECUTE-TASK-005/current/meta.json +3 -4
  116. package/src/skills/manual-testing/SKILL.md +6 -4
  117. package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-001/current/claude-sonnet/trial-1.md +29 -16
  118. package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-001/current/claude-sonnet/trial-2.md +21 -54
  119. package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-001/current/claude-sonnet/trial-3.md +18 -23
  120. package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-001/current/judge.json +17 -17
  121. package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-001/current/meta.json +19 -19
  122. package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-002/current/claude-sonnet/trial-1.md +27 -30
  123. package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-002/current/claude-sonnet/trial-2.md +16 -23
  124. package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-002/current/claude-sonnet/trial-3.md +35 -28
  125. package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-002/current/judge.json +13 -13
  126. package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-002/current/meta.json +15 -15
  127. package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-003/current/claude-sonnet/trial-1.md +76 -0
  128. package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-003/current/claude-sonnet/trial-2.md +71 -0
  129. package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-003/current/claude-sonnet/trial-3.md +85 -0
  130. package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-003/current/judge.json +46 -0
  131. package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-003/current/meta.json +36 -0
  132. package/src/skills/manual-testing/tests/cases/TC-MANUAL-TESTING-003-qa-non-ui-assertion.yaml +65 -0
  133. package/src/skills/manual-testing/tests/index.yaml +5 -0
  134. package/src/skills/manual-testing/tests/rubrics/qa-non-ui-assertion.md +31 -0
  135. package/src/skills/review-result/scripts/verify-artifacts.js +42 -12
@@ -0,0 +1,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
+ export function parseTtl(ttl, now = Date.now()) {
207
+ if (ttl === 'infinite') {
208
+ return Number.MAX_SAFE_INTEGER;
209
+ }
210
+ const untilMatch = ttl.match(/^(\d+)d$/);
211
+ if (untilMatch) {
212
+ return now + parseInt(untilMatch[1], 10) * 24 * 60 * 60 * 1000;
213
+ }
214
+ const hourMatch = ttl.match(/^(\d+)h$/);
215
+ if (hourMatch) {
216
+ return now + parseInt(hourMatch[1], 10) * 60 * 60 * 1000;
217
+ }
218
+ const minMatch = ttl.match(/^(\d+)m$/);
219
+ if (minMatch) {
220
+ return now + parseInt(minMatch[1], 10) * 60 * 1000;
221
+ }
222
+ if (ttl === 'until_utc_midnight') {
223
+ const nextMidnight = new Date(now);
224
+ nextMidnight.setUTCHours(24, 0, 0, 0);
225
+ const minDelay = now + MIN_UTC_MIDNIGHT_DELAY_MS;
226
+ return Math.max(nextMidnight.getTime(), minDelay);
227
+ }
228
+ throw new Error(`Invalid TTL format: ${ttl}. Expected Nm, Nh, Nd, until_utc_midnight, or infinite.`);
229
+ }
230
+
231
+ export function classifySync(rules, agentId, { exitCode, stderr }) {
232
+ const truncatedStderr = truncateStderr(stderr);
233
+ const agentRules = rules.agents.get(agentId) || [];
234
+ for (const rule of agentRules) {
235
+ const matched = rule.pattern
236
+ ? (() => {
237
+ try {
238
+ return rule.pattern.test(truncatedStderr);
239
+ } catch (e) {
240
+ return false;
241
+ }
242
+ })()
243
+ : matchRule(rule, exitCode, truncatedStderr);
244
+ if (matched) {
245
+ return {
246
+ class: rule.class,
247
+ rule_id: rule.id,
248
+ ttl: rule.ttl,
249
+ reason: truncatedStderr,
250
+ };
251
+ }
252
+ }
253
+ for (const rule of rules.common) {
254
+ const matched = rule.pattern
255
+ ? (() => {
256
+ try {
257
+ return rule.pattern.test(truncatedStderr);
258
+ } catch (e) {
259
+ return false;
260
+ }
261
+ })()
262
+ : matchRule(rule, exitCode, truncatedStderr);
263
+ if (matched) {
264
+ return {
265
+ class: rule.class,
266
+ rule_id: rule.id,
267
+ ttl: rule.ttl,
268
+ reason: truncatedStderr,
269
+ };
270
+ }
271
+ }
272
+ return null;
273
+ }
274
+
@@ -0,0 +1,60 @@
1
+ import { loadRules, classify, parseTtl, classifySync, InvalidRulesConfigError } from './error-classifier.mjs';
2
+ import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+
6
+ const tmp = mkdtempSync(path.join(os.tmpdir(), 'ec-test-'));
7
+ const configDir = path.join(tmp, '.workflow/config');
8
+ mkdirSync(configDir, { recursive: true });
9
+ const configPath = path.join(configDir, 'agent-health-rules.yaml');
10
+
11
+ writeFileSync(configPath, `version: "1.0"
12
+ common:
13
+ - id: "net-econnreset"
14
+ class: "transient"
15
+ ttl: "5m"
16
+ pattern: "ECONNRESET|ETIMEDOUT"
17
+ exit_codes: "any"
18
+ agents:
19
+ qwen-code:
20
+ rules:
21
+ - id: "qwen-quota"
22
+ class: "unavailable"
23
+ ttl: "until_utc_midnight"
24
+ pattern: "Qwen OAuth quota exceeded"
25
+ exit_codes: "any"
26
+ `);
27
+
28
+ const rules = loadRules(tmp);
29
+ console.log('loadRules result:', { commonLen: rules.common.length, agentsSize: rules.agents.size });
30
+
31
+ const result1 = await classify(rules, 'qwen-code', { exitCode: 1, stderr: 'Qwen OAuth quota exceeded' });
32
+ console.log('classify qwen-quota:', result1?.class, result1?.rule_id);
33
+
34
+ const result2 = await classify(rules, 'qwen-code', { exitCode: 1, stderr: 'Some other error' });
35
+ console.log('classify no match:', result2);
36
+
37
+ const result3 = await classify(rules, 'claude-sonnet', { exitCode: 1, stderr: 'ECONNRESET' });
38
+ console.log('classify common:', result3?.class, result3?.rule_id);
39
+
40
+ const result4 = classifySync(rules, 'qwen-code', { exitCode: 1, stderr: 'Qwen OAuth quota exceeded' });
41
+ console.log('classifySync qwen-quota:', result4?.class, result4?.rule_id);
42
+
43
+ const ttl1 = parseTtl('5m');
44
+ console.log('parseTtl 5m:', typeof ttl1, ttl1 > Date.now());
45
+
46
+ const ttl2 = parseTtl('infinite');
47
+ console.log('parseTtl infinite:', ttl2 === Number.MAX_SAFE_INTEGER);
48
+
49
+ const now = new Date('2026-04-21T12:00:00Z').getTime();
50
+ const ttl3 = parseTtl('until_utc_midnight', now);
51
+ console.log('parseTtl until_utc_midnight:', ttl3 >= now);
52
+
53
+ const ttl4 = parseTtl('1h');
54
+ console.log('parseTtl 1h:', ttl4 > now);
55
+
56
+ const ttl5 = parseTtl('1d');
57
+ console.log('parseTtl 1d:', ttl5 > now);
58
+
59
+ rmSync(tmp, { recursive: true, force: true });
60
+ console.log('All tests passed!');
@@ -0,0 +1,58 @@
1
+ import { loadRules, classify, classifySync } from './error-classifier.mjs';
2
+ import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+
6
+ const tmp = mkdtempSync(path.join(os.tmpdir(), 'ec-test-'));
7
+ const configDir = path.join(tmp, '.workflow/config');
8
+ mkdirSync(configDir, { recursive: true });
9
+ const configPath = path.join(configDir, 'agent-health-rules.yaml');
10
+
11
+ writeFileSync(configPath, `version: "1.0"
12
+ common:
13
+ - id: "common-test"
14
+ class: "transient"
15
+ ttl: "5m"
16
+ pattern: "common-error"
17
+ exit_codes: "any"
18
+ agents:
19
+ claude-sonnet:
20
+ rules:
21
+ - id: "claude-specific"
22
+ class: "unavailable"
23
+ ttl: "10m"
24
+ pattern: "claude-specific-error"
25
+ exit_codes: "any"
26
+ claude-opus:
27
+ extends: claude-sonnet
28
+ kilo-glm:
29
+ rules:
30
+ - id: "kilo-own"
31
+ class: "misconfigured"
32
+ ttl: "1h"
33
+ pattern: "kilo-error"
34
+ exit_codes: "any"
35
+ `);
36
+
37
+ const rules = loadRules(tmp);
38
+ console.log('loadRules with extends:');
39
+
40
+ const opusRules = rules.agents.get('claude-opus');
41
+ console.log('claude-opus rules (from extends):', opusRules?.length);
42
+ console.log('claude-opus first rule:', opusRules?.[0]?.id);
43
+
44
+ const glmRules = rules.agents.get('kilo-glm');
45
+ console.log('kilo-glm rules (own):', glmRules?.length);
46
+ console.log('kilo-glm first rule:', glmRules?.[0]?.id);
47
+
48
+ const test1 = classifySync(rules, 'claude-opus', { exitCode: 1, stderr: 'claude-specific-error' });
49
+ console.log('classify claude-opus with specific error:', test1?.class, test1?.rule_id);
50
+
51
+ const test2 = classifySync(rules, 'claude-opus', { exitCode: 1, stderr: 'common-error' });
52
+ console.log('classify claude-opus with common error:', test2?.class, test2?.rule_id);
53
+
54
+ const test3 = classifySync(rules, 'kilo-glm', { exitCode: 1, stderr: 'kilo-error' });
55
+ console.log('classify kilo-glm with own error:', test3?.class, test3?.rule_id);
56
+
57
+ rmSync(tmp, { recursive: true, force: true });
58
+ console.log('Extends tests passed!');
@@ -0,0 +1,21 @@
1
+ import { InvalidRulesConfigError } from './error-classifier.mjs';
2
+ import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+
6
+ const tmp = mkdtempSync(path.join(os.tmpdir(), 'ec-test-'));
7
+ const configDir = path.join(tmp, '.workflow/config');
8
+ mkdirSync(configDir, { recursive: true });
9
+ const configPath = path.join(configDir, 'agent-health-rules.yaml');
10
+
11
+ writeFileSync(configPath, 'version: "2.0"\ncommon: []');
12
+
13
+ try {
14
+ const { loadRules } = await import('./error-classifier.mjs');
15
+ loadRules(tmp);
16
+ console.log('ERROR: should have thrown');
17
+ } catch (e) {
18
+ console.log('Version error works:', e.name === 'InvalidRulesConfigError', e.message.includes('2.0'));
19
+ }
20
+
21
+ rmSync(tmp, { recursive: true, force: true });
@@ -16,8 +16,6 @@ import fs from "fs";
16
16
  import path from "path";
17
17
  import { findProjectRoot } from "workflow-ai/lib/find-root.mjs";
18
18
  import {
19
- parseFrontmatter,
20
- serializeFrontmatter,
21
19
  printResult,
22
20
  getLastReviewStatus,
23
21
  } from "workflow-ai/lib/utils.mjs";
@@ -98,18 +96,18 @@ function moveToReview(ticketId) {
98
96
  }
99
97
 
100
98
  const content = fs.readFileSync(sourcePath, "utf8");
101
- const { frontmatter, body } = parseFrontmatter(content);
102
99
 
103
- frontmatter.updated_at = new Date().toISOString();
104
-
105
- const newContent = serializeFrontmatter(frontmatter) + body;
100
+ // updated_at не обновляем: verify-artifacts сравнивает mtime файлов с updated_at,
101
+ // чтобы убедиться, что они были изменены агентом. updated_at должен сохранять момент
102
+ // перемещения тикета в in-progress (до начала работы агента) иначе любой
103
+ // легитимно изменённый файл будет ложно отклонён (mtime_edit < updated_at_review).
106
104
 
107
105
  if (!fs.existsSync(REVIEW_DIR)) {
108
106
  fs.mkdirSync(REVIEW_DIR, { recursive: true });
109
107
  }
110
108
 
111
109
  fs.renameSync(sourcePath, targetPath);
112
- fs.writeFileSync(targetPath, newContent, "utf8");
110
+ fs.writeFileSync(targetPath, content, "utf8");
113
111
 
114
112
  return {
115
113
  status: "moved",
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { loadHealth, markHealthy, unhealthy } from '../lib/agent-health-registry.mjs';
4
+
5
+ const projectRoot = process.cwd();
6
+
7
+ function getState() {
8
+ return loadHealth(projectRoot);
9
+ }
10
+
11
+ function showState() {
12
+ const state = getState();
13
+ console.log('---RESULT---');
14
+ console.log(JSON.stringify(state, null, 2));
15
+ console.log('---RESULT---');
16
+ }
17
+
18
+ function resetAgent(agentId) {
19
+ const state = getState();
20
+ const agent = state.agents[agentId];
21
+
22
+ if (!agent) {
23
+ console.log(`Agent "${agentId}" not found in health registry`);
24
+ return;
25
+ }
26
+
27
+ markHealthy(projectRoot, agentId);
28
+ console.log(`Reset health status for agent "${agentId}"`);
29
+ }
30
+
31
+ function resetAll() {
32
+ const unhealthyAgents = unhealthy(projectRoot);
33
+
34
+ if (unhealthyAgents.length === 0) {
35
+ console.log('No unhealthy agents to reset');
36
+ return;
37
+ }
38
+
39
+ const resetList = unhealthyAgents.map(a => a.agentId);
40
+
41
+ for (const agentId of resetList) {
42
+ markHealthy(projectRoot, agentId);
43
+ }
44
+
45
+ console.log(`Reset ${resetList.length} agent(s): ${resetList.join(', ')}`);
46
+ }
47
+
48
+ const args = process.argv.slice(2);
49
+
50
+ if (args.length === 0) {
51
+ showState();
52
+ } else if (args[0] === '--agent' && args[1]) {
53
+ resetAgent(args[1]);
54
+ } else if (args[0] === '--all') {
55
+ resetAll();
56
+ } else {
57
+ console.log('Usage:');
58
+ console.log(' node reset-agent-health.js # Show current state');
59
+ console.log(' node reset-agent-health.js --agent <id> # Reset single agent');
60
+ console.log(' node reset-agent-health.js --all # Reset all unhealthy');
61
+ process.exit(1);
62
+ }
@@ -127,6 +127,7 @@ ticket_prefix: COACH
127
127
  2. **Evidence-Based** — все выводы основаны на данных из завершённых тикетов, планов и логов пайплайна, а не на предположениях. **При анализе лога обязательно строй временную диаграмму ключевых событий по ID артефакта** (тикет, план, отчёт): проследи всю цепочку перемещений/изменений артефакта от первого упоминания до последнего, обращая внимание на события, отстоящие далеко друг от друга по времени, но связанные одним ID. **Антипаттерн:** прочитал начало лога (события archive/cleanup), прочитал середину (события create/decompose), но **не сопоставил** их — упустил коллизию ID или другой паттерн взаимного влияния. Перед формулированием findings задай себе вопрос: «Я проверил всю историю каждого упомянутого ID, или только последнее событие с ним?» **⚠️ Проверка фактической практики перед нормативной правкой:** если правка вводит новое правило про путь, имя, формат, расположение — **обязательно `Grep` по всему проекту** (код, конфиги, скилы, тикеты) на ключевой термин этого правила, чтобы измерить **масштаб уже существующей практики**. Один-два аномальных артефакта — не основание объявлять их новой нормой. Если фактическая практика противоположна гипотезе — гипотеза неверна, или (если стейкхолдер действительно хочет миграцию) нужен явный миграционный план и согласие на масштаб правок. Антипаттерн: получил короткий ответ стейкхолдера на развилку → принял за сильное правило → пошёл править скилы → не проверил, что в проекте 20+ артефактов уже живут по противоположному правилу. Перед каждой нормативной правкой задай себе вопрос: «Сколько уже существующих файлов/строк проекта противоречат тому, что я собираюсь записать?» Если ответ > 5 — остановись и переспроси у стейкхолдера, точно ли это миграция. **⚠️ Обязательный diff формулировок при анализе цепочки артефактов:** когда анализируешь инцидент, прошедший через несколько стадий (план → тикет → исполнение → ревью), **перед назначением виновного** обязан построчно сопоставить формулировки критериев на каждом стыке: (1) дословная строка критерия в плане, (2) дословная строка в тикете, (3) что реально проверяет assertion/тест, (4) что ревьюер проверял. Виновник — стадия, на которой произошла первая потеря семантики. Антипаттерн: прочитал план и увидел расхождение с результатом → обвинил последнюю стадию (ревьюера), не проверив, на какой промежуточной стадии формулировка была ослаблена. Гипотеза «ревьюер должен был поймать» невалидна, если ревьюер работал по формулировке тикета, а тикет уже не содержал потерянного уточнения.
128
128
  **⚠️ Антипаттерн «уход в формулировки вместо root cause»:** стейкхолдер задаёт вопрос о наблюдаемом дефекте («почему не поймали?»), а коуч анализирует текст формулировок, семантику переносов, чеклисты — вместо того чтобы ответить на прямой вопрос: какой конкретный шаг в какой конкретной стадии не выполнил конкретное физическое действие (открыть файл, посмотреть на картинку, запустить команду). Формулировки — это причина второго порядка; причина первого порядка — «агент X не сделал действие Y». Всегда начинай с причины первого порядка, потом объясняй, почему инструкции это допустили.
129
129
  **⚠️ Антипаттерн «оценка по результату вместо сверки с инструкцией»:** при анализе действия агента — **не оценивай** его «разумность» или «допустимость» по своему суждению. Вместо этого открой скил агента и **дословно сверь** действие с инструкцией. Если инструкция говорит «разбей тикет», а агент объединил шаги — это нарушение, даже если результат выглядит «приемлемо». Коуч не имеет права смягчать finding на основании того, что дефект «небольшой» или «единичный» — скил либо нарушен, либо нет.
130
+ **⚠️ Антипаттерн «пересказ вместо цитаты» при утверждениях о коде:** перед утверждением вида «скрипт/функция X использует/читает/пишет Y» обязан открыть файл и **дословно процитировать** строку, на которой это поведение происходит. Пересказ по памяти (даже свежей) теряет операторы-fallback (`a || b`, `a ?? b`), условные ветви, ранние return'ы — те детали, которые как раз и задают реальное поведение. Источник ошибки: агент видит ключевое слово в строке, строит «достаточное» утверждение о поведении и идёт дальше. Правило: если утверждение про код войдёт в финдинг, CHG, черновик правки или ответ стейкхолдеру — строка должна быть в отчёте целиком (либо скопированной в цитату, либо явной ссылкой `file:line`, открытой и перечитанной непосредственно перед утверждением).
130
131
  3. **Итеративность** — улучшай скилы инкрементально. Маленькие точечные улучшения > масштабные переписывания.
131
132
  4. **Обратная совместимость** — улучшения не должны ломать существующие воркфлоу и интеграции.
132
133
  5. **Актуальность знаний** — активно ищи в интернете лучшие практики, фреймворки и подходы для обогащения скилов.
@@ -1,9 +1,8 @@
1
1
  {
2
- "date": "2026-04-19T12:30:16.942Z",
3
- "skill_sha": "3c4e051",
2
+ "date": "2026-04-21T08:57:32.459Z",
3
+ "skill_sha": "6df42d0",
4
4
  "status": "passed",
5
5
  "duration_ms": 1,
6
- "l1_skipped": true,
7
6
  "per_model": {
8
7
  "claude-sonnet": {
9
8
  "passed": true,
@@ -1,9 +1,8 @@
1
1
  {
2
- "date": "2026-04-19T12:30:16.947Z",
3
- "skill_sha": "3c4e051",
2
+ "date": "2026-04-21T08:57:32.466Z",
3
+ "skill_sha": "6df42d0",
4
4
  "status": "passed",
5
5
  "duration_ms": 2,
6
- "l1_skipped": true,
7
6
  "per_model": {
8
7
  "claude-sonnet": {
9
8
  "passed": true,
@@ -1,49 +1,41 @@
1
- ## Анализ gap: «Нужно исправить дефект маппинга полей»
1
+ ## Декомпозиция недочётов
2
2
 
3
- ### Применение алгоритма scope-check
3
+ ### Обработка Gap: «Нужно исправить дефект маппинга полей»
4
4
 
5
- **Шаг 2a Проверка раздела «Исключено из scope»:**
5
+ **Выполняю проверку scope по алгоритму `algorithms/scope-check.md`:**
6
6
 
7
- Из контекста задачи:
7
+ **Шаг 2a — Проверка exclusions из scope плана:**
8
8
 
9
- | Параметр | Значение |
10
- |----------|----------|
11
- | Gap | «Нужно исправить дефект маппинга полей» |
12
- | Секция плана | «Исключено из scope: исправление дефектов» |
9
+ - `plan_exclusions = ["исправление дефектов"]`
10
+ - Gap описывает: исправление дефекта маппинга полей
11
+ - Сопоставление по смыслу: «исправить дефект маппинга» подпадает под исключение «исправление дефектов»
13
12
 
14
- **Проверка:**
15
- - `plan_exclusions` = `["исправление дефектов"]`
16
- - Gap описывает исправление дефекта (дефект маппинга полей)
17
- - Gap **прямо подпадает** под исключение
18
-
19
- **Результат алгоритма:**
13
+ **Результат проверки:**
20
14
  ```
21
- {
15
+ check_scope(gap) → {
22
16
  in_scope: false,
23
17
  reason: "Gap подпадает под исключение из scope плана: 'исправление дефектов'",
24
18
  check_failed: "2a"
25
19
  }
26
20
  ```
27
21
 
28
- ---
22
+ Тикет **не создаётся**.
29
23
 
30
- ## Новые требования (вне scope)
24
+ ---
31
25
 
32
- Следующий gap **исключён** и не конвертируется в тикет:
26
+ ### Новые требования (вне scope плана)
33
27
 
34
- > **Gap:** «Нужно исправить дефект маппинга полей»
35
- >
36
- > **Причина отклонения:** Gap scope-исключён — план явно содержит раздел «Исключено из scope: исправление дефектов». Данная работа выходит за рамки текущего плана.
37
- >
38
- > **Проверка:** 2a (exclusion check)
39
- >
40
- > **Рекомендация:** Оформить как отдельную инициативу или новый план, если исправление дефекта маппинга полей признаётся приоритетным.
28
+ - **«Нужно исправить дефект маппинга полей»** — причина: gap семантически совпадает с пунктом «Исключено из scope: исправление дефектов». Исправление дефектов явно выведено за рамки плана; создание fix-тикета нарушило бы границы scope. Требует отдельного решения на уровне планирования (новый план или явное снятие исключения).
41
29
 
42
30
  ---
43
31
 
44
- ## ---RESULT---
45
-
46
- **Тикеты созданы:** 0
47
-
48
- **Gaps, отклонённых как вне scope:** 1
49
- - «Нужно исправить дефект маппинга полей» — scope-исключён (проверка 2a: «Исключено из scope: исправление дефектов»)
32
+ ```
33
+ ---RESULT---
34
+ status: default
35
+ created_tickets: (нет)
36
+ rejected_gaps:
37
+ - gap: "Нужно исправить дефект маппинга полей"
38
+ reason: "Вне scope — подпадает под исключение плана: 'исправление дефектов'"
39
+ check_failed: "2a"
40
+ ---RESULT---
41
+ ```