micro-models-agent 0.51.1 → 0.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/dist/cli/commands.js +162 -38
  2. package/dist/cli/completer.js +5 -5
  3. package/dist/cli/main.js +42 -54
  4. package/dist/cli/repl-commands.js +138 -38
  5. package/dist/cli/repl.js +175 -89
  6. package/dist/cli/run-result.js +11 -0
  7. package/dist/cli/security-commands.js +6 -6
  8. package/dist/cli/setup.js +21 -15
  9. package/dist/config/config.js +54 -27
  10. package/dist/config/defaults.js +17 -0
  11. package/dist/config/domains.js +179 -0
  12. package/dist/config/index.js +2 -1
  13. package/dist/config/security.js +28 -8
  14. package/dist/core/agent.js +162 -30
  15. package/dist/core/bootstrap.js +94 -17
  16. package/dist/core/crash-handler.js +51 -0
  17. package/dist/core/environment.js +199 -0
  18. package/dist/core/session-logger.js +60 -6
  19. package/dist/core/version.js +2 -0
  20. package/dist/i18n/en.json +120 -39
  21. package/dist/i18n/ru.json +91 -10
  22. package/dist/llm/openai-compat.js +191 -53
  23. package/dist/llm/orchestrator.js +5 -3
  24. package/dist/logger/app-logger.js +50 -4
  25. package/dist/main.js +1288 -635
  26. package/dist/modules/browser/session.js +4 -0
  27. package/dist/modules/certification/cli.js +58 -19
  28. package/dist/modules/certification/loader.js +2 -1
  29. package/dist/modules/certification/manifest.js +22 -14
  30. package/dist/modules/certification/runner.js +91 -5
  31. package/dist/modules/certification/scenarios.js +290 -7
  32. package/dist/modules/context/fact-extractor.js +6 -0
  33. package/dist/modules/context/manager.js +19 -2
  34. package/dist/modules/execution/audit-runners.js +61 -7
  35. package/dist/modules/execution/execution-plugin.js +219 -60
  36. package/dist/modules/execution/module.js +207 -18
  37. package/dist/modules/execution/moe-executor.js +33 -20
  38. package/dist/modules/execution/plan-store.js +39 -0
  39. package/dist/modules/execution/plan-tool.js +188 -19
  40. package/dist/modules/execution/planner.js +27 -23
  41. package/dist/modules/execution/stuck-detector.js +244 -8
  42. package/dist/modules/execution/tracker.js +8 -6
  43. package/dist/modules/execution/verifier.js +15 -2
  44. package/dist/modules/hallucination/detector.js +4 -0
  45. package/dist/modules/hallucination/factual.js +45 -5
  46. package/dist/modules/indexer/module.js +1 -0
  47. package/dist/modules/lsp/client.js +123 -12
  48. package/dist/modules/lsp/index.js +1 -1
  49. package/dist/modules/lsp/module.js +30 -2
  50. package/dist/modules/lsp/probe.js +11 -1
  51. package/dist/modules/lsp/startup-check.js +5 -2
  52. package/dist/modules/plugins/builtin/lint-on-write.js +144 -41
  53. package/dist/modules/plugins/manager.js +57 -13
  54. package/dist/modules/pricing/index.js +61 -0
  55. package/dist/modules/pricing/prices.js +129 -0
  56. package/dist/modules/providers/create.js +22 -0
  57. package/dist/modules/providers/fallback.js +79 -0
  58. package/dist/modules/providers/health.js +46 -0
  59. package/dist/modules/providers/index.js +5 -0
  60. package/dist/modules/providers/manager.js +161 -0
  61. package/dist/modules/providers/presets.js +128 -0
  62. package/dist/modules/providers/registry.js +22 -0
  63. package/dist/modules/providers/types.js +1 -0
  64. package/dist/modules/registry.js +1 -0
  65. package/dist/modules/security/command-validator.js +14 -0
  66. package/dist/modules/security/encryption.js +6 -6
  67. package/dist/modules/security/network-validator.js +17 -0
  68. package/dist/modules/security/path-validator.js +22 -26
  69. package/dist/modules/session/store.js +10 -10
  70. package/dist/tools/approve.js +1 -0
  71. package/dist/tools/attach-image.js +12 -0
  72. package/dist/tools/bash.js +27 -4
  73. package/dist/tools/browser.js +1 -0
  74. package/dist/tools/chunk-query.js +1 -0
  75. package/dist/tools/create-dir.js +1 -0
  76. package/dist/tools/delete-file.js +1 -0
  77. package/dist/tools/download-file.js +1 -0
  78. package/dist/tools/edit-file.js +2 -1
  79. package/dist/tools/enable-tools.js +1 -0
  80. package/dist/tools/executor.js +17 -7
  81. package/dist/tools/file-info.js +1 -0
  82. package/dist/tools/glob-tool.js +1 -0
  83. package/dist/tools/grep-tool.js +54 -13
  84. package/dist/tools/list-dir.js +1 -0
  85. package/dist/tools/load-skill.js +1 -0
  86. package/dist/tools/mcp-call.js +1 -0
  87. package/dist/tools/move-file.js +1 -0
  88. package/dist/tools/path-utils.js +51 -1
  89. package/dist/tools/pipeline-run.js +1 -0
  90. package/dist/tools/process-kill.js +11 -0
  91. package/dist/tools/process-list.js +1 -0
  92. package/dist/tools/process-log.js +9 -0
  93. package/dist/tools/question.js +1 -0
  94. package/dist/tools/read-file.js +94 -6
  95. package/dist/tools/recall.js +1 -0
  96. package/dist/tools/remember.js +1 -0
  97. package/dist/tools/scope-check.js +7 -5
  98. package/dist/tools/search-history.js +1 -0
  99. package/dist/tools/subagent.js +4 -4
  100. package/dist/tools/web-browse.js +1 -0
  101. package/dist/tools/web-fetch.js +27 -6
  102. package/dist/tools/web-search.js +70 -43
  103. package/dist/tools/write-file.js +1 -0
  104. package/dist/ui/line-editor.js +142 -23
  105. package/dist/ui/line-math.js +8 -4
  106. package/dist/ui/renderer.js +57 -7
  107. package/package.json +50 -48
