@pentoshi/clai 2.0.18 → 2.0.19

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 (43) hide show
  1. package/dist/agent/context-manager.js +24 -9
  2. package/dist/agent/context-manager.js.map +1 -1
  3. package/dist/agent/events.d.ts +1 -1
  4. package/dist/agent/loop-guard.d.ts +9 -0
  5. package/dist/agent/loop-guard.js +45 -3
  6. package/dist/agent/loop-guard.js.map +1 -1
  7. package/dist/agent/runner.d.ts +7 -0
  8. package/dist/agent/runner.js +169 -23
  9. package/dist/agent/runner.js.map +1 -1
  10. package/dist/agent/task-analyzer.d.ts +3 -3
  11. package/dist/agent/task-analyzer.js +16 -4
  12. package/dist/agent/task-analyzer.js.map +1 -1
  13. package/dist/commands/update.js +1 -1
  14. package/dist/prompts/index.d.ts +1 -1
  15. package/dist/prompts/index.js +8 -1
  16. package/dist/prompts/index.js.map +1 -1
  17. package/dist/repl.js +17 -0
  18. package/dist/repl.js.map +1 -1
  19. package/dist/safety/classifier.js.map +1 -1
  20. package/dist/tools/fs.d.ts +33 -5
  21. package/dist/tools/fs.js +22 -19
  22. package/dist/tools/fs.js.map +1 -1
  23. package/dist/tools/registry.d.ts +1 -0
  24. package/dist/tools/registry.js +14 -12
  25. package/dist/tools/registry.js.map +1 -1
  26. package/dist/tui/App.js +3 -2
  27. package/dist/tui/App.js.map +1 -1
  28. package/dist/tui/components/ConfirmModal.js +9 -2
  29. package/dist/tui/components/ConfirmModal.js.map +1 -1
  30. package/dist/tui/components/Pager.js +14 -1
  31. package/dist/tui/components/Pager.js.map +1 -1
  32. package/dist/tui/components/PickerPanel.d.ts +2 -1
  33. package/dist/tui/components/PickerPanel.js +4 -2
  34. package/dist/tui/components/PickerPanel.js.map +1 -1
  35. package/dist/tui/confirm.d.ts +1 -1
  36. package/dist/tui/confirm.js +6 -0
  37. package/dist/tui/confirm.js.map +1 -1
  38. package/dist/tui/render-lines.js +23 -9
  39. package/dist/tui/render-lines.js.map +1 -1
  40. package/dist/tui/state.d.ts +1 -1
  41. package/dist/tui/state.js +4 -1
  42. package/dist/tui/state.js.map +1 -1
  43. package/package.json +1 -1
@@ -16,8 +16,8 @@ export function estimateMessagesTokens(messages) {
16
16
  }
17
17
  return sum;
18
18
  }
