micro-models-agent 0.41.2 → 0.43.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.
- package/bin/mma.mjs +41 -41
- package/dist/cli/commands.js +19 -9
- package/dist/cli/completer.js +37 -36
- package/dist/cli/index.js +2 -2
- package/dist/cli/main.js +23 -48
- package/dist/cli/repl-commands.js +12 -40
- package/dist/cli/repl.js +87 -217
- package/dist/cli/security-commands.js +7 -5
- package/dist/cli/setup.js +26 -8
- package/dist/config/config.js +5 -52
- package/dist/config/defaults.js +5 -29
- package/dist/config/experts.js +1 -1
- package/dist/config/index.js +3 -3
- package/dist/config/security.js +10 -3
- package/dist/core/agent-moe.js +10 -2
- package/dist/core/agent.js +82 -273
- package/dist/core/bootstrap.js +13 -80
- package/dist/core/index.js +2 -2
- package/dist/core/prompt-builder.js +2 -23
- package/dist/core/session-logger.js +4 -46
- package/dist/i18n/en.json +2 -75
- package/dist/i18n/ru.json +1 -74
- package/dist/index.js +1 -1
- package/dist/llm/image-utils.js +5 -4
- package/dist/llm/index.js +4 -4
- package/dist/llm/model-loader.js +6 -6
- package/dist/llm/openai-compat.js +34 -40
- package/dist/llm/orchestrator.js +29 -33
- package/dist/llm/response.js +9 -9
- package/dist/logger/app-logger.js +1 -1
- package/dist/logger/index.js +1 -1
- package/dist/main.js +648 -183
- package/dist/migration/backup.js +13 -13
- package/dist/migration/detect.js +11 -11
- package/dist/migration/index.js +2 -2
- package/dist/modules/browser/actions.js +4 -34
- package/dist/modules/browser/bridge-server.mjs +202 -202
- package/dist/modules/browser/cookie-store.js +6 -6
- package/dist/modules/browser/index.js +5 -7
- package/dist/modules/browser/module.js +7 -8
- package/dist/modules/browser/session.js +84 -87
- package/dist/modules/browser/snapshot.js +58 -92
- package/dist/modules/browser/types.js +1 -4
- package/dist/modules/certification/cli.js +4 -2
- package/dist/modules/certification/fact-checker.js +3 -1
- package/dist/modules/certification/loader.js +9 -3
- package/dist/modules/certification/runner.js +4 -1
- package/dist/modules/context/index.js +1 -1
- package/dist/modules/context/manager.js +86 -160
- package/dist/modules/execution/auditor.js +25 -177
- package/dist/modules/execution/module.js +544 -201
- package/dist/modules/execution/moe-executor.js +0 -25
- package/dist/modules/execution/plan-store.js +3 -1
- package/dist/modules/execution/plan-validator.js +10 -10
- package/dist/modules/execution/planner.js +1 -6
- package/dist/modules/execution/stuck-detector.js +10 -173
- package/dist/modules/execution/verifier.js +42 -86
- package/dist/modules/hallucination/confidence.js +1 -8
- package/dist/modules/hallucination/detector.js +5 -2
- package/dist/modules/hallucination/factual.js +64 -3
- package/dist/modules/hallucination/index.js +1 -1
- package/dist/modules/hallucination/js-identifiers.js +0 -190
- package/dist/modules/hallucination/llm-judge.js +3 -1
- package/dist/modules/indexer/cache.js +7 -9
- package/dist/modules/indexer/index.js +3 -3
- package/dist/modules/indexer/module.js +42 -95
- package/dist/modules/indexer/walker.js +17 -17
- package/dist/modules/lsp/client.js +31 -74
- package/dist/modules/lsp/config.js +33 -87
- package/dist/modules/lsp/index.js +3 -3
- package/dist/modules/lsp/module.js +21 -185
- package/dist/modules/mcp/module.js +6 -2
- package/dist/modules/memory/index.js +1 -1
- package/dist/modules/memory/module.js +23 -71
- package/dist/modules/memory/search.js +9 -11
- package/dist/modules/memory/store.js +13 -13
- package/dist/modules/pipelines/engine.js +10 -10
- package/dist/modules/pipelines/index.js +3 -3
- package/dist/modules/pipelines/parser.js +14 -17
- package/dist/modules/pipelines/template.js +1 -1
- package/dist/modules/plugins/builtin/lint-on-write.js +16 -21
- package/dist/modules/plugins/builtin/notify.js +2 -3
- package/dist/modules/plugins/index.js +1 -1
- package/dist/modules/plugins/loader.js +17 -59
- package/dist/modules/plugins/manager.js +17 -73
- package/dist/modules/processes/index.js +1 -1
- package/dist/modules/processes/registry.js +46 -135
- package/dist/modules/registry.js +2 -4
- package/dist/modules/security/audit-notifier.js +39 -39
- package/dist/modules/security/command-validator.js +8 -2
- package/dist/modules/security/data-sanitizer.js +9 -1
- package/dist/modules/security/encryption.js +56 -58
- package/dist/modules/security/network-validator.js +9 -1
- package/dist/modules/security/path-validator.js +3 -1
- package/dist/modules/security/security-policies.js +19 -3
- package/dist/modules/security/session-encryption.js +1 -1
- package/dist/modules/security/session-isolation.js +8 -8
- package/dist/modules/session/index.js +3 -3
- package/dist/modules/session/module.js +5 -5
- package/dist/modules/session/store.js +9 -3
- package/dist/modules/skills/module.js +2 -1
- package/dist/modules/updater/checker.js +6 -70
- package/dist/modules/updater/index.js +1 -2
- package/dist/modules/user-profile/compressor.js +2 -2
- package/dist/modules/user-profile/index.js +1 -1
- package/dist/modules/user-profile/profile.js +9 -9
- package/dist/tools/attach-image.js +1 -1
- package/dist/tools/bash.js +19 -178
- package/dist/tools/browser.js +29 -46
- package/dist/tools/executor.js +5 -4
- package/dist/tools/file-info.js +12 -13
- package/dist/tools/filter-tools.js +2 -9
- package/dist/tools/glob-tool.js +11 -11
- package/dist/tools/grep-tool.js +3 -1
- package/dist/tools/index.js +2 -13
- package/dist/tools/list-dir.js +17 -18
- package/dist/tools/load-skill.js +3 -1
- package/dist/tools/path-utils.js +4 -4
- package/dist/tools/pipeline-run.js +25 -25
- package/dist/tools/process-kill.js +11 -11
- package/dist/tools/process-list.js +22 -20
- package/dist/tools/process-log.js +18 -22
- package/dist/tools/question.js +3 -1
- package/dist/tools/read-file.js +2 -10
- package/dist/tools/recall.js +37 -44
- package/dist/tools/registry.js +4 -15
- package/dist/tools/remember.js +29 -29
- package/dist/tools/scope-check.js +9 -9
- package/dist/tools/subagent.js +9 -54
- package/dist/tools/user-input.js +1 -1
- package/dist/tools/web-browse.js +3 -3
- package/dist/tools/web-fetch.js +3 -3
- package/dist/tools/web-search.js +3 -3
- package/dist/tools/write-file.js +3 -1
- package/dist/ui/box.js +5 -1
- package/dist/ui/index.js +6 -6
- package/dist/ui/md-formatter.js +33 -33
- package/dist/ui/output.js +5 -5
- package/dist/ui/renderer.js +10 -15
- package/dist/ui/table.js +1 -1
- package/package.json +48 -48
- package/dist/cli/plugin-commands.js +0 -36
- package/dist/cli/run-result.js +0 -22
- package/dist/core/version.js +0 -24
- package/dist/modules/artifacts/store.js +0 -61
- package/dist/modules/browser/bridge-client.js +0 -199
- package/dist/modules/browser/bridge-path.js +0 -10
- package/dist/modules/browser/driver.js +0 -136
- package/dist/modules/context/chunk-query.js +0 -100
- package/dist/modules/context/fact-extractor.js +0 -162
- package/dist/modules/context/history.js +0 -15
- package/dist/modules/execution/audit-runners.js +0 -152
- package/dist/modules/execution/execution-plugin.js +0 -272
- package/dist/modules/execution/plan-tool.js +0 -508
- package/dist/modules/execution/windows-commands.js +0 -41
- package/dist/modules/indexer/project-profile.js +0 -183
- package/dist/modules/lsp/check-tool.js +0 -58
- package/dist/modules/lsp/command.js +0 -60
- package/dist/modules/lsp/probe.js +0 -76
- package/dist/modules/lsp/project-root.js +0 -32
- package/dist/modules/lsp/startup-check.js +0 -141
- package/dist/modules/processes/detect.js +0 -34
- package/dist/modules/skills/matcher.js +0 -27
- package/dist/modules/updater/module.js +0 -116
- package/dist/tools/chunk-query.js +0 -99
- package/dist/tools/download-file.js +0 -116
- package/dist/tools/enable-tools.js +0 -58
- package/dist/tools/hidden-tools-block.js +0 -37
- package/dist/ui/line-editor.js +0 -703
- package/dist/ui/line-math.js +0 -69
- package/dist/ui/plan-view.js +0 -103
|
@@ -2,7 +2,6 @@ import { getExpertConfig } from "../../config/experts";
|
|
|
2
2
|
import { filterToolsByTags } from "../../tools/filter-tools";
|
|
3
3
|
import { StuckDetector } from "./stuck-detector";
|
|
4
4
|
import { t } from "../../i18n/index";
|
|
5
|
-
import { ArtifactStore } from "../artifacts/store";
|
|
6
5
|
const TRANSIENT_ERROR_PATTERNS = [
|
|
7
6
|
"timeout",
|
|
8
7
|
"Timeout",
|
|
@@ -103,38 +102,14 @@ async function executeSubtask(subtask, deps, _sharedContext) {
|
|
|
103
102
|
read_only_files: readOnlyFiles,
|
|
104
103
|
tool_tags: expertConfig.tool_tags,
|
|
105
104
|
};
|
|
106
|
-
if (deps.config.subagent?.resultMode === "file") {
|
|
107
|
-
subagentArgs.result_mode = "file";
|
|
108
|
-
subagentArgs.artifact_name = subtask.id;
|
|
109
|
-
}
|
|
110
105
|
const result = await subagentTool.handler({
|
|
111
106
|
config: deps.config,
|
|
112
107
|
baseDir: deps.baseDir,
|
|
113
108
|
logger: deps.logger,
|
|
114
109
|
llmProvider: deps.llmProvider,
|
|
115
110
|
toolExecutor: deps.toolExecutor,
|
|
116
|
-
sessionId: "moe",
|
|
117
111
|
}, subagentArgs);
|
|
118
112
|
if (result.success) {
|
|
119
|
-
if (deps.config.subagent?.resultMode === "file") {
|
|
120
|
-
const pathMatch = result.output.match(/saved to artifact: (.+)/);
|
|
121
|
-
const abs = pathMatch ? pathMatch[1].trim() : "";
|
|
122
|
-
if (abs) {
|
|
123
|
-
const store = new ArtifactStore({
|
|
124
|
-
baseDir: deps.baseDir,
|
|
125
|
-
sessionId: "moe",
|
|
126
|
-
});
|
|
127
|
-
const full = store.read(abs.split(/[\\/]/).pop() || ""); // file name only, scoped to root
|
|
128
|
-
deps.logger.debug(`MoE subtask "${subtask.id}" attached artifact ${abs} (${full?.length ?? 0} chars) to Router`);
|
|
129
|
-
return {
|
|
130
|
-
subtaskId: subtask.id,
|
|
131
|
-
success: true,
|
|
132
|
-
summary: `Artifact: ${abs}`,
|
|
133
|
-
result: full ?? result.output,
|
|
134
|
-
durationMs: Date.now() - startTime,
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
113
|
return {
|
|
139
114
|
subtaskId: subtask.id,
|
|
140
115
|
success: true,
|
|
@@ -29,7 +29,9 @@ function listDir(dir, baseDir) {
|
|
|
29
29
|
if (!existsSync(dir))
|
|
30
30
|
return [];
|
|
31
31
|
const files = readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
32
|
-
return files
|
|
32
|
+
return files
|
|
33
|
+
.map((f) => readPlanFile(join(dir, f), baseDir))
|
|
34
|
+
.filter((p) => p !== null);
|
|
33
35
|
}
|
|
34
36
|
function toMeta(plan, status) {
|
|
35
37
|
return {
|
|
@@ -3,7 +3,7 @@ export function validatePlan(plan, config) {
|
|
|
3
3
|
const warnings = [];
|
|
4
4
|
const autoFixes = [];
|
|
5
5
|
if (!plan.subtasks || plan.subtasks.length === 0) {
|
|
6
|
-
return { valid: false, errors: [
|
|
6
|
+
return { valid: false, errors: ['Plan has no subtasks'], warnings: [], autoFixes: [] };
|
|
7
7
|
}
|
|
8
8
|
errors.push(...expertTagsExist(plan.subtasks, config));
|
|
9
9
|
errors.push(...hasCycle(plan.subtasks));
|
|
@@ -30,7 +30,7 @@ export function expertTagsExist(subtasks, config) {
|
|
|
30
30
|
const knownExperts = new Set(Object.keys(config.experts || {}));
|
|
31
31
|
for (const subtask of subtasks) {
|
|
32
32
|
if (!knownExperts.has(subtask.expert_tag)) {
|
|
33
|
-
errors.push(`Subtask "${subtask.id}" references unknown expert_tag "${subtask.expert_tag}". Known experts: ${Array.from(knownExperts).join(
|
|
33
|
+
errors.push(`Subtask "${subtask.id}" references unknown expert_tag "${subtask.expert_tag}". Known experts: ${Array.from(knownExperts).join(', ')}`);
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
return errors;
|
|
@@ -81,12 +81,12 @@ export function hasAllowedFilesOverlap(subtasks) {
|
|
|
81
81
|
}
|
|
82
82
|
for (const [file, writers] of writeFiles) {
|
|
83
83
|
if (writers.length > 1) {
|
|
84
|
-
const noDep = writers.filter(
|
|
85
|
-
const sub = subtasks.find(
|
|
86
|
-
return sub && !sub.depends_on?.some(
|
|
84
|
+
const noDep = writers.filter(w => {
|
|
85
|
+
const sub = subtasks.find(s => s.id === w);
|
|
86
|
+
return sub && !sub.depends_on?.some(d => writers.includes(d));
|
|
87
87
|
});
|
|
88
88
|
if (noDep.length > 1) {
|
|
89
|
-
errors.push(`Write-write overlap on "${file}" between subtasks: ${writers.join(
|
|
89
|
+
errors.push(`Write-write overlap on "${file}" between subtasks: ${writers.join(', ')} without dependency chain`);
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
}
|
|
@@ -104,7 +104,7 @@ export function deleteReadOverlap(subtasks) {
|
|
|
104
104
|
continue;
|
|
105
105
|
const readFiles = new Set(reader.read_only_files);
|
|
106
106
|
const writeFiles = writer.allowed_files;
|
|
107
|
-
const overlapping = writeFiles.filter(
|
|
107
|
+
const overlapping = writeFiles.filter(f => {
|
|
108
108
|
for (const rf of readFiles) {
|
|
109
109
|
if (f.startsWith(rf) || rf.startsWith(f))
|
|
110
110
|
return true;
|
|
@@ -125,7 +125,7 @@ export function deleteReadOverlap(subtasks) {
|
|
|
125
125
|
subtaskId: reader.id,
|
|
126
126
|
dependsOn: writer.id,
|
|
127
127
|
canAutoFix: false,
|
|
128
|
-
error: `Read-after-write overlap between "${writer.id}" (writer) and "${reader.id}" (reader) on "${overlapping.join(
|
|
128
|
+
error: `Read-after-write overlap between "${writer.id}" (writer) and "${reader.id}" (reader) on "${overlapping.join(', ')}" — add depends_on: [${writer.id}]`,
|
|
129
129
|
});
|
|
130
130
|
}
|
|
131
131
|
}
|
|
@@ -134,9 +134,9 @@ export function deleteReadOverlap(subtasks) {
|
|
|
134
134
|
return results;
|
|
135
135
|
}
|
|
136
136
|
export function applyAutoFixes(plan, autoFixes) {
|
|
137
|
-
const fixed = { ...plan, subtasks: plan.subtasks.map(
|
|
137
|
+
const fixed = { ...plan, subtasks: plan.subtasks.map(s => ({ ...s })) };
|
|
138
138
|
for (const fix of autoFixes) {
|
|
139
|
-
const sub = fixed.subtasks.find(
|
|
139
|
+
const sub = fixed.subtasks.find(s => s.id === fix.subtaskId);
|
|
140
140
|
if (!sub)
|
|
141
141
|
continue;
|
|
142
142
|
const dependsOnMatch = fix.fix.match(/depends_on:\s*(\S+)/);
|
|
@@ -26,7 +26,7 @@ export class PlanCreator {
|
|
|
26
26
|
const hasActionWord = actionWords.some((w) => task.toLowerCase().includes(w));
|
|
27
27
|
return hasActionWord && words.length > 8;
|
|
28
28
|
}
|
|
29
|
-
static createPlan(title, stepDescriptions, baseDir
|
|
29
|
+
static createPlan(title, stepDescriptions, baseDir) {
|
|
30
30
|
const stepCount = stepDescriptions.length;
|
|
31
31
|
return {
|
|
32
32
|
id: generatePlanId(),
|
|
@@ -35,7 +35,6 @@ export class PlanCreator {
|
|
|
35
35
|
id: i + 1,
|
|
36
36
|
description: desc,
|
|
37
37
|
status: "pending",
|
|
38
|
-
kind: kinds?.[i] ?? "create",
|
|
39
38
|
})),
|
|
40
39
|
createdAt: new Date().toISOString(),
|
|
41
40
|
baseDir,
|
|
@@ -50,7 +49,6 @@ export class PlanCreator {
|
|
|
50
49
|
id: kept.length + i + 1,
|
|
51
50
|
description: desc,
|
|
52
51
|
status: "pending",
|
|
53
|
-
kind: "create",
|
|
54
52
|
}));
|
|
55
53
|
return {
|
|
56
54
|
id: plan.id,
|
|
@@ -81,9 +79,6 @@ export class PlanCreator {
|
|
|
81
79
|
: "[ ]";
|
|
82
80
|
const note = step.note ? ` — ${step.note}` : "";
|
|
83
81
|
lines.push(`${icon} ${step.id}. ${step.description}${note}`);
|
|
84
|
-
for (const sub of step.subtasks ?? []) {
|
|
85
|
-
lines.push(` ${sub.done ? "[x]" : "[ ]"} ${sub.text}`);
|
|
86
|
-
}
|
|
87
82
|
}
|
|
88
83
|
return lines.join("\n");
|
|
89
84
|
}
|
|
@@ -35,36 +35,6 @@ export function classifyError(output) {
|
|
|
35
35
|
}
|
|
36
36
|
return "systematic";
|
|
37
37
|
}
|
|
38
|
-
/**
|
|
39
|
-
* Tools that only observe state and never change it. A long run of only these
|
|
40
|
-
* with no write/exec in between means the model is reading/exploring without
|
|
41
|
-
* producing anything — a read-only loop that the stuck detector must surface
|
|
42
|
-
* (observed: 50+ read_file iterations with zero writes after a false-positive
|
|
43
|
-
* audit rejected a completed task).
|
|
44
|
-
*/
|
|
45
|
-
const READ_ONLY_TOOLS = new Set([
|
|
46
|
-
"read_file",
|
|
47
|
-
"glob",
|
|
48
|
-
"grep",
|
|
49
|
-
"list_dir",
|
|
50
|
-
"file_info",
|
|
51
|
-
"browser",
|
|
52
|
-
"web_search",
|
|
53
|
-
"web_fetch",
|
|
54
|
-
"web_browse",
|
|
55
|
-
"search_history",
|
|
56
|
-
"recall",
|
|
57
|
-
"project_map",
|
|
58
|
-
"chunk_query",
|
|
59
|
-
"process_list",
|
|
60
|
-
"process_log",
|
|
61
|
-
"plan",
|
|
62
|
-
"todo",
|
|
63
|
-
"verify",
|
|
64
|
-
"load_skill",
|
|
65
|
-
]);
|
|
66
|
-
/** Consecutive read-only tool calls that constitute a loop. */
|
|
67
|
-
const READ_ONLY_LOOP_THRESHOLD = 10;
|
|
68
38
|
export class StuckDetector {
|
|
69
39
|
threshold;
|
|
70
40
|
errorThreshold;
|
|
@@ -82,10 +52,6 @@ export class StuckDetector {
|
|
|
82
52
|
escalationThreshold = 3;
|
|
83
53
|
fileRewriteCount = new Map();
|
|
84
54
|
fileRewriteThreshold = 3;
|
|
85
|
-
lastBashCommand = "";
|
|
86
|
-
lastBashOutput = "";
|
|
87
|
-
emptyBashRunCount = 0;
|
|
88
|
-
readOnlyStreak = 0;
|
|
89
55
|
constructor(threshold = 6, errorThreshold = 3) {
|
|
90
56
|
this.threshold = threshold;
|
|
91
57
|
this.errorThreshold = errorThreshold;
|
|
@@ -104,22 +70,6 @@ export class StuckDetector {
|
|
|
104
70
|
if (this.recentToolCalls.length > this.maxRecentCalls) {
|
|
105
71
|
this.recentToolCalls.shift();
|
|
106
72
|
}
|
|
107
|
-
// Track the consecutive read-only streak. A write/exec tool resets it;
|
|
108
|
-
// plan/todo bookkeeping counts as read-only (it is not real progress).
|
|
109
|
-
if (READ_ONLY_TOOLS.has(name)) {
|
|
110
|
-
this.readOnlyStreak++;
|
|
111
|
-
}
|
|
112
|
-
else {
|
|
113
|
-
this.readOnlyStreak = 0;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
/**
|
|
117
|
-
* True after a long run of read-only tool calls with no write in between.
|
|
118
|
-
* The model is exploring/looping without producing anything — inject a
|
|
119
|
-
* recovery hint so it stops re-reading and actually edits files.
|
|
120
|
-
*/
|
|
121
|
-
hasReadOnlyLoop() {
|
|
122
|
-
return this.readOnlyStreak >= READ_ONLY_LOOP_THRESHOLD;
|
|
123
73
|
}
|
|
124
74
|
recordToolError(toolName, output) {
|
|
125
75
|
this.toolErrors.set(toolName, (this.toolErrors.get(toolName) || 0) + 1);
|
|
@@ -131,28 +81,6 @@ export class StuckDetector {
|
|
|
131
81
|
getLastErrorOutput() {
|
|
132
82
|
return this.lastErrorOutput;
|
|
133
83
|
}
|
|
134
|
-
/**
|
|
135
|
-
* Record every bash invocation (successful or not) so stuck detection can
|
|
136
|
-
* reason about repeated runs with empty output, entry-point issues, and
|
|
137
|
-
* platform mistakes (PowerShell cmdlets in cmd.exe, bash heredocs, ...).
|
|
138
|
-
*/
|
|
139
|
-
recordBashOutput(command, output) {
|
|
140
|
-
const trimmed = output.trim();
|
|
141
|
-
if (command === this.lastBashCommand && !trimmed) {
|
|
142
|
-
this.emptyBashRunCount++;
|
|
143
|
-
}
|
|
144
|
-
else {
|
|
145
|
-
this.emptyBashRunCount = 0;
|
|
146
|
-
}
|
|
147
|
-
this.lastBashCommand = command;
|
|
148
|
-
this.lastBashOutput = output;
|
|
149
|
-
}
|
|
150
|
-
getLastBashCommand() {
|
|
151
|
-
return this.lastBashCommand;
|
|
152
|
-
}
|
|
153
|
-
getLastBashOutput() {
|
|
154
|
-
return this.lastBashOutput;
|
|
155
|
-
}
|
|
156
84
|
getLastFailedTool() {
|
|
157
85
|
return this.lastFailedTool;
|
|
158
86
|
}
|
|
@@ -230,7 +158,9 @@ export class StuckDetector {
|
|
|
230
158
|
getHints() {
|
|
231
159
|
const hints = [];
|
|
232
160
|
const desc = this.currentStepDescription.toLowerCase();
|
|
233
|
-
if (desc.includes("install") ||
|
|
161
|
+
if (desc.includes("install") ||
|
|
162
|
+
desc.includes("npm") ||
|
|
163
|
+
desc.includes("pip")) {
|
|
234
164
|
hints.push("Check if a lock file exists (package-lock.json, poetry.lock). If missing, run the install command first.");
|
|
235
165
|
}
|
|
236
166
|
if (desc.includes("test") || desc.includes("spec")) {
|
|
@@ -242,32 +172,17 @@ export class StuckDetector {
|
|
|
242
172
|
if (desc.includes("deploy") || desc.includes("publish")) {
|
|
243
173
|
hints.push("Verify credentials and network access before deploying.");
|
|
244
174
|
}
|
|
245
|
-
|
|
246
|
-
// reports the PRIMARY reason (priority: stuck > tool-errors > consecutive
|
|
247
|
-
// > repetitive > read-only). Repeating that same state here as a hint
|
|
248
|
-
// would duplicate the guidance in the same cooldown block. Only add the
|
|
249
|
-
// bullet when the recovery message is NOT already covering that state.
|
|
250
|
-
const primary = this.getPrimaryReason();
|
|
251
|
-
if (this.hasRepetitiveToolCalls() && primary !== "repetitive") {
|
|
175
|
+
if (this.hasRepetitiveToolCalls()) {
|
|
252
176
|
hints.push("You are calling the same tool repeatedly with the same arguments. Try a different approach.");
|
|
253
177
|
}
|
|
254
|
-
if (this.hasConsecutiveFailures()
|
|
178
|
+
if (this.hasConsecutiveFailures()) {
|
|
255
179
|
hints.push("Multiple different tools are failing. Check if the environment is set up correctly.");
|
|
256
180
|
}
|
|
257
|
-
if (this.hasReadOnlyLoop() && primary !== "read-only") {
|
|
258
|
-
hints.push("You have made many read-only tool calls (read_file/glob/grep/browser) without writing anything. Stop exploring — make the edit/write the task needs, or use the plan tool to decide next steps.");
|
|
259
|
-
}
|
|
260
181
|
return hints;
|
|
261
182
|
}
|
|
262
183
|
getActionableHints() {
|
|
263
184
|
const hints = [];
|
|
264
185
|
const error = this.lastErrorOutput;
|
|
265
|
-
// Bash-specific hints do not depend on a tool failure — they fire even
|
|
266
|
-
// when the command "succeeded" with empty output (missing entry point).
|
|
267
|
-
const isScriptRun = /(^|[\s&])(bun|node|tsx|ts-node|deno|python|python3)\S*\s+[^|&]+\s+[^\s]+$/.test(this.lastBashCommand);
|
|
268
|
-
if (isScriptRun && !this.lastBashOutput.trim() && this.emptyBashRunCount >= 2) {
|
|
269
|
-
hints.push(`The command "${this.lastBashCommand}" ran ${this.emptyBashRunCount} times with EMPTY output. The program likely has no entry point — read the file with read_file and check that it actually calls its main function with process.argv / CLI arguments and prints results (console.log). Then run it again.`);
|
|
270
|
-
}
|
|
271
186
|
if (!error)
|
|
272
187
|
return hints;
|
|
273
188
|
// Runtime incompatibility — tool/module crashes on this Node version
|
|
@@ -305,25 +220,6 @@ export class StuckDetector {
|
|
|
305
220
|
const file = this.getExcessiveRewriteFile();
|
|
306
221
|
hints.push(`File ${file} has been rewritten ${this.getFileRewriteCount(file)} times without success. Stop rewriting and try a fundamentally different approach.`);
|
|
307
222
|
}
|
|
308
|
-
// Repeated identical bash runs with empty output — likely a missing
|
|
309
|
-
// entry point (the program never calls its main function).
|
|
310
|
-
// PowerShell cmdlets used inside cmd.exe (Windows shell mismatch).
|
|
311
|
-
if (/Write-Host|Get-Content|Select-String|Out-File|Set-Content/i.test(error)) {
|
|
312
|
-
hints.push("That looks like a PowerShell cmdlet — the shell here is cmd.exe. Use echo/type for output or the read_file/write_file tools instead.");
|
|
313
|
-
}
|
|
314
|
-
// Model invented a Bun API that does not exist.
|
|
315
|
-
if (/Bun\.\w+ is not a function|Bun\.\w+ is not defined|Bun\.\w+ is not a constructor/i.test(error)) {
|
|
316
|
-
hints.push("That Bun API does not exist. Verify the API name in the Bun docs — common ones are Bun.file, Bun.write, Bun.spawn, Bun.serve. For file checks use fs.existsSync from node:fs.");
|
|
317
|
-
}
|
|
318
|
-
// The model keeps writing a file whose output shows a type/syntax
|
|
319
|
-
// error — read the actual error and fix it, don't rewrite blindly.
|
|
320
|
-
if (/error TS\d+|typecheck failed|syntax check failed/i.test(error)) {
|
|
321
|
-
hints.push("The file still has a type/syntax error (see the error line in the output). Read the file with read_file around the reported line, fix the actual error, then re-run — rewriting the whole file blindly usually makes it worse.");
|
|
322
|
-
}
|
|
323
|
-
// Bash heredoc in cmd.exe.
|
|
324
|
-
if (/unexpected.*<<|Непредвиденное появление/i.test(error)) {
|
|
325
|
-
hints.push("Heredoc (<< EOF) is a bash feature — this shell is cmd.exe and does not support it. Write the file with write_file instead.");
|
|
326
|
-
}
|
|
327
223
|
return hints;
|
|
328
224
|
}
|
|
329
225
|
getToolAlternative() {
|
|
@@ -391,16 +287,10 @@ export class StuckDetector {
|
|
|
391
287
|
if (errorTool) {
|
|
392
288
|
return t("exec.tool_errors", { tool: errorTool[0], count: errorTool[1] });
|
|
393
289
|
}
|
|
394
|
-
if (this.hasReadOnlyLoop()) {
|
|
395
|
-
return t("exec.read_only_loop", {
|
|
396
|
-
count: String(this.readOnlyStreak),
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
290
|
return "";
|
|
400
291
|
}
|
|
401
292
|
getRecoveryMessage() {
|
|
402
|
-
|
|
403
|
-
if (reason === "stuck") {
|
|
293
|
+
if (this.isStuck()) {
|
|
404
294
|
return t("exec.stuck_recovery", {
|
|
405
295
|
iterations: this.iterationsOnCurrentStep,
|
|
406
296
|
stepId: String(this.currentStepId ?? "?"),
|
|
@@ -408,50 +298,24 @@ export class StuckDetector {
|
|
|
408
298
|
});
|
|
409
299
|
}
|
|
410
300
|
// Per-tool error recovery (more specific — e.g., "bash failed 3 times")
|
|
411
|
-
|
|
412
|
-
|
|
301
|
+
const errorTool = Array.from(this.toolErrors.entries()).find(([_, c]) => c >= this.errorThreshold);
|
|
302
|
+
if (errorTool) {
|
|
413
303
|
return t("exec.tool_errors_recovery", {
|
|
414
304
|
tool: errorTool[0],
|
|
415
305
|
count: errorTool[1],
|
|
416
306
|
});
|
|
417
307
|
}
|
|
418
308
|
// Consecutive failures from different tools (generic — e.g., "5 consecutive failures")
|
|
419
|
-
if (
|
|
309
|
+
if (this.hasConsecutiveFailures()) {
|
|
420
310
|
return t("exec.consecutive_failures_recovery", {
|
|
421
311
|
count: this.consecutiveFailures,
|
|
422
312
|
});
|
|
423
313
|
}
|
|
424
|
-
if (
|
|
314
|
+
if (this.hasRepetitiveToolCalls()) {
|
|
425
315
|
return this.getRepetitiveToolMessage();
|
|
426
316
|
}
|
|
427
|
-
if (reason === "read-only") {
|
|
428
|
-
return t("exec.read_only_loop_recovery", {
|
|
429
|
-
count: String(this.readOnlyStreak),
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
317
|
return "";
|
|
433
318
|
}
|
|
434
|
-
/**
|
|
435
|
-
* Single source of truth for the recovery reason priority. Used by BOTH
|
|
436
|
-
* getRecoveryMessage() and getHints() so the two cannot drift apart: the
|
|
437
|
-
* state bullet that getRecoveryMessage already reports is NOT repeated as a
|
|
438
|
-
* hint (priority: stuck > tool-errors > consecutive > repetitive >
|
|
439
|
-
* read-only).
|
|
440
|
-
*/
|
|
441
|
-
getPrimaryReason() {
|
|
442
|
-
if (this.isStuck())
|
|
443
|
-
return "stuck";
|
|
444
|
-
const errorTool = Array.from(this.toolErrors.entries()).find(([_, c]) => c >= this.errorThreshold);
|
|
445
|
-
if (errorTool)
|
|
446
|
-
return "tool-errors";
|
|
447
|
-
if (this.hasConsecutiveFailures())
|
|
448
|
-
return "consecutive";
|
|
449
|
-
if (this.hasRepetitiveToolCalls())
|
|
450
|
-
return "repetitive";
|
|
451
|
-
if (this.hasReadOnlyLoop())
|
|
452
|
-
return "read-only";
|
|
453
|
-
return null;
|
|
454
|
-
}
|
|
455
319
|
reset() {
|
|
456
320
|
this.currentStepId = null;
|
|
457
321
|
this.iterationsOnCurrentStep = 0;
|
|
@@ -462,29 +326,6 @@ export class StuckDetector {
|
|
|
462
326
|
this.recentToolCalls = [];
|
|
463
327
|
this.fileRewriteCount.clear();
|
|
464
328
|
this.escalationCount = 0;
|
|
465
|
-
this.lastBashCommand = "";
|
|
466
|
-
this.lastBashOutput = "";
|
|
467
|
-
this.emptyBashRunCount = 0;
|
|
468
|
-
this.readOnlyStreak = 0;
|
|
469
|
-
}
|
|
470
|
-
/**
|
|
471
|
-
* Reset only step-progress state, keeping the tool-call history and the
|
|
472
|
-
* read-only streak. Used when the plan is complete but the loop continues
|
|
473
|
-
* (e.g. the audit gate rejected a finished task): step-stuck warnings are
|
|
474
|
-
* silenced, but a read-without-write loop is still detected.
|
|
475
|
-
*/
|
|
476
|
-
resetStepProgress() {
|
|
477
|
-
this.currentStepId = null;
|
|
478
|
-
this.iterationsOnCurrentStep = 0;
|
|
479
|
-
this.toolErrors.clear();
|
|
480
|
-
this.consecutiveFailures = 0;
|
|
481
|
-
this.lastFailedTool = "";
|
|
482
|
-
this.lastErrorOutput = "";
|
|
483
|
-
this.fileRewriteCount.clear();
|
|
484
|
-
this.escalationCount = 0;
|
|
485
|
-
this.lastBashCommand = "";
|
|
486
|
-
this.lastBashOutput = "";
|
|
487
|
-
this.emptyBashRunCount = 0;
|
|
488
329
|
}
|
|
489
330
|
/**
|
|
490
331
|
* Reset all per-step state when moving to a new step. Prevents errors and
|
|
@@ -500,10 +341,6 @@ export class StuckDetector {
|
|
|
500
341
|
this.lastErrorOutput = "";
|
|
501
342
|
this.recentToolCalls = [];
|
|
502
343
|
this.fileRewriteCount.clear();
|
|
503
|
-
this.lastBashCommand = "";
|
|
504
|
-
this.lastBashOutput = "";
|
|
505
|
-
this.emptyBashRunCount = 0;
|
|
506
|
-
this.readOnlyStreak = 0;
|
|
507
344
|
// Preserve escalationCount — prevents agent from "gaming" stuck detection
|
|
508
345
|
// by marking a step done and moving to the next one
|
|
509
346
|
}
|
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
import { existsSync } from
|
|
2
|
-
import { resolve, extname
|
|
3
|
-
import { spawn } from
|
|
4
|
-
import { t } from
|
|
5
|
-
import { validateExpertConfig } from
|
|
6
|
-
import { findProjectRoot } from "../lsp/project-root";
|
|
7
|
-
import { extractFileLikeTokens, stripUrls } from "../hallucination/js-identifiers";
|
|
8
|
-
import { findExistingFile } from "./auditor";
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { resolve, extname } from 'path';
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import { t } from '../../i18n/index';
|
|
5
|
+
import { validateExpertConfig } from '../../config/experts';
|
|
9
6
|
export class StepVerifier {
|
|
10
7
|
baseDir;
|
|
11
8
|
constructor(baseDir) {
|
|
@@ -16,51 +13,26 @@ export class StepVerifier {
|
|
|
16
13
|
const exists = existsSync(resolved);
|
|
17
14
|
return {
|
|
18
15
|
passed: exists,
|
|
19
|
-
message: exists ? t(
|
|
16
|
+
message: exists ? t('verify.file_exists', { path }) : t('verify.file_not_found', { path }),
|
|
20
17
|
};
|
|
21
18
|
}
|
|
22
19
|
async runScript(scriptName) {
|
|
23
20
|
try {
|
|
24
21
|
await this.runAsync(`bun run ${scriptName}`, this.baseDir, 60_000);
|
|
25
|
-
return { passed: true, message: t(
|
|
22
|
+
return { passed: true, message: t('verify.script_passed', { script: scriptName }) };
|
|
26
23
|
}
|
|
27
24
|
catch (e) {
|
|
28
|
-
return {
|
|
29
|
-
passed: false,
|
|
30
|
-
message: t("verify.script_failed", { script: scriptName, message: e.message }),
|
|
31
|
-
};
|
|
25
|
+
return { passed: false, message: t('verify.script_failed', { script: scriptName, message: e.message }) };
|
|
32
26
|
}
|
|
33
27
|
}
|
|
34
28
|
async runTypeCheck() {
|
|
35
|
-
const tsconfigPath = resolve(this.baseDir,
|
|
29
|
+
const tsconfigPath = resolve(this.baseDir, 'tsconfig.json');
|
|
36
30
|
if (!existsSync(tsconfigPath)) {
|
|
37
|
-
return { passed: true, message:
|
|
31
|
+
return { passed: true, message: 'No tsconfig.json found — skipping type check' };
|
|
38
32
|
}
|
|
39
33
|
try {
|
|
40
|
-
await this.runAsync(
|
|
41
|
-
return { passed: true, message:
|
|
42
|
-
}
|
|
43
|
-
catch (e) {
|
|
44
|
-
const stderr = e.stderr?.toString() || e.stdout?.toString() || e.message;
|
|
45
|
-
return { passed: false, message: `TypeScript type check failed: ${stderr.slice(0, 500)}` };
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Type-check a specific file, resolving the project root from the FILE's
|
|
50
|
-
* location (not the agent baseDir). For nested layouts (`baseDir/proj/` with
|
|
51
|
-
* its own tsconfig.json) the old baseDir-based check silently skipped every
|
|
52
|
-
* type error (observed in ses_mst2r0r5: zero type feedback for the whole
|
|
53
|
-
* session). Markers mirror the TypeScript LSP server config.
|
|
54
|
-
*/
|
|
55
|
-
async runTypeCheckForFile(filePath) {
|
|
56
|
-
const projectRoot = findProjectRoot(filePath, this.baseDir, ["tsconfig.json", "package.json"]);
|
|
57
|
-
const tsconfigPath = join(projectRoot, "tsconfig.json");
|
|
58
|
-
if (!existsSync(tsconfigPath)) {
|
|
59
|
-
return { passed: true, message: "No tsconfig.json found — skipping type check" };
|
|
60
|
-
}
|
|
61
|
-
try {
|
|
62
|
-
await this.runAsync("npx tsc --noEmit --skipLibCheck", projectRoot, 60_000);
|
|
63
|
-
return { passed: true, message: "TypeScript type check passed" };
|
|
34
|
+
await this.runAsync('npx tsc --noEmit', this.baseDir, 60_000);
|
|
35
|
+
return { passed: true, message: 'TypeScript type check passed' };
|
|
64
36
|
}
|
|
65
37
|
catch (e) {
|
|
66
38
|
const stderr = e.stderr?.toString() || e.stdout?.toString() || e.message;
|
|
@@ -68,23 +40,21 @@ export class StepVerifier {
|
|
|
68
40
|
}
|
|
69
41
|
}
|
|
70
42
|
async runTests() {
|
|
71
|
-
const pkgPath = resolve(this.baseDir,
|
|
43
|
+
const pkgPath = resolve(this.baseDir, 'package.json');
|
|
72
44
|
if (!existsSync(pkgPath)) {
|
|
73
|
-
return { passed: true, message:
|
|
45
|
+
return { passed: true, message: 'No package.json found — skipping tests' };
|
|
74
46
|
}
|
|
75
47
|
try {
|
|
76
|
-
const pkg = JSON.parse(require(
|
|
48
|
+
const pkg = JSON.parse(require('fs').readFileSync(pkgPath, 'utf-8'));
|
|
77
49
|
if (!pkg.scripts?.test) {
|
|
78
|
-
return { passed: true, message:
|
|
50
|
+
return { passed: true, message: 'No test script defined — skipping tests' };
|
|
79
51
|
}
|
|
80
52
|
}
|
|
81
|
-
catch {
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
return this.runScript("test");
|
|
53
|
+
catch { /* fall through — attempt to run */ }
|
|
54
|
+
return this.runScript('test');
|
|
85
55
|
}
|
|
86
56
|
async verifyArtifactFiles(files) {
|
|
87
|
-
return Promise.all(files.map(
|
|
57
|
+
return Promise.all(files.map(f => this.checkFileExists(f)));
|
|
88
58
|
}
|
|
89
59
|
async verifyMoEManifest(plan, config, allToolTags) {
|
|
90
60
|
const errors = [];
|
|
@@ -117,55 +87,45 @@ export class StepVerifier {
|
|
|
117
87
|
};
|
|
118
88
|
}
|
|
119
89
|
async verifyStep(stepDescription) {
|
|
120
|
-
const fileMatches =
|
|
121
|
-
// No named files — this step cannot be verified by file existence. Do NOT
|
|
122
|
-
// trivially pass (empty every() = true); signal noFiles so the tool guides
|
|
123
|
-
// the model to run a real check instead.
|
|
124
|
-
if (fileMatches.length === 0) {
|
|
125
|
-
return { passed: false, syntaxValid: true, failed: [], noFiles: true };
|
|
126
|
-
}
|
|
90
|
+
const fileMatches = stepDescription.match(/\b[\w./-]+\.[a-z]+/gi) || [];
|
|
127
91
|
const results = [];
|
|
128
92
|
let syntaxValid = true;
|
|
129
93
|
for (const filePath of fileMatches) {
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
results.push({ passed: false, message: t("verify.syntax_error", { path: filePath }) });
|
|
94
|
+
const result = await this.checkFileExists(filePath);
|
|
95
|
+
results.push(result);
|
|
96
|
+
if (result.passed) {
|
|
97
|
+
const fullPath = resolve(this.baseDir, filePath);
|
|
98
|
+
if (!(await this.validateSyntax(fullPath))) {
|
|
99
|
+
syntaxValid = false;
|
|
100
|
+
results.push({ passed: false, message: t('verify.syntax_error', { path: filePath }) });
|
|
101
|
+
}
|
|
139
102
|
}
|
|
140
103
|
}
|
|
141
104
|
return {
|
|
142
|
-
passed: results.every(
|
|
105
|
+
passed: results.every(r => r.passed),
|
|
143
106
|
syntaxValid,
|
|
144
|
-
failed: results.filter(
|
|
145
|
-
noFiles: false,
|
|
107
|
+
failed: results.filter(r => !r.passed),
|
|
146
108
|
};
|
|
147
109
|
}
|
|
148
110
|
async validateSyntax(filePath) {
|
|
149
111
|
const ext = extname(filePath);
|
|
150
|
-
if (ext ===
|
|
112
|
+
if (ext === '.ts' || ext === '.tsx') {
|
|
151
113
|
try {
|
|
152
114
|
await this.runAsync(`npx tsc --noEmit --skipLibCheck ${filePath}`, this.baseDir, 10000);
|
|
153
115
|
return true;
|
|
154
116
|
}
|
|
155
117
|
catch (err) {
|
|
156
|
-
if (err.status === 127 ||
|
|
157
|
-
err.message.includes("not found") ||
|
|
158
|
-
err.message.includes("ENOENT")) {
|
|
118
|
+
if (err.status === 127 || err.message.includes('not found') || err.message.includes('ENOENT')) {
|
|
159
119
|
return true;
|
|
160
120
|
}
|
|
161
|
-
const stderr = err.stderr?.toString() ||
|
|
162
|
-
if (stderr.includes(
|
|
121
|
+
const stderr = err.stderr?.toString() || '';
|
|
122
|
+
if (stderr.includes('error TS') && !stderr.includes('Cannot find module')) {
|
|
163
123
|
return false;
|
|
164
124
|
}
|
|
165
125
|
return true;
|
|
166
126
|
}
|
|
167
127
|
}
|
|
168
|
-
if (ext ===
|
|
128
|
+
if (ext === '.js' || ext === '.jsx') {
|
|
169
129
|
try {
|
|
170
130
|
await this.runAsync(`node --check ${filePath}`, this.baseDir, 5000);
|
|
171
131
|
return true;
|
|
@@ -186,25 +146,21 @@ export class StepVerifier {
|
|
|
186
146
|
cwd,
|
|
187
147
|
shell: true,
|
|
188
148
|
windowsHide: true,
|
|
189
|
-
stdio: [
|
|
190
|
-
});
|
|
191
|
-
let stdout = "";
|
|
192
|
-
let stderr = "";
|
|
193
|
-
child.stdout?.on("data", (d) => {
|
|
194
|
-
stdout += d.toString();
|
|
195
|
-
});
|
|
196
|
-
child.stderr?.on("data", (d) => {
|
|
197
|
-
stderr += d.toString();
|
|
149
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
198
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(); });
|
|
199
155
|
const timer = setTimeout(() => {
|
|
200
156
|
child.kill();
|
|
201
157
|
reject(new Error(`Command timed out after ${timeoutMs}ms`));
|
|
202
158
|
}, timeoutMs);
|
|
203
|
-
child.on(
|
|
159
|
+
child.on('error', (err) => {
|
|
204
160
|
clearTimeout(timer);
|
|
205
161
|
reject(err);
|
|
206
162
|
});
|
|
207
|
-
child.on(
|
|
163
|
+
child.on('close', (code) => {
|
|
208
164
|
clearTimeout(timer);
|
|
209
165
|
if (code === 0) {
|
|
210
166
|
resolve({ stdout, stderr });
|