micro-models-agent 0.39.1 → 0.40.0

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 (96) hide show
  1. package/bin/mma.mjs +41 -41
  2. package/dist/cli/commands.js +116 -3
  3. package/dist/cli/main.js +35 -8
  4. package/dist/cli/repl-commands.js +633 -0
  5. package/dist/cli/repl.js +110 -611
  6. package/dist/cli/setup.js +32 -12
  7. package/dist/config/config.js +46 -30
  8. package/dist/config/defaults.js +10 -1
  9. package/dist/config/security.js +15 -8
  10. package/dist/core/agent-moe.js +24 -12
  11. package/dist/core/agent.js +281 -47
  12. package/dist/core/bootstrap.js +52 -36
  13. package/dist/core/session-logger.js +35 -2
  14. package/dist/core/workspace.js +76 -0
  15. package/dist/i18n/en.json +79 -15
  16. package/dist/i18n/index.js +12 -9
  17. package/dist/i18n/ru.json +79 -15
  18. package/dist/index.js +13 -13
  19. package/dist/llm/openai-compat.js +39 -10
  20. package/dist/logger/app-logger.js +83 -16
  21. package/dist/logger/file-log.js +151 -0
  22. package/dist/main.js +492 -283
  23. package/dist/modules/browser/bridge-server.mjs +113 -105
  24. package/dist/modules/browser/session.js +108 -60
  25. package/dist/modules/certification/cli.js +176 -0
  26. package/dist/modules/certification/fact-checker.js +84 -0
  27. package/dist/modules/certification/loader.js +111 -0
  28. package/dist/modules/certification/manifest.js +50 -0
  29. package/dist/modules/certification/runner.js +162 -0
  30. package/dist/modules/certification/scenarios.js +124 -0
  31. package/dist/modules/certification/types.js +1 -0
  32. package/dist/modules/context/manager.js +119 -10
  33. package/dist/modules/execution/auditor.js +33 -39
  34. package/dist/modules/execution/index.js +8 -6
  35. package/dist/modules/execution/module.js +474 -32
  36. package/dist/modules/execution/moe-executor.js +97 -40
  37. package/dist/modules/execution/plan-coverage.js +68 -0
  38. package/dist/modules/execution/plan-persister.js +46 -0
  39. package/dist/modules/execution/plan-store.js +159 -0
  40. package/dist/modules/execution/planner.js +63 -13
  41. package/dist/modules/execution/stuck-detector.js +252 -39
  42. package/dist/modules/execution/tracker.js +21 -7
  43. package/dist/modules/execution/verifier.js +46 -17
  44. package/dist/modules/hallucination/confidence.js +7 -2
  45. package/dist/modules/hallucination/consistency.js +8 -42
  46. package/dist/modules/hallucination/detector.js +26 -21
  47. package/dist/modules/hallucination/factual.js +170 -150
  48. package/dist/modules/hallucination/index.js +5 -4
  49. package/dist/modules/hallucination/js-identifiers.js +72 -0
  50. package/dist/modules/hallucination/llm-judge.js +103 -0
  51. package/dist/modules/index.js +5 -5
  52. package/dist/modules/lsp/client.js +235 -0
  53. package/dist/modules/lsp/config.js +81 -0
  54. package/dist/modules/lsp/index.js +3 -0
  55. package/dist/modules/lsp/module.js +68 -0
  56. package/dist/modules/lsp/types.js +1 -0
  57. package/dist/modules/mcp/client.js +8 -2
  58. package/dist/modules/memory/store.js +4 -0
  59. package/dist/modules/plugins/builtin/lint-on-write.js +143 -38
  60. package/dist/modules/processes/index.js +1 -2
  61. package/dist/modules/processes/registry.js +125 -35
  62. package/dist/modules/processes/runner.js +9 -110
  63. package/dist/modules/security/audit-log.js +30 -10
  64. package/dist/modules/security/command-validator.js +42 -16
  65. package/dist/modules/security/content-scanner.js +9 -8
  66. package/dist/modules/security/network-validator.js +2 -2
  67. package/dist/modules/security/path-validator.js +64 -10
  68. package/dist/modules/security/security-policies.js +221 -67
  69. package/dist/modules/security/session-encryption.js +42 -25
  70. package/dist/modules/session/manager.js +15 -10
  71. package/dist/modules/session/store.js +62 -8
  72. package/dist/modules/skills/index.js +2 -3
  73. package/dist/modules/skills/module.js +10 -23
  74. package/dist/tools/bash.js +287 -90
  75. package/dist/tools/create-dir.js +0 -1
  76. package/dist/tools/delete-file.js +0 -1
  77. package/dist/tools/edit-file.js +10 -8
  78. package/dist/tools/executor.js +57 -7
  79. package/dist/tools/grep-tool.js +51 -29
  80. package/dist/tools/index.js +55 -40
  81. package/dist/tools/load-skill.js +14 -18
  82. package/dist/tools/move-file.js +3 -2
  83. package/dist/tools/pipeline-run.js +1 -1
  84. package/dist/tools/read-file.js +15 -5
  85. package/dist/tools/search-history.js +42 -22
  86. package/dist/tools/subagent.js +21 -12
  87. package/dist/tools/web-browse.js +54 -25
  88. package/dist/tools/web-fetch.js +60 -34
  89. package/dist/tools/web-search.js +39 -20
  90. package/dist/tools/write-file.js +13 -10
  91. package/dist/ui/diff.js +9 -16
  92. package/dist/ui/renderer.js +69 -6
  93. package/package.json +48 -45
  94. package/dist/modules/context/history.js +0 -15
  95. package/dist/modules/processes/detect.js +0 -34
  96. package/dist/modules/skills/matcher.js +0 -27
