micro-models-agent 0.41.0 → 0.41.2
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 +9 -19
- package/dist/cli/completer.js +36 -37
- package/dist/cli/index.js +2 -2
- package/dist/cli/main.js +48 -23
- package/dist/cli/plugin-commands.js +36 -0
- package/dist/cli/repl-commands.js +40 -12
- package/dist/cli/repl.js +217 -87
- package/dist/cli/run-result.js +22 -0
- package/dist/cli/security-commands.js +5 -7
- package/dist/cli/setup.js +8 -26
- package/dist/config/config.js +52 -5
- package/dist/config/defaults.js +29 -5
- package/dist/config/experts.js +1 -1
- package/dist/config/index.js +3 -3
- package/dist/config/security.js +3 -10
- package/dist/core/agent-moe.js +2 -10
- package/dist/core/agent.js +273 -82
- package/dist/core/bootstrap.js +80 -13
- package/dist/core/index.js +2 -2
- package/dist/core/prompt-builder.js +23 -2
- package/dist/core/session-logger.js +46 -4
- package/dist/core/version.js +24 -0
- package/dist/i18n/en.json +75 -2
- package/dist/i18n/ru.json +74 -1
- package/dist/index.js +1 -1
- package/dist/llm/image-utils.js +4 -5
- package/dist/llm/index.js +4 -4
- package/dist/llm/model-loader.js +6 -6
- package/dist/llm/openai-compat.js +40 -34
- package/dist/llm/orchestrator.js +33 -29
- 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 +1111 -954
- package/dist/migration/backup.js +13 -13
- package/dist/migration/detect.js +11 -11
- package/dist/migration/index.js +2 -2
- package/dist/modules/artifacts/store.js +61 -0
- package/dist/modules/browser/actions.js +34 -4
- package/dist/modules/browser/bridge-client.js +199 -0
- package/dist/modules/browser/bridge-path.js +10 -0
- package/dist/modules/browser/bridge-server.mjs +202 -202
- package/dist/modules/browser/cookie-store.js +6 -6
- package/dist/modules/browser/driver.js +136 -0
- package/dist/modules/browser/index.js +7 -5
- package/dist/modules/browser/module.js +8 -7
- package/dist/modules/browser/session.js +87 -84
- package/dist/modules/browser/snapshot.js +92 -58
- package/dist/modules/browser/types.js +4 -1
- package/dist/modules/certification/cli.js +2 -4
- package/dist/modules/certification/fact-checker.js +1 -3
- package/dist/modules/certification/loader.js +3 -9
- package/dist/modules/certification/runner.js +1 -4
- package/dist/modules/context/chunk-query.js +100 -0
- package/dist/modules/context/fact-extractor.js +162 -0
- package/dist/modules/context/history.js +15 -0
- package/dist/modules/context/index.js +1 -1
- package/dist/modules/context/manager.js +160 -86
- package/dist/modules/execution/audit-runners.js +152 -0
- package/dist/modules/execution/auditor.js +177 -25
- package/dist/modules/execution/execution-plugin.js +272 -0
- package/dist/modules/execution/module.js +201 -544
- package/dist/modules/execution/moe-executor.js +25 -0
- package/dist/modules/execution/plan-store.js +1 -3
- package/dist/modules/execution/plan-tool.js +508 -0
- package/dist/modules/execution/plan-validator.js +10 -10
- package/dist/modules/execution/planner.js +6 -1
- package/dist/modules/execution/stuck-detector.js +173 -10
- package/dist/modules/execution/verifier.js +86 -42
- package/dist/modules/execution/windows-commands.js +41 -0
- package/dist/modules/hallucination/confidence.js +8 -1
- package/dist/modules/hallucination/detector.js +2 -5
- package/dist/modules/hallucination/factual.js +3 -64
- package/dist/modules/hallucination/index.js +1 -1
- package/dist/modules/hallucination/js-identifiers.js +190 -0
- package/dist/modules/hallucination/llm-judge.js +1 -3
- package/dist/modules/indexer/cache.js +9 -7
- package/dist/modules/indexer/index.js +3 -3
- package/dist/modules/indexer/module.js +95 -42
- package/dist/modules/indexer/project-profile.js +183 -0
- package/dist/modules/indexer/walker.js +17 -17
- package/dist/modules/lsp/check-tool.js +58 -0
- package/dist/modules/lsp/client.js +74 -31
- package/dist/modules/lsp/command.js +60 -0
- package/dist/modules/lsp/config.js +87 -33
- package/dist/modules/lsp/index.js +3 -3
- package/dist/modules/lsp/module.js +185 -21
- package/dist/modules/lsp/probe.js +76 -0
- package/dist/modules/lsp/project-root.js +32 -0
- package/dist/modules/lsp/startup-check.js +141 -0
- package/dist/modules/mcp/module.js +2 -6
- package/dist/modules/memory/index.js +1 -1
- package/dist/modules/memory/module.js +71 -23
- package/dist/modules/memory/search.js +11 -9
- 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 +17 -14
- package/dist/modules/pipelines/template.js +1 -1
- package/dist/modules/plugins/builtin/lint-on-write.js +21 -16
- package/dist/modules/plugins/builtin/notify.js +3 -2
- package/dist/modules/plugins/index.js +1 -1
- package/dist/modules/plugins/loader.js +59 -17
- package/dist/modules/plugins/manager.js +73 -17
- package/dist/modules/processes/detect.js +34 -0
- package/dist/modules/processes/index.js +1 -1
- package/dist/modules/processes/registry.js +135 -46
- package/dist/modules/registry.js +4 -2
- package/dist/modules/security/audit-notifier.js +39 -39
- package/dist/modules/security/command-validator.js +2 -8
- package/dist/modules/security/data-sanitizer.js +1 -9
- package/dist/modules/security/encryption.js +58 -56
- package/dist/modules/security/network-validator.js +1 -9
- package/dist/modules/security/path-validator.js +1 -3
- package/dist/modules/security/security-policies.js +3 -19
- 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 +3 -9
- package/dist/modules/skills/matcher.js +27 -0
- package/dist/modules/skills/module.js +1 -2
- package/dist/modules/updater/checker.js +70 -6
- package/dist/modules/updater/index.js +2 -1
- package/dist/modules/updater/module.js +116 -0
- 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 +178 -19
- package/dist/tools/browser.js +46 -29
- package/dist/tools/chunk-query.js +99 -0
- package/dist/tools/download-file.js +116 -0
- package/dist/tools/enable-tools.js +58 -0
- package/dist/tools/executor.js +4 -5
- package/dist/tools/file-info.js +13 -12
- package/dist/tools/filter-tools.js +9 -2
- package/dist/tools/glob-tool.js +11 -11
- package/dist/tools/grep-tool.js +1 -3
- package/dist/tools/hidden-tools-block.js +37 -0
- package/dist/tools/index.js +13 -2
- package/dist/tools/list-dir.js +18 -17
- package/dist/tools/load-skill.js +1 -3
- 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 +20 -22
- package/dist/tools/process-log.js +22 -18
- package/dist/tools/question.js +1 -3
- package/dist/tools/read-file.js +10 -2
- package/dist/tools/recall.js +44 -37
- package/dist/tools/registry.js +15 -4
- package/dist/tools/remember.js +29 -29
- package/dist/tools/scope-check.js +9 -9
- package/dist/tools/subagent.js +54 -9
- 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 +1 -3
- package/dist/ui/box.js +1 -5
- package/dist/ui/index.js +6 -6
- package/dist/ui/line-editor.js +703 -0
- package/dist/ui/line-math.js +69 -0
- package/dist/ui/md-formatter.js +33 -33
- package/dist/ui/output.js +5 -5
- package/dist/ui/plan-view.js +103 -0
- package/dist/ui/renderer.js +15 -10
- package/dist/ui/table.js +1 -1
- package/package.json +48 -48
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import { t } from "../../i18n/index";
|
|
2
|
-
import { PlanCreator } from "./planner";
|
|
3
2
|
import { PlanTracker } from "./tracker";
|
|
4
3
|
import { StepVerifier } from "./verifier";
|
|
5
4
|
import { StuckDetector } from "./stuck-detector";
|
|
6
|
-
import { Auditor } from "./auditor";
|
|
5
|
+
import { Auditor, findExistingFile } from "./auditor";
|
|
7
6
|
import { PlanStore } from "./plan-store";
|
|
8
|
-
import { checkPlanCoverage } from "./plan-coverage";
|
|
9
7
|
import { getMessageText } from "../../llm/provider";
|
|
8
|
+
import { extractFileLikeTokens, stripUrls } from "../hallucination/js-identifiers";
|
|
10
9
|
import { existsSync, readFileSync } from "fs";
|
|
11
10
|
import { resolve } from "path";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const FORCE_SKIP_THRESHOLD = 10;
|
|
11
|
+
import { createPlanToolDefinitions } from "./plan-tool";
|
|
12
|
+
import { createExecutionPlugin, STUCK_RECOVERY_COOLDOWN, } from "./execution-plugin";
|
|
15
13
|
export class ExecutionModule {
|
|
16
14
|
name = "execution";
|
|
17
15
|
tracker = null;
|
|
@@ -20,17 +18,9 @@ export class ExecutionModule {
|
|
|
20
18
|
auditor;
|
|
21
19
|
store;
|
|
22
20
|
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();
|
|
30
21
|
// Skip the first audit after restoring a plan from disk — gives the user
|
|
31
22
|
// one chance to change the subject before the audit gate locks in.
|
|
32
23
|
_auditSkipsRemaining = 0;
|
|
33
|
-
stuckNotified = false;
|
|
34
24
|
/**
|
|
35
25
|
* Deferred <system-summary> messages. Injected by onBeforeTool/onAfterTool
|
|
36
26
|
* but flushed in onBeforeThink, so they never land between an assistant
|
|
@@ -38,6 +28,41 @@ export class ExecutionModule {
|
|
|
38
28
|
* buries the warnings the model should react to).
|
|
39
29
|
*/
|
|
40
30
|
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
|
+
})();
|
|
41
66
|
constructor(baseDir, stuckThreshold = 6) {
|
|
42
67
|
this.baseDir = baseDir;
|
|
43
68
|
this.verifier = new StepVerifier(baseDir);
|
|
@@ -54,7 +79,16 @@ export class ExecutionModule {
|
|
|
54
79
|
const plan = this.store.loadActive();
|
|
55
80
|
if (!plan)
|
|
56
81
|
return false;
|
|
57
|
-
|
|
82
|
+
const restored = new PlanTracker(plan);
|
|
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;
|
|
58
92
|
// Advance to the first non-done step
|
|
59
93
|
this.tracker.syncCurrentStep();
|
|
60
94
|
this._auditSkipsRemaining = 1; // restored plan — one free pass
|
|
@@ -82,9 +116,7 @@ export class ExecutionModule {
|
|
|
82
116
|
}
|
|
83
117
|
const plan = this.tracker.getPlan();
|
|
84
118
|
const audit = await this.auditor.audit(plan);
|
|
85
|
-
const pendingSteps = plan.steps.flatMap((s) => s.status !== "done" && s.status !== "skipped"
|
|
86
|
-
? [`${s.id}. ${s.description}`]
|
|
87
|
-
: []);
|
|
119
|
+
const pendingSteps = plan.steps.flatMap((s) => s.status !== "done" && s.status !== "skipped" ? [`${s.id}. ${s.description}`] : []);
|
|
88
120
|
const done = plan.steps.filter((s) => s.status === "done").length;
|
|
89
121
|
const passed = audit.passed && pendingSteps.length === 0;
|
|
90
122
|
return {
|
|
@@ -97,7 +129,7 @@ export class ExecutionModule {
|
|
|
97
129
|
};
|
|
98
130
|
}
|
|
99
131
|
getSystemPromptBlock() {
|
|
100
|
-
if (!this.tracker)
|
|
132
|
+
if (!this.tracker || this.tracker.isComplete())
|
|
101
133
|
return null;
|
|
102
134
|
return {
|
|
103
135
|
content: this.tracker.toPromptBlock(),
|
|
@@ -106,505 +138,52 @@ export class ExecutionModule {
|
|
|
106
138
|
estimatedTokens: 200,
|
|
107
139
|
};
|
|
108
140
|
}
|
|
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
|
+
}
|
|
109
163
|
getToolDefinitions() {
|
|
110
|
-
return
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
];
|
|
164
|
+
return createPlanToolDefinitions({
|
|
165
|
+
store: this.store,
|
|
166
|
+
verifier: this.verifier,
|
|
167
|
+
trackerRef: this.trackerRef,
|
|
168
|
+
preserveActive: () => this.preserveActive(),
|
|
169
|
+
setPlan: (p) => this.setPlan(p),
|
|
170
|
+
missingStepDeliverables: (s) => this.missingStepDeliverables(s),
|
|
171
|
+
stillExistingDeliverables: (s) => this.stillExistingDeliverables(s),
|
|
172
|
+
parseKinds: (a, st) => this.parseKinds(a, st),
|
|
173
|
+
getTaskText: (c) => this.getTaskText(c),
|
|
174
|
+
baseDir: this.baseDir,
|
|
175
|
+
});
|
|
401
176
|
}
|
|
402
177
|
getPlugin() {
|
|
403
|
-
return {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
};
|
|
178
|
+
return createExecutionPlugin({
|
|
179
|
+
stuckDetector: this.stuckDetector,
|
|
180
|
+
trackerRef: this.trackerRef,
|
|
181
|
+
pendingMessages: this.pendingMessages,
|
|
182
|
+
forbiddenBashFailures: this.forbiddenBashFailures,
|
|
183
|
+
state: this.state,
|
|
184
|
+
checkPlanAlignment: (call) => this.checkPlanAlignment(call),
|
|
185
|
+
advancePlanIfStepComplete: (cm, sl) => this.advancePlanIfStepComplete(cm, sl),
|
|
186
|
+
});
|
|
608
187
|
}
|
|
609
188
|
preserveActive() {
|
|
610
189
|
if (!this.tracker)
|
|
@@ -636,11 +215,11 @@ export class ExecutionModule {
|
|
|
636
215
|
];
|
|
637
216
|
if (allowedAlways.includes(call.name))
|
|
638
217
|
return null;
|
|
639
|
-
const stepPaths = (step.description
|
|
218
|
+
const stepPaths = extractFileLikeTokens(stripUrls(step.description)).map((p) => p.toLowerCase());
|
|
640
219
|
if (stepPaths.length === 0)
|
|
641
220
|
return null;
|
|
642
|
-
const argStr = JSON.stringify(call.arguments)
|
|
643
|
-
const callPaths = (argStr
|
|
221
|
+
const argStr = JSON.stringify(call.arguments);
|
|
222
|
+
const callPaths = extractFileLikeTokens(stripUrls(argStr)).map((p) => p.toLowerCase());
|
|
644
223
|
if (callPaths.length === 0)
|
|
645
224
|
return null;
|
|
646
225
|
// Files that already belong to COMPLETED (done/skipped) steps are
|
|
@@ -650,15 +229,13 @@ export class ExecutionModule {
|
|
|
650
229
|
const finishedPaths = new Set();
|
|
651
230
|
for (const s of plan.steps) {
|
|
652
231
|
if (s.status === "done" || s.status === "skipped") {
|
|
653
|
-
|
|
654
|
-
.
|
|
655
|
-
|
|
656
|
-
ps.forEach((p) => finishedPaths.add(p));
|
|
232
|
+
extractFileLikeTokens(stripUrls(s.description))
|
|
233
|
+
.map((p) => p.toLowerCase())
|
|
234
|
+
.forEach((p) => finishedPaths.add(p));
|
|
657
235
|
}
|
|
658
236
|
}
|
|
659
237
|
const offPath = callPaths.some((p) => {
|
|
660
|
-
if (finishedPaths.has(p) ||
|
|
661
|
-
[...finishedPaths].some((s) => s.includes(p))) {
|
|
238
|
+
if (finishedPaths.has(p) || [...finishedPaths].some((s) => s.includes(p))) {
|
|
662
239
|
return false;
|
|
663
240
|
}
|
|
664
241
|
return !stepPaths.some((s) => p.includes(s) || s.includes(p));
|
|
@@ -676,7 +253,12 @@ export class ExecutionModule {
|
|
|
676
253
|
if (!step)
|
|
677
254
|
return;
|
|
678
255
|
const stepText = step.description.toLowerCase();
|
|
679
|
-
|
|
256
|
+
// Shared file-token filter (allow-list of real extensions, extension-only
|
|
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)) || [];
|
|
680
262
|
// Step gate: verify dependencies when mentioned — check BEFORE file paths
|
|
681
263
|
const isDepsStep = stepText.includes("install") ||
|
|
682
264
|
stepText.includes("зависим") ||
|
|
@@ -698,8 +280,8 @@ export class ExecutionModule {
|
|
|
698
280
|
const hasLockFile = lockFiles.some((f) => existsSync(resolve(this.baseDir, f)));
|
|
699
281
|
if (!hasLockFile) {
|
|
700
282
|
if (contextManager) {
|
|
701
|
-
const hints = this.depsGateHints.get(step.id) || 0;
|
|
702
|
-
this.depsGateHints.set(step.id, hints + 1);
|
|
283
|
+
const hints = this.state.depsGateHints.get(step.id) || 0;
|
|
284
|
+
this.state.depsGateHints.set(step.id, hints + 1);
|
|
703
285
|
const force = hints >= 1;
|
|
704
286
|
const content = force
|
|
705
287
|
? t("exec.step_gate_deps_force", { step: String(step.id) })
|
|
@@ -718,28 +300,32 @@ export class ExecutionModule {
|
|
|
718
300
|
if (stepPaths.length === 0)
|
|
719
301
|
return;
|
|
720
302
|
const allExist = stepPaths.every((p) => existsSync(resolve(this.baseDir, p)));
|
|
721
|
-
|
|
303
|
+
const allGone = stepPaths.every((p) => !existsSync(resolve(this.baseDir, p)));
|
|
304
|
+
const satisfied = step.kind === "delete" ? allGone && !allExist : allExist;
|
|
305
|
+
if (!satisfied)
|
|
722
306
|
return;
|
|
723
307
|
// Step gate: verify files have content (not empty)
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
308
|
+
if (step.kind !== "delete") {
|
|
309
|
+
const emptyFiles = [];
|
|
310
|
+
for (const p of stepPaths) {
|
|
311
|
+
try {
|
|
312
|
+
const content = readFileSync(resolve(this.baseDir, p), "utf-8");
|
|
313
|
+
if (content.trim().length < 10) {
|
|
314
|
+
emptyFiles.push(p);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
// If we can't read it, rely on existsSync result
|
|
730
319
|
}
|
|
731
320
|
}
|
|
732
|
-
|
|
733
|
-
|
|
321
|
+
if (emptyFiles.length > 0 && contextManager) {
|
|
322
|
+
contextManager.addMessage({
|
|
323
|
+
role: "user",
|
|
324
|
+
content: `<system-summary>${t("exec.step_gate_empty", { step: String(step.id), files: emptyFiles.join(", ") })}</system-summary>`,
|
|
325
|
+
});
|
|
326
|
+
return;
|
|
734
327
|
}
|
|
735
328
|
}
|
|
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
|
-
}
|
|
743
329
|
// All checks passed — advance
|
|
744
330
|
this.tracker?.updateStepStatus(step.id, "done");
|
|
745
331
|
const hadNext = this.tracker?.advance() ?? false;
|
|
@@ -761,6 +347,68 @@ export class ExecutionModule {
|
|
|
761
347
|
content: `<system-summary>${nextMsg}</system-summary>`,
|
|
762
348
|
});
|
|
763
349
|
}
|
|
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 };
|
|
764
412
|
}
|
|
765
413
|
/**
|
|
766
414
|
* Extract the original task statement from the first user message in the
|
|
@@ -771,7 +419,16 @@ export class ExecutionModule {
|
|
|
771
419
|
if (!manager)
|
|
772
420
|
return "";
|
|
773
421
|
const history = manager.getActiveHistory();
|
|
774
|
-
|
|
422
|
+
// The first user message may be an injected <system-summary> (compaction
|
|
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
|
+
});
|
|
775
432
|
if (!firstUser)
|
|
776
433
|
return "";
|
|
777
434
|
return getMessageText(firstUser.content).trim();
|