micro-models-agent 0.43.0 → 0.43.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.
Files changed (2) hide show
  1. package/dist/main.js +650 -537
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -2358,7 +2358,9 @@ var init_en = __esm(() => {
2358
2358
  "file.is_directory": "Is a directory, use rmdir: {path}",
2359
2359
  "file.replaced_in": "Replaced in {path}",
2360
2360
  "file.string_not_found": `String not found in file:
2361
- {str}`,
2361
+ {str}
2362
+
2363
+ Use read_file on {path} to see the current content before editing — the target may differ (whitespace, line endings) or was already replaced.`,
2362
2364
  "file.moved": "Moved {from} → {to}",
2363
2365
  "file.not_found_short": "Not found: {path}",
2364
2366
  "file.notfound_resolved": "File not found: {path} (resolved to {resolved})",
@@ -2540,6 +2542,9 @@ Command: {command}`,
2540
2542
  "plan.kinds_invalid": 'Invalid step kind(s): {kinds}. Use "create" or "delete".',
2541
2543
  "plan.kinds_not_array": 'kinds must be an array of "create" or "delete"',
2542
2544
  "plan.deliverables_remain": "Cannot mark step {step} done: the files it names still exist: {files}. This step is kind=delete — delete these files first (or set status=skipped if they should stay).",
2545
+ "plan.typecheck_failed": `Cannot mark step {step} done: the last check still reports a compile error:
2546
+ {error}
2547
+ Fix the error and re-edit the file (a clean write clears the failure), or mark the step status=skipped if it is not needed.`,
2543
2548
  "plan.show_header": "Plan status:",
2544
2549
  "plan.show_empty": "(plan has no steps)",
2545
2550
  "plan.list_header": "Plans:",
@@ -3012,7 +3017,9 @@ var init_ru = __esm(() => {
3012
3017
  "file.is_directory": "Это каталог, используйте rmdir: {path}",
3013
3018
  "file.replaced_in": "Заменено в {path}",
3014
3019
  "file.string_not_found": `Строка не найдена в файле:
3015
- {str}`,
3020
+ {str}
3021
+
3022
+ Используй read_file для {path}, чтобы увидеть текущее содержимое файла перед редактированием — искомая строка может отличаться (пробелы, переводы строк) или уже была заменена.`,
3016
3023
  "file.moved": "Перемещён {from} → {to}",
3017
3024
  "file.not_found_short": "Не найдено: {path}",
3018
3025
  "file.empty": "(пусто)",
@@ -3192,6 +3199,9 @@ var init_ru = __esm(() => {
3192
3199
  "plan.kinds_invalid": 'Недопустимый kind шага: {kinds}. Используй "create" или "delete".',
3193
3200
  "plan.kinds_not_array": 'kinds должен быть массивом значений "create" или "delete"',
3194
3201
  "plan.deliverables_remain": "Нельзя пометить шаг {step} как выполненный: указанные файлы всё ещё существуют: {files}. Шаг имеет kind=delete — сначала удали их (или поставь status=skipped, если они должны остаться).",
3202
+ "plan.typecheck_failed": `Нельзя отметить шаг {step} выполненным: последняя проверка всё ещё сообщает об ошибке компиляции:
3203
+ {error}
3204
+ Исправь ошибку и снова отредактируй файл (чистая запись снимает блокировку), либо отметь шаг status=skipped, если он не нужен.`,
3195
3205
  "plan.show_header": "Статус плана:",
3196
3206
  "plan.show_empty": "(в плане нет шагов)",
3197
3207
  "plan.list_header": "Планы:",
@@ -5697,12 +5707,17 @@ class ToolExecutor {
5697
5707
  toolCallId: call.id
5698
5708
  };
5699
5709
  }
5700
- for (const plugin of this.pluginManager.getAllPlugins()) {
5701
- if (plugin.onAfterTool) {
5702
- try {
5703
- await plugin.onAfterTool(this.ctx, call, result);
5704
- } catch (e) {
5705
- this.ctx.logger.warn(`Plugin onAfterTool error: ${e.message}`);
5710
+ if (signal?.aborted) {
5711
+ this.ctx.logger.debug(`Tool ${call.name}: abort, skipping onAfterTool plugins`);
5712
+ } else {
5713
+ const pluginCtx = signal ? { ...this.ctx, signal } : this.ctx;
5714
+ for (const plugin of this.pluginManager.getAllPlugins()) {
5715
+ if (plugin.onAfterTool) {
5716
+ try {
5717
+ await plugin.onAfterTool(pluginCtx, call, result);
5718
+ } catch (e) {
5719
+ this.ctx.logger.warn(`Plugin onAfterTool error: ${e.message}`);
5720
+ }
5706
5721
  }
5707
5722
  }
5708
5723
  }
@@ -6759,7 +6774,7 @@ var init_edit_file = __esm(() => {
6759
6774
  if (!content.includes(oldStr)) {
6760
6775
  return {
6761
6776
  success: false,
6762
- output: t("file.string_not_found", { str: oldStr })
6777
+ output: t("file.string_not_found", { str: oldStr, path })
6763
6778
  };
6764
6779
  }
6765
6780
  const updated = content.replace(oldStr, newStr);
@@ -11697,6 +11712,8 @@ ${t("tool.truncated", { tokens: Math.ceil(removedChars / 2) })}`;
11697
11712
  slog.logToolCall(call, iteration);
11698
11713
  const tokensBeforeTool = contextManager.getEstimatedTokens();
11699
11714
  const result = await toolExecutor.execute(call, this.abortController?.signal);
11715
+ if (this.shutdownRequested)
11716
+ break;
11700
11717
  const duration = Date.now() - startTime;
11701
11718
  if (!result.success)
11702
11719
  anyToolFailed = true;
@@ -11775,6 +11792,8 @@ ${t("tool.truncated", { tokens: Math.ceil(removedChars / 2) })}`;
11775
11792
  }
11776
11793
  }
11777
11794
  }
11795
+ if (this.shutdownRequested)
11796
+ break;
11778
11797
  if (anyToolFailed) {
11779
11798
  consecutiveToolFailures++;
11780
11799
  } else {
@@ -16808,18 +16827,25 @@ class LintOnWritePlugin {
16808
16827
  const fullPath = resolve18(ctx.baseDir, path);
16809
16828
  if (!existsSync31(fullPath))
16810
16829
  return;
16830
+ const signal = ctx.signal;
16831
+ if (signal?.aborted)
16832
+ return;
16811
16833
  const ext = extname4(fullPath);
16812
- const syntaxError = await this.checkSyntax(fullPath, ext, ctx.baseDir);
16834
+ const syntaxError = await this.checkSyntax(fullPath, ext, ctx.baseDir, signal);
16835
+ if (signal?.aborted)
16836
+ return;
16813
16837
  if (syntaxError) {
16814
16838
  result.output += `
16815
16839
 
16816
16840
  [Syntax check failed]: ${syntaxError}`;
16817
16841
  return;
16818
16842
  }
16819
- await this.runProjectLint(ctx, result);
16820
- await this.runProjectTypeCheck(fullPath, ctx.baseDir, result);
16843
+ await this.runProjectLint(ctx, result, signal);
16844
+ if (signal?.aborted)
16845
+ return;
16846
+ await this.runProjectTypeCheck(fullPath, ctx.baseDir, result, signal);
16821
16847
  }
16822
- async checkSyntax(filePath, ext, baseDir) {
16848
+ async checkSyntax(filePath, ext, baseDir, signal) {
16823
16849
  if (ext === ".ts" || ext === ".tsx" || ext === ".cts" || ext === ".mts") {
16824
16850
  let content = "";
16825
16851
  try {
@@ -16833,7 +16859,9 @@ class LintOnWritePlugin {
16833
16859
  return cached.error;
16834
16860
  }
16835
16861
  try {
16836
- await runAsync(`bun build --no-bundle --target=bun "${filePath}"`, baseDir, 1e4);
16862
+ await runAsync(`bun build --no-bundle --target=bun "${filePath}"`, baseDir, 1e4, signal);
16863
+ if (signal?.aborted)
16864
+ return null;
16837
16865
  syntaxCache.set(filePath, { hash, error: null });
16838
16866
  return null;
16839
16867
  } catch (err) {
@@ -16849,7 +16877,7 @@ class LintOnWritePlugin {
16849
16877
  }
16850
16878
  if (ext === ".js" || ext === ".jsx" || ext === ".cjs" || ext === ".mjs") {
16851
16879
  try {
16852
- await runAsync(`node --check "${filePath}"`, baseDir, 5000);
16880
+ await runAsync(`node --check "${filePath}"`, baseDir, 5000, signal);
16853
16881
  return null;
16854
16882
  } catch (err) {
16855
16883
  const stderr = err.stderr?.toString() || "";
@@ -16860,7 +16888,7 @@ class LintOnWritePlugin {
16860
16888
  }
16861
16889
  return null;
16862
16890
  }
16863
- async runProjectLint(ctx, result) {
16891
+ async runProjectLint(ctx, result, signal) {
16864
16892
  try {
16865
16893
  const packageJsonPath = join25(ctx.baseDir, "package.json");
16866
16894
  if (!existsSync31(packageJsonPath)) {
@@ -16872,7 +16900,7 @@ class LintOnWritePlugin {
16872
16900
  return;
16873
16901
  }
16874
16902
  ctx.logger.debug(`Running lint: ${lintScript}`);
16875
- await runAsync(lintScript, ctx.baseDir, 30000);
16903
+ await runAsync(lintScript, ctx.baseDir, 30000, signal);
16876
16904
  ctx.logger.debug("Lint passed");
16877
16905
  } catch (err) {
16878
16906
  const stderr = (err.stderr?.toString?.() || "").trim();
@@ -16886,7 +16914,7 @@ class LintOnWritePlugin {
16886
16914
  [Project lint failed]: ${detail}`;
16887
16915
  }
16888
16916
  }
16889
- async runProjectTypeCheck(filePath, baseDir, result) {
16917
+ async runProjectTypeCheck(filePath, baseDir, result, signal) {
16890
16918
  const projectRoot = findProjectRoot(filePath, baseDir, ["tsconfig.json", "package.json"]);
16891
16919
  const tsconfigPath = join25(projectRoot, "tsconfig.json");
16892
16920
  if (!existsSync31(tsconfigPath)) {
@@ -16903,7 +16931,7 @@ class LintOnWritePlugin {
16903
16931
  return;
16904
16932
  }
16905
16933
  this._checkTimestamp = now;
16906
- this._checkPromise = this.runTscCheck(projectRoot);
16934
+ this._checkPromise = this.runTscCheck(projectRoot, signal);
16907
16935
  const error = await this._checkPromise;
16908
16936
  if (error) {
16909
16937
  result.output += `
@@ -16911,9 +16939,9 @@ class LintOnWritePlugin {
16911
16939
  [Project typecheck failed]: ${error}`;
16912
16940
  }
16913
16941
  }
16914
- async runTscCheck(baseDir) {
16942
+ async runTscCheck(baseDir, signal) {
16915
16943
  try {
16916
- await runAsync(`npx tsc --noEmit --skipLibCheck`, baseDir, 30000);
16944
+ await runAsync(`npx tsc --noEmit --skipLibCheck`, baseDir, 30000, signal);
16917
16945
  return null;
16918
16946
  } catch (err) {
16919
16947
  if (err.status === 127 || err.message.includes("not found") || err.message.includes("ENOENT")) {
@@ -16929,7 +16957,7 @@ class LintOnWritePlugin {
16929
16957
  }
16930
16958
  }
16931
16959
  }
16932
- function runAsync(command, cwd, timeoutMs) {
16960
+ function runAsync(command, cwd, timeoutMs, signal) {
16933
16961
  return new Promise((resolve19, reject) => {
16934
16962
  const child = spawn5(command, {
16935
16963
  cwd,
@@ -16950,15 +16978,27 @@ function runAsync(command, cwd, timeoutMs) {
16950
16978
  child.kill();
16951
16979
  reject(new Error(`Command timed out after ${timeoutMs}ms`));
16952
16980
  }, timeoutMs);
16981
+ const onAbort = () => child.kill();
16982
+ if (signal?.aborted) {
16983
+ child.kill();
16984
+ } else {
16985
+ signal?.addEventListener("abort", onAbort, { once: true });
16986
+ }
16953
16987
  child.on("error", (err) => {
16954
16988
  clearTimeout(timer);
16955
- reject(err);
16989
+ signal?.removeEventListener("abort", onAbort);
16990
+ if (signal?.aborted) {
16991
+ resolve19({ stdout, stderr });
16992
+ } else {
16993
+ reject(err);
16994
+ }
16956
16995
  });
16957
16996
  child.on("close", (code) => {
16958
16997
  clearTimeout(timer);
16998
+ signal?.removeEventListener("abort", onAbort);
16959
16999
  stdout += decoder.decode();
16960
17000
  stderr += decoder.decode();
16961
- if (code === 0) {
17001
+ if (signal?.aborted || code === 0) {
16962
17002
  resolve19({ stdout, stderr });
16963
17003
  } else {
16964
17004
  const err = new Error(`Command failed with exit code ${code}`);
@@ -17372,114 +17412,426 @@ var init_plan_coverage = __esm(() => {
17372
17412
  ]);
17373
17413
  });
17374
17414
 
17375
- // src/modules/execution/plan-tool.ts
17376
- function createPlanToolDefinitions(deps) {
17377
- return [
17378
- {
17379
- name: "plan",
17380
- alwaysOn: true,
17381
- description: `Create, update, show, abort, list, switch, delete, purge, or re-plan multi-step plans.
17382
-
17383
- Actions:
17384
- - create: Start a new plan. Previous active plan is auto-preserved: incomplete → draft, complete → archive.
17385
- - update: Mark step status (done/failed/skipped), or rebuild plan with new steps.
17386
- - show: Print current plan checklist.
17387
- - abort: Archive the current plan (or the plan given by id) and clear the active slot.
17388
- - list: Show all plans (active, drafts, archived) with progress.
17389
- - switch: Make a different plan active (by plan id).
17390
- - re-plan: Iterative replanning: keep completed steps, replace remaining with new steps.
17391
- - delete: Permanently delete a plan by id (plan delete id=plan_xxx).
17392
- - purge: Delete ALL plans (active, drafts, archived).
17415
+ // src/modules/execution/windows-commands.ts
17416
+ function firstWord(command) {
17417
+ const segs = command.trim().split(/[\s;|&]+/);
17418
+ return (segs[0] || "").replace(/[^\w-]/g, "").toLowerCase();
17419
+ }
17420
+ function forbiddenWindowsCommand(command) {
17421
+ const m = command.match(/^\s*cd\s+\S+\s*&&\s*(\S+)/);
17422
+ if (m)
17423
+ return forbiddenWindowsCommand(m[1]);
17424
+ const word = firstWord(command);
17425
+ if (word && FORBIDDEN_COMMANDS.has(word))
17426
+ return word;
17427
+ return null;
17428
+ }
17429
+ var FORBIDDEN_COMMANDS;
17430
+ var init_windows_commands = __esm(() => {
17431
+ FORBIDDEN_COMMANDS = new Set([
17432
+ "grep",
17433
+ "sed",
17434
+ "ls",
17435
+ "find",
17436
+ "rm",
17437
+ "touch",
17438
+ "which",
17439
+ "diff",
17440
+ "cp",
17441
+ "mv"
17442
+ ]);
17443
+ });
17393
17444
 
17394
- Write CONCRETE steps with exact file paths and commands:
17395
- - Specify WHICH files to create with exact paths (e.g. "create src/components/Header.tsx with navigation and logo")
17396
- - Specify WHICH packages to install (e.g. "run npm install react react-dom")
17397
- - Specify WHICH CLI commands to run with exact arguments
17398
- - Each step should include at least one file extension (.ts, .tsx, .json, etc.) or a command verb (install, create, build, run, add, init)
17399
- - Good: "Создать src/components/Header.tsx с навигацией и логотипом"
17400
- - Bad: "Настройка проекта" (too vague — what exactly needs to be configured?)
17401
- - For steps that REMOVE files, pass kinds: ["delete"] aligned with steps (the done-gate verifies those files are gone instead of demanding they exist).`,
17402
- parameters: {
17403
- type: "object",
17404
- properties: {
17405
- action: {
17406
- type: "string",
17407
- enum: [
17408
- "create",
17409
- "update",
17410
- "show",
17411
- "abort",
17412
- "list",
17413
- "switch",
17414
- "re-plan",
17415
- "delete",
17416
- "purge"
17417
- ]
17418
- },
17419
- title: { type: "string" },
17420
- steps: { type: "array", items: { type: "string" } },
17421
- kinds: {
17422
- type: "array",
17423
- items: { type: "string", enum: ["create", "delete"] },
17424
- description: 'Optional per-step intent, aligned 1:1 with steps. Pass "delete" for steps whose purpose is REMOVING files — the done-gate then requires those files to be GONE (not present). Defaults to "create".'
17425
- },
17426
- step: { type: "number" },
17427
- status: { type: "string", enum: ["done", "failed", "skipped"] },
17428
- note: { type: "string" },
17429
- id: { type: "string", description: "Plan id (for switch action)" }
17430
- },
17431
- required: ["action"]
17432
- },
17433
- handler: async (_ctx, args) => {
17434
- const action = String(args.action);
17435
- if (action === "list") {
17436
- const metas = deps.store.listAll();
17437
- if (metas.length === 0) {
17438
- return { success: true, output: t("plan.list_empty") };
17439
- }
17440
- const lines = metas.map((m) => {
17441
- const icon = m.status === "active" ? "[*]" : m.status === "draft" ? "[ ]" : "[-]";
17442
- const namePart = m.name ? ` (${m.name})` : "";
17443
- return `${icon} ${m.id}${namePart} — ${m.title} ${m.doneCount}/${m.stepCount}`;
17444
- });
17445
- lines.push("");
17446
- lines.push(t("plan.list_legend"));
17447
- return {
17448
- success: true,
17449
- output: `${t("plan.list_header")}
17450
- ${lines.join(`
17451
- `)}`
17452
- };
17445
+ // src/modules/execution/execution-plugin.ts
17446
+ import { platform as platform6 } from "os";
17447
+ function normalizeBrokenPath(p) {
17448
+ return p.replace(/\\/g, "/").replace(/^\.\//, "");
17449
+ }
17450
+ function parseBrokenFile(line) {
17451
+ const trimmed = line.trim();
17452
+ const tsc = /^(.+?)\s*\(\d+,\d+\)\s*:\s*error TS\d+/.exec(trimmed);
17453
+ if (tsc)
17454
+ return normalizeBrokenPath(tsc[1]);
17455
+ const bn = /^(.+?):\d+:\d+\s*:\s*(?:error|SyntaxError)/.exec(trimmed);
17456
+ if (bn)
17457
+ return normalizeBrokenPath(bn[1]);
17458
+ return null;
17459
+ }
17460
+ function extractBrokenFiles(output) {
17461
+ const out = [];
17462
+ for (const line of output.split(`
17463
+ `)) {
17464
+ const marker = /\[(?:Project typecheck failed|Syntax check failed)\]:\s*(.+)$/.exec(line);
17465
+ if (!marker)
17466
+ continue;
17467
+ const errLine = marker[1].trim();
17468
+ out.push({ file: parseBrokenFile(errLine) ?? "", error: errLine.slice(0, 300) });
17469
+ }
17470
+ return out;
17471
+ }
17472
+ function stepTypecheckGate(failures, stepDescription) {
17473
+ if (failures.size === 0)
17474
+ return null;
17475
+ const projectError = failures.get("");
17476
+ if (projectError)
17477
+ return projectError;
17478
+ const stepTokens = extractFileLikeTokens(stripUrls(stepDescription)).map((p) => normalizeBrokenPath(p).toLowerCase());
17479
+ if (stepTokens.length === 0)
17480
+ return null;
17481
+ for (const [file, err] of failures) {
17482
+ const f = normalizeBrokenPath(file).toLowerCase();
17483
+ if (stepTokens.some((tok) => f.endsWith(tok) || tok.endsWith(f)))
17484
+ return err;
17485
+ }
17486
+ return null;
17487
+ }
17488
+ function createExecutionPlugin(deps) {
17489
+ return {
17490
+ name: "execution",
17491
+ onBeforeThink: (ctx) => {
17492
+ if (ctx.contextManager && deps.pendingMessages.length > 0) {
17493
+ for (const m of deps.pendingMessages.splice(0)) {
17494
+ ctx.contextManager.addMessage(m);
17453
17495
  }
17454
- if (action === "switch") {
17455
- const planId = String(args.id || "");
17456
- if (!planId) {
17457
- return { success: false, output: t("plan.switch_no_id") };
17496
+ }
17497
+ if (deps.trackerRef.current?.isComplete()) {
17498
+ deps.stuckDetector.resetStepProgress();
17499
+ deps.state.consecutivePlanWarnings = 0;
17500
+ deps.state.lastStepId = -1;
17501
+ deps.state.stuckNotified = false;
17502
+ deps.state.mutationsWithoutPlan = 0;
17503
+ deps.state.planNudgeSent = false;
17504
+ } else {
17505
+ const step = deps.trackerRef.current?.getCurrentStep();
17506
+ if (deps.trackerRef.current && step) {
17507
+ if (step.id !== deps.state.lastStepId) {
17508
+ deps.state.consecutivePlanWarnings = 0;
17509
+ deps.state.lastStepId = step.id;
17510
+ deps.state.stuckNotified = false;
17458
17511
  }
17459
- const found = deps.store.find(planId);
17460
- if (!found) {
17461
- return {
17462
- success: false,
17463
- output: t("plan.not_found", { id: planId })
17464
- };
17512
+ deps.stuckDetector.setCurrentStep(step.id, step.description);
17513
+ deps.stuckDetector.recordIteration(step.id);
17514
+ deps.state.mutationsWithoutPlan = 0;
17515
+ deps.state.planNudgeSent = false;
17516
+ } else {
17517
+ deps.stuckDetector.reset();
17518
+ deps.state.consecutivePlanWarnings = 0;
17519
+ deps.state.lastStepId = -1;
17520
+ deps.state.stuckNotified = false;
17521
+ const mutations = deps.state.mutationsWithoutPlan;
17522
+ if (mutations >= PLAN_NUDGE_THRESHOLD && !deps.state.planNudgeSent && ctx.contextManager) {
17523
+ deps.state.planNudgeSent = true;
17524
+ ctx.contextManager.addMessage({
17525
+ role: "user",
17526
+ content: `<system-summary>${t("exec.plan_nudge", {
17527
+ count: String(mutations)
17528
+ })}</system-summary>`
17529
+ });
17465
17530
  }
17466
- if (found.status === "active") {
17467
- return {
17468
- success: true,
17469
- output: t("plan.already_active", {
17470
- id: found.plan.id,
17471
- title: found.plan.title
17472
- }),
17473
- display: deps.trackerRef.current?.toPromptBlock()
17474
- };
17531
+ }
17532
+ }
17533
+ const stuckReason = deps.stuckDetector.getStuckReason();
17534
+ if (stuckReason) {
17535
+ const logIt = deps.stuckDetector.isStuck() ? !deps.state.stuckNotified : true;
17536
+ if (logIt) {
17537
+ ctx.logger?.warn(stuckReason);
17538
+ ctx.sessionLog?.plan("stuck-warning", stuckReason, typeof ctx.iteration === "number" ? ctx.iteration : undefined);
17539
+ if (deps.stuckDetector.isStuck())
17540
+ deps.state.stuckNotified = true;
17541
+ }
17542
+ }
17543
+ if (deps.stuckDetector.isStuck() || deps.stuckDetector.hasRepetitiveToolCalls() || deps.stuckDetector.hasReadOnlyLoop()) {
17544
+ const currentIter = typeof ctx.iteration === "number" ? ctx.iteration : 0;
17545
+ if (currentIter - deps.state.lastRecoveryIteration >= STUCK_RECOVERY_COOLDOWN) {
17546
+ const recovery = deps.stuckDetector.getRecoveryMessage();
17547
+ if (recovery && ctx.contextManager) {
17548
+ const lastError = deps.stuckDetector.getLastErrorOutput();
17549
+ const skillHint = lastError ? `
17550
+ If you have relevant skills available, consider loading one with load_skill for expert guidance.` : "";
17551
+ const actionableHints = deps.stuckDetector.getActionableHints();
17552
+ const actionableHintStr = actionableHints.length > 0 ? `
17553
+ ${t("exec.hints", { hints: actionableHints.map((h) => `- ${h}`).join(`
17554
+ `) })}` : "";
17555
+ const alternative = deps.stuckDetector.getToolAlternative();
17556
+ const altHint = alternative ? `
17557
+ Tool "${deps.stuckDetector.getLastFailedTool()}" is failing. Try "${alternative}" instead.` : "";
17558
+ ctx.contextManager.addMessage({
17559
+ role: "user",
17560
+ content: `<system-summary>${recovery}${skillHint}${actionableHintStr}${altHint}</system-summary>`
17561
+ });
17475
17562
  }
17476
- deps.preserveActive();
17477
- if (found.status === "draft") {
17478
- deps.store.removeDraft(found.plan.id);
17479
- } else if (found.status === "archived") {
17480
- deps.store.removeArchived(found.plan.id);
17563
+ const hints = deps.stuckDetector.getHints();
17564
+ if (hints.length > 0 && ctx.contextManager) {
17565
+ const hintMsg = t("exec.hints", {
17566
+ hints: hints.map((h) => `- ${h}`).join(`
17567
+ `)
17568
+ });
17569
+ ctx.contextManager.addMessage({
17570
+ role: "user",
17571
+ content: `<system-summary>${hintMsg}</system-summary>`
17572
+ });
17481
17573
  }
17482
- deps.setPlan(found.plan);
17574
+ deps.stuckDetector.recordEscalation();
17575
+ deps.state.lastRecoveryIteration = currentIter;
17576
+ if (deps.stuckDetector.shouldEscalate() && ctx.onMeta) {
17577
+ const escalation = t("exec.escalation", {
17578
+ stepId: String(deps.trackerRef.current?.getCurrentStep()?.id ?? "?"),
17579
+ description: deps.trackerRef.current?.getCurrentStep()?.description ?? ""
17580
+ });
17581
+ ctx.onMeta(escalation);
17582
+ }
17583
+ if (deps.stuckDetector.getIterationsOnCurrentStep() >= FORCE_SKIP_THRESHOLD && ctx.contextManager) {
17584
+ const step = deps.trackerRef.current?.getCurrentStep();
17585
+ ctx.contextManager.addMessage({
17586
+ role: "user",
17587
+ 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>`
17588
+ });
17589
+ }
17590
+ }
17591
+ }
17592
+ },
17593
+ onBeforeTool: (_ctx, call) => {
17594
+ const warning = deps.checkPlanAlignment(call);
17595
+ if (warning) {
17596
+ deps.pendingMessages.push({
17597
+ role: "user",
17598
+ content: `<system-summary>${warning}</system-summary>`
17599
+ });
17600
+ deps.state.consecutivePlanWarnings++;
17601
+ if (deps.state.consecutivePlanWarnings >= MAX_PLAN_WARNINGS_BEFORE_BLOCK) {
17602
+ deps.pendingMessages.push({
17603
+ role: "user",
17604
+ content: `<system-summary>${t("exec.plan_blocked", { step: String(deps.trackerRef.current?.getCurrentStep()?.id ?? "?"), max: MAX_PLAN_WARNINGS_BEFORE_BLOCK })}</system-summary>`
17605
+ });
17606
+ return t("exec.plan_blocked", {
17607
+ step: String(deps.trackerRef.current?.getCurrentStep()?.id ?? "?"),
17608
+ max: MAX_PLAN_WARNINGS_BEFORE_BLOCK
17609
+ });
17610
+ }
17611
+ } else {
17612
+ deps.state.consecutivePlanWarnings = 0;
17613
+ }
17614
+ return true;
17615
+ },
17616
+ onToolCall: (ctx) => {
17617
+ const toolName = ctx?.toolName;
17618
+ const args = ctx?.args;
17619
+ if (toolName && args) {
17620
+ deps.stuckDetector.recordToolCall(toolName, args);
17621
+ if (!deps.trackerRef.current && (toolName === "write_file" || toolName === "edit_file" || toolName === "bash" || toolName === "download_file")) {
17622
+ deps.state.mutationsWithoutPlan++;
17623
+ }
17624
+ }
17625
+ },
17626
+ onAfterTool: (ctx, call, result) => {
17627
+ if (call.name === "bash") {
17628
+ deps.stuckDetector.recordBashOutput(String(call.arguments?.command ?? ""), String(result.output ?? ""));
17629
+ }
17630
+ const toolText = String(result.output ?? "");
17631
+ const hasTypeError = /error TS\d+|\[Project typecheck failed\]|\[Syntax check failed\]/.test(toolText);
17632
+ if (hasTypeError) {
17633
+ deps.stuckDetector.recordToolError(call.name, toolText.slice(0, 300));
17634
+ }
17635
+ if (call.name === "write_file" || call.name === "edit_file") {
17636
+ const broken = extractBrokenFiles(toolText);
17637
+ if (broken.length > 0) {
17638
+ for (const b of broken)
17639
+ deps.state.typecheckFailures.set(b.file, b.error);
17640
+ } else if (result.success) {
17641
+ deps.state.typecheckFailures.clear();
17642
+ }
17643
+ }
17644
+ if (!result.success) {
17645
+ if (call.name === "bash" && platform6() === "win32") {
17646
+ const cmd = String(call.arguments?.command ?? "");
17647
+ const forbidden = forbiddenWindowsCommand(cmd);
17648
+ if (forbidden) {
17649
+ const n = (deps.forbiddenBashFailures.get(forbidden) || 0) + 1;
17650
+ deps.forbiddenBashFailures.set(forbidden, n);
17651
+ if (n === 2) {
17652
+ deps.pendingMessages.push({
17653
+ role: "user",
17654
+ content: `<system-summary>${t("exec.forbidden_cmd", { cmd: forbidden })}</system-summary>`
17655
+ });
17656
+ }
17657
+ }
17658
+ }
17659
+ if (!hasTypeError) {
17660
+ deps.stuckDetector.recordToolError(call.name, result.output);
17661
+ }
17662
+ const actionableHints = deps.stuckDetector.getActionableHints();
17663
+ const alternative = deps.stuckDetector.getToolAlternative();
17664
+ if (actionableHints.length > 0 || alternative) {
17665
+ const parts = [...actionableHints];
17666
+ if (alternative) {
17667
+ parts.push(`Tool "${call.name}" crashed. Try "${alternative}" instead.`);
17668
+ }
17669
+ deps.pendingMessages.push({
17670
+ role: "user",
17671
+ content: `<system-summary>${t("exec.hints", { hints: parts.map((h) => `- ${h}`).join(`
17672
+ `) })}</system-summary>`
17673
+ });
17674
+ }
17675
+ } else {
17676
+ deps.stuckDetector.recordToolSuccess();
17677
+ if (call.name === "bash" && result.success) {
17678
+ const cmd = String(call.arguments?.command ?? "");
17679
+ const testRun = detectTestResults(String(result.output ?? ""));
17680
+ if (testRun && testRun.failed > 0) {
17681
+ deps.pendingMessages.push({
17682
+ role: "user",
17683
+ 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>`
17684
+ });
17685
+ } else if (testRun && testRun.failed === 0 && testRun.passed > 0) {
17686
+ deps.pendingMessages.push({
17687
+ role: "user",
17688
+ 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>`
17689
+ });
17690
+ } else if (/node|tsx|ts-node|python|npm\s+(start|test|run)/.test(cmd)) {
17691
+ deps.pendingMessages.push({
17692
+ role: "user",
17693
+ 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>`
17694
+ });
17695
+ }
17696
+ }
17697
+ }
17698
+ if (result.success && (call.name === "write_file" || call.name === "edit_file")) {
17699
+ const filePath = call.arguments?.path;
17700
+ if (filePath) {
17701
+ deps.stuckDetector.recordFileRewrite(filePath);
17702
+ if (deps.stuckDetector.hasExcessiveRewrites()) {
17703
+ const file = deps.stuckDetector.getExcessiveRewriteFile();
17704
+ const count = deps.stuckDetector.getFileRewriteCount(file);
17705
+ if (ctx.onMeta) {
17706
+ ctx.onMeta(t("exec.file_rewrite_warning", {
17707
+ file,
17708
+ count: String(count)
17709
+ }));
17710
+ }
17711
+ }
17712
+ }
17713
+ deps.advancePlanIfStepComplete(ctx.contextManager, ctx.sessionLog);
17714
+ }
17715
+ deps.maybeSearchError(ctx, call);
17716
+ }
17717
+ };
17718
+ }
17719
+ var STUCK_RECOVERY_COOLDOWN = 5, MAX_PLAN_WARNINGS_BEFORE_BLOCK = 3, FORCE_SKIP_THRESHOLD = 10, PLAN_NUDGE_THRESHOLD = 2;
17720
+ var init_execution_plugin = __esm(() => {
17721
+ init_i18n();
17722
+ init_bash();
17723
+ init_windows_commands();
17724
+ init_js_identifiers();
17725
+ });
17726
+
17727
+ // src/modules/execution/plan-tool.ts
17728
+ function createPlanToolDefinitions(deps) {
17729
+ return [
17730
+ {
17731
+ name: "plan",
17732
+ alwaysOn: true,
17733
+ description: `Create, update, show, abort, list, switch, delete, purge, or re-plan multi-step plans.
17734
+
17735
+ Actions:
17736
+ - create: Start a new plan. Previous active plan is auto-preserved: incomplete → draft, complete → archive.
17737
+ - update: Mark step status (done/failed/skipped), or rebuild plan with new steps.
17738
+ - show: Print current plan checklist.
17739
+ - abort: Archive the current plan (or the plan given by id) and clear the active slot.
17740
+ - list: Show all plans (active, drafts, archived) with progress.
17741
+ - switch: Make a different plan active (by plan id).
17742
+ - re-plan: Iterative replanning: keep completed steps, replace remaining with new steps.
17743
+ - delete: Permanently delete a plan by id (plan delete id=plan_xxx).
17744
+ - purge: Delete ALL plans (active, drafts, archived).
17745
+
17746
+ Write CONCRETE steps with exact file paths and commands:
17747
+ - Specify WHICH files to create with exact paths (e.g. "create src/components/Header.tsx with navigation and logo")
17748
+ - Specify WHICH packages to install (e.g. "run npm install react react-dom")
17749
+ - Specify WHICH CLI commands to run with exact arguments
17750
+ - Each step should include at least one file extension (.ts, .tsx, .json, etc.) or a command verb (install, create, build, run, add, init)
17751
+ - Good: "Создать src/components/Header.tsx с навигацией и логотипом"
17752
+ - Bad: "Настройка проекта" (too vague — what exactly needs to be configured?)
17753
+ - For steps that REMOVE files, pass kinds: ["delete"] aligned with steps (the done-gate verifies those files are gone instead of demanding they exist).`,
17754
+ parameters: {
17755
+ type: "object",
17756
+ properties: {
17757
+ action: {
17758
+ type: "string",
17759
+ enum: [
17760
+ "create",
17761
+ "update",
17762
+ "show",
17763
+ "abort",
17764
+ "list",
17765
+ "switch",
17766
+ "re-plan",
17767
+ "delete",
17768
+ "purge"
17769
+ ]
17770
+ },
17771
+ title: { type: "string" },
17772
+ steps: { type: "array", items: { type: "string" } },
17773
+ kinds: {
17774
+ type: "array",
17775
+ items: { type: "string", enum: ["create", "delete"] },
17776
+ description: 'Optional per-step intent, aligned 1:1 with steps. Pass "delete" for steps whose purpose is REMOVING files — the done-gate then requires those files to be GONE (not present). Defaults to "create".'
17777
+ },
17778
+ step: { type: "number" },
17779
+ status: { type: "string", enum: ["done", "failed", "skipped"] },
17780
+ note: { type: "string" },
17781
+ id: { type: "string", description: "Plan id (for switch action)" }
17782
+ },
17783
+ required: ["action"]
17784
+ },
17785
+ handler: async (_ctx, args) => {
17786
+ const action = String(args.action);
17787
+ if (action === "list") {
17788
+ const metas = deps.store.listAll();
17789
+ if (metas.length === 0) {
17790
+ return { success: true, output: t("plan.list_empty") };
17791
+ }
17792
+ const lines = metas.map((m) => {
17793
+ const icon = m.status === "active" ? "[*]" : m.status === "draft" ? "[ ]" : "[-]";
17794
+ const namePart = m.name ? ` (${m.name})` : "";
17795
+ return `${icon} ${m.id}${namePart} — ${m.title} ${m.doneCount}/${m.stepCount}`;
17796
+ });
17797
+ lines.push("");
17798
+ lines.push(t("plan.list_legend"));
17799
+ return {
17800
+ success: true,
17801
+ output: `${t("plan.list_header")}
17802
+ ${lines.join(`
17803
+ `)}`
17804
+ };
17805
+ }
17806
+ if (action === "switch") {
17807
+ const planId = String(args.id || "");
17808
+ if (!planId) {
17809
+ return { success: false, output: t("plan.switch_no_id") };
17810
+ }
17811
+ const found = deps.store.find(planId);
17812
+ if (!found) {
17813
+ return {
17814
+ success: false,
17815
+ output: t("plan.not_found", { id: planId })
17816
+ };
17817
+ }
17818
+ if (found.status === "active") {
17819
+ return {
17820
+ success: true,
17821
+ output: t("plan.already_active", {
17822
+ id: found.plan.id,
17823
+ title: found.plan.title
17824
+ }),
17825
+ display: deps.trackerRef.current?.toPromptBlock()
17826
+ };
17827
+ }
17828
+ deps.preserveActive();
17829
+ if (found.status === "draft") {
17830
+ deps.store.removeDraft(found.plan.id);
17831
+ } else if (found.status === "archived") {
17832
+ deps.store.removeArchived(found.plan.id);
17833
+ }
17834
+ deps.setPlan(found.plan);
17483
17835
  const display = deps.trackerRef.current?.toPromptBlock();
17484
17836
  return {
17485
17837
  success: true,
@@ -17678,8 +18030,20 @@ ${progress2}`,
17678
18030
  }
17679
18031
  }
17680
18032
  }
17681
- tracker.updateStepStatus(stepId, status);
17682
- if (args.note)
18033
+ if (status === "done") {
18034
+ const blocked = stepTypecheckGate(deps.typecheckFailures, target.description);
18035
+ if (blocked) {
18036
+ return {
18037
+ success: false,
18038
+ output: t("plan.typecheck_failed", {
18039
+ step: String(stepId),
18040
+ error: blocked
18041
+ })
18042
+ };
18043
+ }
18044
+ }
18045
+ tracker.updateStepStatus(stepId, status);
18046
+ if (args.note)
17683
18047
  tracker.addNote(Number(args.step), String(args.note));
17684
18048
  tracker.syncCurrentStep();
17685
18049
  deps.store.saveActive(tracker.getPlan());
@@ -17778,430 +18142,170 @@ ${progress}${vacuousNote}`,
17778
18142
  if (!deleted) {
17779
18143
  return {
17780
18144
  success: false,
17781
- output: t("plan.not_found", { id: planId })
17782
- };
17783
- }
17784
- if (deleted === "active") {
17785
- deps.trackerRef.current = null;
17786
- deps.clearCompleted();
17787
- }
17788
- return {
17789
- success: true,
17790
- output: t("plan.deleted", { id: planId })
17791
- };
17792
- }
17793
- if (action === "purge") {
17794
- const count = deps.store.purgeAll();
17795
- deps.trackerRef.current = null;
17796
- deps.clearCompleted();
17797
- return {
17798
- success: true,
17799
- output: t("plan.purged", { count: String(count) })
17800
- };
17801
- }
17802
- if (!deps.trackerRef.current) {
17803
- return { success: false, output: t("plan.no_active") };
17804
- }
17805
- return {
17806
- success: false,
17807
- output: t("plan.unknown_action", { action })
17808
- };
17809
- }
17810
- },
17811
- {
17812
- name: "todo",
17813
- alwaysOn: true,
17814
- description: "Manage sub-tasks within current plan step. Use to break down complex steps into smaller tasks.",
17815
- parameters: {
17816
- type: "object",
17817
- properties: {
17818
- action: { type: "string", enum: ["add", "done", "list"] },
17819
- items: { type: "array", items: { type: "string" } }
17820
- },
17821
- required: ["action"]
17822
- },
17823
- handler: async (_ctx, args) => {
17824
- if (!deps.trackerRef.current) {
17825
- return { success: false, output: t("plan.no_active") };
17826
- }
17827
- const tracker = deps.trackerRef.current;
17828
- const action = String(args.action);
17829
- const currentStep = tracker.getCurrentStep();
17830
- if (!currentStep) {
17831
- return { success: false, output: t("plan.step_not_found") };
17832
- }
17833
- if (action === "add" && Array.isArray(args.items)) {
17834
- const items = args.items.map(String);
17835
- const existing = currentStep.subtasks ?? [];
17836
- let nextId = existing.reduce((m, s) => Math.max(m, s.id), 0) + 1;
17837
- for (const item of items) {
17838
- existing.push({ id: nextId++, text: item, done: false });
17839
- }
17840
- currentStep.subtasks = existing;
17841
- deps.store.saveActive(tracker.getPlan());
17842
- const display = tracker.toPromptBlock();
17843
- return {
17844
- success: true,
17845
- output: t("todo.added", {
17846
- count: String(items.length),
17847
- items: items.join(", ")
17848
- }),
17849
- display
17850
- };
17851
- }
17852
- if (action === "done") {
17853
- const items = Array.isArray(args.items) ? args.items.map(String) : [];
17854
- if (items.length === 0) {
17855
- return {
17856
- success: false,
17857
- output: t("todo.no_items")
17858
- };
17859
- }
17860
- const subs = currentStep.subtasks ?? [];
17861
- let marked = 0;
17862
- for (const item of items) {
17863
- const sub = subs.find((s) => !s.done && s.text.toLowerCase() === item.toLowerCase());
17864
- if (sub) {
17865
- sub.done = true;
17866
- marked++;
17867
- }
17868
- }
17869
- if (marked === 0) {
17870
- return {
17871
- success: false,
17872
- output: t("todo.subtask_not_found", {
17873
- items: items.join(", ")
17874
- })
17875
- };
17876
- }
17877
- currentStep.subtasks = subs;
17878
- deps.store.saveActive(tracker.getPlan());
17879
- const doneCount = subs.filter((s) => s.done).length;
17880
- const display = tracker.toPromptBlock();
17881
- let output = t("todo.marked_done", {
17882
- count: String(marked)
17883
- });
17884
- output += ` (${doneCount}/${subs.length})`;
17885
- if (doneCount === subs.length) {
17886
- output += `
17887
- All sub-tasks done — call plan update step=${currentStep.id} status=done to complete this step.`;
17888
- }
17889
- return { success: true, output, display };
17890
- }
17891
- if (action === "list") {
17892
- const subs = currentStep.subtasks ?? [];
17893
- const lines = subs.length ? subs.map((s) => `${s.done ? "[x]" : "[ ]"} ${s.text}`) : ["(no sub-tasks)"];
17894
- return {
17895
- success: true,
17896
- output: `Step ${currentStep.id}: ${currentStep.description}
17897
- ${lines.join(`
17898
- `)}`
17899
- };
17900
- }
17901
- return {
17902
- success: false,
17903
- output: t("todo.unknown_action", { action })
17904
- };
17905
- }
17906
- },
17907
- {
17908
- name: "verify",
17909
- alwaysOn: true,
17910
- description: "Run verification for the current step. Checks files mentioned in the step description.",
17911
- parameters: {
17912
- type: "object",
17913
- properties: {
17914
- step: { type: "number", description: "Step number to verify" }
17915
- }
17916
- },
17917
- handler: async (_ctx, args) => {
17918
- if (!deps.trackerRef.current)
17919
- return { success: false, output: t("plan.no_active") };
17920
- const tracker = deps.trackerRef.current;
17921
- const step = args.step ? tracker.getStep(Number(args.step)) : tracker.getCurrentStep();
17922
- if (!step)
17923
- return { success: false, output: t("plan.step_not_found") };
17924
- const result = await deps.verifier.verifyStep(step.description);
17925
- if (result.noFiles) {
17926
- return {
17927
- success: true,
17928
- output: t("verify.no_files", { step: String(step.id) })
17929
- };
17930
- }
17931
- return {
17932
- success: result.passed,
17933
- output: result.passed ? t("verify.passed") : t("verify.failed", {
17934
- details: result.failed.map((f) => f.message).join("; ")
17935
- })
17936
- };
17937
- }
17938
- }
17939
- ];
17940
- }
17941
- var init_plan_tool = __esm(() => {
17942
- init_i18n();
17943
- init_plan_coverage();
17944
- });
17945
-
17946
- // src/modules/execution/windows-commands.ts
17947
- function firstWord(command) {
17948
- const segs = command.trim().split(/[\s;|&]+/);
17949
- return (segs[0] || "").replace(/[^\w-]/g, "").toLowerCase();
17950
- }
17951
- function forbiddenWindowsCommand(command) {
17952
- const m = command.match(/^\s*cd\s+\S+\s*&&\s*(\S+)/);
17953
- if (m)
17954
- return forbiddenWindowsCommand(m[1]);
17955
- const word = firstWord(command);
17956
- if (word && FORBIDDEN_COMMANDS.has(word))
17957
- return word;
17958
- return null;
17959
- }
17960
- var FORBIDDEN_COMMANDS;
17961
- var init_windows_commands = __esm(() => {
17962
- FORBIDDEN_COMMANDS = new Set([
17963
- "grep",
17964
- "sed",
17965
- "ls",
17966
- "find",
17967
- "rm",
17968
- "touch",
17969
- "which",
17970
- "diff",
17971
- "cp",
17972
- "mv"
17973
- ]);
17974
- });
17975
-
17976
- // src/modules/execution/execution-plugin.ts
17977
- import { platform as platform6 } from "os";
17978
- function createExecutionPlugin(deps) {
17979
- return {
17980
- name: "execution",
17981
- onBeforeThink: (ctx) => {
17982
- if (ctx.contextManager && deps.pendingMessages.length > 0) {
17983
- for (const m of deps.pendingMessages.splice(0)) {
17984
- ctx.contextManager.addMessage(m);
17985
- }
17986
- }
17987
- if (deps.trackerRef.current?.isComplete()) {
17988
- deps.stuckDetector.resetStepProgress();
17989
- deps.state.consecutivePlanWarnings = 0;
17990
- deps.state.lastStepId = -1;
17991
- deps.state.stuckNotified = false;
17992
- deps.state.mutationsWithoutPlan = 0;
17993
- deps.state.planNudgeSent = false;
17994
- } else {
17995
- const step = deps.trackerRef.current?.getCurrentStep();
17996
- if (deps.trackerRef.current && step) {
17997
- if (step.id !== deps.state.lastStepId) {
17998
- deps.state.consecutivePlanWarnings = 0;
17999
- deps.state.lastStepId = step.id;
18000
- deps.state.stuckNotified = false;
18001
- }
18002
- deps.stuckDetector.setCurrentStep(step.id, step.description);
18003
- deps.stuckDetector.recordIteration(step.id);
18004
- deps.state.mutationsWithoutPlan = 0;
18005
- deps.state.planNudgeSent = false;
18006
- } else {
18007
- deps.stuckDetector.reset();
18008
- deps.state.consecutivePlanWarnings = 0;
18009
- deps.state.lastStepId = -1;
18010
- deps.state.stuckNotified = false;
18011
- const mutations = deps.state.mutationsWithoutPlan;
18012
- if (mutations >= PLAN_NUDGE_THRESHOLD && !deps.state.planNudgeSent && ctx.contextManager) {
18013
- deps.state.planNudgeSent = true;
18014
- ctx.contextManager.addMessage({
18015
- role: "user",
18016
- content: `<system-summary>${t("exec.plan_nudge", {
18017
- count: String(mutations)
18018
- })}</system-summary>`
18019
- });
18020
- }
18021
- }
18022
- }
18023
- const stuckReason = deps.stuckDetector.getStuckReason();
18024
- if (stuckReason) {
18025
- const logIt = deps.stuckDetector.isStuck() ? !deps.state.stuckNotified : true;
18026
- if (logIt) {
18027
- ctx.logger?.warn(stuckReason);
18028
- ctx.sessionLog?.plan("stuck-warning", stuckReason, typeof ctx.iteration === "number" ? ctx.iteration : undefined);
18029
- if (deps.stuckDetector.isStuck())
18030
- deps.state.stuckNotified = true;
18031
- }
18032
- }
18033
- if (deps.stuckDetector.isStuck() || deps.stuckDetector.hasRepetitiveToolCalls() || deps.stuckDetector.hasReadOnlyLoop()) {
18034
- const currentIter = typeof ctx.iteration === "number" ? ctx.iteration : 0;
18035
- if (currentIter - deps.state.lastRecoveryIteration >= STUCK_RECOVERY_COOLDOWN) {
18036
- const recovery = deps.stuckDetector.getRecoveryMessage();
18037
- if (recovery && ctx.contextManager) {
18038
- const lastError = deps.stuckDetector.getLastErrorOutput();
18039
- const skillHint = lastError ? `
18040
- If you have relevant skills available, consider loading one with load_skill for expert guidance.` : "";
18041
- const actionableHints = deps.stuckDetector.getActionableHints();
18042
- const actionableHintStr = actionableHints.length > 0 ? `
18043
- ${t("exec.hints", { hints: actionableHints.map((h) => `- ${h}`).join(`
18044
- `) })}` : "";
18045
- const alternative = deps.stuckDetector.getToolAlternative();
18046
- const altHint = alternative ? `
18047
- Tool "${deps.stuckDetector.getLastFailedTool()}" is failing. Try "${alternative}" instead.` : "";
18048
- ctx.contextManager.addMessage({
18049
- role: "user",
18050
- content: `<system-summary>${recovery}${skillHint}${actionableHintStr}${altHint}</system-summary>`
18051
- });
18052
- }
18053
- const hints = deps.stuckDetector.getHints();
18054
- if (hints.length > 0 && ctx.contextManager) {
18055
- const hintMsg = t("exec.hints", {
18056
- hints: hints.map((h) => `- ${h}`).join(`
18057
- `)
18058
- });
18059
- ctx.contextManager.addMessage({
18060
- role: "user",
18061
- content: `<system-summary>${hintMsg}</system-summary>`
18062
- });
18063
- }
18064
- deps.stuckDetector.recordEscalation();
18065
- deps.state.lastRecoveryIteration = currentIter;
18066
- if (deps.stuckDetector.shouldEscalate() && ctx.onMeta) {
18067
- const escalation = t("exec.escalation", {
18068
- stepId: String(deps.trackerRef.current?.getCurrentStep()?.id ?? "?"),
18069
- description: deps.trackerRef.current?.getCurrentStep()?.description ?? ""
18070
- });
18071
- ctx.onMeta(escalation);
18072
- }
18073
- if (deps.stuckDetector.getIterationsOnCurrentStep() >= FORCE_SKIP_THRESHOLD && ctx.contextManager) {
18074
- const step = deps.trackerRef.current?.getCurrentStep();
18075
- ctx.contextManager.addMessage({
18076
- role: "user",
18077
- 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>`
18078
- });
18145
+ output: t("plan.not_found", { id: planId })
18146
+ };
18147
+ }
18148
+ if (deleted === "active") {
18149
+ deps.trackerRef.current = null;
18150
+ deps.clearCompleted();
18079
18151
  }
18152
+ return {
18153
+ success: true,
18154
+ output: t("plan.deleted", { id: planId })
18155
+ };
18080
18156
  }
18081
- }
18082
- },
18083
- onBeforeTool: (_ctx, call) => {
18084
- const warning = deps.checkPlanAlignment(call);
18085
- if (warning) {
18086
- deps.pendingMessages.push({
18087
- role: "user",
18088
- content: `<system-summary>${warning}</system-summary>`
18089
- });
18090
- deps.state.consecutivePlanWarnings++;
18091
- if (deps.state.consecutivePlanWarnings >= MAX_PLAN_WARNINGS_BEFORE_BLOCK) {
18092
- deps.pendingMessages.push({
18093
- role: "user",
18094
- content: `<system-summary>${t("exec.plan_blocked", { step: String(deps.trackerRef.current?.getCurrentStep()?.id ?? "?"), max: MAX_PLAN_WARNINGS_BEFORE_BLOCK })}</system-summary>`
18095
- });
18096
- return t("exec.plan_blocked", {
18097
- step: String(deps.trackerRef.current?.getCurrentStep()?.id ?? "?"),
18098
- max: MAX_PLAN_WARNINGS_BEFORE_BLOCK
18099
- });
18157
+ if (action === "purge") {
18158
+ const count = deps.store.purgeAll();
18159
+ deps.trackerRef.current = null;
18160
+ deps.clearCompleted();
18161
+ return {
18162
+ success: true,
18163
+ output: t("plan.purged", { count: String(count) })
18164
+ };
18100
18165
  }
18101
- } else {
18102
- deps.state.consecutivePlanWarnings = 0;
18103
- }
18104
- return true;
18105
- },
18106
- onToolCall: (ctx) => {
18107
- const toolName = ctx?.toolName;
18108
- const args = ctx?.args;
18109
- if (toolName && args) {
18110
- deps.stuckDetector.recordToolCall(toolName, args);
18111
- if (!deps.trackerRef.current && (toolName === "write_file" || toolName === "edit_file" || toolName === "bash" || toolName === "download_file")) {
18112
- deps.state.mutationsWithoutPlan++;
18166
+ if (!deps.trackerRef.current) {
18167
+ return { success: false, output: t("plan.no_active") };
18113
18168
  }
18169
+ return {
18170
+ success: false,
18171
+ output: t("plan.unknown_action", { action })
18172
+ };
18114
18173
  }
18115
18174
  },
18116
- onAfterTool: (ctx, call, result) => {
18117
- if (call.name === "bash") {
18118
- deps.stuckDetector.recordBashOutput(String(call.arguments?.command ?? ""), String(result.output ?? ""));
18119
- }
18120
- const toolText = String(result.output ?? "");
18121
- const hasTypeError = /error TS\d+|\[Project typecheck failed\]|\[Syntax check failed\]/.test(toolText);
18122
- if (hasTypeError) {
18123
- deps.stuckDetector.recordToolError(call.name, toolText.slice(0, 300));
18124
- }
18125
- if (!result.success) {
18126
- if (call.name === "bash" && platform6() === "win32") {
18127
- const cmd = String(call.arguments?.command ?? "");
18128
- const forbidden = forbiddenWindowsCommand(cmd);
18129
- if (forbidden) {
18130
- const n = (deps.forbiddenBashFailures.get(forbidden) || 0) + 1;
18131
- deps.forbiddenBashFailures.set(forbidden, n);
18132
- if (n === 2) {
18133
- deps.pendingMessages.push({
18134
- role: "user",
18135
- content: `<system-summary>${t("exec.forbidden_cmd", { cmd: forbidden })}</system-summary>`
18136
- });
18137
- }
18138
- }
18175
+ {
18176
+ name: "todo",
18177
+ alwaysOn: true,
18178
+ description: "Manage sub-tasks within current plan step. Use to break down complex steps into smaller tasks.",
18179
+ parameters: {
18180
+ type: "object",
18181
+ properties: {
18182
+ action: { type: "string", enum: ["add", "done", "list"] },
18183
+ items: { type: "array", items: { type: "string" } }
18184
+ },
18185
+ required: ["action"]
18186
+ },
18187
+ handler: async (_ctx, args) => {
18188
+ if (!deps.trackerRef.current) {
18189
+ return { success: false, output: t("plan.no_active") };
18139
18190
  }
18140
- if (!hasTypeError) {
18141
- deps.stuckDetector.recordToolError(call.name, result.output);
18191
+ const tracker = deps.trackerRef.current;
18192
+ const action = String(args.action);
18193
+ const currentStep = tracker.getCurrentStep();
18194
+ if (!currentStep) {
18195
+ return { success: false, output: t("plan.step_not_found") };
18142
18196
  }
18143
- const actionableHints = deps.stuckDetector.getActionableHints();
18144
- const alternative = deps.stuckDetector.getToolAlternative();
18145
- if (actionableHints.length > 0 || alternative) {
18146
- const parts = [...actionableHints];
18147
- if (alternative) {
18148
- parts.push(`Tool "${call.name}" crashed. Try "${alternative}" instead.`);
18197
+ if (action === "add" && Array.isArray(args.items)) {
18198
+ const items = args.items.map(String);
18199
+ const existing = currentStep.subtasks ?? [];
18200
+ let nextId = existing.reduce((m, s) => Math.max(m, s.id), 0) + 1;
18201
+ for (const item of items) {
18202
+ existing.push({ id: nextId++, text: item, done: false });
18149
18203
  }
18150
- deps.pendingMessages.push({
18151
- role: "user",
18152
- content: `<system-summary>${t("exec.hints", { hints: parts.map((h) => `- ${h}`).join(`
18153
- `) })}</system-summary>`
18154
- });
18204
+ currentStep.subtasks = existing;
18205
+ deps.store.saveActive(tracker.getPlan());
18206
+ const display = tracker.toPromptBlock();
18207
+ return {
18208
+ success: true,
18209
+ output: t("todo.added", {
18210
+ count: String(items.length),
18211
+ items: items.join(", ")
18212
+ }),
18213
+ display
18214
+ };
18155
18215
  }
18156
- } else {
18157
- deps.stuckDetector.recordToolSuccess();
18158
- if (call.name === "bash" && result.success) {
18159
- const cmd = String(call.arguments?.command ?? "");
18160
- const testRun = detectTestResults(String(result.output ?? ""));
18161
- if (testRun && testRun.failed > 0) {
18162
- deps.pendingMessages.push({
18163
- role: "user",
18164
- 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>`
18165
- });
18166
- } else if (testRun && testRun.failed === 0 && testRun.passed > 0) {
18167
- deps.pendingMessages.push({
18168
- role: "user",
18169
- 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>`
18170
- });
18171
- } else if (/node|tsx|ts-node|python|npm\s+(start|test|run)/.test(cmd)) {
18172
- deps.pendingMessages.push({
18173
- role: "user",
18174
- 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>`
18175
- });
18216
+ if (action === "done") {
18217
+ const items = Array.isArray(args.items) ? args.items.map(String) : [];
18218
+ if (items.length === 0) {
18219
+ return {
18220
+ success: false,
18221
+ output: t("todo.no_items")
18222
+ };
18176
18223
  }
18177
- }
18178
- }
18179
- if (result.success && (call.name === "write_file" || call.name === "edit_file")) {
18180
- const filePath = call.arguments?.path;
18181
- if (filePath) {
18182
- deps.stuckDetector.recordFileRewrite(filePath);
18183
- if (deps.stuckDetector.hasExcessiveRewrites()) {
18184
- const file = deps.stuckDetector.getExcessiveRewriteFile();
18185
- const count = deps.stuckDetector.getFileRewriteCount(file);
18186
- if (ctx.onMeta) {
18187
- ctx.onMeta(t("exec.file_rewrite_warning", {
18188
- file,
18189
- count: String(count)
18190
- }));
18224
+ const subs = currentStep.subtasks ?? [];
18225
+ let marked = 0;
18226
+ for (const item of items) {
18227
+ const sub = subs.find((s) => !s.done && s.text.toLowerCase() === item.toLowerCase());
18228
+ if (sub) {
18229
+ sub.done = true;
18230
+ marked++;
18191
18231
  }
18192
18232
  }
18233
+ if (marked === 0) {
18234
+ return {
18235
+ success: false,
18236
+ output: t("todo.subtask_not_found", {
18237
+ items: items.join(", ")
18238
+ })
18239
+ };
18240
+ }
18241
+ currentStep.subtasks = subs;
18242
+ deps.store.saveActive(tracker.getPlan());
18243
+ const doneCount = subs.filter((s) => s.done).length;
18244
+ const display = tracker.toPromptBlock();
18245
+ let output = t("todo.marked_done", {
18246
+ count: String(marked)
18247
+ });
18248
+ output += ` (${doneCount}/${subs.length})`;
18249
+ if (doneCount === subs.length) {
18250
+ output += `
18251
+ All sub-tasks done — call plan update step=${currentStep.id} status=done to complete this step.`;
18252
+ }
18253
+ return { success: true, output, display };
18193
18254
  }
18194
- deps.advancePlanIfStepComplete(ctx.contextManager, ctx.sessionLog);
18255
+ if (action === "list") {
18256
+ const subs = currentStep.subtasks ?? [];
18257
+ const lines = subs.length ? subs.map((s) => `${s.done ? "[x]" : "[ ]"} ${s.text}`) : ["(no sub-tasks)"];
18258
+ return {
18259
+ success: true,
18260
+ output: `Step ${currentStep.id}: ${currentStep.description}
18261
+ ${lines.join(`
18262
+ `)}`
18263
+ };
18264
+ }
18265
+ return {
18266
+ success: false,
18267
+ output: t("todo.unknown_action", { action })
18268
+ };
18269
+ }
18270
+ },
18271
+ {
18272
+ name: "verify",
18273
+ alwaysOn: true,
18274
+ description: "Run verification for the current step. Checks files mentioned in the step description.",
18275
+ parameters: {
18276
+ type: "object",
18277
+ properties: {
18278
+ step: { type: "number", description: "Step number to verify" }
18279
+ }
18280
+ },
18281
+ handler: async (_ctx, args) => {
18282
+ if (!deps.trackerRef.current)
18283
+ return { success: false, output: t("plan.no_active") };
18284
+ const tracker = deps.trackerRef.current;
18285
+ const step = args.step ? tracker.getStep(Number(args.step)) : tracker.getCurrentStep();
18286
+ if (!step)
18287
+ return { success: false, output: t("plan.step_not_found") };
18288
+ const result = await deps.verifier.verifyStep(step.description);
18289
+ if (result.noFiles) {
18290
+ return {
18291
+ success: true,
18292
+ output: t("verify.no_files", { step: String(step.id) })
18293
+ };
18294
+ }
18295
+ return {
18296
+ success: result.passed,
18297
+ output: result.passed ? t("verify.passed") : t("verify.failed", {
18298
+ details: result.failed.map((f) => f.message).join("; ")
18299
+ })
18300
+ };
18195
18301
  }
18196
- deps.maybeSearchError(ctx, call);
18197
18302
  }
18198
- };
18303
+ ];
18199
18304
  }
18200
- var STUCK_RECOVERY_COOLDOWN = 5, MAX_PLAN_WARNINGS_BEFORE_BLOCK = 3, FORCE_SKIP_THRESHOLD = 10, PLAN_NUDGE_THRESHOLD = 2;
18201
- var init_execution_plugin = __esm(() => {
18305
+ var init_plan_tool = __esm(() => {
18202
18306
  init_i18n();
18203
- init_bash();
18204
- init_windows_commands();
18307
+ init_plan_coverage();
18308
+ init_execution_plugin();
18205
18309
  });
18206
18310
 
18207
18311
  // src/modules/execution/module.ts
@@ -18233,7 +18337,8 @@ class ExecutionModule {
18233
18337
  stuckNotified: false,
18234
18338
  depsGateHints: new Map,
18235
18339
  mutationsWithoutPlan: 0,
18236
- planNudgeSent: false
18340
+ planNudgeSent: false,
18341
+ typecheckFailures: new Map
18237
18342
  };
18238
18343
  trackerRef = (() => {
18239
18344
  const self = this;
@@ -18416,6 +18521,7 @@ class ExecutionModule {
18416
18521
  stillExistingDeliverables: (s) => this.stillExistingDeliverables(s),
18417
18522
  parseKinds: (a, st) => this.parseKinds(a, st),
18418
18523
  getTaskText: (c) => this.getTaskText(c),
18524
+ typecheckFailures: this.state.typecheckFailures,
18419
18525
  baseDir: this.baseDir
18420
18526
  });
18421
18527
  }
@@ -21162,8 +21268,6 @@ async function bootstrap(configDir, projectDir, noAgentsMd, exitOnComplete) {
21162
21268
  exitOnComplete,
21163
21269
  contextManager,
21164
21270
  activeToolTags,
21165
- sessionId: sessionManager.getActiveMeta()?.id,
21166
- sessionContext: sessionManager.getSessionContext() ?? undefined,
21167
21271
  recursionDepth: 0,
21168
21272
  fileOperationsCount: 0,
21169
21273
  sessionHistory: {
@@ -21191,6 +21295,14 @@ async function bootstrap(configDir, projectDir, noAgentsMd, exitOnComplete) {
21191
21295
  }
21192
21296
  }
21193
21297
  };
21298
+ Object.defineProperty(toolCtx, "sessionId", {
21299
+ get: () => sessionManager.getActiveMeta()?.id,
21300
+ configurable: true
21301
+ });
21302
+ Object.defineProperty(toolCtx, "sessionContext", {
21303
+ get: () => sessionManager.getSessionContext() ?? undefined,
21304
+ configurable: true
21305
+ });
21194
21306
  const toolExecutor = new ToolExecutor(toolRegistry, toolCtx, pluginManager);
21195
21307
  toolCtx.llmProvider = llmProvider;
21196
21308
  toolCtx.toolExecutor = toolExecutor;
@@ -21363,6 +21475,7 @@ async function bootstrap(configDir, projectDir, noAgentsMd, exitOnComplete) {
21363
21475
  sessionManager,
21364
21476
  skillsModule,
21365
21477
  pluginManager,
21478
+ toolCtx,
21366
21479
  configDir: dir,
21367
21480
  baseDir,
21368
21481
  noAgentsMd: skipAgentsMd