@@ -1,17 +1,58 @@
1
- import { t } from '../../i18n/index';
1
+ import { t } from "../../i18n/index";
2
+ const TRANSIENT_PATTERNS = [
3
+ /timeout/i,
4
+ /timed out/i,
5
+ /ECONNREFUSED/i,
6
+ /ETIMEDOUT/i,
7
+ /ECONNRESET/i,
8
+ /killed/i,
9
+ /SIGTERM/i,
10
+ /SIGKILL/i,
11
+ /rate limit/i,
12
+ /429/i,
13
+ /503/i,
14
+ /502/i,
15
+ ];
16
+ const BLOCKING_PATTERNS = [
17
+ /EACCES/i,
18
+ /permission denied/i,
19
+ /EPERM/i,
20
+ /ENOENT.*node_modules/i,
21
+ /ENOTDIR/i,
22
+ /invalid.*token/i,
23
+ /unauthorized/i,
24
+ /401/i,
25
+ /403/i,
26
+ ];
27
+ export function classifyError(output) {
28
+ for (const p of TRANSIENT_PATTERNS) {
29
+ if (p.test(output))
30
+ return "transient";
31
+ }
32
+ for (const p of BLOCKING_PATTERNS) {
33
+ if (p.test(output))
34
+ return "blocking";
35
+ }
36
+ return "systematic";
37
+ }
2
38
  export class StuckDetector {
3
39
  threshold;
4
40
  errorThreshold;
5
41
  iterationsOnCurrentStep = 0;
6
42
  currentStepId = null;
7
43
  toolErrors = new Map();
8
- currentStepDescription = '';
44
+ currentStepDescription = "";
9
45
  recentToolCalls = [];
10
46
  maxRecentCalls = 10;
11
47
  repetitionThreshold = 3;
12
48
  consecutiveFailures = 0;
13
- lastFailedTool = '';
14
- constructor(threshold = 8, errorThreshold = 3) {
49
+ lastFailedTool = "";
50
+ lastErrorOutput = "";
51
+ escalationCount = 0;
52
+ escalationThreshold = 3;
53
+ fileRewriteCount = new Map();
54
+ fileRewriteThreshold = 3;
55
+ constructor(threshold = 6, errorThreshold = 3) {
15
56
  this.threshold = threshold;
16
57
  this.errorThreshold = errorThreshold;
17
58
  }
@@ -20,8 +61,7 @@ export class StuckDetector {
20
61
  this.iterationsOnCurrentStep++;
21
62
  }
22
63
  else {
23
- this.currentStepId = stepId;
24
- this.iterationsOnCurrentStep = 1;
64
+ this.resetStepState(stepId);
25
65
  }
26
66
  }
27
67
  recordToolCall(name, args) {
@@ -31,19 +71,30 @@ export class StuckDetector {
31
71
  this.recentToolCalls.shift();
32
72
  }
33
73
  }
34
- recordToolError(toolName) {
74
+ recordToolError(toolName, output) {
35
75
  this.toolErrors.set(toolName, (this.toolErrors.get(toolName) || 0) + 1);
36
76
  this.consecutiveFailures++;
37
77
  this.lastFailedTool = toolName;
78
+ if (output)
79
+ this.lastErrorOutput = output;
80
+ }
81
+ getLastErrorOutput() {
82
+ return this.lastErrorOutput;
83
+ }
84
+ getLastFailedTool() {
85
+ return this.lastFailedTool;
86
+ }
87
+ getIterationsOnCurrentStep() {
88
+ return this.iterationsOnCurrentStep;
38
89
  }
39
90
  recordToolSuccess() {
40
91
  this.consecutiveFailures = 0;
41
- this.lastFailedTool = '';
92
+ this.lastFailedTool = "";
93
+ this.escalationCount = 0;
42
94
  }
43
95
  setCurrentStep(stepId, description) {
44
96
  if (stepId !== this.currentStepId) {
45
- this.currentStepId = stepId;
46
- this.iterationsOnCurrentStep = 1;
97
+ this.resetStepState(stepId);
47
98
  }
48
99
  this.currentStepDescription = description;
49
100
  }
@@ -54,22 +105,37 @@ export class StuckDetector {
54
105
  if (toolName) {
55
106
  return (this.toolErrors.get(toolName) || 0) >= this.errorThreshold;
56
107
  }
57
- return Array.from(this.toolErrors.values()).some(c => c >= this.errorThreshold);
108
+ return Array.from(this.toolErrors.values()).some((c) => c >= this.errorThreshold);
58
109
  }
59
110
  hasRepetitiveToolCalls() {
60
111
  if (this.recentToolCalls.length < this.repetitionThreshold)
61
112
  return false;
62
113
  const last = this.recentToolCalls[this.recentToolCalls.length - 1];
63
- let count = 1;
114
+ // Check consecutive identical calls (original logic)
115
+ let consecutiveCount = 1;
64
116
  for (let i = this.recentToolCalls.length - 2; i >= 0; i--) {
65
- if (this.recentToolCalls[i].name === last.name && this.recentToolCalls[i].argsKey === last.argsKey) {
66
- count++;
117
+ if (this.recentToolCalls[i].name === last.name &&
118
+ this.recentToolCalls[i].argsKey === last.argsKey) {
119
+ consecutiveCount++;
67
120
  }
68
121
  else {
69
122
  break;
70
123
  }
71
124
  }
72
- return count >= this.repetitionThreshold;
125
+ if (consecutiveCount >= this.repetitionThreshold)
126
+ return true;
127
+ // Also detect interleaved patterns (A, B, A, B, A) — count any single
128
+ // tool+args combo in the window and flag if it appears >= threshold times
129
+ const counts = new Map();
130
+ for (const call of this.recentToolCalls) {
131
+ const key = `${call.name}::${call.argsKey}`;
132
+ counts.set(key, (counts.get(key) || 0) + 1);
133
+ }
134
+ for (const count of counts.values()) {
135
+ if (count >= this.repetitionThreshold)
136
+ return true;
137
+ }
138
+ return false;
73
139
  }
74
140
  hasConsecutiveFailures() {
75
141
  return this.consecutiveFailures >= this.errorThreshold;
@@ -77,58 +143,205 @@ export class StuckDetector {
77
143
  getConsecutiveFailuresCount() {
78
144
  return this.consecutiveFailures;
79
145
  }
146
+ recordEscalation() {
147
+ this.escalationCount++;
148
+ }
149
+ shouldEscalate() {
150
+ return this.escalationCount >= this.escalationThreshold;
151
+ }
152
+ resetEscalation() {
153
+ this.escalationCount = 0;
154
+ }
155
+ getEscalationCount() {
156
+ return this.escalationCount;
157
+ }
158
+ getHints() {
159
+ const hints = [];
160
+ const desc = this.currentStepDescription.toLowerCase();
161
+ if (desc.includes("install") ||
162
+ desc.includes("npm") ||
163
+ desc.includes("pip")) {
164
+ hints.push("Check if a lock file exists (package-lock.json, poetry.lock). If missing, run the install command first.");
165
+ }
166
+ if (desc.includes("test") || desc.includes("spec")) {
167
+ hints.push("Make sure the source files exist and have real code before running tests.");
168
+ }
169
+ if (desc.includes("build") || desc.includes("compile")) {
170
+ hints.push("Check that all dependencies are installed and source files are not empty.");
171
+ }
172
+ if (desc.includes("deploy") || desc.includes("publish")) {
173
+ hints.push("Verify credentials and network access before deploying.");
174
+ }
175
+ if (this.hasRepetitiveToolCalls()) {
176
+ hints.push("You are calling the same tool repeatedly with the same arguments. Try a different approach.");
177
+ }
178
+ if (this.hasConsecutiveFailures()) {
179
+ hints.push("Multiple different tools are failing. Check if the environment is set up correctly.");
180
+ }
181
+ return hints;
182
+ }
183
+ getActionableHints() {
184
+ const hints = [];
185
+ const error = this.lastErrorOutput;
186
+ if (!error)
187
+ return hints;
188
+ // Runtime incompatibility — tool/module crashes on this Node version
189
+ if (/Cannot read properties of undefined|is not a function|is not a constructor/i.test(error)) {
190
+ if (/node_modules/.test(error)) {
191
+ hints.push("A dependency is incompatible with your Node.js version. Check if there is an alternative package or use a different runtime.");
192
+ }
193
+ }
194
+ // TypeScript syntax errors — model generated invalid code
195
+ if (/TS1005|TS1003|TS1109|TS1128|TS1434/i.test(error)) {
196
+ hints.push("TypeScript syntax error. Read the error message carefully — it tells you the exact line and column. Fix the syntax before retrying.");
197
+ }
198
+ // TypeScript type errors — model doesn't understand the types
199
+ if (/TS2322|TS2345|TS2769|TS7006|TS7016/i.test(error)) {
200
+ hints.push("TypeScript type mismatch. Check the type signature of the function/API you are using. If a package lacks type declarations, install @types/<package> or use skipLibCheck.");
201
+ }
202
+ // Module not found — missing dependency or wrong import
203
+ if (/Cannot find module|Module not found|ERR_MODULE_NOT_FOUND/i.test(error)) {
204
+ hints.push("Module not found. Run npm install or check if the import path is correct.");
205
+ }
206
+ // Permission / access errors
207
+ if (/EACCES|EPERM|permission denied/i.test(error)) {
208
+ hints.push("Permission denied. Check file permissions or run with appropriate privileges.");
209
+ }
210
+ // Network errors
211
+ if (/ECONNREFUSED|ETIMEDOUT|ENOTFOUND|fetch failed/i.test(error)) {
212
+ hints.push("Network error. Check if the server is running and accessible.");
213
+ }
214
+ // Command not found
215
+ if (/is not recognized|command not found|not found in path/i.test(error)) {
216
+ hints.push("Command not found. Check if the tool is installed and in PATH.");
217
+ }
218
+ // File rewrite loop — same file keeps failing
219
+ if (this.hasExcessiveRewrites()) {
220
+ const file = this.getExcessiveRewriteFile();
221
+ hints.push(`File ${file} has been rewritten ${this.getFileRewriteCount(file)} times without success. Stop rewriting and try a fundamentally different approach.`);
222
+ }
223
+ return hints;
224
+ }
225
+ getToolAlternative() {
226
+ const tool = this.lastFailedTool;
227
+ const error = this.lastErrorOutput;
228
+ if (!tool)
229
+ return null;
230
+ // If a tool fails with a runtime crash, suggest alternatives
231
+ if (/Cannot read properties of undefined|is not a function|TypeError|ReferenceError/i.test(error)) {
232
+ // Map of tools to their common alternatives
233
+ const alternatives = {
234
+ "ts-node": "tsx",
235
+ jest: "vitest",
236
+ mocha: "vitest",
237
+ webpack: "vite",
238
+ rollup: "vite",
239
+ parcel: "vite",
240
+ babel: "tsc",
241
+ eslint: "biome",
242
+ prettier: "biome",
243
+ };
244
+ for (const [failed, alt] of Object.entries(alternatives)) {
245
+ if (tool.includes(failed) || error.includes(failed)) {
246
+ return alt;
247
+ }
248
+ }
249
+ }
250
+ return null;
251
+ }
252
+ recordFileRewrite(filePath) {
253
+ const count = (this.fileRewriteCount.get(filePath) || 0) + 1;
254
+ this.fileRewriteCount.set(filePath, count);
255
+ }
256
+ getFileRewriteCount(filePath) {
257
+ return this.fileRewriteCount.get(filePath) || 0;
258
+ }
259
+ hasExcessiveRewrites() {
260
+ return Array.from(this.fileRewriteCount.values()).some((c) => c >= this.fileRewriteThreshold);
261
+ }
262
+ getExcessiveRewriteFile() {
263
+ for (const [file, count] of this.fileRewriteCount) {
264
+ if (count >= this.fileRewriteThreshold)
265
+ return file;
266
+ }
267
+ return null;
268
+ }
80
269
  getRepetitiveToolMessage() {
81
270
  if (!this.hasRepetitiveToolCalls())
82
- return '';
271
+ return "";
83
272
  const last = this.recentToolCalls[this.recentToolCalls.length - 1];
84
- return t('exec.repetitive_tool', { tool: last.name, count: this.repetitionThreshold });
273
+ return t("exec.repetitive_tool", {
274
+ tool: last.name,
275
+ count: this.repetitionThreshold,
276
+ });
85
277
  }
86
278
  getStuckReason() {
87
279
  if (this.isStuck()) {
88
- return t('exec.stuck', { stepId: String(this.currentStepId), description: this.currentStepDescription, iterations: this.iterationsOnCurrentStep });
280
+ return t("exec.stuck", {
281
+ stepId: String(this.currentStepId ?? "?"),
282
+ description: this.currentStepDescription,
283
+ iterations: this.iterationsOnCurrentStep,
284
+ });
89
285
  }
90
286
  const errorTool = Array.from(this.toolErrors.entries()).find(([_, c]) => c >= this.errorThreshold);
91
287
  if (errorTool) {
92
- return t('exec.tool_errors', { tool: errorTool[0], count: errorTool[1] });
288
+ return t("exec.tool_errors", { tool: errorTool[0], count: errorTool[1] });
93
289
  }
94
- return '';
290
+ return "";
95
291
  }
96
292
  getRecoveryMessage() {
97
293
  if (this.isStuck()) {
98
- return t('exec.stuck_recovery', { iterations: this.iterationsOnCurrentStep });
294
+ return t("exec.stuck_recovery", {
295
+ iterations: this.iterationsOnCurrentStep,
296
+ stepId: String(this.currentStepId ?? "?"),
297
+ description: this.currentStepDescription,
298
+ });
99
299
  }
100
300
  // Per-tool error recovery (more specific — e.g., "bash failed 3 times")
101
301
  const errorTool = Array.from(this.toolErrors.entries()).find(([_, c]) => c >= this.errorThreshold);
102
302
  if (errorTool) {
103
- return t('exec.tool_errors_recovery', { tool: errorTool[0], count: errorTool[1] });
303
+ return t("exec.tool_errors_recovery", {
304
+ tool: errorTool[0],
305
+ count: errorTool[1],
306
+ });
104
307
  }
105
308
  // Consecutive failures from different tools (generic — e.g., "5 consecutive failures")
106
309
  if (this.hasConsecutiveFailures()) {
107
- return t('exec.consecutive_failures_recovery', { count: this.consecutiveFailures });
310
+ return t("exec.consecutive_failures_recovery", {
311
+ count: this.consecutiveFailures,
312
+ });
108
313
  }
109
314
  if (this.hasRepetitiveToolCalls()) {
110
315
  return this.getRepetitiveToolMessage();
111
316
  }
112
- return '';
113
- }
114
- checkOffTrack(toolName, call) {
115
- if (!this.currentStepDescription)
116
- return null;
117
- const toolArgStr = JSON.stringify(call.arguments).toLowerCase();
118
- const pathInStep = (this.currentStepDescription.match(/\b[\w./-]+\.[a-z]+/gi) || [])
119
- .map(p => p.toLowerCase());
120
- const pathInCall = (toolArgStr.match(/\b[\w./-]+\.[a-z]+/gi) || [])
121
- .map(p => p.toLowerCase());
122
- const overlaps = pathInCall.some(p => pathInStep.some(s => p.includes(s) || s.includes(p)));
123
- if (!overlaps && pathInStep.length > 0 && pathInCall.length > 0) {
124
- return t('exec.off_track', { stepId: String(this.currentStepId), description: this.currentStepDescription, tool: toolName });
125
- }
126
- return null;
317
+ return "";
127
318
  }
128
319
  reset() {
320
+ this.currentStepId = null;
129
321
  this.iterationsOnCurrentStep = 0;
130
322
  this.toolErrors.clear();
131
323
  this.consecutiveFailures = 0;
132
- this.lastFailedTool = '';
324
+ this.lastFailedTool = "";
325
+ this.lastErrorOutput = "";
326
+ this.recentToolCalls = [];
327
+ this.fileRewriteCount.clear();
328
+ this.escalationCount = 0;
329
+ }
330
+ /**
331
+ * Reset all per-step state when moving to a new step. Prevents errors and
332
+ * tool repetitions from one step leaking into the next (false-positive
333
+ * stuck/recovery on the first iteration of the new step).
334
+ */
335
+ resetStepState(stepId) {
336
+ this.currentStepId = stepId;
337
+ this.iterationsOnCurrentStep = 1;
338
+ this.toolErrors.clear();
339
+ this.consecutiveFailures = 0;
340
+ this.lastFailedTool = "";
341
+ this.lastErrorOutput = "";
342
+ this.recentToolCalls = [];
343
+ this.fileRewriteCount.clear();
344
+ // Preserve escalationCount — prevents agent from "gaming" stuck detection
345
+ // by marking a step done and moving to the next one
133
346
  }
134
347
  }
@@ -1,4 +1,4 @@
1
- import { PlanCreator } from './planner';
1
+ import { PlanCreator } from "./planner";
2
2
  export class PlanTracker {
3
3
  plan;
4
4
  currentStepIndex = 0;
@@ -15,7 +15,7 @@ export class PlanTracker {
15
15
  return this.plan.steps[this.currentStepIndex];
16
16
  }
17
17
  getStep(id) {
18
- return this.plan.steps.find(s => s.id === id);
18
+ return this.plan.steps.find((s) => s.id === id);
19
19
  }
20
20
  updateStepStatus(id, status) {
21
21
  const step = this.getStep(id);
@@ -30,22 +30,36 @@ export class PlanTracker {
30
30
  advance() {
31
31
  if (this.currentStepIndex < this.plan.steps.length - 1) {
32
32
  this.currentStepIndex++;
33
- this.plan.steps[this.currentStepIndex].status = 'in_progress';
33
+ this.plan.steps[this.currentStepIndex].status = "in_progress";
34
34
  return true;
35
35
  }
36
36
  return false;
37
37
  }
38
+ /**
39
+ * Advance the pointer past any done/skipped steps without mutating their
40
+ * status. Keeps currentStepIndex in sync when the model marks steps done via
41
+ * the `plan update` tool (which never calls advance()). Without this,
42
+ * getCurrentStep() stays on the first step forever and the stuck detector
43
+ * accumulates false-positive "no progress" warnings.
44
+ */
45
+ syncCurrentStep() {
46
+ while (this.currentStepIndex < this.plan.steps.length - 1 &&
47
+ (this.plan.steps[this.currentStepIndex].status === "done" ||
48
+ this.plan.steps[this.currentStepIndex].status === "skipped")) {
49
+ this.currentStepIndex++;
50
+ }
51
+ }
38
52
  isComplete() {
39
- return this.plan.steps.every(s => s.status === 'done' || s.status === 'skipped');
53
+ return this.plan.steps.every((s) => s.status === "done" || s.status === "skipped");
40
54
  }
41
55
  getProgressString() {
42
- const done = this.plan.steps.filter(s => s.status === 'done').length;
56
+ const done = this.plan.steps.filter((s) => s.status === "done").length;
43
57
  const total = this.plan.steps.length;
44
58
  const pct = total > 0 ? Math.round((done / total) * 100) : 0;
45
59
  const barWidth = 10;
46
60
  const filled = Math.round((done / total) * barWidth);
47
- const bar = '█'.repeat(filled) + '░'.repeat(barWidth - filled);
48
- return `[Plan: ${this.plan.title}] ${done}/${total} ${bar} ${pct}%`;
61
+ const bar = "█".repeat(filled) + "░".repeat(barWidth - filled);
62
+ return `[${this.plan.id}] ${this.plan.title} ${done}/${total} ${bar} ${pct}%`;
49
63
  }
50
64
  toPromptBlock() {
51
65
  return PlanCreator.toPromptBlock(this.plan, this.currentStepIndex);
@@ -1,6 +1,6 @@
1
1
  import { existsSync } from 'fs';
2
2
  import { resolve, extname } from 'path';
3
- import { execSync } from 'child_process';
3
+ import { spawn } from 'child_process';
4
4
  import { t } from '../../i18n/index';
5
5
  import { validateExpertConfig } from '../../config/experts';
6
6
  export class StepVerifier {
@@ -18,12 +18,7 @@ export class StepVerifier {
18
18
  }
19
19
  async runScript(scriptName) {
20
20
  try {
21
- execSync(`bun run ${scriptName}`, {
22
- cwd: this.baseDir,
23
- encoding: 'utf-8',
24
- timeout: 60_000,
25
- stdio: 'pipe',
26
- });
21
+ await this.runAsync(`bun run ${scriptName}`, this.baseDir, 60_000);
27
22
  return { passed: true, message: t('verify.script_passed', { script: scriptName }) };
28
23
  }
29
24
  catch (e) {
@@ -36,12 +31,7 @@ export class StepVerifier {
36
31
  return { passed: true, message: 'No tsconfig.json found — skipping type check' };
37
32
  }
38
33
  try {
39
- execSync('npx tsc --noEmit', {
40
- cwd: this.baseDir,
41
- encoding: 'utf-8',
42
- timeout: 60_000,
43
- stdio: 'pipe',
44
- });
34
+ await this.runAsync('npx tsc --noEmit', this.baseDir, 60_000);
45
35
  return { passed: true, message: 'TypeScript type check passed' };
46
36
  }
47
37
  catch (e) {
@@ -105,7 +95,7 @@ export class StepVerifier {
105
95
  results.push(result);
106
96
  if (result.passed) {
107
97
  const fullPath = resolve(this.baseDir, filePath);
108
- if (!this.validateSyntax(fullPath)) {
98
+ if (!(await this.validateSyntax(fullPath))) {
109
99
  syntaxValid = false;
110
100
  results.push({ passed: false, message: t('verify.syntax_error', { path: filePath }) });
111
101
  }
@@ -117,11 +107,11 @@ export class StepVerifier {
117
107
  failed: results.filter(r => !r.passed),
118
108
  };
119
109
  }
120
- validateSyntax(filePath) {
110
+ async validateSyntax(filePath) {
121
111
  const ext = extname(filePath);
122
112
  if (ext === '.ts' || ext === '.tsx') {
123
113
  try {
124
- execSync(`npx tsc --noEmit --skipLibCheck ${filePath}`, { stdio: 'pipe', timeout: 10000 });
114
+ await this.runAsync(`npx tsc --noEmit --skipLibCheck ${filePath}`, this.baseDir, 10000);
125
115
  return true;
126
116
  }
127
117
  catch (err) {
@@ -137,7 +127,7 @@ export class StepVerifier {
137
127
  }
138
128
  if (ext === '.js' || ext === '.jsx') {
139
129
  try {
140
- execSync(`node --check ${filePath}`, { stdio: 'pipe', timeout: 5000 });
130
+ await this.runAsync(`node --check ${filePath}`, this.baseDir, 5000);
141
131
  return true;
142
132
  }
143
133
  catch {
@@ -146,4 +136,43 @@ export class StepVerifier {
146
136
  }
147
137
  return true;
148
138
  }
139
+ /**
140
+ * Run a command asynchronously (non-blocking event loop) and resolve when it
141
+ * exits. Rejects with the captured stderr/stdout on non-zero exit or timeout.
142
+ */
143
+ runAsync(command, cwd, timeoutMs) {
144
+ return new Promise((resolve, reject) => {
145
+ const child = spawn(command, {
146
+ cwd,
147
+ shell: true,
148
+ windowsHide: true,
149
+ stdio: ['ignore', 'pipe', 'pipe'],
150
+ });
151
+ let stdout = '';
152
+ let stderr = '';
153
+ child.stdout?.on('data', (d) => { stdout += d.toString(); });
154
+ child.stderr?.on('data', (d) => { stderr += d.toString(); });
155
+ const timer = setTimeout(() => {
156
+ child.kill();
157
+ reject(new Error(`Command timed out after ${timeoutMs}ms`));
158
+ }, timeoutMs);
159
+ child.on('error', (err) => {
160
+ clearTimeout(timer);
161
+ reject(err);
162
+ });
163
+ child.on('close', (code) => {
164
+ clearTimeout(timer);
165
+ if (code === 0) {
166
+ resolve({ stdout, stderr });
167
+ }
168
+ else {
169
+ const err = new Error(`Command failed with exit code ${code}`);
170
+ err.stdout = stdout;
171
+ err.stderr = stderr;
172
+ err.status = code;
173
+ reject(err);
174
+ }
175
+ });
176
+ });
177
+ }
149
178
  }
@@ -30,14 +30,19 @@ export class ConfidenceCheck {
30
30
  }
31
31
  }
32
32
  // Language-agnostic: very low word diversity (same words repeated)
33
- const words = response.toLowerCase().split(/\s+/).filter(w => w.length > 2);
33
+ const words = response
34
+ .toLowerCase()
35
+ .split(/\s+/)
36
+ .filter((w) => w.length > 2);
34
37
  if (words.length >= 10) {
35
38
  const unique = new Set(words);
36
39
  const diversity = unique.size / words.length;
37
40
  if (diversity < 0.25) {
38
41
  return {
39
42
  status: "warn",
40
- reason: t("hall.repetitive", { pct: Math.round((1 - diversity) * 100) }),
43
+ reason: t("hall.repetitive", {
44
+ pct: Math.round((1 - diversity) * 100),
45
+ }),
41
46
  };
42
47
  }
43
48
  }
@@ -1,4 +1,3 @@
1
- import { t } from '../../i18n/index';
2
1
  export class ConsistencyCheck {
3
2
  decisions = [];
4
3
  createdFiles = new Set();
@@ -15,46 +14,13 @@ export class ConsistencyCheck {
15
14
  getCreatedFiles() {
16
15
  return Array.from(this.createdFiles);
17
16
  }
18
- validate(response) {
19
- const lower = response.toLowerCase();
20
- for (const d of this.decisions) {
21
- const decisionWords = d.decision.toLowerCase().split(/\s+/).filter(w => w.length > 3);
22
- const contradicts = decisionWords.some(word => {
23
- // English patterns
24
- if (lower.includes(`instead of ${word}`))
25
- return true;
26
- if (lower.includes(`not ${word}`))
27
- return true;
28
- if (lower.includes(`replacing ${word} with`))
29
- return true;
30
- if (lower.includes(`switching to`))
31
- return true;
32
- if (lower.includes(`changing from ${word}`))
33
- return true;
34
- if (lower.includes(`abandoning ${word}`))
35
- return true;
36
- // Russian patterns
37
- if (lower.includes(`вместо ${word}`))
38
- return true;
39
- if (lower.includes(`заменяя ${word}`))
40
- return true;
41
- if (lower.includes(`заменяем ${word}`))
42
- return true;
43
- if (lower.includes(`переключаемся на`))
44
- return true;
45
- if (lower.includes(`от ${word} к`))
46
- return true;
47
- if (lower.includes(`отказываемся от ${word}`))
48
- return true;
49
- return false;
50
- });
51
- if (contradicts) {
52
- return {
53
- status: 'warn',
54
- reason: t('hall.contradiction', { decision: d.decision, location: d.location }),
55
- };
56
- }
57
- }
58
- return { status: 'pass' };
17
+ getDeletedFiles() {
18
+ return Array.from(this.deletedFiles);
19
+ }
20
+ getDecisions() {
21
+ return this.decisions;
22
+ }
23
+ hasDecisions() {
24
+ return this.decisions.length > 0;
59
25
  }
60
26
  }