@@ -1,6 +1,7 @@
1
1
  import { t } from "../../i18n/index";
2
- import { PlanCreator } from "./planner";
2
+ import { PlanCreator, getPlanProgress } from "./planner";
3
3
  import { checkPlanCoverage } from "./plan-coverage";
4
+ import { stepTypecheckGate } from "./execution-plugin";
4
5
  /**
5
6
  * The plan / todo / verify tool definitions for the execution module. The
6
7
  * handlers were extracted verbatim from ExecutionModule.getToolDefinitions() —
@@ -11,14 +12,25 @@ export function createPlanToolDefinitions(deps) {
11
12
  return [
12
13
  {
13
14
  name: "plan",
15
+ icon: "📋",
14
16
  alwaysOn: true,
15
- description: 'Create, update, show, abort, list, switch, or re-plan multi-step plans.\n\nActions:\n- create: Start a new plan. Previous active plan is auto-preserved: incomplete → draft, complete → archive.\n- update: Mark step status (done/failed/skipped), or rebuild plan with new steps.\n- show: Print current plan checklist.\n- abort: Archive current plan and clear active slot.\n- list: Show all plans (active, drafts, archived) with progress.\n- switch: Make a different plan active (by plan id).\n- re-plan: Iterative replanning: keep completed steps, replace remaining with new steps.\n\nWrite CONCRETE steps with exact file paths and commands:\n- Specify WHICH files to create with exact paths (e.g. "create src/components/Header.tsx with navigation and logo")\n- Specify WHICH packages to install (e.g. "run npm install react react-dom")\n- Specify WHICH CLI commands to run with exact arguments\n- Each step should include at least one file extension (.ts, .tsx, .json, etc.) or a command verb (install, create, build, run, add, init)\n- Good: "Создать src/components/Header.tsx с навигацией и логотипом"\n- Bad: "Настройка проекта" (too vague — what exactly needs to be configured?)\n- For steps that REMOVE files, pass kinds: ["delete"] aligned with steps (the done-gate verifies those files are gone instead of demanding they exist).',
17
+ description: 'Create, update, show, abort, list, switch, delete, purge, or re-plan multi-step plans.\n\nActions:\n- create: Start a new plan. Previous active plan is auto-preserved: incomplete → draft, complete → archive.\n- update: Mark step status (done/failed/skipped), or rebuild plan with new steps.\n- show: Print current plan checklist.\n- abort: Archive the current plan (or the plan given by id) and clear the active slot.\n- list: Show all plans (active, drafts, archived) with progress.\n- switch: Make a different plan active (by plan id).\n- re-plan: Iterative replanning: keep completed steps, replace remaining with new steps.\n- delete: Permanently delete a plan by id (plan delete id=plan_xxx).\n- purge: Delete ALL plans (active, drafts, archived).\n\nWrite CONCRETE steps with exact file paths and commands:\n- Specify WHICH files to create with exact paths (e.g. "create src/components/Header.tsx with navigation and logo")\n- Specify WHICH packages to install (e.g. "run npm install react react-dom")\n- Specify WHICH CLI commands to run with exact arguments\n- Each step should include at least one file extension (.ts, .tsx, .json, etc.) or a command verb (install, create, build, run, add, init)\n- Good: "Создать src/components/Header.tsx с навигацией и логотипом"\n- Bad: "Настройка проекта" (too vague — what exactly needs to be configured?)\n- For steps that REMOVE files, pass kinds: ["delete"] aligned with steps (the done-gate verifies those files are gone instead of demanding they exist).',
16
18
  parameters: {
17
19
  type: "object",
18
20
  properties: {
19
21
  action: {
20
22
  type: "string",
21
- enum: ["create", "update", "show", "abort", "list", "switch", "re-plan"],
23
+ enum: [
24
+ "create",
25
+ "update",
26
+ "show",
27
+ "abort",
28
+ "list",
29
+ "switch",
30
+ "re-plan",
31
+ "delete",
32
+ "purge",
33
+ ],
22
34
  },
23
35
  title: { type: "string" },
24
36
  steps: { type: "array", items: { type: "string" } },
@@ -46,6 +58,8 @@ export function createPlanToolDefinitions(deps) {
46
58
  const namePart = m.name ? ` (${m.name})` : "";
47
59
  return `${icon} ${m.id}${namePart} — ${m.title} ${m.doneCount}/${m.stepCount}`;
48
60
  });
61
+ lines.push("");
62
+ lines.push(t("plan.list_legend"));
49
63
  return {
50
64
  success: true,
51
65
  output: `${t("plan.list_header")}\n${lines.join("\n")}`,
@@ -63,6 +77,19 @@ export function createPlanToolDefinitions(deps) {
63
77
  output: t("plan.not_found", { id: planId }),
64
78
  };
65
79
  }
80
+ // Switching to the plan that is already active must be a no-op, not
81
+ // a duplicate (observed: switch on the active plan preserved the
82
+ // active.json and re-set the same plan, leaving two copies).
83
+ if (found.status === "active") {
84
+ return {
85
+ success: true,
86
+ output: t("plan.already_active", {
87
+ id: found.plan.id,
88
+ title: found.plan.title,
89
+ }),
90
+ display: deps.trackerRef.current?.toPromptBlock(),
91
+ };
92
+ }
66
93
  deps.preserveActive();
67
94
  if (found.status === "draft") {
68
95
  deps.store.removeDraft(found.plan.id);
@@ -71,7 +98,10 @@ export function createPlanToolDefinitions(deps) {
71
98
  deps.store.removeArchived(found.plan.id);
72
99
  }
73
100
  deps.setPlan(found.plan);
74
- const display = PlanCreator.toPromptBlock(found.plan, 0);
101
+ // Display from the tracker (setPlan syncs the pointer past any
102
+ // done/skipped steps) so "current: step N" points at the first
103
+ // pending step of the switched-to plan, not always step 1.
104
+ const display = deps.trackerRef.current?.toPromptBlock();
75
105
  return {
76
106
  success: true,
77
107
  output: t("plan.switched", {
@@ -94,7 +124,7 @@ export function createPlanToolDefinitions(deps) {
94
124
  const replanned = PlanCreator.replan(oldPlan, newSteps, args.title ? String(args.title) : undefined);
95
125
  const keptCount = replanned.steps.length - newSteps.length;
96
126
  deps.setPlan(replanned);
97
- const display = PlanCreator.toPromptBlock(replanned, keptCount);
127
+ const display = deps.trackerRef.current?.toPromptBlock();
98
128
  return {
99
129
  success: true,
100
130
  output: t("plan.replanned", {
@@ -220,13 +250,14 @@ export function createPlanToolDefinitions(deps) {
220
250
  if (tracker.isComplete()) {
221
251
  const plan = tracker.getPlan();
222
252
  deps.store.archivePlan(plan);
253
+ deps.recordCompleted(plan); // final audit still verifies it
223
254
  deps.trackerRef.current = null;
224
- const done = plan.steps.filter((s) => s.status === "done").length;
255
+ const p = getPlanProgress(plan);
225
256
  return {
226
257
  success: true,
227
258
  output: t("plan.completed_archived", {
228
259
  id: plan.id,
229
- done: String(done),
260
+ done: String(p.settled),
230
261
  total: String(plan.steps.length),
231
262
  }),
232
263
  };
@@ -272,6 +303,29 @@ export function createPlanToolDefinitions(deps) {
272
303
  // intent. "create" (default) → named files must exist; "delete"
273
304
  // → named files must be GONE. Intent is model-declared (kind),
274
305
  // never keyword-inferred (rule #10).
306
+ //
307
+ // Auto-switch: when kind is "create" but ALL named files are gone,
308
+ // check if the agent actually deleted them (via delete_file/bash rm).
309
+ // Only then switch to "delete" — files that were never created
310
+ // (create-kind steps) remain blocked so the agent must create them.
311
+ if (status === "done" && target.kind !== "delete") {
312
+ const missing = deps.missingStepDeliverables(target);
313
+ if (missing.length > 0) {
314
+ const existing = deps.stillExistingDeliverables(target);
315
+ const allGone = deps.hasStepDeliverables(target) && existing.length === 0;
316
+ if (allGone) {
317
+ // Confirm the agent actually deleted these files (not just
318
+ // never created them) before auto-switching.
319
+ const wasDeleted = deps.wasFileDeleted
320
+ ? missing.every((f) => deps.wasFileDeleted(f))
321
+ : false;
322
+ if (wasDeleted) {
323
+ target.kind = "delete";
324
+ deps.store.saveActive(tracker.getPlan());
325
+ }
326
+ }
327
+ }
328
+ }
275
329
  if (status === "done") {
276
330
  if (target.kind === "delete") {
277
331
  const leftovers = deps.stillExistingDeliverables(target);
@@ -298,26 +352,52 @@ export function createPlanToolDefinitions(deps) {
298
352
  }
299
353
  }
300
354
  }
355
+ // Typecheck gate: "done" while the last edit still fails to compile
356
+ // is a self-certification the harness must refuse (observed: steps
357
+ // marked done with the project typecheck failing on every write —
358
+ // a "fix it later" cascade that never got fixed). The model must
359
+ // either fix the errors (a clean re-edit clears the map) or mark
360
+ // the step skipped.
361
+ if (status === "done") {
362
+ const blocked = stepTypecheckGate(deps.typecheckFailures, target.description);
363
+ if (blocked) {
364
+ return {
365
+ success: false,
366
+ output: t("plan.typecheck_failed", {
367
+ step: String(stepId),
368
+ error: blocked,
369
+ }),
370
+ };
371
+ }
372
+ }
301
373
  tracker.updateStepStatus(stepId, status);
302
374
  if (args.note)
303
375
  tracker.addNote(Number(args.step), String(args.note));
304
376
  tracker.syncCurrentStep();
305
377
  deps.store.saveActive(tracker.getPlan());
378
+ // A "done" step that names no file tokens has a vacuous gate — its
379
+ // completion cannot be auto-verified. Inform the model so it can
380
+ // run an explicit check or add file paths to the step description.
381
+ const vacuousGate = status === "done" && !deps.hasStepDeliverables(target);
382
+ const vacuousNote = vacuousGate
383
+ ? `\n${t("plan.no_deliverables", { step: String(stepId) })}`
384
+ : "";
306
385
  // If this update completed the last step → auto-archive. This breaks
307
386
  // the loop where a finished plan stays active and anchors the model
308
387
  // to "continue step N, do NOT create a new plan".
309
388
  if (tracker.isComplete()) {
310
389
  const plan = tracker.getPlan();
311
390
  deps.store.archivePlan(plan);
391
+ deps.recordCompleted(plan); // final audit still verifies it
312
392
  deps.trackerRef.current = null;
313
- const done = plan.steps.filter((s) => s.status === "done").length;
393
+ const p = getPlanProgress(plan);
314
394
  return {
315
395
  success: true,
316
396
  output: `${t("plan.step_status", { step: String(args.step), status: String(args.status || "done") })}\n${t("plan.completed_archived", {
317
397
  id: plan.id,
318
- done: String(done),
398
+ done: String(p.settled),
319
399
  total: String(plan.steps.length),
320
- })}`,
400
+ })}${vacuousNote}`,
321
401
  };
322
402
  }
323
403
  const progress = tracker.getProgressString();
@@ -325,15 +405,26 @@ export function createPlanToolDefinitions(deps) {
325
405
  _ctx.sessionLog?.plan("step-update", `${t("plan.step_status", { step: String(args.step), status: String(args.status || "done") })} | ${progress} | current: step ${tracker.getCurrentStepIndex() + 1}`);
326
406
  return {
327
407
  success: true,
328
- output: `${t("plan.step_status", { step: String(args.step), status: String(args.status || "done") })}\n${progress}`,
408
+ output: `${t("plan.step_status", { step: String(args.step), status: String(args.status || "done") })}\n${progress}${vacuousNote}`,
329
409
  display,
330
410
  };
331
411
  }
332
412
  if (action === "update" &&
333
413
  Array.isArray(args.steps) &&
334
414
  args.steps.length > 0 &&
335
- deps.trackerRef.current) {
415
+ !args.step) {
336
416
  const tracker = deps.trackerRef.current;
417
+ if (!tracker) {
418
+ return { success: false, output: t("plan.no_active") };
419
+ }
420
+ // Rebuilding a plan that already has progress silently discards
421
+ // every status (ses_mt4fn58c: the model sent its whole step list
422
+ // with ✅-emoji statuses expecting progress marks and got a fresh
423
+ // plan back). A not-yet-started plan is safe to rebuild.
424
+ const progressed = tracker.getPlan().steps.some((s) => s.status !== "pending");
425
+ if (progressed) {
426
+ return { success: false, output: t("plan.bulk_update_rejected") };
427
+ }
337
428
  const title = String(args.title || tracker.getPlan().title);
338
429
  const steps = args.steps.map(String);
339
430
  const parsed = deps.parseKinds(args, steps);
@@ -342,7 +433,7 @@ export function createPlanToolDefinitions(deps) {
342
433
  }
343
434
  const plan = PlanCreator.createPlan(title, steps, deps.baseDir, parsed.kinds ?? []);
344
435
  deps.setPlan(plan);
345
- const display = PlanCreator.toPromptBlock(plan, 0);
436
+ const display = deps.trackerRef.current?.toPromptBlock();
346
437
  const output = t("plan.updated", {
347
438
  title,
348
439
  steps: String(steps.length),
@@ -354,15 +445,91 @@ export function createPlanToolDefinitions(deps) {
354
445
  };
355
446
  }
356
447
  if (action === "abort") {
448
+ const planId = args.id ? String(args.id) : "";
449
+ if (planId) {
450
+ // Abort a SPECIFIC plan (not the active one). Draft → archive,
451
+ // active → archive + clear tracker. Archived plans are already
452
+ // terminal — there is nothing to abort.
453
+ const found = deps.store.find(planId);
454
+ if (!found) {
455
+ return {
456
+ success: false,
457
+ output: t("plan.not_found", { id: planId }),
458
+ };
459
+ }
460
+ if (found.status === "archived") {
461
+ return {
462
+ success: false,
463
+ output: t("plan.already_archived", { id: planId }),
464
+ };
465
+ }
466
+ deps.store.archivePlan(found.plan); // clears active if it was active
467
+ if (found.status === "active") {
468
+ deps.trackerRef.current = null;
469
+ deps.clearCompleted();
470
+ }
471
+ return {
472
+ success: true,
473
+ output: t("plan.aborted_id", { id: found.plan.id }),
474
+ };
475
+ }
357
476
  const tracker = deps.trackerRef.current;
358
- if (tracker) {
359
- deps.store.archivePlan(tracker.getPlan());
477
+ if (!tracker) {
478
+ // Abort with no active plan and no id — previously returned
479
+ // success:true, which made the model believe it aborted a plan
480
+ // that did not exist (observed: 3× abort on an empty list).
481
+ return { success: false, output: t("plan.no_active") };
360
482
  }
483
+ deps.store.archivePlan(tracker.getPlan());
361
484
  deps.trackerRef.current = null;
362
485
  deps.store.clearActive();
486
+ deps.clearCompleted();
363
487
  return { success: true, output: t("plan.aborted") };
364
488
  }
489
+ if (action === "delete") {
490
+ const planId = String(args.id || "");
491
+ if (!planId) {
492
+ return { success: false, output: t("plan.delete_no_id") };
493
+ }
494
+ const deleted = deps.store.deletePlan(planId);
495
+ if (!deleted) {
496
+ return {
497
+ success: false,
498
+ output: t("plan.not_found", { id: planId }),
499
+ };
500
+ }
501
+ if (deleted === "active") {
502
+ deps.trackerRef.current = null;
503
+ deps.clearCompleted();
504
+ }
505
+ return {
506
+ success: true,
507
+ output: t("plan.deleted", { id: planId }),
508
+ };
509
+ }
510
+ if (action === "purge") {
511
+ const count = deps.store.purgeAll();
512
+ deps.trackerRef.current = null;
513
+ deps.clearCompleted();
514
+ return {
515
+ success: true,
516
+ output: t("plan.purged", { count: String(count) }),
517
+ };
518
+ }
365
519
  if (!deps.trackerRef.current) {
520
+ // Update with an id that points at a draft/archived plan: say the
521
+ // plan is read-only instead of a bare "No active plan" (observed:
522
+ // 10+ iterations re-updating an archived plan and getting the same
523
+ // unhelpful answer back).
524
+ if (args.id) {
525
+ const found = deps.store.find(String(args.id));
526
+ if (found && found.status !== "active") {
527
+ return {
528
+ success: false,
529
+ output: t("plan.readonly", { id: found.plan.id, status: found.status }),
530
+ };
531
+ }
532
+ }
366
533
  return { success: false, output: t("plan.no_active") };
367
534
  }
368
535
  return {
@@ -373,6 +540,7 @@ export function createPlanToolDefinitions(deps) {
373
540
  },
374
541
  {
375
542
  name: "todo",
543
+ icon: "☑️",
376
544
  alwaysOn: true,
377
545
  description: "Manage sub-tasks within current plan step. Use to break down complex steps into smaller tasks.",
378
546
  parameters: {
@@ -450,7 +618,7 @@ export function createPlanToolDefinitions(deps) {
450
618
  });
451
619
  output += ` (${doneCount}/${subs.length})`;
452
620
  if (doneCount === subs.length) {
453
- output += `\nAll sub-tasks done — call plan update step=${currentStep.id} status=done to complete this step.`;
621
+ output += `\n${t("plan.subtasks_done", { stepId: String(currentStep.id) })}`;
454
622
  }
455
623
  return { success: true, output, display };
456
624
  }
@@ -458,10 +626,10 @@ export function createPlanToolDefinitions(deps) {
458
626
  const subs = currentStep.subtasks ?? [];
459
627
  const lines = subs.length
460
628
  ? subs.map((s) => `${s.done ? "[x]" : "[ ]"} ${s.text}`)
461
- : ["(no sub-tasks)"];
629
+ : [t("plan.no_subtasks")];
462
630
  return {
463
631
  success: true,
464
- output: `Step ${currentStep.id}: ${currentStep.description}\n${lines.join("\n")}`,
632
+ output: `${t("plan.step_label", { stepId: String(currentStep.id), description: currentStep.description })}\n${lines.join("\n")}`,
465
633
  };
466
634
  }
467
635
  return {
@@ -472,6 +640,7 @@ export function createPlanToolDefinitions(deps) {
472
640
  },
473
641
  {
474
642
  name: "verify",
643
+ icon: "✅",
475
644
  alwaysOn: true,
476
645
  description: "Run verification for the current step. Checks files mentioned in the step description.",
477
646
  parameters: {
@@ -487,7 +656,7 @@ export function createPlanToolDefinitions(deps) {
487
656
  const step = args.step ? tracker.getStep(Number(args.step)) : tracker.getCurrentStep();
488
657
  if (!step)
489
658
  return { success: false, output: t("plan.step_not_found") };
490
- const result = await deps.verifier.verifyStep(step.description);
659
+ const result = await deps.verifier.verifyStep(step.description, step.kind);
491
660
  if (result.noFiles) {
492
661
  return {
493
662
  success: true,
@@ -6,26 +6,20 @@ function generatePlanId() {
6
6
  }
7
7
  return `plan_${id}`;
8
8
  }
9
+ /**
10
+ * Single source of truth for plan progress counting (ses_mt4fn58c: `show`
11
+ * said "6/7 done, complete" while `list` said "7/7" for the same plan —
12
+ * one counted only "done", the other counted done+skipped). A skipped step
13
+ * needs no further work, so it counts as settled everywhere.
14
+ */
15
+ export function getPlanProgress(plan) {
16
+ const done = plan.steps.filter((s) => s.status === "done").length;
17
+ const skipped = plan.steps.filter((s) => s.status === "skipped").length;
18
+ const total = plan.steps.length;
19
+ const settled = done + skipped;
20
+ return { done, skipped, total, settled, terminal: total > 0 && settled === total };
21
+ }
9
22
  export class PlanCreator {
10
- static isMultiStep(task) {
11
- const fileCount = (task.match(/\b[\w./-]+\.[a-z]+\b/gi) || []).length;
12
- if (fileCount > 1)
13
- return true;
14
- const actionWords = [
15
- "implement",
16
- "create",
17
- "add",
18
- "build",
19
- "setup",
20
- "configure",
21
- "write",
22
- "make",
23
- "develop",
24
- ];
25
- const words = task.split(/\s+/);
26
- const hasActionWord = actionWords.some((w) => task.toLowerCase().includes(w));
27
- return hasActionWord && words.length > 8;
28
- }
29
23
  static createPlan(title, stepDescriptions, baseDir, kinds) {
30
24
  const stepCount = stepDescriptions.length;
31
25
  return {
@@ -46,16 +40,21 @@ export class PlanCreator {
46
40
  const baseTitle = plan.title.replace(/^\[\d+[^]]*\]\s*/, "");
47
41
  const newTitle = title || baseTitle;
48
42
  const totalSteps = kept.length + newSteps.length;
49
- const added = newSteps.map((desc, i) => ({
50
- id: kept.length + i + 1,
43
+ const added = newSteps.map((desc) => ({
44
+ id: -1, // renumbered below
51
45
  description: desc,
52
46
  status: "pending",
53
47
  kind: "create",
54
48
  }));
49
+ // Renumber every step (kept + added) sequentially. Kept steps keep their
50
+ // status and kind but may collide with the added ids when the original
51
+ // plan skipped/pending steps in the middle (e.g. done 1, done 3 → kept
52
+ // [1,3], added [2…] collides with kept id 3).
53
+ const steps = [...kept, ...added].map((s, i) => ({ ...s, id: i + 1 }));
55
54
  return {
56
55
  id: plan.id,
57
56
  title: `[${totalSteps}] ${newTitle}`,
58
- steps: [...kept, ...added],
57
+ steps,
59
58
  createdAt: plan.createdAt,
60
59
  baseDir: plan.baseDir,
61
60
  name: plan.name,
@@ -63,10 +62,15 @@ export class PlanCreator {
63
62
  }
64
63
  static toPromptBlock(plan, currentStepIndex) {
65
64
  const date = plan.createdAt.slice(0, 10);
65
+ const p = getPlanProgress(plan);
66
+ const skippedPart = p.skipped > 0 ? `, ${p.skipped} skipped` : "";
67
+ const progress = p.terminal
68
+ ? `${p.settled}/${p.total} done${skippedPart}, complete`
69
+ : `${p.settled}/${p.total} done${skippedPart}, current: step ${currentStepIndex + 1}`;
66
70
  const lines = [
67
71
  `[${plan.id}] ${plan.title}`,
68
72
  `Dir: ${plan.baseDir}`,
69
- `Created: ${date} | Progress: ${plan.steps.filter((s) => s.status === "done").length}/${plan.steps.length} done, current: step ${currentStepIndex + 1}`,
73
+ `Created: ${date} | Progress: ${progress}`,
70
74
  ``,
71
75
  ];
72
76
  for (const step of plan.steps) {