micro-models-agent 0.47.0 → 0.48.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/README.md +358 -312
  2. package/dist/cli/commands.js +323 -0
  3. package/dist/cli/completer.js +167 -0
  4. package/dist/cli/index.js +2 -0
  5. package/dist/cli/main.js +165 -0
  6. package/dist/cli/plugin-commands.js +36 -0
  7. package/dist/cli/repl-commands.js +661 -0
  8. package/dist/cli/repl.js +616 -0
  9. package/dist/cli/run-result.js +22 -0
  10. package/dist/cli/security-commands.js +164 -0
  11. package/dist/cli/setup.js +231 -0
  12. package/dist/config/config.js +249 -0
  13. package/dist/config/defaults.js +124 -0
  14. package/dist/config/experts.js +15 -0
  15. package/dist/config/index.js +3 -0
  16. package/dist/config/security.js +193 -0
  17. package/dist/config/types.js +1 -0
  18. package/dist/core/agent-moe.js +102 -0
  19. package/dist/core/agent.js +886 -0
  20. package/dist/core/bootstrap.js +404 -0
  21. package/dist/core/index.js +2 -0
  22. package/dist/core/prompt-builder.js +76 -0
  23. package/dist/core/session-logger.js +197 -0
  24. package/dist/core/types.js +1 -0
  25. package/dist/core/version.js +24 -0
  26. package/dist/core/workspace.js +76 -0
  27. package/dist/i18n/en.json +598 -0
  28. package/dist/i18n/index.js +46 -0
  29. package/dist/i18n/ru.json +598 -0
  30. package/dist/index.js +22 -0
  31. package/dist/llm/image-utils.js +143 -0
  32. package/dist/llm/index.js +4 -0
  33. package/dist/llm/model-loader.js +78 -0
  34. package/dist/llm/openai-compat.js +359 -0
  35. package/dist/llm/orchestrator.js +198 -0
  36. package/dist/llm/provider.js +10 -0
  37. package/dist/llm/response.js +39 -0
  38. package/dist/llm/token-counter.js +39 -0
  39. package/dist/llm/types.js +1 -0
  40. package/dist/logger/app-logger.js +143 -0
  41. package/dist/logger/file-log.js +151 -0
  42. package/dist/logger/index.js +1 -0
  43. package/dist/main.js +672 -357
  44. package/dist/migration/backup.js +45 -0
  45. package/dist/migration/detect.js +50 -0
  46. package/dist/migration/index.js +2 -0
  47. package/dist/modules/artifacts/store.js +61 -0
  48. package/dist/modules/browser/actions.js +76 -0
  49. package/dist/modules/browser/bridge-client.js +199 -0
  50. package/dist/modules/browser/bridge-path.js +10 -0
  51. package/dist/modules/browser/bridge-server.mjs +202 -202
  52. package/dist/modules/browser/cookie-store.js +24 -0
  53. package/dist/modules/browser/driver.js +136 -0
  54. package/dist/modules/browser/index.js +7 -0
  55. package/dist/modules/browser/module.js +29 -0
  56. package/dist/modules/browser/session.js +338 -0
  57. package/dist/modules/browser/snapshot.js +148 -0
  58. package/dist/modules/browser/types.js +12 -0
  59. package/dist/modules/certification/cli.js +174 -0
  60. package/dist/modules/certification/fact-checker.js +82 -0
  61. package/dist/modules/certification/loader.js +105 -0
  62. package/dist/modules/certification/manifest.js +50 -0
  63. package/dist/modules/certification/runner.js +159 -0
  64. package/dist/modules/certification/scenarios.js +124 -0
  65. package/dist/modules/certification/types.js +1 -0
  66. package/dist/modules/context/chunk-query.js +100 -0
  67. package/dist/modules/context/fact-extractor.js +162 -0
  68. package/dist/modules/context/history.js +15 -0
  69. package/dist/modules/context/index.js +1 -0
  70. package/dist/modules/context/manager.js +423 -0
  71. package/dist/modules/execution/audit-runners.js +152 -0
  72. package/dist/modules/execution/auditor.js +218 -0
  73. package/dist/modules/execution/execution-plugin.js +272 -0
  74. package/dist/modules/execution/index.js +8 -0
  75. package/dist/modules/execution/module.js +436 -0
  76. package/dist/modules/execution/moe-executor.js +291 -0
  77. package/dist/modules/execution/plan-coverage.js +68 -0
  78. package/dist/modules/execution/plan-persister.js +46 -0
  79. package/dist/modules/execution/plan-store.js +157 -0
  80. package/dist/modules/execution/plan-tool.js +508 -0
  81. package/dist/modules/execution/plan-validator.js +153 -0
  82. package/dist/modules/execution/planner.js +90 -0
  83. package/dist/modules/execution/stuck-detector.js +510 -0
  84. package/dist/modules/execution/tracker.js +67 -0
  85. package/dist/modules/execution/types.js +1 -0
  86. package/dist/modules/execution/verifier.js +222 -0
  87. package/dist/modules/execution/windows-commands.js +41 -0
  88. package/dist/modules/hallucination/confidence.js +66 -0
  89. package/dist/modules/hallucination/consistency.js +26 -0
  90. package/dist/modules/hallucination/detector.js +43 -0
  91. package/dist/modules/hallucination/factual.js +129 -0
  92. package/dist/modules/hallucination/index.js +5 -0
  93. package/dist/modules/hallucination/js-identifiers.js +262 -0
  94. package/dist/modules/hallucination/llm-judge.js +101 -0
  95. package/dist/modules/index.js +5 -0
  96. package/dist/modules/indexer/cache.js +40 -0
  97. package/dist/modules/indexer/index.js +3 -0
  98. package/dist/modules/indexer/module.js +245 -0
  99. package/dist/modules/indexer/project-profile.js +183 -0
  100. package/dist/modules/indexer/walker.js +101 -0
  101. package/dist/modules/lsp/check-tool.js +58 -0
  102. package/dist/modules/lsp/client.js +278 -0
  103. package/dist/modules/lsp/command.js +60 -0
  104. package/dist/modules/lsp/config.js +135 -0
  105. package/dist/modules/lsp/index.js +3 -0
  106. package/dist/modules/lsp/module.js +232 -0
  107. package/dist/modules/lsp/probe.js +76 -0
  108. package/dist/modules/lsp/project-root.js +32 -0
  109. package/dist/modules/lsp/startup-check.js +141 -0
  110. package/dist/modules/lsp/types.js +1 -0
  111. package/dist/modules/mcp/client.js +399 -0
  112. package/dist/modules/mcp/index.js +3 -0
  113. package/dist/modules/mcp/module.js +142 -0
  114. package/dist/modules/mcp/registry.js +15 -0
  115. package/dist/modules/memory/index.js +1 -0
  116. package/dist/modules/memory/module.js +96 -0
  117. package/dist/modules/memory/search.js +42 -0
  118. package/dist/modules/memory/store.js +69 -0
  119. package/dist/modules/pipelines/engine.js +60 -0
  120. package/dist/modules/pipelines/index.js +3 -0
  121. package/dist/modules/pipelines/parser.js +56 -0
  122. package/dist/modules/pipelines/template.js +14 -0
  123. package/dist/modules/plugins/builtin/lint-on-write.js +231 -0
  124. package/dist/modules/plugins/builtin/notify.js +9 -0
  125. package/dist/modules/plugins/index.js +1 -0
  126. package/dist/modules/plugins/loader.js +70 -0
  127. package/dist/modules/plugins/manager.js +217 -0
  128. package/dist/modules/plugins/types.js +1 -0
  129. package/dist/modules/processes/detect.js +34 -0
  130. package/dist/modules/processes/index.js +2 -0
  131. package/dist/modules/processes/registry.js +327 -0
  132. package/dist/modules/processes/runner.js +23 -0
  133. package/dist/modules/registry.js +47 -0
  134. package/dist/modules/security/audit-log.js +136 -0
  135. package/dist/modules/security/audit-notifier.js +292 -0
  136. package/dist/modules/security/command-validator.js +205 -0
  137. package/dist/modules/security/content-scanner.js +53 -0
  138. package/dist/modules/security/data-sanitizer.js +89 -0
  139. package/dist/modules/security/encryption.js +242 -0
  140. package/dist/modules/security/index.js +14 -0
  141. package/dist/modules/security/network-validator.js +71 -0
  142. package/dist/modules/security/path-validator.js +207 -0
  143. package/dist/modules/security/rate-limiter.js +119 -0
  144. package/dist/modules/security/security-policies.js +531 -0
  145. package/dist/modules/security/session-encryption.js +210 -0
  146. package/dist/modules/security/session-isolation.js +95 -0
  147. package/dist/modules/session/index.js +3 -0
  148. package/dist/modules/session/manager.js +172 -0
  149. package/dist/modules/session/module.js +24 -0
  150. package/dist/modules/session/store.js +222 -0
  151. package/dist/modules/session/types.js +1 -0
  152. package/dist/modules/skills/index.js +2 -0
  153. package/dist/modules/skills/loader.js +72 -0
  154. package/dist/modules/skills/matcher.js +27 -0
  155. package/dist/modules/skills/module.js +129 -0
  156. package/dist/modules/types.js +1 -0
  157. package/dist/modules/updater/checker.js +96 -0
  158. package/dist/modules/updater/index.js +2 -0
  159. package/dist/modules/updater/module.js +116 -0
  160. package/dist/modules/user-profile/compressor.js +16 -0
  161. package/dist/modules/user-profile/index.js +1 -0
  162. package/dist/modules/user-profile/profile.js +68 -0
  163. package/dist/skills/builtin/git.md +36 -36
  164. package/dist/skills/builtin/typescript.md +35 -35
  165. package/dist/tools/approve.js +32 -0
  166. package/dist/tools/attach-image.js +89 -0
  167. package/dist/tools/bash.js +496 -0
  168. package/dist/tools/browser.js +114 -0
  169. package/dist/tools/chunk-query.js +99 -0
  170. package/dist/tools/create-dir.js +55 -0
  171. package/dist/tools/delete-file.js +62 -0
  172. package/dist/tools/download-file.js +116 -0
  173. package/dist/tools/edit-file.js +79 -0
  174. package/dist/tools/enable-tools.js +58 -0
  175. package/dist/tools/executor.js +144 -0
  176. package/dist/tools/file-info.js +46 -0
  177. package/dist/tools/filter-tools.js +17 -0
  178. package/dist/tools/glob-tool.js +26 -0
  179. package/dist/tools/grep-tool.js +84 -0
  180. package/dist/tools/hidden-tools-block.js +37 -0
  181. package/dist/tools/index.js +78 -0
  182. package/dist/tools/list-dir.js +48 -0
  183. package/dist/tools/load-skill.js +42 -0
  184. package/dist/tools/mcp-call.js +68 -0
  185. package/dist/tools/move-file.js +85 -0
  186. package/dist/tools/path-utils.js +51 -0
  187. package/dist/tools/pipeline-run.js +144 -0
  188. package/dist/tools/preview.js +2 -0
  189. package/dist/tools/process-kill.js +29 -0
  190. package/dist/tools/process-list.js +36 -0
  191. package/dist/tools/process-log.js +45 -0
  192. package/dist/tools/question.js +140 -0
  193. package/dist/tools/read-file.js +91 -0
  194. package/dist/tools/recall.js +117 -0
  195. package/dist/tools/registry.js +47 -0
  196. package/dist/tools/remember.js +67 -0
  197. package/dist/tools/scope-check.js +30 -0
  198. package/dist/tools/search-history.js +84 -0
  199. package/dist/tools/subagent.js +196 -0
  200. package/dist/tools/types.js +1 -0
  201. package/dist/tools/user-input.js +123 -0
  202. package/dist/tools/web-browse.js +86 -0
  203. package/dist/tools/web-fetch.js +98 -0
  204. package/dist/tools/web-search.js +78 -0
  205. package/dist/tools/write-file.js +81 -0
  206. package/dist/ui/box.js +77 -0
  207. package/dist/ui/colors.js +4 -0
  208. package/dist/ui/diff.js +178 -0
  209. package/dist/ui/index.js +6 -0
  210. package/dist/ui/line-editor.js +703 -0
  211. package/dist/ui/line-math.js +69 -0
  212. package/dist/ui/md-formatter.js +212 -0
  213. package/dist/ui/output.js +13 -0
  214. package/dist/ui/plan-view.js +103 -0
  215. package/dist/ui/renderer.js +209 -0
  216. package/dist/ui/spinner.js +70 -0
  217. package/dist/ui/table.js +144 -0
  218. package/package.json +48 -48
