micro-models-agent 0.46.2 → 0.46.3

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 +312 -312
  2. package/dist/main.js +711 -784
  3. package/dist/modules/browser/bridge-server.mjs +202 -202
  4. package/dist/skills/builtin/git.md +36 -36
  5. package/dist/skills/builtin/typescript.md +35 -35
  6. package/package.json +48 -48
  7. package/dist/cli/commands.js +0 -323
  8. package/dist/cli/completer.js +0 -167
  9. package/dist/cli/index.js +0 -2
  10. package/dist/cli/main.js +0 -165
  11. package/dist/cli/plugin-commands.js +0 -36
  12. package/dist/cli/repl-commands.js +0 -661
  13. package/dist/cli/repl.js +0 -616
  14. package/dist/cli/run-result.js +0 -22
  15. package/dist/cli/security-commands.js +0 -164
  16. package/dist/cli/setup.js +0 -231
  17. package/dist/config/config.js +0 -249
  18. package/dist/config/defaults.js +0 -124
  19. package/dist/config/experts.js +0 -15
  20. package/dist/config/index.js +0 -3
  21. package/dist/config/security.js +0 -193
  22. package/dist/config/types.js +0 -1
  23. package/dist/core/agent-moe.js +0 -102
  24. package/dist/core/agent.js +0 -886
  25. package/dist/core/bootstrap.js +0 -404
  26. package/dist/core/index.js +0 -2
  27. package/dist/core/prompt-builder.js +0 -76
  28. package/dist/core/session-logger.js +0 -197
  29. package/dist/core/types.js +0 -1
  30. package/dist/core/version.js +0 -24
  31. package/dist/core/workspace.js +0 -76
  32. package/dist/i18n/en.json +0 -598
  33. package/dist/i18n/index.js +0 -46
  34. package/dist/i18n/ru.json +0 -598
  35. package/dist/index.js +0 -22
  36. package/dist/llm/image-utils.js +0 -143
  37. package/dist/llm/index.js +0 -4
  38. package/dist/llm/model-loader.js +0 -78
  39. package/dist/llm/openai-compat.js +0 -359
  40. package/dist/llm/orchestrator.js +0 -198
  41. package/dist/llm/provider.js +0 -10
  42. package/dist/llm/response.js +0 -39
  43. package/dist/llm/token-counter.js +0 -39
  44. package/dist/llm/types.js +0 -1
  45. package/dist/logger/app-logger.js +0 -143
  46. package/dist/logger/file-log.js +0 -151
  47. package/dist/logger/index.js +0 -1
  48. package/dist/migration/backup.js +0 -45
  49. package/dist/migration/detect.js +0 -50
  50. package/dist/migration/index.js +0 -2
  51. package/dist/modules/artifacts/store.js +0 -61
  52. package/dist/modules/browser/actions.js +0 -76
  53. package/dist/modules/browser/bridge-client.js +0 -199
  54. package/dist/modules/browser/bridge-path.js +0 -10
  55. package/dist/modules/browser/cookie-store.js +0 -24
  56. package/dist/modules/browser/driver.js +0 -136
  57. package/dist/modules/browser/index.js +0 -7
  58. package/dist/modules/browser/module.js +0 -29
  59. package/dist/modules/browser/session.js +0 -338
  60. package/dist/modules/browser/snapshot.js +0 -148
  61. package/dist/modules/browser/types.js +0 -12
  62. package/dist/modules/certification/cli.js +0 -174
  63. package/dist/modules/certification/fact-checker.js +0 -82
  64. package/dist/modules/certification/loader.js +0 -105
  65. package/dist/modules/certification/manifest.js +0 -50
  66. package/dist/modules/certification/runner.js +0 -159
  67. package/dist/modules/certification/scenarios.js +0 -124
  68. package/dist/modules/certification/types.js +0 -1
  69. package/dist/modules/context/chunk-query.js +0 -100
  70. package/dist/modules/context/fact-extractor.js +0 -162
  71. package/dist/modules/context/history.js +0 -15
  72. package/dist/modules/context/index.js +0 -1
  73. package/dist/modules/context/manager.js +0 -423
  74. package/dist/modules/execution/audit-runners.js +0 -152
  75. package/dist/modules/execution/auditor.js +0 -218
  76. package/dist/modules/execution/execution-plugin.js +0 -272
  77. package/dist/modules/execution/index.js +0 -8
  78. package/dist/modules/execution/module.js +0 -436
  79. package/dist/modules/execution/moe-executor.js +0 -291
  80. package/dist/modules/execution/plan-coverage.js +0 -68
  81. package/dist/modules/execution/plan-persister.js +0 -46
  82. package/dist/modules/execution/plan-store.js +0 -157
  83. package/dist/modules/execution/plan-tool.js +0 -508
  84. package/dist/modules/execution/plan-validator.js +0 -153
  85. package/dist/modules/execution/planner.js +0 -90
  86. package/dist/modules/execution/stuck-detector.js +0 -510
  87. package/dist/modules/execution/tracker.js +0 -67
  88. package/dist/modules/execution/types.js +0 -1
  89. package/dist/modules/execution/verifier.js +0 -222
  90. package/dist/modules/execution/windows-commands.js +0 -41
  91. package/dist/modules/hallucination/confidence.js +0 -66
  92. package/dist/modules/hallucination/consistency.js +0 -26
  93. package/dist/modules/hallucination/detector.js +0 -43
  94. package/dist/modules/hallucination/factual.js +0 -129
  95. package/dist/modules/hallucination/index.js +0 -5
  96. package/dist/modules/hallucination/js-identifiers.js +0 -262
  97. package/dist/modules/hallucination/llm-judge.js +0 -101
  98. package/dist/modules/index.js +0 -5
  99. package/dist/modules/indexer/cache.js +0 -40
  100. package/dist/modules/indexer/index.js +0 -3
  101. package/dist/modules/indexer/module.js +0 -245
  102. package/dist/modules/indexer/project-profile.js +0 -183
  103. package/dist/modules/indexer/walker.js +0 -101
  104. package/dist/modules/lsp/check-tool.js +0 -58
  105. package/dist/modules/lsp/client.js +0 -278
  106. package/dist/modules/lsp/command.js +0 -60
  107. package/dist/modules/lsp/config.js +0 -135
  108. package/dist/modules/lsp/index.js +0 -3
  109. package/dist/modules/lsp/module.js +0 -232
  110. package/dist/modules/lsp/probe.js +0 -76
  111. package/dist/modules/lsp/project-root.js +0 -32
  112. package/dist/modules/lsp/startup-check.js +0 -141
  113. package/dist/modules/lsp/types.js +0 -1
  114. package/dist/modules/mcp/client.js +0 -399
  115. package/dist/modules/mcp/index.js +0 -3
  116. package/dist/modules/mcp/module.js +0 -142
  117. package/dist/modules/mcp/registry.js +0 -15
  118. package/dist/modules/memory/index.js +0 -1
  119. package/dist/modules/memory/module.js +0 -96
  120. package/dist/modules/memory/search.js +0 -42
  121. package/dist/modules/memory/store.js +0 -69
  122. package/dist/modules/pipelines/engine.js +0 -60
  123. package/dist/modules/pipelines/index.js +0 -3
  124. package/dist/modules/pipelines/parser.js +0 -56
  125. package/dist/modules/pipelines/template.js +0 -14
  126. package/dist/modules/plugins/builtin/lint-on-write.js +0 -231
  127. package/dist/modules/plugins/builtin/notify.js +0 -9
  128. package/dist/modules/plugins/index.js +0 -1
  129. package/dist/modules/plugins/loader.js +0 -70
  130. package/dist/modules/plugins/manager.js +0 -217
  131. package/dist/modules/plugins/types.js +0 -1
  132. package/dist/modules/processes/detect.js +0 -34
  133. package/dist/modules/processes/index.js +0 -2
  134. package/dist/modules/processes/registry.js +0 -327
  135. package/dist/modules/processes/runner.js +0 -23
  136. package/dist/modules/registry.js +0 -47
  137. package/dist/modules/security/audit-log.js +0 -136
  138. package/dist/modules/security/audit-notifier.js +0 -292
  139. package/dist/modules/security/command-validator.js +0 -205
  140. package/dist/modules/security/content-scanner.js +0 -53
  141. package/dist/modules/security/data-sanitizer.js +0 -89
  142. package/dist/modules/security/encryption.js +0 -242
  143. package/dist/modules/security/index.js +0 -14
  144. package/dist/modules/security/network-validator.js +0 -71
  145. package/dist/modules/security/path-validator.js +0 -207
  146. package/dist/modules/security/rate-limiter.js +0 -119
  147. package/dist/modules/security/security-policies.js +0 -531
  148. package/dist/modules/security/session-encryption.js +0 -210
  149. package/dist/modules/security/session-isolation.js +0 -95
  150. package/dist/modules/session/index.js +0 -3
  151. package/dist/modules/session/manager.js +0 -172
  152. package/dist/modules/session/module.js +0 -24
  153. package/dist/modules/session/store.js +0 -222
  154. package/dist/modules/session/types.js +0 -1
  155. package/dist/modules/skills/index.js +0 -2
  156. package/dist/modules/skills/loader.js +0 -72
  157. package/dist/modules/skills/matcher.js +0 -27
  158. package/dist/modules/skills/module.js +0 -129
  159. package/dist/modules/types.js +0 -1
  160. package/dist/modules/updater/checker.js +0 -96
  161. package/dist/modules/updater/index.js +0 -2
  162. package/dist/modules/updater/module.js +0 -116
  163. package/dist/modules/user-profile/compressor.js +0 -16
  164. package/dist/modules/user-profile/index.js +0 -1
  165. package/dist/modules/user-profile/profile.js +0 -68
  166. package/dist/tools/approve.js +0 -32
  167. package/dist/tools/attach-image.js +0 -89
  168. package/dist/tools/bash.js +0 -496
  169. package/dist/tools/browser.js +0 -114
  170. package/dist/tools/chunk-query.js +0 -99
  171. package/dist/tools/create-dir.js +0 -55
  172. package/dist/tools/delete-file.js +0 -62
  173. package/dist/tools/download-file.js +0 -116
  174. package/dist/tools/edit-file.js +0 -79
  175. package/dist/tools/enable-tools.js +0 -58
  176. package/dist/tools/executor.js +0 -144
  177. package/dist/tools/file-info.js +0 -46
  178. package/dist/tools/filter-tools.js +0 -17
  179. package/dist/tools/glob-tool.js +0 -26
  180. package/dist/tools/grep-tool.js +0 -84
  181. package/dist/tools/hidden-tools-block.js +0 -37
  182. package/dist/tools/index.js +0 -78
  183. package/dist/tools/list-dir.js +0 -48
  184. package/dist/tools/load-skill.js +0 -42
  185. package/dist/tools/mcp-call.js +0 -68
  186. package/dist/tools/move-file.js +0 -85
  187. package/dist/tools/path-utils.js +0 -51
  188. package/dist/tools/pipeline-run.js +0 -144
  189. package/dist/tools/preview.js +0 -2
  190. package/dist/tools/process-kill.js +0 -29
  191. package/dist/tools/process-list.js +0 -36
  192. package/dist/tools/process-log.js +0 -45
  193. package/dist/tools/question.js +0 -140
  194. package/dist/tools/read-file.js +0 -91
  195. package/dist/tools/recall.js +0 -117
  196. package/dist/tools/registry.js +0 -47
  197. package/dist/tools/remember.js +0 -67
  198. package/dist/tools/scope-check.js +0 -30
  199. package/dist/tools/search-history.js +0 -84
  200. package/dist/tools/subagent.js +0 -196
  201. package/dist/tools/types.js +0 -1
  202. package/dist/tools/user-input.js +0 -123
  203. package/dist/tools/web-browse.js +0 -86
  204. package/dist/tools/web-fetch.js +0 -98
  205. package/dist/tools/web-search.js +0 -78
  206. package/dist/tools/write-file.js +0 -81
  207. package/dist/ui/box.js +0 -77
  208. package/dist/ui/colors.js +0 -4
  209. package/dist/ui/diff.js +0 -178
  210. package/dist/ui/index.js +0 -6
  211. package/dist/ui/line-editor.js +0 -703
  212. package/dist/ui/line-math.js +0 -69
  213. package/dist/ui/md-formatter.js +0 -212
  214. package/dist/ui/output.js +0 -13
  215. package/dist/ui/plan-view.js +0 -103
  216. package/dist/ui/renderer.js +0 -209
  217. package/dist/ui/spinner.js +0 -70
  218. package/dist/ui/table.js +0 -144