19
- const DEFAULT_BUDGET_TOKENS = 24_000;
20
- const DEFAULT_KEEP_RECENT = 8;
19
+ const DEFAULT_BUDGET_TOKENS = 32_000;
20
+ const DEFAULT_KEEP_RECENT = 12;
21
21
  /**
22
22
  * Replace older messages with a single condensed "memory" message while
23
23
  * preserving the system prompt and the most recent N messages.
@@ -75,12 +75,27 @@ export function compactMessages(messages, options = {}) {
75
75
  export async function compactMessagesWithSummary(messages, summarize, options = {}, sessionTranscript) {
76
76
  const before = messages.length;
77
77
  const beforeTokens = estimateMessagesTokens(messages);
78
- const keepRecent = Math.max(2, options.keepRecent ?? DEFAULT_KEEP_RECENT);
78
+ const isForced = options.budgetTokens === 0;
79
+ let keepRecent = Math.max(2, options.keepRecent ?? DEFAULT_KEEP_RECENT);
79
80
  const start = messages[0]?.role === "system" ? 1 : 0;
80
- const tailStart = Math.max(start, messages.length - keepRecent);
81
- const older = messages.slice(start, tailStart);
82
- if (older.length === 0) {
83
- return { messages: [...messages], before, after: before, beforeTokens, afterTokens: beforeTokens, summarized: false };
81
+ let tailStart = Math.max(start, messages.length - keepRecent);
82
+ let older = messages.slice(start, tailStart);
83
+ // If forced and the older slice would be empty, try keeping fewer recent
84
+ // messages (minimum 1) so we have something to compact (e.g. the first user prompt).
85
+ if (older.length === 0 && isForced && messages.length >= start + 2) {
86
+ keepRecent = 1;
87
+ tailStart = messages.length - 1;
88
+ older = messages.slice(start, tailStart);
89
+ }
90
+ if (older.length === 0 && !sessionTranscript?.trim()) {
91
+ return {
92
+ messages: [...messages],
93
+ before,
94
+ after: before,
95
+ beforeTokens,
96
+ afterTokens: beforeTokens,
97
+ summarized: false,
98
+ };
84
99
  }
85
100
  const messageTranscript = older
86
101
  .map((message) => `${message.role.toUpperCase()}: ${redactSecrets(message.content)}`)
@@ -107,7 +122,7 @@ export async function compactMessagesWithSummary(messages, summarize, options =
107
122
  ...messages.slice(tailStart),
108
123
  ];
109
124
  const afterTokens = estimateMessagesTokens(compacted);
110
- if (afterTokens >= beforeTokens) {
125
+ if (afterTokens >= beforeTokens && !isForced) {
111
126
  const deterministicCompacted = compactMessages(messages, { ...options, budgetTokens: 0 });
112
127
  const detTokens = estimateMessagesTokens(deterministicCompacted);
113
128
  if (detTokens < beforeTokens) {
@@ -144,7 +159,7 @@ export async function compactMessagesWithSummary(messages, summarize, options =
144
159
  }
145
160
  const compacted = compactMessages(messages, { ...options, budgetTokens: 0 });
146
161
  const afterTokens = estimateMessagesTokens(compacted);
147
- if (afterTokens >= beforeTokens) {
162
+ if (afterTokens >= beforeTokens && !isForced) {
148
163
  return {
149
164
  messages: [...messages],
150
165
  before,
@@ -1 +1 @@
1
- {"version":3,"file":"context-manager.js","sourceRoot":"","sources":["../../src/agent/context-manager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"context-manager.js","sourceRoot":"","sources":["../../src/agent/context-manager.ts"],"names":[],"mappings":"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"}
@@ -48,7 +48,7 @@ export type AgentEvent = {
48
48
  } | {
49
49
  type: "confirm-request";
50
50
  id: string;
51
- kind: "tool" | "pentest" | "reset";
51
+ kind: "tool" | "pentest" | "reset" | "continue";
52
52
  prompt: string;
53
53
  } | {
54
54
  type: "turn-end";
@@ -31,6 +31,10 @@ export declare class LoopGuard {
31
31
  * expected to fix the cause (install a tool, create a dir) and retry. Only
32
32
  * calls that already SUCCEEDED are deduped, since re-running them wastes a
33
33
  * step and risks an infinite summarize loop.
34
+ *
35
+ * Read-only tools (web.fetch, fs.read, etc.) get a higher threshold (3)
36
+ * because they may legitimately need re-calling after context compaction
37
+ * removes their results.
34
38
  */
