micro-models-agent 0.51.1 → 0.52.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/dist/cli/commands.js +162 -38
- package/dist/cli/completer.js +5 -5
- package/dist/cli/main.js +42 -54
- package/dist/cli/repl-commands.js +138 -38
- package/dist/cli/repl.js +175 -89
- package/dist/cli/run-result.js +11 -0
- package/dist/cli/security-commands.js +6 -6
- package/dist/cli/setup.js +21 -15
- package/dist/config/config.js +54 -27
- package/dist/config/defaults.js +17 -0
- package/dist/config/domains.js +179 -0
- package/dist/config/index.js +2 -1
- package/dist/config/security.js +28 -8
- package/dist/core/agent.js +162 -30
- package/dist/core/bootstrap.js +94 -17
- package/dist/core/crash-handler.js +51 -0
- package/dist/core/environment.js +199 -0
- package/dist/core/session-logger.js +60 -6
- package/dist/core/version.js +2 -0
- package/dist/i18n/en.json +120 -39
- package/dist/i18n/ru.json +91 -10
- package/dist/llm/openai-compat.js +191 -53
- package/dist/llm/orchestrator.js +5 -3
- package/dist/logger/app-logger.js +50 -4
- package/dist/main.js +1288 -635
- package/dist/modules/browser/session.js +4 -0
- package/dist/modules/certification/cli.js +58 -19
- package/dist/modules/certification/loader.js +2 -1
- package/dist/modules/certification/manifest.js +22 -14
- package/dist/modules/certification/runner.js +91 -5
- package/dist/modules/certification/scenarios.js +290 -7
- package/dist/modules/context/fact-extractor.js +6 -0
- package/dist/modules/context/manager.js +19 -2
- package/dist/modules/execution/audit-runners.js +61 -7
- package/dist/modules/execution/execution-plugin.js +219 -60
- package/dist/modules/execution/module.js +207 -18
- package/dist/modules/execution/moe-executor.js +33 -20
- package/dist/modules/execution/plan-store.js +39 -0
- package/dist/modules/execution/plan-tool.js +188 -19
- package/dist/modules/execution/planner.js +27 -23
- package/dist/modules/execution/stuck-detector.js +244 -8
- package/dist/modules/execution/tracker.js +8 -6
- package/dist/modules/execution/verifier.js +15 -2
- package/dist/modules/hallucination/detector.js +4 -0
- package/dist/modules/hallucination/factual.js +45 -5
- package/dist/modules/indexer/module.js +1 -0
- package/dist/modules/lsp/client.js +123 -12
- package/dist/modules/lsp/index.js +1 -1
- package/dist/modules/lsp/module.js +30 -2
- package/dist/modules/lsp/probe.js +11 -1
- package/dist/modules/lsp/startup-check.js +5 -2
- package/dist/modules/plugins/builtin/lint-on-write.js +144 -41
- package/dist/modules/plugins/manager.js +57 -13
- package/dist/modules/pricing/index.js +61 -0
- package/dist/modules/pricing/prices.js +129 -0
- package/dist/modules/providers/create.js +22 -0
- package/dist/modules/providers/fallback.js +79 -0
- package/dist/modules/providers/health.js +46 -0
- package/dist/modules/providers/index.js +5 -0
- package/dist/modules/providers/manager.js +161 -0
- package/dist/modules/providers/presets.js +128 -0
- package/dist/modules/providers/registry.js +22 -0
- package/dist/modules/providers/types.js +1 -0
- package/dist/modules/registry.js +1 -0
- package/dist/modules/security/command-validator.js +14 -0
- package/dist/modules/security/encryption.js +6 -6
- package/dist/modules/security/network-validator.js +17 -0
- package/dist/modules/security/path-validator.js +22 -26
- package/dist/modules/session/store.js +10 -10
- package/dist/tools/approve.js +1 -0
- package/dist/tools/attach-image.js +12 -0
- package/dist/tools/bash.js +27 -4
- package/dist/tools/browser.js +1 -0
- package/dist/tools/chunk-query.js +1 -0
- package/dist/tools/create-dir.js +1 -0
- package/dist/tools/delete-file.js +1 -0
- package/dist/tools/download-file.js +1 -0
- package/dist/tools/edit-file.js +2 -1
- package/dist/tools/enable-tools.js +1 -0
- package/dist/tools/executor.js +17 -7
- package/dist/tools/file-info.js +1 -0
- package/dist/tools/glob-tool.js +1 -0
- package/dist/tools/grep-tool.js +54 -13
- package/dist/tools/list-dir.js +1 -0
- package/dist/tools/load-skill.js +1 -0
- package/dist/tools/mcp-call.js +1 -0
- package/dist/tools/move-file.js +1 -0
- package/dist/tools/path-utils.js +51 -1
- package/dist/tools/pipeline-run.js +1 -0
- package/dist/tools/process-kill.js +11 -0
- package/dist/tools/process-list.js +1 -0
- package/dist/tools/process-log.js +9 -0
- package/dist/tools/question.js +1 -0
- package/dist/tools/read-file.js +94 -6
- package/dist/tools/recall.js +1 -0
- package/dist/tools/remember.js +1 -0
- package/dist/tools/scope-check.js +7 -5
- package/dist/tools/search-history.js +1 -0
- package/dist/tools/subagent.js +4 -4
- package/dist/tools/web-browse.js +1 -0
- package/dist/tools/web-fetch.js +27 -6
- package/dist/tools/web-search.js +70 -43
- package/dist/tools/write-file.js +1 -0
- package/dist/ui/line-editor.js +142 -23
- package/dist/ui/line-math.js +8 -4
- package/dist/ui/renderer.js +57 -7
- package/package.json +50 -48
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import { t } from "../../i18n/index";
|
|
2
2
|
import { PlanTracker } from "./tracker";
|
|
3
3
|
import { StepVerifier } from "./verifier";
|
|
4
|
-
import { StuckDetector } from "./stuck-detector";
|
|
4
|
+
import { StuckDetector, isSearchableError, buildSearchQuery } from "./stuck-detector";
|
|
5
5
|
import { Auditor, findExistingFile } from "./auditor";
|
|
6
6
|
import { PlanStore } from "./plan-store";
|
|
7
7
|
import { getMessageText } from "../../llm/provider";
|
|
8
8
|
import { extractFileLikeTokens, stripUrls } from "../hallucination/js-identifiers";
|
|
9
9
|
import { existsSync, readFileSync } from "fs";
|
|
10
10
|
import { resolve } from "path";
|
|
11
|
+
import { performWebSearch } from "../../tools/web-search";
|
|
12
|
+
import { getSessionSecurityConfig } from "../security/session-isolation";
|
|
11
13
|
import { createPlanToolDefinitions } from "./plan-tool";
|
|
12
|
-
import { createExecutionPlugin, STUCK_RECOVERY_COOLDOWN, } from "./execution-plugin";
|
|
14
|
+
import { createExecutionPlugin, STUCK_RECOVERY_COOLDOWN, STUCK_WARN_REPEAT_EVERY, } from "./execution-plugin";
|
|
15
|
+
/**
|
|
16
|
+
* Minimum interval between automatic error searches. The spec asked for a
|
|
17
|
+
* STUCK_RECOVERY_COOLDOWN-iteration cap, but onAfterTool has no iteration
|
|
18
|
+
* counter (executor context), so the same anti-spam protection is
|
|
19
|
+
* time-based. Injectable via the constructor for tests (0 disables it).
|
|
20
|
+
*/
|
|
21
|
+
const ERROR_SEARCH_MIN_INTERVAL_MS = 30000;
|
|
13
22
|
export class ExecutionModule {
|
|
14
23
|
name = "execution";
|
|
15
24
|
tracker = null;
|
|
@@ -21,6 +30,13 @@ export class ExecutionModule {
|
|
|
21
30
|
// Skip the first audit after restoring a plan from disk — gives the user
|
|
22
31
|
// one chance to change the subject before the audit gate locks in.
|
|
23
32
|
_auditSkipsRemaining = 0;
|
|
33
|
+
/**
|
|
34
|
+
* The last plan that was COMPLETED and auto-archived. `plan update` on the
|
|
35
|
+
* final step clears the tracker, which used to make runFinalAudit() return
|
|
36
|
+
* null and silently skip the final audit (tests + tsc + file checks) for a
|
|
37
|
+
* finished task. Keep the completed plan so the audit gate still runs.
|
|
38
|
+
*/
|
|
39
|
+
completedPlan = null;
|
|
24
40
|
/**
|
|
25
41
|
* Deferred <system-summary> messages. Injected by onBeforeTool/onAfterTool
|
|
26
42
|
* but flushed in onBeforeThink, so they never land between an assistant
|
|
@@ -33,6 +49,13 @@ export class ExecutionModule {
|
|
|
33
49
|
// failure is evidence-based (actual command failures), not task
|
|
34
50
|
// classification (rule #10).
|
|
35
51
|
forbiddenBashFailures = new Map();
|
|
52
|
+
searchRunner;
|
|
53
|
+
webSearchThreshold;
|
|
54
|
+
searchesThisSession = 0;
|
|
55
|
+
retryWebSearchSignatures = new Set();
|
|
56
|
+
lastErrorSearchAt = 0;
|
|
57
|
+
errorSearchCooldownMs;
|
|
58
|
+
hallucinationDetector = null;
|
|
36
59
|
/**
|
|
37
60
|
* The execution-plugin's mutable state. Owned HERE and passed BY REFERENCE
|
|
38
61
|
* to the plugin factory — the plugin writes deps.state.* directly, so the
|
|
@@ -43,7 +66,12 @@ export class ExecutionModule {
|
|
|
43
66
|
consecutivePlanWarnings: 0,
|
|
44
67
|
lastStepId: -1,
|
|
45
68
|
stuckNotified: false,
|
|
69
|
+
lastStuckWarnKey: "",
|
|
70
|
+
lastStuckWarnIter: -STUCK_WARN_REPEAT_EVERY,
|
|
46
71
|
depsGateHints: new Map(),
|
|
72
|
+
mutationsWithoutPlan: 0,
|
|
73
|
+
planNudgeSent: false,
|
|
74
|
+
typecheckFailures: new Map(),
|
|
47
75
|
};
|
|
48
76
|
/**
|
|
49
77
|
* Read/write accessor for the tracker, shared with the plan-tool and
|
|
@@ -63,18 +91,40 @@ export class ExecutionModule {
|
|
|
63
91
|
},
|
|
64
92
|
};
|
|
65
93
|
})();
|
|
66
|
-
constructor(baseDir, stuckThreshold = 6) {
|
|
94
|
+
constructor(baseDir, stuckThreshold = 6, webSearchThreshold = 5, searchRunner = performWebSearch, errorSearchCooldownMs = ERROR_SEARCH_MIN_INTERVAL_MS) {
|
|
67
95
|
this.baseDir = baseDir;
|
|
68
96
|
this.verifier = new StepVerifier(baseDir);
|
|
69
|
-
this.stuckDetector = new StuckDetector(stuckThreshold);
|
|
97
|
+
this.stuckDetector = new StuckDetector(stuckThreshold, 3, webSearchThreshold);
|
|
70
98
|
this.auditor = new Auditor(baseDir);
|
|
71
99
|
this.store = new PlanStore(baseDir);
|
|
100
|
+
this.webSearchThreshold = webSearchThreshold;
|
|
101
|
+
this.searchRunner = searchRunner;
|
|
102
|
+
this.errorSearchCooldownMs = errorSearchCooldownMs;
|
|
72
103
|
}
|
|
73
104
|
setPlan(plan) {
|
|
74
105
|
this.tracker = new PlanTracker(plan);
|
|
106
|
+
// Advance the pointer past any done/skipped steps of a restored/replanned
|
|
107
|
+
// plan so display ("current: step N") and the off-track alignment match
|
|
108
|
+
// reality instead of always pointing at step 1.
|
|
109
|
+
this.tracker.syncCurrentStep();
|
|
75
110
|
this.store.saveActive(plan);
|
|
111
|
+
this.completedPlan = null; // a new plan supersedes the completed one
|
|
76
112
|
this._auditSkipsRemaining = 0; // plan explicitly created — arm the audit gate
|
|
77
113
|
}
|
|
114
|
+
/** Wire the hallucination detector so the plan tool can check whether
|
|
115
|
+
* a file was tracked as deleted (auto-switch kind gate). */
|
|
116
|
+
setHallucinationDetector(detector) {
|
|
117
|
+
this.hallucinationDetector = detector;
|
|
118
|
+
}
|
|
119
|
+
/** Remember the last completed plan so the final audit can still verify it
|
|
120
|
+
* after the tracker was cleared by the auto-archive. */
|
|
121
|
+
recordCompleted(plan) {
|
|
122
|
+
this.completedPlan = plan;
|
|
123
|
+
}
|
|
124
|
+
/** Forget the completed plan (abort/delete/purge — nothing left to audit). */
|
|
125
|
+
clearCompleted() {
|
|
126
|
+
this.completedPlan = null;
|
|
127
|
+
}
|
|
78
128
|
restorePlan() {
|
|
79
129
|
const plan = this.store.loadActive();
|
|
80
130
|
if (!plan)
|
|
@@ -97,19 +147,107 @@ export class ExecutionModule {
|
|
|
97
147
|
getTracker() {
|
|
98
148
|
return this.tracker;
|
|
99
149
|
}
|
|
150
|
+
/** The currently tracked plan (single source of truth for UI consumers).
|
|
151
|
+
* Null in a fresh session — a disk-resident plan is restored only when
|
|
152
|
+
* resuming a session with history, so the REPL must never see stale plans. */
|
|
153
|
+
getActivePlan() {
|
|
154
|
+
return this.tracker?.getPlan() ?? null;
|
|
155
|
+
}
|
|
100
156
|
getStore() {
|
|
101
157
|
return this.store;
|
|
102
158
|
}
|
|
103
159
|
getStuckDetector() {
|
|
104
160
|
return this.stuckDetector;
|
|
105
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Automatic web search for a repeatedly failing error (>= errorWebSearch.threshold
|
|
164
|
+
* identical signatures). Fires the search fire-and-forget (never blocks the tool
|
|
165
|
+
* result) and queues the results as a <system-summary> in pendingMessages.
|
|
166
|
+
*
|
|
167
|
+
* Gates: webSearch master switch + errorWebSearch switch → repeated signature
|
|
168
|
+
* → not-searched (or retried) → session cap → meaningful error text →
|
|
169
|
+
* cooldown. One search per signature; empty results trigger a single retry
|
|
170
|
+
* (unmarkErrorSearched once), then the signature stays searched.
|
|
171
|
+
*/
|
|
172
|
+
maybeSearchError(ctx, call) {
|
|
173
|
+
if (ctx.config?.webSearch?.enabled === false)
|
|
174
|
+
return;
|
|
175
|
+
const cfg = ctx.config?.errorWebSearch;
|
|
176
|
+
if (!cfg?.enabled)
|
|
177
|
+
return;
|
|
178
|
+
const sig = this.stuckDetector.getRepeatedErrorSignature();
|
|
179
|
+
if (!sig)
|
|
180
|
+
return;
|
|
181
|
+
if (this.stuckDetector.hasErrorSearched(sig) && !this.retryWebSearchSignatures.has(sig))
|
|
182
|
+
return;
|
|
183
|
+
if (this.searchesThisSession >= (cfg.maxSearchesPerSession ?? 3))
|
|
184
|
+
return;
|
|
185
|
+
const lastError = this.stuckDetector.getLastErrorOutput();
|
|
186
|
+
if (!isSearchableError(lastError)) {
|
|
187
|
+
ctx.logger?.debug(t("exec.error_search_no_query"));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const now = Date.now();
|
|
191
|
+
if (now - this.lastErrorSearchAt < this.errorSearchCooldownMs)
|
|
192
|
+
return;
|
|
193
|
+
const stepDesc = this.tracker?.getCurrentStep()?.description ?? "";
|
|
194
|
+
const query = buildSearchQuery(stepDesc, this.stuckDetector.getLastBashCommand(), lastError, call.name, cfg.maxQueryChars ?? 200);
|
|
195
|
+
const count = this.stuckDetector.getErrorSignatureCount(sig);
|
|
196
|
+
this.stuckDetector.markErrorSearched(sig);
|
|
197
|
+
this.searchesThisSession++;
|
|
198
|
+
this.lastErrorSearchAt = now;
|
|
199
|
+
const networkConfig = getSessionSecurityConfig(ctx.config, ctx.sessionContext).network;
|
|
200
|
+
this.searchRunner(query, cfg.maxResults ?? 5, networkConfig, cfg.requestTimeoutMs ?? 10000)
|
|
201
|
+
.then((res) => {
|
|
202
|
+
if (!res.success || res.results.length === 0) {
|
|
203
|
+
if (!this.retryWebSearchSignatures.has(sig)) {
|
|
204
|
+
this.retryWebSearchSignatures.add(sig);
|
|
205
|
+
this.stuckDetector.unmarkErrorSearched(sig);
|
|
206
|
+
}
|
|
207
|
+
ctx.logger?.warn(t("exec.error_search_failed", { query }));
|
|
208
|
+
ctx.sessionLog?.plan("web-search", `empty/failed: ${query}`);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const resultsText = res.results
|
|
212
|
+
.map((r, i) => `${i + 1}. ${r.title} \u2014 ${r.url}\n ${r.snippet}`)
|
|
213
|
+
.join("\n");
|
|
214
|
+
this.pendingMessages.push({
|
|
215
|
+
role: "user",
|
|
216
|
+
content: `<system-summary>${t("exec.error_search_results", {
|
|
217
|
+
sig,
|
|
218
|
+
count: String(count),
|
|
219
|
+
query,
|
|
220
|
+
results: resultsText,
|
|
221
|
+
})}</system-summary>`,
|
|
222
|
+
});
|
|
223
|
+
})
|
|
224
|
+
.catch((e) => ctx.logger?.warn(`error web search: ${e.message}`));
|
|
225
|
+
}
|
|
106
226
|
/**
|
|
107
227
|
* Final audit before the agent may declare the task done. Returns null when
|
|
108
228
|
* no plan is active; otherwise checks plan completion + artifact existence.
|
|
109
229
|
*/
|
|
110
230
|
async runFinalAudit() {
|
|
111
|
-
|
|
112
|
-
|
|
231
|
+
// No active tracker: audit the last completed plan instead of bailing.
|
|
232
|
+
// A plan finished via `plan update` auto-archives and clears the tracker;
|
|
233
|
+
// without this the final audit (tests + tsc + file checks) was silently
|
|
234
|
+
// skipped and the model could declare success on broken artifacts.
|
|
235
|
+
if (!this.tracker) {
|
|
236
|
+
if (!this.completedPlan)
|
|
237
|
+
return null;
|
|
238
|
+
const plan = this.completedPlan;
|
|
239
|
+
const audit = await this.auditor.audit(plan);
|
|
240
|
+
const pendingSteps = plan.steps.flatMap((s) => s.status !== "done" && s.status !== "skipped" ? [`${s.id}. ${s.description}`] : []);
|
|
241
|
+
const done = plan.steps.filter((s) => s.status === "done").length;
|
|
242
|
+
return {
|
|
243
|
+
passed: audit.passed && pendingSteps.length === 0,
|
|
244
|
+
done,
|
|
245
|
+
total: plan.steps.length,
|
|
246
|
+
pendingSteps,
|
|
247
|
+
missingFiles: audit.missingFiles,
|
|
248
|
+
summary: audit.summary,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
113
251
|
if (this._auditSkipsRemaining > 0) {
|
|
114
252
|
this._auditSkipsRemaining--;
|
|
115
253
|
return null;
|
|
@@ -167,11 +305,18 @@ export class ExecutionModule {
|
|
|
167
305
|
trackerRef: this.trackerRef,
|
|
168
306
|
preserveActive: () => this.preserveActive(),
|
|
169
307
|
setPlan: (p) => this.setPlan(p),
|
|
308
|
+
recordCompleted: (p) => this.recordCompleted(p),
|
|
309
|
+
clearCompleted: () => this.clearCompleted(),
|
|
310
|
+
hasStepDeliverables: (s) => this.hasStepDeliverables(s),
|
|
170
311
|
missingStepDeliverables: (s) => this.missingStepDeliverables(s),
|
|
171
312
|
stillExistingDeliverables: (s) => this.stillExistingDeliverables(s),
|
|
172
313
|
parseKinds: (a, st) => this.parseKinds(a, st),
|
|
173
314
|
getTaskText: (c) => this.getTaskText(c),
|
|
315
|
+
typecheckFailures: this.state.typecheckFailures,
|
|
174
316
|
baseDir: this.baseDir,
|
|
317
|
+
wasFileDeleted: this.hallucinationDetector
|
|
318
|
+
? (path) => this.hallucinationDetector.getConsistencyCheck().getDeletedFiles().includes(path)
|
|
319
|
+
: undefined,
|
|
175
320
|
});
|
|
176
321
|
}
|
|
177
322
|
getPlugin() {
|
|
@@ -181,8 +326,10 @@ export class ExecutionModule {
|
|
|
181
326
|
pendingMessages: this.pendingMessages,
|
|
182
327
|
forbiddenBashFailures: this.forbiddenBashFailures,
|
|
183
328
|
state: this.state,
|
|
329
|
+
store: this.store,
|
|
184
330
|
checkPlanAlignment: (call) => this.checkPlanAlignment(call),
|
|
185
331
|
advancePlanIfStepComplete: (cm, sl) => this.advancePlanIfStepComplete(cm, sl),
|
|
332
|
+
maybeSearchError: (ctx, call) => this.maybeSearchError(ctx, call),
|
|
186
333
|
});
|
|
187
334
|
}
|
|
188
335
|
preserveActive() {
|
|
@@ -218,7 +365,11 @@ export class ExecutionModule {
|
|
|
218
365
|
const stepPaths = extractFileLikeTokens(stripUrls(step.description)).map((p) => p.toLowerCase());
|
|
219
366
|
if (stepPaths.length === 0)
|
|
220
367
|
return null;
|
|
221
|
-
|
|
368
|
+
// Only path-ish arguments are inspected. JSON-stringifying the whole args
|
|
369
|
+
// blob made write_file/edit_file `content` feed its code tokens into the
|
|
370
|
+
// off-path check (e.g. `import "./styles.css"` inside the file body) and
|
|
371
|
+
// raised phantom alignment warnings for on-track writes.
|
|
372
|
+
const argStr = this.pathArgStrings(call.arguments).join(" ");
|
|
222
373
|
const callPaths = extractFileLikeTokens(stripUrls(argStr)).map((p) => p.toLowerCase());
|
|
223
374
|
if (callPaths.length === 0)
|
|
224
375
|
return null;
|
|
@@ -248,6 +399,28 @@ export class ExecutionModule {
|
|
|
248
399
|
tool: call.name,
|
|
249
400
|
});
|
|
250
401
|
}
|
|
402
|
+
/** String values of the path-like arguments of a tool call. Limits the
|
|
403
|
+
* off-path alignment check to arguments that actually name files (path,
|
|
404
|
+
* file, dir, src, dst, source, target, input, output, destination, glob,
|
|
405
|
+
* workdir, cwd, command, pattern) and never scans payload content. */
|
|
406
|
+
pathArgStrings(args) {
|
|
407
|
+
if (!args)
|
|
408
|
+
return [];
|
|
409
|
+
const out = [];
|
|
410
|
+
for (const [key, value] of Object.entries(args)) {
|
|
411
|
+
if (!/path|file|dir|src|dst|source|target|input|output|destination|glob|workdir|cwd|command|pattern|query/i.test(key)) {
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (typeof value === "string")
|
|
415
|
+
out.push(value);
|
|
416
|
+
else if (Array.isArray(value)) {
|
|
417
|
+
for (const v of value)
|
|
418
|
+
if (typeof v === "string")
|
|
419
|
+
out.push(v);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return out;
|
|
423
|
+
}
|
|
251
424
|
advancePlanIfStepComplete(contextManager, sessionLog) {
|
|
252
425
|
const step = this.tracker?.getCurrentStep();
|
|
253
426
|
if (!step)
|
|
@@ -259,11 +432,11 @@ export class ExecutionModule {
|
|
|
259
432
|
// allGone trivially true and auto-advancing a delete step while the real
|
|
260
433
|
// files to delete still exist (the raw regex path did exactly that).
|
|
261
434
|
const stepPaths = extractFileLikeTokens(stripUrls(step.description)) || [];
|
|
262
|
-
// Step gate: verify dependencies when mentioned — check BEFORE file paths
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
435
|
+
// Step gate: verify dependencies when mentioned — check BEFORE file paths.
|
|
436
|
+
// Requires an actual package-manager verb (rule #10 — no bare keyword
|
|
437
|
+
// matching: "init" alone or "setup project" must NOT trigger the lockfile
|
|
438
|
+
// hint, only concrete dependency-install commands do).
|
|
439
|
+
const isDepsStep = /(?:npm|bun|yarn|pnpm|pip|pip3|pipenv|poetry|composer|deno|go|gem|cargo)\s+(?:install|add|init|i|get)\b|bun\s+(?:add|install)|npm\s+(?:i|install|add)|(?:install|установ\w*)\s+(?:dependencies|зависимост\w*|пакет\w*|packages?)/i.test(stepText);
|
|
267
440
|
if (isDepsStep) {
|
|
268
441
|
const lockFiles = [
|
|
269
442
|
"package-lock.json",
|
|
@@ -299,23 +472,32 @@ export class ExecutionModule {
|
|
|
299
472
|
}
|
|
300
473
|
if (stepPaths.length === 0)
|
|
301
474
|
return;
|
|
302
|
-
|
|
303
|
-
|
|
475
|
+
// Resolve tokens with the SAME subtree walk the deliverable gate and the
|
|
476
|
+
// auditor use (findExistingFile), so a nested file
|
|
477
|
+
// ("bicycle-shop/src/bicycles.ts" for token "src/bicycles.ts") counts as
|
|
478
|
+
// present. The old exact existsSync(resolve()) never matched nested
|
|
479
|
+
// projects, so auto-advance stayed silent even after the model wrote the
|
|
480
|
+
// real file.
|
|
481
|
+
const resolved = stepPaths.map((p) => findExistingFile(this.baseDir, p));
|
|
482
|
+
const allExist = resolved.every((r) => r !== null);
|
|
483
|
+
const allGone = resolved.every((r) => r === null);
|
|
304
484
|
const satisfied = step.kind === "delete" ? allGone && !allExist : allExist;
|
|
305
485
|
if (!satisfied)
|
|
306
486
|
return;
|
|
307
487
|
// Step gate: verify files have content (not empty)
|
|
308
488
|
if (step.kind !== "delete") {
|
|
309
489
|
const emptyFiles = [];
|
|
310
|
-
for (const
|
|
490
|
+
for (const r of resolved) {
|
|
491
|
+
if (!r)
|
|
492
|
+
continue;
|
|
311
493
|
try {
|
|
312
|
-
const content = readFileSync(
|
|
494
|
+
const content = readFileSync(r, "utf-8");
|
|
313
495
|
if (content.trim().length < 10) {
|
|
314
|
-
emptyFiles.push(
|
|
496
|
+
emptyFiles.push(r);
|
|
315
497
|
}
|
|
316
498
|
}
|
|
317
499
|
catch {
|
|
318
|
-
// If we can't read it, rely on
|
|
500
|
+
// If we can't read it, rely on the existence result
|
|
319
501
|
}
|
|
320
502
|
}
|
|
321
503
|
if (emptyFiles.length > 0 && contextManager) {
|
|
@@ -354,6 +536,7 @@ export class ExecutionModule {
|
|
|
354
536
|
if (this.tracker?.isComplete()) {
|
|
355
537
|
const completed = this.tracker.getPlan();
|
|
356
538
|
this.store.archivePlan(completed);
|
|
539
|
+
this.recordCompleted(completed); // final audit still verifies it
|
|
357
540
|
this.tracker = null;
|
|
358
541
|
sessionLog?.plan("auto-archive", `Plan ${completed.id} complete — archived`);
|
|
359
542
|
}
|
|
@@ -383,6 +566,12 @@ export class ExecutionModule {
|
|
|
383
566
|
return [];
|
|
384
567
|
return tokens.filter((p) => findExistingFile(this.baseDir, p));
|
|
385
568
|
}
|
|
569
|
+
/** Whether a step description names at least one real file token. Steps
|
|
570
|
+
* with no file tokens (pure commands, verification runs) cannot be
|
|
571
|
+
* auto-verified — the done-gate is vacuous for them. */
|
|
572
|
+
hasStepDeliverables(step) {
|
|
573
|
+
return extractFileLikeTokens(stripUrls(step.description)).length > 0;
|
|
574
|
+
}
|
|
386
575
|
/**
|
|
387
576
|
* Validate the optional per-step `kinds` argument for plan create and
|
|
388
577
|
* update-with-steps. Returns length-aligned kinds or an i18n error string.
|
|
@@ -86,7 +86,6 @@ async function executeSubtask(subtask, deps, _sharedContext) {
|
|
|
86
86
|
}
|
|
87
87
|
const maxAttempts = expertConfig.max_attempts || 3;
|
|
88
88
|
let lastError = "";
|
|
89
|
-
let lastResult = null;
|
|
90
89
|
const stuckDetector = new StuckDetector(maxAttempts * 2, maxAttempts);
|
|
91
90
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
92
91
|
stuckDetector.recordIteration(1);
|
|
@@ -144,7 +143,6 @@ async function executeSubtask(subtask, deps, _sharedContext) {
|
|
|
144
143
|
};
|
|
145
144
|
}
|
|
146
145
|
lastError = result.output;
|
|
147
|
-
lastResult = result;
|
|
148
146
|
stuckDetector.recordToolError("subagent", result.output);
|
|
149
147
|
// Inject actionable hints into the next retry's prompt
|
|
150
148
|
if (attempt < maxAttempts) {
|
|
@@ -260,23 +258,41 @@ export class MoEExecutor {
|
|
|
260
258
|
}
|
|
261
259
|
for (let waveIdx = 0; waveIdx < waves.length; waveIdx++) {
|
|
262
260
|
const wave = waves[waveIdx];
|
|
263
|
-
|
|
264
|
-
|
|
261
|
+
// Cap concurrent subtasks per wave (rateLimits.maxParallelTasks, default 5).
|
|
262
|
+
const maxParallel = this.deps.config?.security?.rateLimits?.maxParallelTasks ?? 5;
|
|
263
|
+
let active = 0;
|
|
264
|
+
const waiters = [];
|
|
265
|
+
const acquire = () => active < maxParallel
|
|
266
|
+
? Promise.resolve(active++)
|
|
267
|
+
: new Promise((resolve) => waiters.push(() => resolve(active++)));
|
|
268
|
+
const release = () => {
|
|
269
|
+
active--;
|
|
270
|
+
const next = waiters.shift();
|
|
271
|
+
if (next)
|
|
272
|
+
next();
|
|
273
|
+
};
|
|
274
|
+
const wavePromises = wave.map(async (subtask) => {
|
|
275
|
+
await acquire();
|
|
276
|
+
let result;
|
|
277
|
+
try {
|
|
278
|
+
result = await executeSubtask(subtask, this.deps, plan.shared_context);
|
|
279
|
+
}
|
|
280
|
+
catch (e) {
|
|
281
|
+
result = {
|
|
282
|
+
subtaskId: subtask.id,
|
|
283
|
+
success: false,
|
|
284
|
+
summary: `Unhandled error: ${subtask.id}`,
|
|
285
|
+
result: "",
|
|
286
|
+
error: e instanceof Error ? e.message : String(e),
|
|
287
|
+
durationMs: 0,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
finally {
|
|
291
|
+
release();
|
|
292
|
+
}
|
|
265
293
|
results.push(result);
|
|
266
294
|
return result;
|
|
267
|
-
})
|
|
268
|
-
.catch((e) => {
|
|
269
|
-
const errResult = {
|
|
270
|
-
subtaskId: subtask.id,
|
|
271
|
-
success: false,
|
|
272
|
-
summary: `Unhandled error: ${subtask.id}`,
|
|
273
|
-
result: "",
|
|
274
|
-
error: e.message,
|
|
275
|
-
durationMs: 0,
|
|
276
|
-
};
|
|
277
|
-
results.push(errResult);
|
|
278
|
-
return errResult;
|
|
279
|
-
}));
|
|
295
|
+
});
|
|
280
296
|
await Promise.all(wavePromises);
|
|
281
297
|
}
|
|
282
298
|
const failed = results.filter((r) => !r.success);
|
|
@@ -285,7 +301,4 @@ export class MoEExecutor {
|
|
|
285
301
|
}
|
|
286
302
|
return { success: failed.length === 0, results, errors, warnings };
|
|
287
303
|
}
|
|
288
|
-
getChainMaxAttempts() {
|
|
289
|
-
return 3;
|
|
290
|
-
}
|
|
291
304
|
}
|
|
@@ -154,4 +154,43 @@ export class PlanStore {
|
|
|
154
154
|
return { plan: archived, status: "archived" };
|
|
155
155
|
return null;
|
|
156
156
|
}
|
|
157
|
+
// ---- Deletion (the user asked for plans to actually go away) ----
|
|
158
|
+
/** Permanently delete a plan wherever it lives. Returns its status, or null
|
|
159
|
+
* when no plan with that id exists. */
|
|
160
|
+
deletePlan(id) {
|
|
161
|
+
const active = this.loadActive();
|
|
162
|
+
if (active && active.id === id) {
|
|
163
|
+
this.clearActive();
|
|
164
|
+
return "active";
|
|
165
|
+
}
|
|
166
|
+
const draftPath = join(this.draftsDir, `${id}.json`);
|
|
167
|
+
if (existsSync(draftPath)) {
|
|
168
|
+
rmSync(draftPath, { force: true });
|
|
169
|
+
return "draft";
|
|
170
|
+
}
|
|
171
|
+
const archivedPath = join(this.archiveDir, `${id}.json`);
|
|
172
|
+
if (existsSync(archivedPath)) {
|
|
173
|
+
rmSync(archivedPath, { force: true });
|
|
174
|
+
return "archived";
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
/** Delete every plan (active, drafts, archive). Returns the number removed. */
|
|
179
|
+
purgeAll() {
|
|
180
|
+
let n = 0;
|
|
181
|
+
const active = this.loadActive();
|
|
182
|
+
if (active) {
|
|
183
|
+
this.clearActive();
|
|
184
|
+
n++;
|
|
185
|
+
}
|
|
186
|
+
for (const p of this.listDrafts()) {
|
|
187
|
+
this.removeDraft(p.id);
|
|
188
|
+
n++;
|
|
189
|
+
}
|
|
190
|
+
for (const p of this.listArchived()) {
|
|
191
|
+
this.removeArchived(p.id);
|
|
192
|
+
n++;
|
|
193
|
+
}
|
|
194
|
+
return n;
|
|
195
|
+
}
|
|
157
196
|
}
|