@@ -1,152 +0,0 @@
1
- import { existsSync, readdirSync } from "fs";
2
- import { dirname, join, resolve } from "path";
3
- import { detectTestResults } from "../../tools/bash";
4
- import { processRegistry } from "../processes";
5
- /** Directories never searched for test files. */
6
- export const SKIP_DIRS = new Set([
7
- "node_modules",
8
- ".git",
9
- ".mma",
10
- "dist",
11
- "build",
12
- "coverage",
13
- ".next",
14
- ".nuxt",
15
- "vendor",
16
- ]);
17
- const TEST_EXT_RE = /\.(test|spec)\.[jt]sx?$/i;
18
- const TEST_STEP_RE = /\b(test(ing|s)?|тест(ы|ирование|ировать)?|провер\w*\s+тест|запустить\s+тест)\b|bun test|npm test|vitest|pytest|go test|jest|mocha/i;
19
- /** Recursive, depth-limited walk that stops early on the first test file. */
20
- export function findTestFile(dir, depth = 0) {
21
- if (depth > 5)
22
- return null;
23
- let entries;
24
- try {
25
- entries = readdirSync(dir, { withFileTypes: true });
26
- }
27
- catch {
28
- return null;
29
- }
30
- for (const e of entries) {
31
- const full = join(dir, e.name);
32
- if (e.isDirectory()) {
33
- if (SKIP_DIRS.has(e.name))
34
- continue;
35
- const found = findTestFile(full, depth + 1);
36
- if (found)
37
- return found;
38
- }
39
- else if (TEST_EXT_RE.test(e.name)) {
40
- return full;
41
- }
42
- }
43
- return null;
44
- }
45
- /** The plan's step descriptions mention running/checking tests. */
46
- export function hasTestStep(plan) {
47
- return plan.steps.some((s) => TEST_STEP_RE.test(s.description));
48
- }
49
- /** Extract failing test names from a runner's output (bun/vitest style). */
50
- function extractFailingNames(output, limit = 5) {
51
- const names = [];
52
- for (const m of output.matchAll(/\(fail\)\s*([^\n]+)/g)) {
53
- const name = m[1].trim();
54
- if (name && !names.includes(name))
55
- names.push(name);
56
- if (names.length >= limit)
57
- break;
58
- }
59
- return names;
60
- }
61
- export async function runTests(baseDir) {
62
- const entry = processRegistry.start("bun test", baseDir);
63
- const exited = await processRegistry.waitForExit(entry.id, 90_000);
64
- const output = entry.log.join("\n");
65
- processRegistry.remove(entry.id);
66
- if (!exited) {
67
- // Timed out — we cannot claim success or failure from an unfinished run.
68
- return {
69
- checked: true,
70
- passed: true, // don't block completion on an inconclusive run
71
- failed: 0,
72
- passedCount: 0,
73
- detail: "test run timed out after 90s — result unknown",
74
- command: "bun test",
75
- };
76
- }
77
- const run = detectTestResults(output);
78
- if (!run) {
79
- // No recognizable runner summary. A non-zero exit means the test run
80
- // (or its runner) failed — the audit must not pass on it; report a
81
- // single "at least one failed" since we cannot count them. Exit 0
82
- // with unrecognized output stays checked-but-inconclusive.
83
- return {
84
- checked: true,
85
- passed: entry.exitCode === 0,
86
- failed: entry.exitCode === 0 ? 0 : 1,
87
- passedCount: 0,
88
- detail: output.slice(0, 200).trim(),
89
- command: "bun test",
90
- };
91
- }
92
- const names = extractFailingNames(output);
93
- return {
94
- checked: true,
95
- passed: run.failed === 0,
96
- failed: run.failed,
97
- passedCount: run.passed,
98
- detail: names.length
99
- ? names.join("; ")
100
- : run.summary || `${run.failed} failed / ${run.passed} passed`,
101
- command: "bun test",
102
- };
103
- }
104
- /** First `error TS…` line in a tsc output, or null when none/inconclusive. */
105
- export function parseTypecheckErrors(output) {
106
- const line = output.split("\n").find((l) => /error TS\d+/.test(l));
107
- return line ? line.trim().slice(0, 300) : null;
108
- }
109
- /**
110
- * Locate the nearest project root that owns a tsconfig.json.
111
- *
112
- * The agent frequently creates the project in a nested subfolder of its
113
- * baseDir (e.g. `bicycle-shop/`), so the tsconfig lives at
114
- * `baseDir/bicycle-shop/` and the old `existsSync(baseDir/tsconfig.json)`
115
- * gate silently skipped the final typecheck — the audit passed even with
116
- * dozens of tsc errors (observed in both analyzed sessions). This walks each
117
- * candidate (baseDir plus the resolved step files) upward to the nearest
118
- * tsconfig.json, preferring the shallowest hit so the baseDir project wins
119
- * over a stray tsconfig further up. Returns null when no tsconfig exists in
120
- * any reachable subtree. `existingFiles` must contain resolved absolute paths
121
- * (relative tokens would resolve against the process cwd, not baseDir).
122
- */
123
- export function findTypecheckRoot(baseDir, existingFiles = []) {
124
- const candidates = [baseDir, ...existingFiles];
125
- let best = null;
126
- for (const start of candidates) {
127
- let dir = resolve(start);
128
- for (let depth = 0; depth <= 10; depth++) {
129
- if (existsSync(join(dir, "tsconfig.json"))) {
130
- if (!best || depth < best.depth)
131
- best = { depth, root: dir };
132
- break;
133
- }
134
- const parent = dirname(dir);
135
- if (parent === dir)
136
- break;
137
- dir = parent;
138
- }
139
- }
140
- return best?.root ?? null;
141
- }
142
- export async function runTypecheck(baseDir) {
143
- const entry = processRegistry.start("npx --no-install tsc --noEmit --skipLibCheck", baseDir);
144
- const exited = await processRegistry.waitForExit(entry.id, 90_000);
145
- const output = entry.log.join("\n");
146
- processRegistry.remove(entry.id);
147
- // A timeout, or output with no `error TS` lines (missing typescript / wrong
148
- // working dir) is inconclusive — never block completion on those.
149
- if (!exited)
150
- return null;
151
- return parseTypecheckErrors(output);
152
- }
@@ -1,218 +0,0 @@
1
- import { existsSync, readdirSync } from "fs";
2
- import { resolve, join, basename } from "path";
3
- import { t } from "../../i18n/index";
4
- import { extractFileLikeTokens, stripUrls } from "../hallucination/js-identifiers";
5
- import { SKIP_DIRS, findTestFile, findTypecheckRoot, hasTestStep, runTests, runTypecheck, } from "./audit-runners";
6
- export { parseTypecheckErrors } from "./audit-runners";
7
- const MASS_EDIT_THRESHOLD = 10;
8
- /** Subtree walk depth cap for file resolution. */
9
- const RESOLVE_MAX_DEPTH = 8;
10
- /**
11
- * Resolve a plan-step file token to an existing file.
12
- *
13
- * A step description frequently names a file by its bare basename
14
- * ("App.tsx") or a partially-prefixed path ("src/App.tsx") while the real
15
- * file lives in a nested project directory relative to the agent baseDir
16
- * (e.g. `bicycle-shop/src/App.tsx`). Naively resolving against baseDir
17
- * reports the file as missing — observed in session ses_mssl5izw where the
18
- * final audit gate rejected a fully-completed task with "1 files missing"
19
- * forever, sending the model into a read/retry loop.
20
- *
21
- * Strategy: exact resolve first, then a bounded subtree walk matching either
22
- * the exact basename or a path suffix (so both "App.tsx" and
23
- * "src/App.tsx" find "bicycle-shop/src/App.tsx").
24
- */
25
- export function findExistingFile(baseDir, filePath) {
26
- const direct = resolve(baseDir, filePath);
27
- if (existsSync(direct))
28
- return direct;
29
- const name = basename(filePath).toLowerCase();
30
- const suffix = filePath.replace(/\\/g, "/").toLowerCase();
31
- let found = null;
32
- const walk = (dir, depth) => {
33
- if (found || depth > RESOLVE_MAX_DEPTH)
34
- return;
35
- let entries;
36
- try {
37
- entries = readdirSync(dir, { withFileTypes: true });
38
- }
39
- catch {
40
- return;
41
- }
42
- for (const e of entries) {
43
- if (found)
44
- return;
45
- const full = join(dir, e.name);
46
- if (e.isDirectory()) {
47
- if (SKIP_DIRS.has(e.name))
48
- continue;
49
- walk(full, depth + 1);
50
- }
51
- else if (e.name.toLowerCase() === name ||
52
- full
53
- .replace(/\\/g, "/")
54
- .toLowerCase()
55
- .endsWith("/" + suffix)) {
56
- found = full;
57
- return;
58
- }
59
- }
60
- };
61
- walk(baseDir, 0);
62
- return found;
63
- }
64
- export class Auditor {
65
- baseDir;
66
- constructor(baseDir) {
67
- this.baseDir = baseDir;
68
- }
69
- async audit(plan) {
70
- // Only steps the model claims completed may require their named files to
71
- // exist. Skipped/pending steps must never contribute "missing files"
72
- // (observed: a skipped step's prose mention of "type.ts" kept the audit
73
- // gate rejecting a finished task with "1 files missing" forever).
74
- const doneCreate = plan.steps.filter((s) => s.status === "done" && s.kind !== "delete");
75
- const doneDelete = plan.steps.filter((s) => s.status === "done" && s.kind === "delete");
76
- const createText = doneCreate.map((s) => s.description.replace(/\([^)]*\)/g, " ")).join(" ");
77
- const deleteText = doneDelete.map((s) => s.description.replace(/\([^)]*\)/g, " ")).join(" ");
78
- // File tokens are extracted with the SAME shared filter as the
79
- // hallucination factual check (js-identifiers.ts). URLs are stripped
80
- // first so a hostname / https://host/repo.git in a step description
81
- // can never be reported as a missing file (observed: a git-remote
82
- // plan flagged `whom-pointing-jewel-maine.trycloudflare.com`).
83
- const createFiles = extractFileLikeTokens(stripUrls(createText));
84
- const deleteFiles = extractFileLikeTokens(stripUrls(deleteText));
85
- const missingFiles = [];
86
- const existingFiles = [];
87
- const leftoverFiles = [];
88
- for (const filePath of createFiles) {
89
- const resolved = findExistingFile(this.baseDir, filePath);
90
- if (resolved) {
91
- // Keep the resolved absolute path: findTypecheckRoot walks up
92
- // from it to the nearest tsconfig, and a relative token would
93
- // resolve against the process cwd instead of baseDir.
94
- existingFiles.push(resolved);
95
- }
96
- else {
97
- missingFiles.push(filePath);
98
- }
99
- }
100
- // A done delete-kind step requires its named files to be GONE.
101
- for (const filePath of deleteFiles) {
102
- if (findExistingFile(this.baseDir, filePath)) {
103
- leftoverFiles.push(filePath);
104
- }
105
- }
106
- // Real test run: when the plan includes a verification step, do not
107
- // trust the model's word (or exit codes) — actually execute the tests
108
- // and check the runner summary. This catches the "all 7 passing ✅"
109
- // final answers that hide 3 failing tests.
110
- let testRun = null;
111
- if (hasTestStep(plan) && findTestFile(this.baseDir)) {
112
- try {
113
- testRun = await runTests(this.baseDir);
114
- }
115
- catch {
116
- testRun = null; // infra failure — don't block on it
117
- }
118
- }
119
- // Real typecheck: the audit must catch "the build is broken" even when
120
- // every file exists (observed: deleting .js files left the .tsx sources
121
- // with 11 tsc errors and the agent still declared success). Runs only
122
- // when a tsconfig.json is reachable from baseDir or the resolved step
123
- // files AND the file checks already passed — an audit that is doomed by
124
- // missing/leftover files must not pay a 90s tsc spawn. Inconclusive
125
- // results never block.
126
- let typecheckError = null;
127
- if (missingFiles.length === 0 && leftoverFiles.length === 0) {
128
- const root = findTypecheckRoot(this.baseDir, existingFiles);
129
- if (root) {
130
- try {
131
- typecheckError = await runTypecheck(root);
132
- }
133
- catch {
134
- typecheckError = null;
135
- }
136
- }
137
- }
138
- const doneSteps = plan.steps.filter((s) => s.status === "done").length;
139
- // done + skipped are both deliberate terminal states: `done` claims the
140
- // work, `skipped` documents a decision that the step is not needed (the
141
- // deliverable gate's documented escape hatch). Neither is a lie the
142
- // audit must catch — a plan with a legitimately skipped step must be
143
- // able to pass (observed: skipped step 1 kept the task rejected forever).
144
- const terminalSteps = plan.steps.filter((s) => s.status === "done" || s.status === "skipped").length;
145
- const totalSteps = plan.steps.length;
146
- const auditedFiles = [...new Set([...createFiles, ...deleteFiles])];
147
- let massEditWarning = null;
148
- if (auditedFiles.length > MASS_EDIT_THRESHOLD) {
149
- massEditWarning = t("exec.mass_edit_warning", {
150
- count: String(auditedFiles.length),
151
- });
152
- }
153
- const testsFailing = testRun !== null && !testRun.passed;
154
- const typecheckFailing = typecheckError !== null;
155
- const passed = missingFiles.length === 0 && leftoverFiles.length === 0 && !testsFailing && !typecheckFailing;
156
- // A "Task complete ✓" summary for an unfinished plan is a lie the model
157
- // reads as a green light (observed: audit gate injected "[✓] Task
158
- // complete: 1/3 steps done" while the plan had 2 pending steps). The
159
- // summary must only claim completion when every step is actually done.
160
- const stepsPending = terminalSteps < totalSteps;
161
- let summary;
162
- if (missingFiles.length > 0) {
163
- summary = t("exec.audit_fail", {
164
- done: doneSteps,
165
- total: totalSteps,
166
- files: missingFiles.length,
167
- });
168
- }
169
- else if (leftoverFiles.length > 0) {
170
- summary = t("exec.audit_leftovers", {
171
- done: doneSteps,
172
- total: totalSteps,
173
- files: leftoverFiles.length,
174
- });
175
- }
176
- else if (testsFailing) {
177
- summary = t("exec.audit_fail_tests", {
178
- done: doneSteps,
179
- total: totalSteps,
180
- failed: String(testRun.failed),
181
- passed: String(testRun.passedCount),
182
- detail: testRun.detail,
183
- });
184
- }
185
- else if (typecheckFailing) {
186
- summary = t("exec.audit_fail_typecheck", {
187
- done: doneSteps,
188
- total: totalSteps,
189
- missing: String(missingFiles.length),
190
- typeError: typecheckError,
191
- });
192
- }
193
- else if (stepsPending) {
194
- summary = t("exec.audit_pending", {
195
- done: doneSteps,
196
- total: totalSteps,
197
- });
198
- }
199
- else {
200
- summary = t("exec.audit_pass", {
201
- done: doneSteps,
202
- total: totalSteps,
203
- files: existingFiles.length,
204
- });
205
- }
206
- return {
207
- passed,
208
- missingFiles,
209
- leftoverFiles,
210
- createdFiles: existingFiles,
211
- modifiedFiles: [],
212
- summary,
213
- massEditWarning,
214
- testRun,
215
- typecheckError,
216
- };
217
- }
218
- }
@@ -1,272 +0,0 @@
1
- import { t } from "../../i18n/index";
2
- import { detectTestResults } from "../../tools/bash";
3
- import { forbiddenWindowsCommand } from "./windows-commands";
4
- import { platform } from "os";
5
- /**
6
- * Cooldown (in iterations) between stuck-recovery injections. Exported so the
7
- * ExecutionModule can seed its state's lastRecoveryIteration with the same
8
- * offset the plugin compares against.
9
- */
10
- export const STUCK_RECOVERY_COOLDOWN = 5;
11
- const MAX_PLAN_WARNINGS_BEFORE_BLOCK = 3;
12
- const FORCE_SKIP_THRESHOLD = 10;
13
- /**
14
- * The execution plugin hooks, extracted verbatim from ExecutionModule.getPlugin().
15
- * All shared mutable state (pendingMessages, forbiddenBashFailures, state,
16
- * tracker) is owned by the module and mutated by reference through `deps`.
17
- */
18
- export function createExecutionPlugin(deps) {
19
- return {
20
- name: "execution",
21
- onBeforeThink: (ctx) => {
22
- // Flush deferred <system-summary> messages first so they are visible
23
- // to the next model call but never interleave tool messages.
24
- if (ctx.contextManager && deps.pendingMessages.length > 0) {
25
- for (const m of deps.pendingMessages.splice(0)) {
26
- ctx.contextManager.addMessage(m);
27
- }
28
- }
29
- if (deps.trackerRef.current?.isComplete()) {
30
- // All steps done/skipped — stop counting "no progress" against
31
- // the last step and silence step-stuck warnings for a finished
32
- // plan, BUT keep the tool-call history so a read-without-write
33
- // loop is still caught (observed: plan complete + false-positive
34
- // audit → the model re-read files for 50+ iterations with zero
35
- // writes and no recovery hint).
36
- deps.stuckDetector.resetStepProgress();
37
- deps.state.consecutivePlanWarnings = 0;
38
- deps.state.lastStepId = -1;
39
- deps.state.stuckNotified = false;
40
- }
41
- else {
42
- const step = deps.trackerRef.current?.getCurrentStep();
43
- if (deps.trackerRef.current && step) {
44
- if (step.id !== deps.state.lastStepId) {
45
- deps.state.consecutivePlanWarnings = 0;
46
- deps.state.lastStepId = step.id;
47
- deps.state.stuckNotified = false;
48
- }
49
- deps.stuckDetector.setCurrentStep(step.id, step.description);
50
- deps.stuckDetector.recordIteration(step.id);
51
- }
52
- else {
53
- deps.stuckDetector.reset();
54
- deps.state.consecutivePlanWarnings = 0;
55
- deps.state.lastStepId = -1;
56
- deps.state.stuckNotified = false;
57
- const iter = typeof ctx.iteration === "number" ? ctx.iteration : 0;
58
- if (iter === 3 && !deps.trackerRef.current && ctx.contextManager) {
59
- ctx.contextManager.addMessage({
60
- role: "user",
61
- 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>`,
62
- });
63
- }
64
- if (iter >= 6 && !deps.trackerRef.current && ctx.contextManager) {
65
- ctx.contextManager.addMessage({
66
- role: "user",
67
- 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>`,
68
- });
69
- }
70
- }
71
- }
72
- const stuckReason = deps.stuckDetector.getStuckReason();
73
- if (stuckReason) {
74
- // Log once per stuck episode instead of spamming every iteration.
75
- const logIt = deps.stuckDetector.isStuck() ? !deps.state.stuckNotified : true;
76
- if (logIt) {
77
- ctx.logger?.warn(stuckReason);
78
- ctx.sessionLog?.plan("stuck-warning", stuckReason, typeof ctx.iteration === "number" ? ctx.iteration : undefined);
79
- if (deps.stuckDetector.isStuck())
80
- deps.state.stuckNotified = true;
81
- }
82
- }
83
- if (deps.stuckDetector.isStuck() ||
84
- deps.stuckDetector.hasRepetitiveToolCalls() ||
85
- deps.stuckDetector.hasReadOnlyLoop()) {
86
- const currentIter = typeof ctx.iteration === "number" ? ctx.iteration : 0;
87
- if (currentIter - deps.state.lastRecoveryIteration >= STUCK_RECOVERY_COOLDOWN) {
88
- const recovery = deps.stuckDetector.getRecoveryMessage();
89
- if (recovery && ctx.contextManager) {
90
- const lastError = deps.stuckDetector.getLastErrorOutput();
91
- const skillHint = lastError
92
- ? "\nIf you have relevant skills available, consider loading one with load_skill for expert guidance."
93
- : "";
94
- // Actionable hints based on actual error output
95
- const actionableHints = deps.stuckDetector.getActionableHints();
96
- const actionableHintStr = actionableHints.length > 0
97
- ? `\n${t("exec.hints", { hints: actionableHints.map((h) => `- ${h}`).join("\n") })}`
98
- : "";
99
- // Tool alternative suggestion
100
- const alternative = deps.stuckDetector.getToolAlternative();
101
- const altHint = alternative
102
- ? `\nTool "${deps.stuckDetector.getLastFailedTool()}" is failing. Try "${alternative}" instead.`
103
- : "";
104
- ctx.contextManager.addMessage({
105
- role: "user",
106
- content: `<system-summary>${recovery}${skillHint}${actionableHintStr}${altHint}</system-summary>`,
107
- });
108
- }
109
- const hints = deps.stuckDetector.getHints();
110
- if (hints.length > 0 && ctx.contextManager) {
111
- const hintMsg = t("exec.hints", {
112
- hints: hints.map((h) => `- ${h}`).join("\n"),
113
- });
114
- ctx.contextManager.addMessage({
115
- role: "user",
116
- content: `<system-summary>${hintMsg}</system-summary>`,
117
- });
118
- }
119
- deps.stuckDetector.recordEscalation();
120
- deps.state.lastRecoveryIteration = currentIter;
121
- if (deps.stuckDetector.shouldEscalate() && ctx.onMeta) {
122
- const escalation = t("exec.escalation", {
123
- stepId: String(deps.trackerRef.current?.getCurrentStep()?.id ?? "?"),
124
- description: deps.trackerRef.current?.getCurrentStep()?.description ?? "",
125
- });
126
- ctx.onMeta(escalation);
127
- }
128
- // Force skip after too many iterations on the same step
129
- if (deps.stuckDetector.getIterationsOnCurrentStep() >= FORCE_SKIP_THRESHOLD &&
130
- ctx.contextManager) {
131
- const step = deps.trackerRef.current?.getCurrentStep();
132
- ctx.contextManager.addMessage({
133
- role: "user",
134
- content: `<system-summary>STOP. Step ${step?.id ?? "?"} ("${step?.description ?? ""}") took ${deps.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>`,
135
- });
136
- }
137
- }
138
- }
139
- },
140
- onBeforeTool: (_ctx, call) => {
141
- const warning = deps.checkPlanAlignment(call);
142
- if (warning) {
143
- deps.pendingMessages.push({
144
- role: "user",
145
- content: `<system-summary>${warning}</system-summary>`,
146
- });
147
- deps.state.consecutivePlanWarnings++;
148
- if (deps.state.consecutivePlanWarnings >= MAX_PLAN_WARNINGS_BEFORE_BLOCK) {
149
- deps.pendingMessages.push({
150
- role: "user",
151
- content: `<system-summary>${t("exec.plan_blocked", { step: String(deps.trackerRef.current?.getCurrentStep()?.id ?? "?"), max: MAX_PLAN_WARNINGS_BEFORE_BLOCK })}</system-summary>`,
152
- });
153
- // Block with an explicit reason so the model sees WHY the call
154
- // was blocked (executor shows it via tool.blocked_reason).
155
- return t("exec.plan_blocked", {
156
- step: String(deps.trackerRef.current?.getCurrentStep()?.id ?? "?"),
157
- max: MAX_PLAN_WARNINGS_BEFORE_BLOCK,
158
- });
159
- }
160
- }
161
- else {
162
- deps.state.consecutivePlanWarnings = 0;
163
- }
164
- return true;
165
- },
166
- onToolCall: (ctx) => {
167
- const toolName = ctx?.toolName;
168
- const args = ctx?.args;
169
- if (toolName && args) {
170
- deps.stuckDetector.recordToolCall(toolName, args);
171
- }
172
- },
173
- onAfterTool: (ctx, call, result) => {
174
- // Remember every bash invocation (successful or not) so stuck
175
- // detection can reason about e.g. repeated runs with empty output.
176
- if (call.name === "bash") {
177
- deps.stuckDetector.recordBashOutput(String(call.arguments?.command ?? ""), String(result.output ?? ""));
178
- }
179
- // A write/edit/bash that reports success but still carries a
180
- // type/syntax error in its output is NOT a success — the model must
181
- // fix the actual error, not keep rewriting the file blindly. Feed it
182
- // to the stuck detector so the rewrite-loop hint fires.
183
- const toolText = String(result.output ?? "");
184
- if (/error TS\d+|\[Project typecheck failed\]|\[Syntax check failed\]/.test(toolText)) {
185
- deps.stuckDetector.recordToolError(call.name, toolText.slice(0, 300));
186
- }
187
- if (!result.success) {
188
- // Hard stop for forbidden Windows commands: after the second
189
- // failure of the same forbidden command (grep/sed/ls/find/…),
190
- // inject a STOP message so the session stops burning bash calls
191
- // on commands cmd.exe does not have. Mirrors the bash tool's own
192
- // win32-gated hard-stop (src/tools/bash.ts) — on macOS/Linux
193
- // these commands exist and fail for legit reasons.
194
- if (call.name === "bash" && platform() === "win32") {
195
- const cmd = String(call.arguments?.command ?? "");
196
- const forbidden = forbiddenWindowsCommand(cmd);
197
- if (forbidden) {
198
- const n = (deps.forbiddenBashFailures.get(forbidden) || 0) + 1;
199
- deps.forbiddenBashFailures.set(forbidden, n);
200
- if (n === 2) {
201
- deps.pendingMessages.push({
202
- role: "user",
203
- content: `<system-summary>${t("exec.forbidden_cmd", { cmd: forbidden })}</system-summary>`,
204
- });
205
- }
206
- }
207
- }
208
- deps.stuckDetector.recordToolError(call.name, result.output);
209
- // Immediately queue an actionable hint on failure (don't wait for
210
- // the stuck threshold). Flushed in onBeforeThink so it does not
211
- // land between tool messages.
212
- const actionableHints = deps.stuckDetector.getActionableHints();
213
- const alternative = deps.stuckDetector.getToolAlternative();
214
- if (actionableHints.length > 0 || alternative) {
215
- const parts = [...actionableHints];
216
- if (alternative) {
217
- parts.push(`Tool "${call.name}" crashed. Try "${alternative}" instead.`);
218
- }
219
- deps.pendingMessages.push({
220
- role: "user",
221
- content: `<system-summary>${t("exec.hints", { hints: parts.map((h) => `- ${h}`).join("\n") })}</system-summary>`,
222
- });
223
- }
224
- }
225
- else {
226
- deps.stuckDetector.recordToolSuccess();
227
- // When a bash command runs code successfully, suggest marking the
228
- // step done — unless the output shows failing tests, which must
229
- // never be reported as a clean success.
230
- if (call.name === "bash" && result.success) {
231
- const cmd = String(call.arguments?.command ?? "");
232
- const testRun = detectTestResults(String(result.output ?? ""));
233
- if (testRun && testRun.failed > 0) {
234
- deps.pendingMessages.push({
235
- role: "user",
236
- content: `<system-summary>${testRun.framework} reported ${testRun.failed} FAILING test(s) (${testRun.passed} passing). Do NOT mark the current step as done — fix the failing tests (read the failure output, correct the code) and re-run them until all pass.</system-summary>`,
237
- });
238
- }
239
- else if (testRun && testRun.failed === 0 && testRun.passed > 0) {
240
- deps.pendingMessages.push({
241
- role: "user",
242
- content: `<system-summary>${testRun.framework}: all ${testRun.passed} test(s) passed for "${cmd}". You may mark the current step as done via plan update step=N status=done.</system-summary>`,
243
- });
244
- }
245
- else if (/node|tsx|ts-node|python|npm\s+(start|test|run)/.test(cmd)) {
246
- deps.pendingMessages.push({
247
- role: "user",
248
- 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>`,
249
- });
250
- }
251
- }
252
- }
253
- if (result.success && (call.name === "write_file" || call.name === "edit_file")) {
254
- const filePath = call.arguments?.path;
255
- if (filePath) {
256
- deps.stuckDetector.recordFileRewrite(filePath);
257
- if (deps.stuckDetector.hasExcessiveRewrites()) {
258
- const file = deps.stuckDetector.getExcessiveRewriteFile();
259
- const count = deps.stuckDetector.getFileRewriteCount(file);
260
- if (ctx.onMeta) {
261
- ctx.onMeta(t("exec.file_rewrite_warning", {
262
- file: file,
263
- count: String(count),
264
- }));
265
- }
266
- }
267
- }
268
- deps.advancePlanIfStepComplete(ctx.contextManager, ctx.sessionLog);
269
- }
270
- },
271
- };
272
- }
@@ -1,8 +0,0 @@
1
- export { ExecutionModule } from "./module";
2
- export { PlanCreator } from "./planner";
3
- export { PlanTracker } from "./tracker";
4
- export { StepVerifier } from "./verifier";
5
- export { StuckDetector } from "./stuck-detector";
6
- export { Auditor } from "./auditor";
7
- export { PlanPersister } from "./plan-persister";
8
- export { PlanStore } from "./plan-store";