35
39
  shouldBlock(name: string, args: Record<string, unknown>): {
36
40
  block: boolean;
@@ -42,6 +46,11 @@ export declare class LoopGuard {
42
46
  * (e.g., command not found → retry → not found → ...).
43
47
  */
44
48
  hasRepeatedFailures(threshold?: number): boolean;
49
+ /**
50
+ * Reset counters for read-only tools. Called after context compaction
51
+ * so the model can re-fetch data whose results were compacted away.
52
+ */
53
+ resetReadOnly(): void;
45
54
  /**
46
55
  * Get the total number of recorded attempts.
47
56
  */
@@ -1,3 +1,24 @@
1
+ /**
2
+ * Non-mutating tools that may legitimately need re-calling after context
3
+ * compaction removes their earlier results. These get a higher dedup
4
+ * threshold (3 vs 2 for write tools) and their counters can be reset when
5
+ * context is compacted.
6
+ */
7
+ const READ_ONLY_TOOLS = new Set([
8
+ "web.fetch",
9
+ "http.fetch",
10
+ "web.search",
11
+ "dns.lookup",
12
+ "whois.lookup",
13
+ "fs.read",
14
+ "fs.list",
15
+ "fs.search",
16
+ "sysinfo",
17
+ "net.context",
18
+ "tool.check",
19
+ "image.ocr",
20
+ "pdf.read",
21
+ ]);
1
22
  /**
2
23
  * Track and detect tool-call repetition patterns so the agent doesn't
3
24
  * waste steps in loops.
@@ -47,6 +68,10 @@ export class LoopGuard {
47
68
  * expected to fix the cause (install a tool, create a dir) and retry. Only
48
69
  * calls that already SUCCEEDED are deduped, since re-running them wastes a
49
70
  * step and risks an infinite summarize loop.
71
+ *
72
+ * Read-only tools (web.fetch, fs.read, etc.) get a higher threshold (3)
73
+ * because they may legitimately need re-calling after context compaction
74
+ * removes their results.
50
75
  */
51
76
  shouldBlock(name, args) {
52
77
  const sig = this.canonicalize(name, args);
@@ -60,7 +85,10 @@ export class LoopGuard {
60
85
  // that just wrote a file to "use the results you already have" is
61
86
  // nonsensical and has caused models to assume the whole task is done.
62
87
  const isWrite = name === "fs.write" || name === "fs.writeMany" || name === "fs.edit";
63
- if (count === 1) {
88
+ // Read-only tools get a higher threshold — they may need re-calling
89
+ // after context compaction removes their earlier results.
90
+ const threshold = READ_ONLY_TOOLS.has(name) ? 3 : 2;
91
+ if (count < threshold) {
64
92
  return {
65
93
  block: false,
66
94
  reason: isWrite
@@ -68,12 +96,12 @@ export class LoopGuard {
68
96
  : `${name} has already been called with these arguments once and succeeded. Consider using the results you already have.`,
69
97
  };
70
98
  }
71
- // count >= 2 and at least one success: block
99
+ // count >= threshold and at least one success: block
72
100
  return {
73
101
  block: true,
74
102
  reason: isWrite
75
103
  ? `${name} was already called ${count} time(s) with the identical path and content. That file is already written. Continue with the remaining files/steps or give your final answer.`
76
- : `${name} was already called ${count} time(s) with the same arguments. Summarize existing results instead.`,
104
+ : `${name} was already called ${count} time(s) with the same arguments. The data is already in your context — analyze what you have and move to the next step.`,
77
105
  };
78
106
  }
79
107
  getAttemptCount(name, args) {
@@ -90,6 +118,20 @@ export class LoopGuard {
90
118
  const recent = this.attempts.slice(-threshold);
91
119
  return recent.every((a) => !a.ok);
92
120
  }
121
+ /**
122
+ * Reset counters for read-only tools. Called after context compaction
123
+ * so the model can re-fetch data whose results were compacted away.
124
+ */
125
+ resetReadOnly() {
126
+ for (const sig of [...this.signatureCount.keys()]) {
127
+ const name = sig.split("::")[0] ?? "";
128
+ if (READ_ONLY_TOOLS.has(name)) {
129
+ this.signatureCount.delete(sig);
130
+ this.signatureSuccess.delete(sig);
131
+ }
132
+ }
133
+ this.attempts = this.attempts.filter((a) => !READ_ONLY_TOOLS.has(a.callName));
134
+ }
93
135
  /**
94
136
  * Get the total number of recorded attempts.
95
137
  */
@@ -1 +1 @@
1
- {"version":3,"file":"loop-guard.js","sourceRoot":"","sources":["../../src/agent/loop-guard.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"loop-guard.js","sourceRoot":"","sources":["../../src/agent/loop-guard.ts"],"names":[],"mappings":"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"}
@@ -18,6 +18,8 @@ export declare function createSessionPolicy(): SessionPolicy;
18
18
  export interface ConfirmPort {
19
19
  confirmTool(call: ToolCall): Promise<boolean>;
20
20
  confirmPentest(): Promise<boolean>;
21
+ /** Ask the user whether to continue after hitting the step budget. */
22
+ confirmContinue?(steps: number): Promise<boolean>;
21
23
  }
22
24
  export interface AgentRunOptions {
23
25
  provider?: ProviderId | undefined;
@@ -120,6 +122,11 @@ export declare function sameToolCall(a: ToolCall, b: ToolCall): boolean;
120
122
  * copies and note the collapse so the meaning is preserved without the bulk.
121
123
  */
122
124
  export declare function collapseRepeatedText(text: string): string;
125
+ /**
126
+ * Detect pentest/security tasks that need the full step budget.
127
+ * Mirrors looksLikeBuildTask but for security work.
128
+ */
129
+ export declare function looksLikePentestTask(prompt: string, history?: ChatMessage[] | undefined): boolean;
123
130
  /**
124
131
  * Decide whether this turn should get the build workflow (explore → plan →
125
132
  * implement) and a generous step budget. Looks at the current prompt first,
@@ -2,7 +2,7 @@ import { confirm } from "@inquirer/prompts";
2
2
  import chalk from "chalk";
3
3
  import { mkdir, writeFile } from "node:fs/promises";
4
4
  import { homedir } from "node:os";
5
- import { join } from "node:path";
5
+ import { join, resolve } from "node:path";
6
6
  import { streamWithProvider } from "../llm/router.js";
7
7
  import { randomUUID } from "node:crypto";
8
8
  import { jobManager } from "../tools/jobs.js";
@@ -26,6 +26,7 @@ import { analyzeTask } from "./task-analyzer.js";
26
26
  import { LoopGuard } from "./loop-guard.js";
27
27
  import { createPlan, loadPlan, savePlan, markTask, } from "../store/plan.js";
28
28
  import { renderPlanChecklist, renderPlanSidePane } from "../ui/plan-pane.js";
29
+ import { pathInsideSandbox } from "../tools/fs.js";
29
30
  /** Render the plan as a right-side pane on wide terminals, else inline. */
30
31
  function renderPlanForTerminal(plan) {
31
32
  const cols = process.stdout.columns ?? 0;
@@ -547,6 +548,25 @@ const LOCAL_RUNTIME_RE = /\b(?:current\s+(?:directory|dir|cwd|working\s+director
547
548
  // under-budgeting silently truncates a half-built project.
548
549
  const BUILD_TASK_RE = /\b(?:build|create|scaffold|generate|make|set\s*up|setup|bootstrap|init(?:ialize)?|implement|add|write|develop|code|refactor|migrate|convert|wire\s*up|integrate)\b[\s\S]{0,80}\b(?:app|application|project|site|website|web\s*app|server|api|service|component|page|module|feature|cli|script|library|package|frontend|backend|fullstack|game|bot|dashboard|form|endpoint|database|schema|test|tests|suite)\b/i;
549
550
  const BUILD_STACK_RE = /\b(?:react|next(?:\.?js)?|vue|svelte|angular|vite|webpack|express|fastify|nest(?:js)?|django|flask|fastapi|rails|laravel|spring|node(?:\.?js)?|typescript|tailwind|redux|prisma|mongoose|graphql|docker|kubernetes)\b/i;
551
+ // Pentest / security keywords — these tasks are inherently multi-step and
552
+ // always deserve the full step budget, just like build tasks.
553
+ const PENTEST_TASK_RE = /\b(?:pentest|pen[\s-]?test|penetration|security\s*(?:test|audit|scan|assess)|csrf|xss|sqli|sql[\s-]?inject|rce|lfi|rfi|ssrf|idor|xxe|brute[\s-]?force|enumerat|exploit|vulnerabilit|recon|bug[\s-]?bounty|ctf|capture[\s-]?the[\s-]?flag|red[\s-]?team|offensive|nmap|nikto|nuclei|ffuf|gobuster|sqlmap|hydra|metasploit)\b/i;
554
+ /**
555
+ * Detect pentest/security tasks that need the full step budget.
556
+ * Mirrors looksLikeBuildTask but for security work.
557
+ */
558
+ export function looksLikePentestTask(prompt, history) {
559
+ if (PENTEST_TASK_RE.test(prompt))
560
+ return true;
561
+ if (history && history.length > 0) {
562
+ const recent = history.slice(-6);
563
+ for (const msg of recent) {
564
+ if (msg.role === "user" && PENTEST_TASK_RE.test(msg.content))
565
+ return true;
566
+ }
567
+ }
568
+ return false;
569
+ }
550
570
  // Short continuation prompts that, on their own, carry no build signal but
551
571
  // clearly mean "keep going with what we were doing".
552
572
  const CONTINUATION_RE = /^(?:do\s+it|build\s+it|build\s+fully|build\s+it\s+fully|go\s+ahead|continue|proceed|keep\s+going|finish(?:\s+it)?|complete(?:\s+it)?|yes|ok(?:ay)?|make\s+it|run\s+it|next|on\s+your\s+own|build\s+(?:fully\s+)?on\s+your\s+own)\b/i;
@@ -752,6 +772,58 @@ function restoreInteractiveStdin() {
752
772
  /* ignore */
753
773
  }
754
774
  }
775
+ /**
776
+ * Build a rich summary when the agent stops (user declined to continue or
777
+ * maxIterations ceiling hit). Includes plan state, key findings, and clear
778
+ * resume instructions so a later "continue" can pick up exactly here.
779
+ */
780
+ async function buildRichStopSummary(messages, session, steps) {
781
+ const plan = await loadPlan(session.sessionId).catch(() => undefined);
782
+ const parts = [];
783
+ parts.push(`Session paused after ${steps} steps.\n`);
784
+ // Plan state
785
+ if (plan) {
786
+ parts.push("## Plan Status");
787
+ parts.push(`Goal: ${plan.goal}`);
788
+ for (const task of plan.tasks) {
789
+ const icon = task.state === "done"
790
+ ? "✓"
791
+ : task.state === "in_progress"
792
+ ? "▶"
793
+ : task.state === "failed"
794
+ ? "✗"
795
+ : task.state === "skipped"
796
+ ? "↷"
797
+ : "·";
798
+ parts.push(` ${icon} [${task.id}] (${task.state}) ${task.title}${task.note ? ` — ${task.note}` : ""}`);
799
+ }
800
+ const next = plan.tasks.find((t) => t.state === "pending" || t.state === "in_progress");
801
+ if (next) {
802
+ parts.push(`\nNext task to resume: ${next.id} — "${next.title}"`);
803
+ }
804
+ const doneCount = plan.tasks.filter((t) => t.state === "done").length;
805
+ parts.push(`\nProgress: ${doneCount}/${plan.tasks.length} tasks done.`);
806
+ }
807
+ // Key findings from tool results (last 20 tool messages)
808
+ const toolMsgs = messages.filter((m) => m.role === "tool").slice(-20);
809
+ if (toolMsgs.length > 0) {
810
+ parts.push("\n## Key Findings So Far");
811
+ for (const msg of toolMsgs) {
812
+ const firstLine = msg.content.split("\n")[0] ?? "";
813
+ // Extract tool name from the structured format "Tool <name> result ..."
814
+ const toolMatch = firstLine.match(/^Tool (\S+) result/);
815
+ if (toolMatch) {
816
+ parts.push(`- ${toolMatch[1]}: ${firstLine.slice(0, 150)}`);
817
+ }
818
+ else {
819
+ parts.push(`- ${firstLine.slice(0, 150)}`);
820
+ }
821
+ }
822
+ }
823
+ parts.push("\n## To Resume");
824
+ parts.push('Type "continue" to pick up from where this session left off.');
825
+ return parts.join("\n");
826
+ }
755
827
  /**
756
828
  * Tools allowed while an UN-approved plan is active. Before the user runs
757
829
  * /implement, the agent may only (re)create the plan and do read-only
@@ -876,6 +948,12 @@ const inquirerConfirmPort = {
876
948
  default: false,
877
949
  });
878
950
  },
951
+ async confirmContinue(steps) {
952
+ return confirm({
953
+ message: chalk.yellow(` ${steps} steps reached — continue?`),
954
+ default: true,
955
+ });
956
+ },
879
957
  };
880
958
  async function ensurePentestAuthorization(call, autoConfirm, session, confirmPort) {
881
959
  if (!isPentestToolCall(call))
@@ -1166,7 +1244,9 @@ export async function runAgentLoop(prompt, options = {}) {
1166
1244
  // "now"/"latest" that trip the volatile-info regex; without this guard the
1167
1245
  // agent burns its turn searching the date instead of writing files.
1168
1246
  const buildLikeTurn = looksLikeBuildTask(prompt, options.history);
1247
+ const pentestLikeTurn = looksLikePentestTask(prompt, options.history);
1169
1248
  const freshWebSearchRequired = !buildLikeTurn &&
1249
+ !pentestLikeTurn &&
1170
1250
  toolNames.includes("web.search") &&
1171
1251
  requiresFreshWebSearch(prompt);
1172
1252
  const systemSections = [renderAgentSystemPrompt(toolNames.join(", "))];
@@ -1278,14 +1358,15 @@ export async function runAgentLoop(prompt, options = {}) {
1278
1358
  const analysis = analyzeTask(prompt);
1279
1359
  const hasHistory = (options.history?.length ?? 0) > 0;
1280
1360
  const buildLike = buildLikeTurn;
1361
+ const pentestLike = looksLikePentestTask(prompt, options.history);
1281
1362
  let stepBudget = analysis.complexity === "simple"
1282
1363
  ? 20
1283
1364
  : analysis.complexity === "standard"
1284
1365
  ? 40
1285
1366
  : maxSteps;
1286
- if (buildLike) {
1287
- // Scaffolding / multi-file work needs room: many file writes plus a
1288
- // verify/build step. Continuation prompts ("do it") inherit this too.
1367
+ if (buildLike || pentestLike) {
1368
+ // Scaffolding / multi-file work / pentest tasks need room.
1369
+ // Continuation prompts ("do it") inherit this too.
1289
1370
  stepBudget = Math.max(stepBudget, maxSteps);
1290
1371
  }
1291
1372
  else if (hasHistory) {
@@ -1295,7 +1376,7 @@ export async function runAgentLoop(prompt, options = {}) {
1295
1376
  }
1296
1377
  // Hard ceiling on total loop iterations (productive + recovery) so a model
1297
1378
  // stuck emitting only thinking or malformed calls can't loop indefinitely.
1298
- const maxIterations = stepBudget * 3;
1379
+ let maxIterations = stepBudget * 3;
1299
1380
  let productiveSteps = 0;
1300
1381
  let step = -1;
1301
1382
  let nextToolEventId = 0;
@@ -1393,7 +1474,19 @@ export async function runAgentLoop(prompt, options = {}) {
1393
1474
  if (needsPentestAuth) {
1394
1475
  pentestJustConfirmed = true;
1395
1476
  }
1396
- const forceManualConfirm = call.name === "fs.delete";
1477
+ let forceManualConfirm = call.name === "fs.delete";
1478
+ if (call.name.startsWith("fs.") &&
1479
+ !isPreApprovalAllowedTool(call.name)) {
1480
+ const pathArg = typeof call.args.path === "string" ? call.args.path : undefined;
1481
+ if (pathArg) {
1482
+ const expandHomeLocal = (p) => p.startsWith("~/") || p.startsWith("~\\") ? join(homedir(), p.slice(2)) : p === "~" ? homedir() : p;
1483
+ const resolved = resolve(expandHomeLocal(pathArg));
1484
+ const mode = (call.name === "fs.read" || call.name === "fs.list" || call.name === "fs.search") ? "read" : "write";
1485
+ if (!pathInsideSandbox(resolved, mode)) {
1486
+ forceManualConfirm = true;
1487
+ }
1488
+ }
1489
+ }
1397
1490
  if (decision.level === "confirm" && !pentestJustConfirmed) {
1398
1491
  const ok = await confirmToolExecution(call, forceManualConfirm ? false : Boolean(options.autoConfirm), session, confirmPort);
1399
1492
  restoreInteractiveStdin();
@@ -1475,6 +1568,7 @@ export async function runAgentLoop(prompt, options = {}) {
1475
1568
  return;
1476
1569
  printLive(chunk);
1477
1570
  },
1571
+ confirmed: true,
1478
1572
  });
1479
1573
  if (liveBytes > 0 || liveTruncatedNotified) {
1480
1574
  writeToolOutput(toolEventId, "\n", "\n");
@@ -1555,8 +1649,59 @@ export async function runAgentLoop(prompt, options = {}) {
1555
1649
  // `step` is the productive-step index (used for display + audit). It only
1556
1650
  // advances when the previous iteration actually executed a tool.
1557
1651
  step = productiveSteps;
1558
- if (productiveSteps >= stepBudget)
1559
- break;
1652
+ // ── Step budget gate: ask the user instead of hard-stopping ────────
1653
+ if (productiveSteps >= stepBudget) {
1654
+ const askContinue = confirmPort.confirmContinue ?? inquirerConfirmPort.confirmContinue;
1655
+ let shouldContinue = false;
1656
+ try {
1657
+ shouldContinue = await askContinue(productiveSteps);
1658
+ restoreInteractiveStdin();
1659
+ }
1660
+ catch {
1661
+ // Abort / non-interactive — treat as decline.
1662
+ shouldContinue = false;
1663
+ }
1664
+ if (shouldContinue) {
1665
+ // Extend the budget for another chunk of work.
1666
+ const extension = Math.max(40, maxSteps);
1667
+ stepBudget += extension;
1668
+ maxIterations = stepBudget * 3;
1669
+ // Compact older messages to free context space.
1670
+ const compacted = compactMessages(messages, { budgetTokens: 0, keepRecent: 12 });
1671
+ if (compacted.length < messages.length) {
1672
+ messages.splice(0, messages.length, ...compacted);
1673
+ loopGuard.resetReadOnly();
1674
+ await auditLog("agent.compact", {
1675
+ newLength: messages.length,
1676
+ estimatedTokens: estimateMessagesTokens(messages),
1677
+ reason: "step-budget-continue",
1678
+ });
1679
+ }
1680
+ // Inject a progress summary so the model stays focused.
1681
+ const livePlan = await loadPlan(session.sessionId).catch(() => undefined);
1682
+ let progressNote = "The step limit was reached and the user chose to continue. ";
1683
+ progressNote += "Review what you have accomplished so far and continue with the NEXT unfinished step. ";
1684
+ progressNote += "Do NOT repeat work already done. Do NOT re-fetch pages or re-run scans whose results you already have.";
1685
+ if (livePlan) {
1686
+ const doneTasks = livePlan.tasks.filter(t => t.state === "done");
1687
+ const pendingTasks = livePlan.tasks.filter(t => t.state === "pending" || t.state === "in_progress");
1688
+ progressNote += `\n\nPlan progress: ${doneTasks.length}/${livePlan.tasks.length} tasks done.`;
1689
+ if (pendingTasks.length > 0) {
1690
+ progressNote += ` Next: ${pendingTasks[0].id} — "${pendingTasks[0].title}".`;
1691
+ }
1692
+ }
1693
+ messages.push({ role: "user", content: progressNote });
1694
+ writeNotice("info", `continuing — budget extended to ${stepBudget} steps`, chalk.dim(` ℹ continuing — budget extended to ${stepBudget} steps\n`));
1695
+ // Continue the loop — model doesn't know it paused.
1696
+ }
1697
+ else {
1698
+ // User declined — build a rich summary and return.
1699
+ const richSummary = await buildRichStopSummary(messages, session, productiveSteps);
1700
+ writeAssistantMessage(richSummary);
1701
+ lastAnswer = richSummary;
1702
+ return finishTurn(lastAnswer, productiveSteps);
1703
+ }
1704
+ }
1560
1705
  options.signal?.throwIfAborted();
1561
1706
  // `call` and `assistantText` are shared by both paths below: a fresh
1562
1707
  // model round-trip, or draining a previously-queued tool call.
@@ -1678,24 +1823,19 @@ export async function runAgentLoop(prompt, options = {}) {
1678
1823
  // answer and the user has to re-submit the same prompt.
1679
1824
  if (!assistantText.visible.trim() && !call && assistantText.hasThinking) {
1680
1825
  emptyVisibleRetries += 1;
1681
- if (emptyVisibleRetries <= 2) {
1826
+ if (emptyVisibleRetries <= 3) {
1682
1827
  writeThinkingBlock(assistantText.thinkContent);
1683
1828
  writeNotice("warn", "model produced only thinking — nudging it to take action", chalk.yellow(" ⚠ model produced only thinking — nudging it to take action\n"));
1684
1829
  messages.push({
1685
1830
  role: "assistant",
1686
1831
  content: collapseRepeatedText(completion.text),
1687
1832
  });
1833
+ // Keep nudges SHORT — cheap models lose the key instruction in long text.
1688
1834
  const buildNudge = buildLikeTurn && !activePlan
1689
- ? "You only produced internal reasoning with no visible answer or tool call. " +
1690
- "This is a BUILD/SCAFFOLD task with NO plan yet. " +
1691
- "You MUST call plan.create using the ```tool format to create a comprehensive plan BEFORE writing any files or running any commands. " +
1692
- "Do NOT use fs.write, fs.writeMany, fs.edit, shell.exec, shell.start, or pkg.install yet. " +
1693
- "Your ONLY allowed action right now is plan.create (or read/list for exploration)."
1694
- : "You only produced internal reasoning with no visible answer or tool call. " +
1695
- "You MUST either call a tool using the ```tool format or provide your final answer. " +
1696
- "Do NOT wrap your tool call inside considering or reasoning tags — put it in the VISIBLE response, not hidden. " +
1697
- "If images are attached, inspect them directly for visual details (text, colors, layout, spacing, style) instead of using OCR unless explicitly needed. " +
1698
- "Do NOT just think — take action NOW.";
1835
+ ? "No visible output. Emit a ```tool block to call plan.create now. " +
1836
+ "Do NOT hide tool calls in <think> tags — put them in the visible response."
1837
+ : "No visible output. Emit a ```tool block or give your final answer. " +
1838
+ "Do NOT hide tool calls in <think> tags put them in the visible response.";
1699
1839
  messages.push(recoveryUserMessage(buildNudge));
1700
1840
  continue;
1701
1841
  }
@@ -2003,10 +2143,13 @@ export async function runAgentLoop(prompt, options = {}) {
2003
2143
  return finishTurn(lastAnswer, productiveSteps);
2004
2144
  }
2005
2145
  // Compact older messages when the running estimate exceeds budget
2006
- if (estimateMessagesTokens(messages) > 24_000) {
2007
- const compacted = compactMessages(messages);
2146
+ if (estimateMessagesTokens(messages) > 32_000) {
2147
+ const compacted = compactMessages(messages, { keepRecent: 12 });
2008
2148
  if (compacted.length < messages.length) {
2009
2149
  messages.splice(0, messages.length, ...compacted);
2150
+ // Reset read-only tool counters so the model can re-fetch data
2151
+ // whose results were compacted away.
2152
+ loopGuard.resetReadOnly();
2010
2153
  await auditLog("agent.compact", {
2011
2154
  newLength: messages.length,
2012
2155
  estimatedTokens: estimateMessagesTokens(messages),
@@ -2015,8 +2158,11 @@ export async function runAgentLoop(prompt, options = {}) {
2015
2158
  }
2016
2159
  }
2017
2160
  }
2018
- lastAnswer = `Stopped after ${productiveSteps} steps.`;
2019
- writeNotice("warn", lastAnswer, " " + chalk.yellow(lastAnswer) + "\n");
2161
+ // maxIterations ceiling reached (safety net — normally the step budget
2162
+ // gate with user confirmation handles stopping gracefully).
2163
+ const richSummary = await buildRichStopSummary(messages, session, productiveSteps);
2164
+ writeAssistantMessage(richSummary);
2165
+ lastAnswer = richSummary;
2020
2166
  return finishTurn(lastAnswer, productiveSteps);
2021
2167
  }
2022
2168
  catch (error) {