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
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { t } from "../../i18n/index";
|
|
2
|
+
import { PlanCreator } from "./planner";
|
|
2
3
|
import { PlanTracker } from "./tracker";
|
|
3
4
|
import { StepVerifier } from "./verifier";
|
|
4
5
|
import { StuckDetector } from "./stuck-detector";
|
|
5
|
-
import { Auditor
|
|
6
|
+
import { Auditor } from "./auditor";
|
|
6
7
|
import { PlanStore } from "./plan-store";
|
|
8
|
+
import { checkPlanCoverage } from "./plan-coverage";
|
|
7
9
|
import { getMessageText } from "../../llm/provider";
|
|
8
|
-
import { extractFileLikeTokens, stripUrls } from "../hallucination/js-identifiers";
|
|
9
10
|
import { existsSync, readFileSync } from "fs";
|
|
10
11
|
import { resolve } from "path";
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
const STUCK_RECOVERY_COOLDOWN = 5;
|
|
13
|
+
const MAX_PLAN_WARNINGS_BEFORE_BLOCK = 3;
|
|
14
|
+
const FORCE_SKIP_THRESHOLD = 10;
|
|
13
15
|
export class ExecutionModule {
|
|
14
16
|
name = "execution";
|
|
15
17
|
tracker = null;
|
|
@@ -18,9 +20,17 @@ export class ExecutionModule {
|
|
|
18
20
|
auditor;
|
|
19
21
|
store;
|
|
20
22
|
baseDir;
|
|
23
|
+
lastRecoveryIteration = -STUCK_RECOVERY_COOLDOWN;
|
|
24
|
+
consecutivePlanWarnings = 0;
|
|
25
|
+
lastStepId = -1;
|
|
26
|
+
// How many times the deps-step gate has warned for each step id — used to
|
|
27
|
+
// escalate from a soft hint to a MUST-update instruction (models otherwise
|
|
28
|
+
// spin on "install dependencies" steps they invented themselves).
|
|
29
|
+
depsGateHints = new Map();
|
|
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
|
+
stuckNotified = false;
|
|
24
34
|
/**
|
|
25
35
|
* Deferred <system-summary> messages. Injected by onBeforeTool/onAfterTool
|
|
26
36
|
* but flushed in onBeforeThink, so they never land between an assistant
|
|
@@ -28,41 +38,6 @@ export class ExecutionModule {
|
|
|
28
38
|
* buries the warnings the model should react to).
|
|
29
39
|
*/
|
|
30
40
|
pendingMessages = [];
|
|
31
|
-
// Per-command failure counters for forbidden Windows commands (grep/sed/ls/
|
|
32
|
-
// find/… invoked through the bash tool). A hard stop after the second
|
|
33
|
-
// failure is evidence-based (actual command failures), not task
|
|
34
|
-
// classification (rule #10).
|
|
35
|
-
forbiddenBashFailures = new Map();
|
|
36
|
-
/**
|
|
37
|
-
* The execution-plugin's mutable state. Owned HERE and passed BY REFERENCE
|
|
38
|
-
* to the plugin factory — the plugin writes deps.state.* directly, so the
|
|
39
|
-
* assignments are visible on the module without any write-back.
|
|
40
|
-
*/
|
|
41
|
-
state = {
|
|
42
|
-
lastRecoveryIteration: -STUCK_RECOVERY_COOLDOWN,
|
|
43
|
-
consecutivePlanWarnings: 0,
|
|
44
|
-
lastStepId: -1,
|
|
45
|
-
stuckNotified: false,
|
|
46
|
-
depsGateHints: new Map(),
|
|
47
|
-
};
|
|
48
|
-
/**
|
|
49
|
-
* Read/write accessor for the tracker, shared with the plan-tool and
|
|
50
|
-
* execution-plugin factories. The `plan` tool's abort action clears it
|
|
51
|
-
* (current = null) — the setter keeps this.tracker in sync. Object-literal
|
|
52
|
-
* getters bind `this` to the literal, so the instance is captured in a
|
|
53
|
-
* closure instead.
|
|
54
|
-
*/
|
|
55
|
-
trackerRef = (() => {
|
|
56
|
-
const self = this;
|
|
57
|
-
return {
|
|
58
|
-
get current() {
|
|
59
|
-
return self.tracker;
|
|
60
|
-
},
|
|
61
|
-
set current(v) {
|
|
62
|
-
self.tracker = v;
|
|
63
|
-
},
|
|
64
|
-
};
|
|
65
|
-
})();
|
|
66
41
|
constructor(baseDir, stuckThreshold = 6) {
|
|
67
42
|
this.baseDir = baseDir;
|
|
68
43
|
this.verifier = new StepVerifier(baseDir);
|
|
@@ -79,16 +54,7 @@ export class ExecutionModule {
|
|
|
79
54
|
const plan = this.store.loadActive();
|
|
80
55
|
if (!plan)
|
|
81
56
|
return false;
|
|
82
|
-
|
|
83
|
-
// A plan that was already complete when saved must not come back as the
|
|
84
|
-
// active plan — archive it so it cannot re-anchor the agent (restored
|
|
85
|
-
// completed plans caused the "continue step 4" loop on restart).
|
|
86
|
-
if (restored.isComplete()) {
|
|
87
|
-
this.store.archivePlan(plan);
|
|
88
|
-
this.tracker = null;
|
|
89
|
-
return false;
|
|
90
|
-
}
|
|
91
|
-
this.tracker = restored;
|
|
57
|
+
this.tracker = new PlanTracker(plan);
|
|
92
58
|
// Advance to the first non-done step
|
|
93
59
|
this.tracker.syncCurrentStep();
|
|
94
60
|
this._auditSkipsRemaining = 1; // restored plan — one free pass
|
|
@@ -116,7 +82,9 @@ export class ExecutionModule {
|
|
|
116
82
|
}
|
|
117
83
|
const plan = this.tracker.getPlan();
|
|
118
84
|
const audit = await this.auditor.audit(plan);
|
|
119
|
-
const pendingSteps = plan.steps.flatMap((s) => s.status !== "done" && s.status !== "skipped"
|
|
85
|
+
const pendingSteps = plan.steps.flatMap((s) => s.status !== "done" && s.status !== "skipped"
|
|
86
|
+
? [`${s.id}. ${s.description}`]
|
|
87
|
+
: []);
|
|
120
88
|
const done = plan.steps.filter((s) => s.status === "done").length;
|
|
121
89
|
const passed = audit.passed && pendingSteps.length === 0;
|
|
122
90
|
return {
|
|
@@ -129,7 +97,7 @@ export class ExecutionModule {
|
|
|
129
97
|
};
|
|
130
98
|
}
|
|
131
99
|
getSystemPromptBlock() {
|
|
132
|
-
if (!this.tracker
|
|
100
|
+
if (!this.tracker)
|
|
133
101
|
return null;
|
|
134
102
|
return {
|
|
135
103
|
content: this.tracker.toPromptBlock(),
|
|
@@ -138,52 +106,505 @@ export class ExecutionModule {
|
|
|
138
106
|
estimatedTokens: 200,
|
|
139
107
|
};
|
|
140
108
|
}
|
|
141
|
-
/**
|
|
142
|
-
* Short "current plan" line for the compaction summary. Null when no plan
|
|
143
|
-
* is active. For 9B models the explicit instruction "continue it, do NOT
|
|
144
|
-
* create a new plan" matters — after compaction the plan block is visible
|
|
145
|
-
* but the model re-creates the plan anyway (observed: ses_msvuao0h).
|
|
146
|
-
*
|
|
147
|
-
* When the plan is complete, the summary says the plan is done and the
|
|
148
|
-
* model may create a new plan — NOT "continue step 4, do NOT create a
|
|
149
|
-
* new plan" which anchored the ses_msxjbnm4 loop.
|
|
150
|
-
*/
|
|
151
|
-
getPlanSummary() {
|
|
152
|
-
if (!this.tracker)
|
|
153
|
-
return null;
|
|
154
|
-
const plan = this.tracker.getPlan();
|
|
155
|
-
const done = plan.steps.filter((s) => s.status === "done").length;
|
|
156
|
-
const total = plan.steps.length;
|
|
157
|
-
if (this.tracker.isComplete()) {
|
|
158
|
-
return `[${plan.id}] "${plan.title}" ${done}/${total} done — plan complete. You may create a new plan for new work (plan create), or reply with your final answer.`;
|
|
159
|
-
}
|
|
160
|
-
const current = this.tracker.getCurrentStepIndex() + 1;
|
|
161
|
-
return `[${plan.id}] "${plan.title}" ${done}/${total} done, current: step ${current} — continue it (plan show / re-plan), do NOT create a new plan`;
|
|
162
|
-
}
|
|
163
109
|
getToolDefinitions() {
|
|
164
|
-
return
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
110
|
+
return [
|
|
111
|
+
{
|
|
112
|
+
name: "plan",
|
|
113
|
+
description: 'Create, update, show, abort, list, switch, or re-plan multi-step plans.\n\nActions:\n- create: Start a new plan. Previous active plan is auto-preserved: incomplete → draft, complete → archive.\n- update: Mark step status (done/failed/skipped), or rebuild plan with new steps.\n- show: Print current plan checklist.\n- abort: Archive current plan and clear active slot.\n- list: Show all plans (active, drafts, archived) with progress.\n- switch: Make a different plan active (by plan id).\n- re-plan: Iterative replanning: keep completed steps, replace remaining with new steps.\n\nWrite CONCRETE steps with exact file paths and commands:\n- Specify WHICH files to create with exact paths (e.g. "create src/components/Header.tsx with navigation and logo")\n- Specify WHICH packages to install (e.g. "run npm install react react-dom")\n- Specify WHICH CLI commands to run with exact arguments\n- Each step should include at least one file extension (.ts, .tsx, .json, etc.) or a command verb (install, create, build, run, add, init)\n- Good: "Создать src/components/Header.tsx с навигацией и логотипом"\n- Bad: "Настройка проекта" (too vague — what exactly needs to be configured?)',
|
|
114
|
+
parameters: {
|
|
115
|
+
type: "object",
|
|
116
|
+
properties: {
|
|
117
|
+
action: {
|
|
118
|
+
type: "string",
|
|
119
|
+
enum: [
|
|
120
|
+
"create",
|
|
121
|
+
"update",
|
|
122
|
+
"show",
|
|
123
|
+
"abort",
|
|
124
|
+
"list",
|
|
125
|
+
"switch",
|
|
126
|
+
"re-plan",
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
title: { type: "string" },
|
|
130
|
+
steps: { type: "array", items: { type: "string" } },
|
|
131
|
+
step: { type: "number" },
|
|
132
|
+
status: { type: "string", enum: ["done", "failed", "skipped"] },
|
|
133
|
+
note: { type: "string" },
|
|
134
|
+
id: { type: "string", description: "Plan id (for switch action)" },
|
|
135
|
+
},
|
|
136
|
+
required: ["action"],
|
|
137
|
+
},
|
|
138
|
+
handler: async (_ctx, args) => {
|
|
139
|
+
const action = String(args.action);
|
|
140
|
+
if (action === "list") {
|
|
141
|
+
const metas = this.store.listAll();
|
|
142
|
+
if (metas.length === 0) {
|
|
143
|
+
return { success: true, output: t("plan.list_empty") };
|
|
144
|
+
}
|
|
145
|
+
const lines = metas.map((m) => {
|
|
146
|
+
const icon = m.status === "active"
|
|
147
|
+
? "[*]"
|
|
148
|
+
: m.status === "draft"
|
|
149
|
+
? "[ ]"
|
|
150
|
+
: "[-]";
|
|
151
|
+
const namePart = m.name ? ` (${m.name})` : "";
|
|
152
|
+
return `${icon} ${m.id}${namePart} — ${m.title} ${m.doneCount}/${m.stepCount}`;
|
|
153
|
+
});
|
|
154
|
+
return {
|
|
155
|
+
success: true,
|
|
156
|
+
output: `${t("plan.list_header")}\n${lines.join("\n")}`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (action === "switch") {
|
|
160
|
+
const planId = String(args.id || "");
|
|
161
|
+
if (!planId) {
|
|
162
|
+
return { success: false, output: t("plan.switch_no_id") };
|
|
163
|
+
}
|
|
164
|
+
const found = this.store.find(planId);
|
|
165
|
+
if (!found) {
|
|
166
|
+
return {
|
|
167
|
+
success: false,
|
|
168
|
+
output: t("plan.not_found", { id: planId }),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
this.preserveActive();
|
|
172
|
+
if (found.status === "draft") {
|
|
173
|
+
this.store.removeDraft(found.plan.id);
|
|
174
|
+
}
|
|
175
|
+
else if (found.status === "archived") {
|
|
176
|
+
this.store.removeArchived(found.plan.id);
|
|
177
|
+
}
|
|
178
|
+
this.setPlan(found.plan);
|
|
179
|
+
const display = PlanCreator.toPromptBlock(found.plan, 0);
|
|
180
|
+
return {
|
|
181
|
+
success: true,
|
|
182
|
+
output: t("plan.switched", {
|
|
183
|
+
id: found.plan.id,
|
|
184
|
+
title: found.plan.title,
|
|
185
|
+
}),
|
|
186
|
+
display,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
if (action === "re-plan") {
|
|
190
|
+
if (!this.tracker) {
|
|
191
|
+
return { success: false, output: t("plan.no_active") };
|
|
192
|
+
}
|
|
193
|
+
const newSteps = Array.isArray(args.steps)
|
|
194
|
+
? args.steps.map(String)
|
|
195
|
+
: [];
|
|
196
|
+
if (newSteps.length === 0) {
|
|
197
|
+
return { success: false, output: t("plan.replan_no_steps") };
|
|
198
|
+
}
|
|
199
|
+
const oldPlan = this.tracker.getPlan();
|
|
200
|
+
const replanned = PlanCreator.replan(oldPlan, newSteps, args.title ? String(args.title) : undefined);
|
|
201
|
+
const keptCount = replanned.steps.length - newSteps.length;
|
|
202
|
+
this.setPlan(replanned);
|
|
203
|
+
const display = PlanCreator.toPromptBlock(replanned, keptCount);
|
|
204
|
+
return {
|
|
205
|
+
success: true,
|
|
206
|
+
output: t("plan.replanned", {
|
|
207
|
+
kept: String(keptCount),
|
|
208
|
+
steps: String(newSteps.length),
|
|
209
|
+
}),
|
|
210
|
+
display,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
if (action === "create") {
|
|
214
|
+
const title = String(args.title || "Task Plan");
|
|
215
|
+
const steps = Array.isArray(args.steps)
|
|
216
|
+
? args.steps.map(String)
|
|
217
|
+
: [];
|
|
218
|
+
if (steps.length === 0) {
|
|
219
|
+
return { success: false, output: t("plan.no_steps") };
|
|
220
|
+
}
|
|
221
|
+
this.preserveActive();
|
|
222
|
+
const plan = PlanCreator.createPlan(title, steps, this.baseDir);
|
|
223
|
+
this.setPlan(plan);
|
|
224
|
+
const display = PlanCreator.toPromptBlock(plan, 0);
|
|
225
|
+
let output = t("plan.created", {
|
|
226
|
+
title,
|
|
227
|
+
steps: String(steps.length),
|
|
228
|
+
});
|
|
229
|
+
let displayOut = display;
|
|
230
|
+
const taskText = this.getTaskText(_ctx);
|
|
231
|
+
if (taskText) {
|
|
232
|
+
const { missing } = checkPlanCoverage(taskText, steps);
|
|
233
|
+
if (missing.length > 0) {
|
|
234
|
+
const warn = t("plan.coverage_warning", {
|
|
235
|
+
missing: missing.join(", "),
|
|
236
|
+
});
|
|
237
|
+
output += `\n⚠️ ${warn}`;
|
|
238
|
+
displayOut = `${display}\n⚠️ ${warn}`;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
success: true,
|
|
243
|
+
output,
|
|
244
|
+
display: displayOut,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
if (action === "show") {
|
|
248
|
+
if (!this.tracker) {
|
|
249
|
+
return { success: false, output: t("plan.no_active") };
|
|
250
|
+
}
|
|
251
|
+
const display = this.tracker.toPromptBlock();
|
|
252
|
+
return {
|
|
253
|
+
success: true,
|
|
254
|
+
output: `${t("plan.show_header")}\n${display}`,
|
|
255
|
+
display,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
if (action === "update" && args.step && this.tracker) {
|
|
259
|
+
this.tracker.updateStepStatus(Number(args.step), args.status || "done");
|
|
260
|
+
if (args.note)
|
|
261
|
+
this.tracker.addNote(Number(args.step), String(args.note));
|
|
262
|
+
this.tracker.syncCurrentStep();
|
|
263
|
+
this.store.saveActive(this.tracker.getPlan());
|
|
264
|
+
const progress = this.tracker.getProgressString();
|
|
265
|
+
const display = this.tracker.toPromptBlock();
|
|
266
|
+
_ctx.sessionLog?.plan("step-update", `${t("plan.step_status", { step: String(args.step), status: String(args.status || "done") })} | ${progress} | current: step ${this.tracker.getCurrentStepIndex() + 1}`);
|
|
267
|
+
return {
|
|
268
|
+
success: true,
|
|
269
|
+
output: `${t("plan.step_status", { step: String(args.step), status: String(args.status || "done") })}\n${progress}`,
|
|
270
|
+
display,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
if (action === "update" &&
|
|
274
|
+
Array.isArray(args.steps) &&
|
|
275
|
+
args.steps.length > 0 &&
|
|
276
|
+
this.tracker) {
|
|
277
|
+
const title = String(args.title || this.tracker.getPlan().title);
|
|
278
|
+
const steps = args.steps.map(String);
|
|
279
|
+
const plan = PlanCreator.createPlan(title, steps, this.baseDir);
|
|
280
|
+
this.setPlan(plan);
|
|
281
|
+
const display = PlanCreator.toPromptBlock(plan, 0);
|
|
282
|
+
const output = t("plan.updated", {
|
|
283
|
+
title,
|
|
284
|
+
steps: String(steps.length),
|
|
285
|
+
});
|
|
286
|
+
return {
|
|
287
|
+
success: true,
|
|
288
|
+
output,
|
|
289
|
+
display,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
if (action === "abort") {
|
|
293
|
+
if (this.tracker) {
|
|
294
|
+
this.store.archivePlan(this.tracker.getPlan());
|
|
295
|
+
}
|
|
296
|
+
this.tracker = null;
|
|
297
|
+
this.store.clearActive();
|
|
298
|
+
return { success: true, output: t("plan.aborted") };
|
|
299
|
+
}
|
|
300
|
+
if (!this.tracker) {
|
|
301
|
+
return { success: false, output: t("plan.no_active") };
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
success: false,
|
|
305
|
+
output: t("plan.unknown_action", { action }),
|
|
306
|
+
};
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
name: "todo",
|
|
311
|
+
description: "Manage sub-tasks within current plan step. Use to break down complex steps into smaller tasks.",
|
|
312
|
+
parameters: {
|
|
313
|
+
type: "object",
|
|
314
|
+
properties: {
|
|
315
|
+
action: { type: "string", enum: ["add", "done", "list"] },
|
|
316
|
+
items: { type: "array", items: { type: "string" } },
|
|
317
|
+
},
|
|
318
|
+
required: ["action"],
|
|
319
|
+
},
|
|
320
|
+
handler: async (_ctx, args) => {
|
|
321
|
+
if (!this.tracker) {
|
|
322
|
+
return { success: false, output: t("plan.no_active") };
|
|
323
|
+
}
|
|
324
|
+
const action = String(args.action);
|
|
325
|
+
const currentStep = this.tracker.getCurrentStep();
|
|
326
|
+
if (!currentStep) {
|
|
327
|
+
return { success: false, output: t("plan.step_not_found") };
|
|
328
|
+
}
|
|
329
|
+
if (action === "add" && Array.isArray(args.items)) {
|
|
330
|
+
const items = args.items.map(String);
|
|
331
|
+
currentStep.note = currentStep.note
|
|
332
|
+
? `${currentStep.note}; ${items.join(", ")}`
|
|
333
|
+
: items.join(", ");
|
|
334
|
+
return {
|
|
335
|
+
success: true,
|
|
336
|
+
output: t("todo.added", {
|
|
337
|
+
count: String(items.length),
|
|
338
|
+
items: items.join(", "),
|
|
339
|
+
}),
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
if (action === "done") {
|
|
343
|
+
if (!currentStep) {
|
|
344
|
+
return { success: false, output: t("plan.step_not_found") };
|
|
345
|
+
}
|
|
346
|
+
this.tracker.updateStepStatus(currentStep.id, "done");
|
|
347
|
+
this.tracker.syncCurrentStep();
|
|
348
|
+
this.store.saveActive(this.tracker.getPlan());
|
|
349
|
+
const display = this.tracker.toPromptBlock();
|
|
350
|
+
return {
|
|
351
|
+
success: true,
|
|
352
|
+
output: t("plan.step_status", {
|
|
353
|
+
step: String(currentStep.id),
|
|
354
|
+
status: "done",
|
|
355
|
+
}),
|
|
356
|
+
display,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
if (action === "list") {
|
|
360
|
+
const note = currentStep.note || "(no sub-tasks)";
|
|
361
|
+
return {
|
|
362
|
+
success: true,
|
|
363
|
+
output: `Step ${currentStep.id}: ${currentStep.description}\nSub-tasks: ${note}`,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
success: false,
|
|
368
|
+
output: t("todo.unknown_action", { action }),
|
|
369
|
+
};
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
name: "verify",
|
|
374
|
+
description: "Run verification for the current step. Checks files mentioned in the step description.",
|
|
375
|
+
parameters: {
|
|
376
|
+
type: "object",
|
|
377
|
+
properties: {
|
|
378
|
+
step: { type: "number", description: "Step number to verify" },
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
handler: async (_ctx, args) => {
|
|
382
|
+
if (!this.tracker)
|
|
383
|
+
return { success: false, output: t("plan.no_active") };
|
|
384
|
+
const step = args.step
|
|
385
|
+
? this.tracker.getStep(Number(args.step))
|
|
386
|
+
: this.tracker.getCurrentStep();
|
|
387
|
+
if (!step)
|
|
388
|
+
return { success: false, output: t("plan.step_not_found") };
|
|
389
|
+
const result = await this.verifier.verifyStep(step.description);
|
|
390
|
+
return {
|
|
391
|
+
success: result.passed,
|
|
392
|
+
output: result.passed
|
|
393
|
+
? t("verify.passed")
|
|
394
|
+
: t("verify.failed", {
|
|
395
|
+
details: result.failed.map((f) => f.message).join("; "),
|
|
396
|
+
}),
|
|
397
|
+
};
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
];
|
|
176
401
|
}
|
|
177
402
|
getPlugin() {
|
|
178
|
-
return
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
403
|
+
return {
|
|
404
|
+
name: "execution",
|
|
405
|
+
onBeforeThink: (ctx) => {
|
|
406
|
+
// Flush deferred <system-summary> messages first so they are visible
|
|
407
|
+
// to the next model call but never interleave tool messages.
|
|
408
|
+
if (ctx.contextManager && this.pendingMessages.length > 0) {
|
|
409
|
+
for (const m of this.pendingMessages.splice(0)) {
|
|
410
|
+
ctx.contextManager.addMessage(m);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (this.tracker?.isComplete()) {
|
|
414
|
+
// All steps done/skipped — stop counting "no progress" against the
|
|
415
|
+
// last step and silence stuck warnings for a finished plan.
|
|
416
|
+
this.stuckDetector.reset();
|
|
417
|
+
this.consecutivePlanWarnings = 0;
|
|
418
|
+
this.lastStepId = -1;
|
|
419
|
+
this.stuckNotified = false;
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
const step = this.tracker?.getCurrentStep();
|
|
423
|
+
if (this.tracker && step) {
|
|
424
|
+
if (step.id !== this.lastStepId) {
|
|
425
|
+
this.consecutivePlanWarnings = 0;
|
|
426
|
+
this.lastStepId = step.id;
|
|
427
|
+
this.stuckNotified = false;
|
|
428
|
+
}
|
|
429
|
+
this.stuckDetector.setCurrentStep(step.id, step.description);
|
|
430
|
+
this.stuckDetector.recordIteration(step.id);
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
this.stuckDetector.reset();
|
|
434
|
+
this.consecutivePlanWarnings = 0;
|
|
435
|
+
this.lastStepId = -1;
|
|
436
|
+
this.stuckNotified = false;
|
|
437
|
+
const iter = typeof ctx.iteration === "number" ? ctx.iteration : 0;
|
|
438
|
+
if (iter === 3 && !this.tracker && ctx.contextManager) {
|
|
439
|
+
ctx.contextManager.addMessage({
|
|
440
|
+
role: "user",
|
|
441
|
+
content: `<system-summary>You have made 3 tool calls without creating a plan. For any task that involves creating files, installing packages, or multiple steps — you MUST use plan create BEFORE continuing. Use the plan tool now with concrete steps (exact filenames, commands, deliverables). Do NOT make any more write/edit/bash calls until you have a plan.</system-summary>`,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
if (iter >= 6 && !this.tracker && ctx.contextManager) {
|
|
445
|
+
ctx.contextManager.addMessage({
|
|
446
|
+
role: "user",
|
|
447
|
+
content: `<system-summary>STOP. ${iter} iterations without a plan. You MUST call plan create RIGHT NOW. No more tool calls until you create a plan.</system-summary>`,
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
const stuckReason = this.stuckDetector.getStuckReason();
|
|
453
|
+
if (stuckReason) {
|
|
454
|
+
// Log once per stuck episode instead of spamming every iteration.
|
|
455
|
+
const logIt = this.stuckDetector.isStuck()
|
|
456
|
+
? !this.stuckNotified
|
|
457
|
+
: true;
|
|
458
|
+
if (logIt) {
|
|
459
|
+
ctx.logger?.warn(stuckReason);
|
|
460
|
+
ctx.sessionLog?.plan("stuck-warning", stuckReason, typeof ctx.iteration === "number" ? ctx.iteration : undefined);
|
|
461
|
+
if (this.stuckDetector.isStuck())
|
|
462
|
+
this.stuckNotified = true;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (this.stuckDetector.isStuck() ||
|
|
466
|
+
this.stuckDetector.hasRepetitiveToolCalls()) {
|
|
467
|
+
const currentIter = typeof ctx.iteration === "number" ? ctx.iteration : 0;
|
|
468
|
+
if (currentIter - this.lastRecoveryIteration >=
|
|
469
|
+
STUCK_RECOVERY_COOLDOWN) {
|
|
470
|
+
const recovery = this.stuckDetector.getRecoveryMessage();
|
|
471
|
+
if (recovery && ctx.contextManager) {
|
|
472
|
+
const lastError = this.stuckDetector.getLastErrorOutput();
|
|
473
|
+
const skillHint = lastError
|
|
474
|
+
? "\nIf you have relevant skills available, consider loading one with load_skill for expert guidance."
|
|
475
|
+
: "";
|
|
476
|
+
// Actionable hints based on actual error output
|
|
477
|
+
const actionableHints = this.stuckDetector.getActionableHints();
|
|
478
|
+
const actionableHintStr = actionableHints.length > 0
|
|
479
|
+
? `\n${t("exec.hints", { hints: actionableHints.map((h) => `- ${h}`).join("\n") })}`
|
|
480
|
+
: "";
|
|
481
|
+
// Tool alternative suggestion
|
|
482
|
+
const alternative = this.stuckDetector.getToolAlternative();
|
|
483
|
+
const altHint = alternative
|
|
484
|
+
? `\nTool "${this.stuckDetector.getLastFailedTool()}" is failing. Try "${alternative}" instead.`
|
|
485
|
+
: "";
|
|
486
|
+
ctx.contextManager.addMessage({
|
|
487
|
+
role: "user",
|
|
488
|
+
content: `<system-summary>${recovery}${skillHint}${actionableHintStr}${altHint}</system-summary>`,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
const hints = this.stuckDetector.getHints();
|
|
492
|
+
if (hints.length > 0 && ctx.contextManager) {
|
|
493
|
+
const hintMsg = t("exec.hints", {
|
|
494
|
+
hints: hints.map((h) => `- ${h}`).join("\n"),
|
|
495
|
+
});
|
|
496
|
+
ctx.contextManager.addMessage({
|
|
497
|
+
role: "user",
|
|
498
|
+
content: `<system-summary>${hintMsg}</system-summary>`,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
this.stuckDetector.recordEscalation();
|
|
502
|
+
this.lastRecoveryIteration = currentIter;
|
|
503
|
+
if (this.stuckDetector.shouldEscalate() && ctx.onMeta) {
|
|
504
|
+
const escalation = t("exec.escalation", {
|
|
505
|
+
stepId: String(this.tracker?.getCurrentStep()?.id ?? "?"),
|
|
506
|
+
description: this.tracker?.getCurrentStep()?.description ?? "",
|
|
507
|
+
});
|
|
508
|
+
ctx.onMeta(escalation);
|
|
509
|
+
}
|
|
510
|
+
// Force skip after too many iterations on the same step
|
|
511
|
+
if (this.stuckDetector.getIterationsOnCurrentStep() >=
|
|
512
|
+
FORCE_SKIP_THRESHOLD &&
|
|
513
|
+
ctx.contextManager) {
|
|
514
|
+
const step = this.tracker?.getCurrentStep();
|
|
515
|
+
ctx.contextManager.addMessage({
|
|
516
|
+
role: "user",
|
|
517
|
+
content: `<system-summary>STOP. Step ${step?.id ?? "?"} ("${step?.description ?? ""}") took ${this.stuckDetector.getIterationsOnCurrentStep()} iterations with no progress. DO NOT continue this step. Immediately call: plan update step=${step?.id ?? "?"} status=done (if code works despite warnings) OR plan update step=${step?.id ?? "?"} status=skipped note="reason". Do NOT make any other tool calls before updating the plan.</system-summary>`,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
},
|
|
523
|
+
onBeforeTool: (_ctx, call) => {
|
|
524
|
+
const warning = this.checkPlanAlignment(call);
|
|
525
|
+
if (warning) {
|
|
526
|
+
this.pendingMessages.push({
|
|
527
|
+
role: "user",
|
|
528
|
+
content: `<system-summary>${warning}</system-summary>`,
|
|
529
|
+
});
|
|
530
|
+
this.consecutivePlanWarnings++;
|
|
531
|
+
if (this.consecutivePlanWarnings >= MAX_PLAN_WARNINGS_BEFORE_BLOCK) {
|
|
532
|
+
this.pendingMessages.push({
|
|
533
|
+
role: "user",
|
|
534
|
+
content: `<system-summary>${t("exec.plan_blocked", { step: String(this.tracker?.getCurrentStep()?.id ?? "?"), max: MAX_PLAN_WARNINGS_BEFORE_BLOCK })}</system-summary>`,
|
|
535
|
+
});
|
|
536
|
+
// Block with an explicit reason so the model sees WHY the call
|
|
537
|
+
// was blocked (executor shows it via tool.blocked_reason).
|
|
538
|
+
return t("exec.plan_blocked", {
|
|
539
|
+
step: String(this.tracker?.getCurrentStep()?.id ?? "?"),
|
|
540
|
+
max: MAX_PLAN_WARNINGS_BEFORE_BLOCK,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
this.consecutivePlanWarnings = 0;
|
|
546
|
+
}
|
|
547
|
+
return true;
|
|
548
|
+
},
|
|
549
|
+
onToolCall: (ctx) => {
|
|
550
|
+
const toolName = ctx?.toolName;
|
|
551
|
+
const args = ctx?.args;
|
|
552
|
+
if (toolName && args) {
|
|
553
|
+
this.stuckDetector.recordToolCall(toolName, args);
|
|
554
|
+
}
|
|
555
|
+
},
|
|
556
|
+
onAfterTool: (ctx, call, result) => {
|
|
557
|
+
if (!result.success) {
|
|
558
|
+
this.stuckDetector.recordToolError(call.name, result.output);
|
|
559
|
+
// Immediately queue an actionable hint on failure (don't wait for
|
|
560
|
+
// the stuck threshold). Flushed in onBeforeThink so it does not
|
|
561
|
+
// land between tool messages.
|
|
562
|
+
const actionableHints = this.stuckDetector.getActionableHints();
|
|
563
|
+
const alternative = this.stuckDetector.getToolAlternative();
|
|
564
|
+
if (actionableHints.length > 0 || alternative) {
|
|
565
|
+
const parts = [...actionableHints];
|
|
566
|
+
if (alternative) {
|
|
567
|
+
parts.push(`Tool "${call.name}" crashed. Try "${alternative}" instead.`);
|
|
568
|
+
}
|
|
569
|
+
this.pendingMessages.push({
|
|
570
|
+
role: "user",
|
|
571
|
+
content: `<system-summary>${t("exec.hints", { hints: parts.map((h) => `- ${h}`).join("\n") })}</system-summary>`,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
this.stuckDetector.recordToolSuccess();
|
|
577
|
+
// When a bash command runs code successfully, suggest marking step as done
|
|
578
|
+
if (call.name === "bash" && result.success) {
|
|
579
|
+
const cmd = String(call.arguments?.command ?? "");
|
|
580
|
+
if (/node|tsx|ts-node|python|npm\s+(start|test|run)/.test(cmd)) {
|
|
581
|
+
this.pendingMessages.push({
|
|
582
|
+
role: "user",
|
|
583
|
+
content: `<system-summary>The command "${cmd}" completed successfully. If this was testing your code, mark the current step as done via plan update step=N status=done.</system-summary>`,
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if (result.success &&
|
|
589
|
+
(call.name === "write_file" || call.name === "edit_file")) {
|
|
590
|
+
const filePath = call.arguments?.path;
|
|
591
|
+
if (filePath) {
|
|
592
|
+
this.stuckDetector.recordFileRewrite(filePath);
|
|
593
|
+
if (this.stuckDetector.hasExcessiveRewrites()) {
|
|
594
|
+
const file = this.stuckDetector.getExcessiveRewriteFile();
|
|
595
|
+
const count = this.stuckDetector.getFileRewriteCount(file);
|
|
596
|
+
if (ctx.onMeta) {
|
|
597
|
+
ctx.onMeta(t("exec.file_rewrite_warning", {
|
|
598
|
+
file: file,
|
|
599
|
+
count: String(count),
|
|
600
|
+
}));
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
this.advancePlanIfStepComplete(ctx.contextManager, ctx.sessionLog);
|
|
605
|
+
}
|
|
606
|
+
},
|
|
607
|
+
};
|
|
187
608
|
}
|
|
188
609
|
preserveActive() {
|
|
189
610
|
if (!this.tracker)
|
|
@@ -215,11 +636,11 @@ export class ExecutionModule {
|
|
|
215
636
|
];
|
|
216
637
|
if (allowedAlways.includes(call.name))
|
|
217
638
|
return null;
|
|
218
|
-
const stepPaths =
|
|
639
|
+
const stepPaths = (step.description.match(/\b[\w./\\-]+\.[a-z]+/gi) || []).map((p) => p.toLowerCase());
|
|
219
640
|
if (stepPaths.length === 0)
|
|
220
641
|
return null;
|
|
221
|
-
const argStr = JSON.stringify(call.arguments);
|
|
222
|
-
const callPaths =
|
|
642
|
+
const argStr = JSON.stringify(call.arguments).toLowerCase();
|
|
643
|
+
const callPaths = (argStr.match(/\b[\w./\\-]+\.[a-z]+/gi) || []).map((p) => p.toLowerCase());
|
|
223
644
|
if (callPaths.length === 0)
|
|
224
645
|
return null;
|
|
225
646
|
// Files that already belong to COMPLETED (done/skipped) steps are
|
|
@@ -229,13 +650,15 @@ export class ExecutionModule {
|
|
|
229
650
|
const finishedPaths = new Set();
|
|
230
651
|
for (const s of plan.steps) {
|
|
231
652
|
if (s.status === "done" || s.status === "skipped") {
|
|
232
|
-
|
|
233
|
-
.
|
|
234
|
-
|
|
653
|
+
const ps = s.description
|
|
654
|
+
.match(/\b[\w./\\-]+\.[a-z]+/gi)
|
|
655
|
+
?.map((p) => p.toLowerCase()) ?? [];
|
|
656
|
+
ps.forEach((p) => finishedPaths.add(p));
|
|
235
657
|
}
|
|
236
658
|
}
|
|
237
659
|
const offPath = callPaths.some((p) => {
|
|
238
|
-
if (finishedPaths.has(p) ||
|
|
660
|
+
if (finishedPaths.has(p) ||
|
|
661
|
+
[...finishedPaths].some((s) => s.includes(p))) {
|
|
239
662
|
return false;
|
|
240
663
|
}
|
|
241
664
|
return !stepPaths.some((s) => p.includes(s) || s.includes(p));
|
|
@@ -253,12 +676,7 @@ export class ExecutionModule {
|
|
|
253
676
|
if (!step)
|
|
254
677
|
return;
|
|
255
678
|
const stepText = step.description.toLowerCase();
|
|
256
|
-
|
|
257
|
-
// segments rejected). A step like "Удалить .js, оставить .tsx/.ts" must
|
|
258
|
-
// NOT produce the phantom token "tsx/.ts" — that never exists, making
|
|
259
|
-
// allGone trivially true and auto-advancing a delete step while the real
|
|
260
|
-
// files to delete still exist (the raw regex path did exactly that).
|
|
261
|
-
const stepPaths = extractFileLikeTokens(stripUrls(step.description)) || [];
|
|
679
|
+
const stepPaths = step.description.match(/\b[\w./\\-]+\.[a-z]+/gi) || [];
|
|
262
680
|
// Step gate: verify dependencies when mentioned — check BEFORE file paths
|
|
263
681
|
const isDepsStep = stepText.includes("install") ||
|
|
264
682
|
stepText.includes("зависим") ||
|
|
@@ -280,8 +698,8 @@ export class ExecutionModule {
|
|
|
280
698
|
const hasLockFile = lockFiles.some((f) => existsSync(resolve(this.baseDir, f)));
|
|
281
699
|
if (!hasLockFile) {
|
|
282
700
|
if (contextManager) {
|
|
283
|
-
const hints = this.
|
|
284
|
-
this.
|
|
701
|
+
const hints = this.depsGateHints.get(step.id) || 0;
|
|
702
|
+
this.depsGateHints.set(step.id, hints + 1);
|
|
285
703
|
const force = hints >= 1;
|
|
286
704
|
const content = force
|
|
287
705
|
? t("exec.step_gate_deps_force", { step: String(step.id) })
|
|
@@ -300,32 +718,28 @@ export class ExecutionModule {
|
|
|
300
718
|
if (stepPaths.length === 0)
|
|
301
719
|
return;
|
|
302
720
|
const allExist = stepPaths.every((p) => existsSync(resolve(this.baseDir, p)));
|
|
303
|
-
|
|
304
|
-
const satisfied = step.kind === "delete" ? allGone && !allExist : allExist;
|
|
305
|
-
if (!satisfied)
|
|
721
|
+
if (!allExist)
|
|
306
722
|
return;
|
|
307
723
|
// Step gate: verify files have content (not empty)
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
emptyFiles.push(p);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
catch {
|
|
318
|
-
// If we can't read it, rely on existsSync result
|
|
724
|
+
const emptyFiles = [];
|
|
725
|
+
for (const p of stepPaths) {
|
|
726
|
+
try {
|
|
727
|
+
const content = readFileSync(resolve(this.baseDir, p), "utf-8");
|
|
728
|
+
if (content.trim().length < 10) {
|
|
729
|
+
emptyFiles.push(p);
|
|
319
730
|
}
|
|
320
731
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
role: "user",
|
|
324
|
-
content: `<system-summary>${t("exec.step_gate_empty", { step: String(step.id), files: emptyFiles.join(", ") })}</system-summary>`,
|
|
325
|
-
});
|
|
326
|
-
return;
|
|
732
|
+
catch {
|
|
733
|
+
// If we can't read it, rely on existsSync result
|
|
327
734
|
}
|
|
328
735
|
}
|
|
736
|
+
if (emptyFiles.length > 0 && contextManager) {
|
|
737
|
+
contextManager.addMessage({
|
|
738
|
+
role: "user",
|
|
739
|
+
content: `<system-summary>${t("exec.step_gate_empty", { step: String(step.id), files: emptyFiles.join(", ") })}</system-summary>`,
|
|
740
|
+
});
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
329
743
|
// All checks passed — advance
|
|
330
744
|
this.tracker?.updateStepStatus(step.id, "done");
|
|
331
745
|
const hadNext = this.tracker?.advance() ?? false;
|
|
@@ -347,68 +761,6 @@ export class ExecutionModule {
|
|
|
347
761
|
content: `<system-summary>${nextMsg}</system-summary>`,
|
|
348
762
|
});
|
|
349
763
|
}
|
|
350
|
-
// Auto-archive a plan that just became complete (all steps done/skipped).
|
|
351
|
-
// Keeps a finished plan from staying active forever and anchoring the agent
|
|
352
|
-
// to a completed task ("continue step 4, do NOT create a new plan" — the
|
|
353
|
-
// ses_msxjbnm4 loop). The per-step deliverable gate already verified files.
|
|
354
|
-
if (this.tracker?.isComplete()) {
|
|
355
|
-
const completed = this.tracker.getPlan();
|
|
356
|
-
this.store.archivePlan(completed);
|
|
357
|
-
this.tracker = null;
|
|
358
|
-
sessionLog?.plan("auto-archive", `Plan ${completed.id} complete — archived`);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
/**
|
|
362
|
-
* File tokens named in a step description that do not resolve to a real
|
|
363
|
-
* file. Uses the same shared token extractor (allow-list of real file
|
|
364
|
-
* extensions, URLs stripped) and the auditor's subtree resolver, so
|
|
365
|
-
* "bicycles.ts" finds "bicycle-shop/src/data/bicycles.ts" and a hostname in
|
|
366
|
-
* the description is never reported as a missing deliverable. Steps with no
|
|
367
|
-
* file tokens (pure commands, verification runs) cannot be verified.
|
|
368
|
-
*/
|
|
369
|
-
missingStepDeliverables(step) {
|
|
370
|
-
const tokens = extractFileLikeTokens(stripUrls(step.description));
|
|
371
|
-
if (tokens.length === 0)
|
|
372
|
-
return [];
|
|
373
|
-
return tokens.filter((p) => !findExistingFile(this.baseDir, p));
|
|
374
|
-
}
|
|
375
|
-
/**
|
|
376
|
-
* File tokens named in a step description that still resolve to a real
|
|
377
|
-
* file. The inverse of missingStepDeliverables — used by the gate for
|
|
378
|
-
* delete-kind steps ("done" means the files are gone).
|
|
379
|
-
*/
|
|
380
|
-
stillExistingDeliverables(step) {
|
|
381
|
-
const tokens = extractFileLikeTokens(stripUrls(step.description));
|
|
382
|
-
if (tokens.length === 0)
|
|
383
|
-
return [];
|
|
384
|
-
return tokens.filter((p) => findExistingFile(this.baseDir, p));
|
|
385
|
-
}
|
|
386
|
-
/**
|
|
387
|
-
* Validate the optional per-step `kinds` argument for plan create and
|
|
388
|
-
* update-with-steps. Returns length-aligned kinds or an i18n error string.
|
|
389
|
-
* Absent kinds → null (caller falls back to all-create semantics). Used by
|
|
390
|
-
* BOTH branches so a rebuild via update can never silently drop kinds.
|
|
391
|
-
*/
|
|
392
|
-
parseKinds(args, steps) {
|
|
393
|
-
if (args.kinds === undefined)
|
|
394
|
-
return { kinds: null, error: null };
|
|
395
|
-
if (!Array.isArray(args.kinds)) {
|
|
396
|
-
return { kinds: null, error: t("plan.kinds_not_array") };
|
|
397
|
-
}
|
|
398
|
-
const kindsRaw = args.kinds.map(String);
|
|
399
|
-
if (kindsRaw.length !== steps.length) {
|
|
400
|
-
return { kinds: null, error: t("plan.kinds_mismatch") };
|
|
401
|
-
}
|
|
402
|
-
const invalid = kindsRaw.filter((k) => k !== "create" && k !== "delete");
|
|
403
|
-
if (invalid.length > 0) {
|
|
404
|
-
return {
|
|
405
|
-
kinds: null,
|
|
406
|
-
error: t("plan.kinds_invalid", {
|
|
407
|
-
kinds: invalid.join(", "),
|
|
408
|
-
}),
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
|
-
return { kinds: kindsRaw, error: null };
|
|
412
764
|
}
|
|
413
765
|
/**
|
|
414
766
|
* Extract the original task statement from the first user message in the
|
|
@@ -419,16 +771,7 @@ export class ExecutionModule {
|
|
|
419
771
|
if (!manager)
|
|
420
772
|
return "";
|
|
421
773
|
const history = manager.getActiveHistory();
|
|
422
|
-
|
|
423
|
-
// block) — it is NOT the task. Coverage must compare the plan against the
|
|
424
|
-
// model's real instruction (observed: a compaction summary's stale file
|
|
425
|
-
// list produced a bogus "plan is missing files" warning).
|
|
426
|
-
const firstUser = history.find((m) => {
|
|
427
|
-
if (m.role !== "user")
|
|
428
|
-
return false;
|
|
429
|
-
const text = getMessageText(m.content);
|
|
430
|
-
return !text.startsWith("<system-summary>") && !text.includes("[Compressed:");
|
|
431
|
-
});
|
|
774
|
+
const firstUser = history.find((m) => m.role === "user");
|
|
432
775
|
if (!firstUser)
|
|
433
776
|
return "";
|
|
434
777
|
return getMessageText(firstUser.content).trim();
|