@@ -0,0 +1,436 @@
1
+ import { t } from "../../i18n/index";
2
+ import { PlanTracker } from "./tracker";
3
+ import { StepVerifier } from "./verifier";
4
+ import { StuckDetector } from "./stuck-detector";
5
+ import { Auditor, findExistingFile } from "./auditor";
6
+ import { PlanStore } from "./plan-store";
7
+ import { getMessageText } from "../../llm/provider";
8
+ import { extractFileLikeTokens, stripUrls } from "../hallucination/js-identifiers";
9
+ import { existsSync, readFileSync } from "fs";
10
+ import { resolve } from "path";
11
+ import { createPlanToolDefinitions } from "./plan-tool";
12
+ import { createExecutionPlugin, STUCK_RECOVERY_COOLDOWN, } from "./execution-plugin";
13
+ export class ExecutionModule {
14
+ name = "execution";
15
+ tracker = null;
16
+ verifier;
17
+ stuckDetector;
18
+ auditor;
19
+ store;
20
+ baseDir;
21
+ // Skip the first audit after restoring a plan from disk — gives the user
22
+ // one chance to change the subject before the audit gate locks in.
23
+ _auditSkipsRemaining = 0;
24
+ /**
25
+ * Deferred <system-summary> messages. Injected by onBeforeTool/onAfterTool
26
+ * but flushed in onBeforeThink, so they never land between an assistant
27
+ * tool_call message and its tool results (which breaks message pairing and
28
+ * buries the warnings the model should react to).
29
+ */
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
+ })();
66
+ constructor(baseDir, stuckThreshold = 6) {
67
+ this.baseDir = baseDir;
68
+ this.verifier = new StepVerifier(baseDir);
69
+ this.stuckDetector = new StuckDetector(stuckThreshold);
70
+ this.auditor = new Auditor(baseDir);
71
+ this.store = new PlanStore(baseDir);
72
+ }
73
+ setPlan(plan) {
74
+ this.tracker = new PlanTracker(plan);
75
+ this.store.saveActive(plan);
76
+ this._auditSkipsRemaining = 0; // plan explicitly created — arm the audit gate
77
+ }
78
+ restorePlan() {
79
+ const plan = this.store.loadActive();
80
+ if (!plan)
81
+ return false;
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;
92
+ // Advance to the first non-done step
93
+ this.tracker.syncCurrentStep();
94
+ this._auditSkipsRemaining = 1; // restored plan — one free pass
95
+ return true;
96
+ }
97
+ getTracker() {
98
+ return this.tracker;
99
+ }
100
+ getStore() {
101
+ return this.store;
102
+ }
103
+ getStuckDetector() {
104
+ return this.stuckDetector;
105
+ }
106
+ /**
107
+ * Final audit before the agent may declare the task done. Returns null when
108
+ * no plan is active; otherwise checks plan completion + artifact existence.
109
+ */
110
+ async runFinalAudit() {
111
+ if (!this.tracker)
112
+ return null;
113
+ if (this._auditSkipsRemaining > 0) {
114
+ this._auditSkipsRemaining--;
115
+ return null;
116
+ }
117
+ const plan = this.tracker.getPlan();
118
+ const audit = await this.auditor.audit(plan);
119
+ const pendingSteps = plan.steps.flatMap((s) => s.status !== "done" && s.status !== "skipped" ? [`${s.id}. ${s.description}`] : []);
120
+ const done = plan.steps.filter((s) => s.status === "done").length;
121
+ const passed = audit.passed && pendingSteps.length === 0;
122
+ return {
123
+ passed,
124
+ done,
125
+ total: plan.steps.length,
126
+ pendingSteps,
127
+ missingFiles: audit.missingFiles,
128
+ summary: audit.summary,
129
+ };
130
+ }
131
+ getSystemPromptBlock() {
132
+ if (!this.tracker || this.tracker.isComplete())
133
+ return null;
134
+ return {
135
+ content: this.tracker.toPromptBlock(),
136
+ priority: "critical",
137
+ essential: true,
138
+ estimatedTokens: 200,
139
+ };
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
+ }
163
+ getToolDefinitions() {
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
+ });
176
+ }
177
+ getPlugin() {
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
+ });
187
+ }
188
+ preserveActive() {
189
+ if (!this.tracker)
190
+ return;
191
+ const plan = this.tracker.getPlan();
192
+ if (plan.steps.every((s) => s.status === "done" || s.status === "skipped")) {
193
+ this.store.archivePlan(plan);
194
+ }
195
+ else {
196
+ this.store.saveDraft(plan);
197
+ }
198
+ }
199
+ checkPlanAlignment(call) {
200
+ if (!this.tracker)
201
+ return null;
202
+ const step = this.tracker.getCurrentStep();
203
+ if (!step)
204
+ return null;
205
+ const allowedAlways = [
206
+ "plan",
207
+ "todo",
208
+ "verify",
209
+ "list_dir",
210
+ "read_file",
211
+ "glob",
212
+ "grep",
213
+ "file_info",
214
+ "load_skill",
215
+ ];
216
+ if (allowedAlways.includes(call.name))
217
+ return null;
218
+ const stepPaths = extractFileLikeTokens(stripUrls(step.description)).map((p) => p.toLowerCase());
219
+ if (stepPaths.length === 0)
220
+ return null;
221
+ const argStr = JSON.stringify(call.arguments);
222
+ const callPaths = extractFileLikeTokens(stripUrls(argStr)).map((p) => p.toLowerCase());
223
+ if (callPaths.length === 0)
224
+ return null;
225
+ // Files that already belong to COMPLETED (done/skipped) steps are
226
+ // legitimate rewrite targets — the agent iterates on them while fixing
227
+ // LSP/type errors. Blocking those would stop normal bug-fixing.
228
+ const plan = this.tracker.getPlan();
229
+ const finishedPaths = new Set();
230
+ for (const s of plan.steps) {
231
+ if (s.status === "done" || s.status === "skipped") {
232
+ extractFileLikeTokens(stripUrls(s.description))
233
+ .map((p) => p.toLowerCase())
234
+ .forEach((p) => finishedPaths.add(p));
235
+ }
236
+ }
237
+ const offPath = callPaths.some((p) => {
238
+ if (finishedPaths.has(p) || [...finishedPaths].some((s) => s.includes(p))) {
239
+ return false;
240
+ }
241
+ return !stepPaths.some((s) => p.includes(s) || s.includes(p));
242
+ });
243
+ if (!offPath)
244
+ return null;
245
+ return t("exec.plan_warning", {
246
+ step: String(step.id),
247
+ description: step.description,
248
+ tool: call.name,
249
+ });
250
+ }
251
+ advancePlanIfStepComplete(contextManager, sessionLog) {
252
+ const step = this.tracker?.getCurrentStep();
253
+ if (!step)
254
+ return;
255
+ const stepText = step.description.toLowerCase();
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)) || [];
262
+ // Step gate: verify dependencies when mentioned — check BEFORE file paths
263
+ const isDepsStep = stepText.includes("install") ||
264
+ stepText.includes("зависим") ||
265
+ stepText.includes("init") ||
266
+ stepText.includes("инициализац");
267
+ if (isDepsStep) {
268
+ const lockFiles = [
269
+ "package-lock.json",
270
+ "bun.lockb",
271
+ "yarn.lock",
272
+ "pnpm-lock.yaml",
273
+ "node_modules",
274
+ "go.sum",
275
+ "Cargo.lock",
276
+ "Pipfile.lock",
277
+ "poetry.lock",
278
+ "requirements.txt",
279
+ ];
280
+ const hasLockFile = lockFiles.some((f) => existsSync(resolve(this.baseDir, f)));
281
+ if (!hasLockFile) {
282
+ if (contextManager) {
283
+ const hints = this.state.depsGateHints.get(step.id) || 0;
284
+ this.state.depsGateHints.set(step.id, hints + 1);
285
+ const force = hints >= 1;
286
+ const content = force
287
+ ? t("exec.step_gate_deps_force", { step: String(step.id) })
288
+ : t("exec.step_gate_deps", {
289
+ step: String(step.id),
290
+ description: step.description,
291
+ });
292
+ contextManager.addMessage({
293
+ role: "user",
294
+ content: `<system-summary>${content}</system-summary>`,
295
+ });
296
+ }
297
+ return;
298
+ }
299
+ }
300
+ if (stepPaths.length === 0)
301
+ return;
302
+ const allExist = stepPaths.every((p) => existsSync(resolve(this.baseDir, p)));
303
+ const allGone = stepPaths.every((p) => !existsSync(resolve(this.baseDir, p)));
304
+ const satisfied = step.kind === "delete" ? allGone && !allExist : allExist;
305
+ if (!satisfied)
306
+ return;
307
+ // Step gate: verify files have content (not empty)
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
319
+ }
320
+ }
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;
327
+ }
328
+ }
329
+ // All checks passed — advance
330
+ this.tracker?.updateStepStatus(step.id, "done");
331
+ const hadNext = this.tracker?.advance() ?? false;
332
+ if (this.tracker) {
333
+ this.store.saveActive(this.tracker.getPlan());
334
+ sessionLog?.plan("auto-advance", `Step ${step.id} auto-completed | ${this.tracker.getProgressString()} | current: step ${this.tracker.getCurrentStepIndex() + 1}`);
335
+ }
336
+ if (contextManager) {
337
+ const nextStep = hadNext ? this.tracker?.getCurrentStep() : null;
338
+ const nextMsg = nextStep
339
+ ? t("exec.step_gate_ok", {
340
+ step: String(step.id),
341
+ nextStep: String(nextStep.id),
342
+ nextDesc: nextStep.description,
343
+ })
344
+ : t("exec.step_gate_last", { step: String(step.id) });
345
+ contextManager.addMessage({
346
+ role: "user",
347
+ content: `<system-summary>${nextMsg}</system-summary>`,
348
+ });
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 };
412
+ }
413
+ /**
414
+ * Extract the original task statement from the first user message in the
415
+ * conversation history. Used to validate plan coverage against requirements.
416
+ */
417
+ getTaskText(ctx) {
418
+ const manager = ctx?.contextManager;
419
+ if (!manager)
420
+ return "";
421
+ const history = manager.getActiveHistory();
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
+ });
432
+ if (!firstUser)
433
+ return "";
434
+ return getMessageText(firstUser.content).trim();
435
+ }
436
+ }