@pikaa-ai/pikaa 0.3.28 → 0.4.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.
package/dist/index.js CHANGED
@@ -1356,6 +1356,7 @@ class DefaultModelClientSession {
1356
1356
  systemMessage.cache_control = { type: "ephemeral" };
1357
1357
  }
1358
1358
  const messages = [systemMessage];
1359
+ const declaredToolCallIds = new Set;
1359
1360
  for (let i = 0;i < params.history.length; i++) {
1360
1361
  const item = params.history[i];
1361
1362
  if (item.type === "user_message") {
@@ -1380,6 +1381,9 @@ class DefaultModelClientSession {
1380
1381
  j++;
1381
1382
  }
1382
1383
  if (toolCalls.length > 0) {
1384
+ for (const tc of toolCalls) {
1385
+ declaredToolCallIds.add(tc.id);
1386
+ }
1383
1387
  messages.push({
1384
1388
  role: "assistant",
1385
1389
  content: cleanedContent || null,
@@ -1416,6 +1420,9 @@ class DefaultModelClientSession {
1416
1420
  });
1417
1421
  j++;
1418
1422
  }
1423
+ for (const tc of toolCalls) {
1424
+ declaredToolCallIds.add(tc.id);
1425
+ }
1419
1426
  messages.push({
1420
1427
  role: "assistant",
1421
1428
  content: null,
@@ -1423,11 +1430,13 @@ class DefaultModelClientSession {
1423
1430
  });
1424
1431
  i = j - 1;
1425
1432
  } else if (item.type === "function_call_output") {
1426
- messages.push({
1427
- role: "tool",
1428
- tool_call_id: item.callId,
1429
- content: item.output
1430
- });
1433
+ if (declaredToolCallIds.has(item.callId)) {
1434
+ messages.push({
1435
+ role: "tool",
1436
+ tool_call_id: item.callId,
1437
+ content: item.output
1438
+ });
1439
+ }
1431
1440
  }
1432
1441
  }
1433
1442
  const toolsPayload = typeof params.tools?.toModelToolsSchema === "function" ? params.tools.toModelToolsSchema() : typeof params.tools?.toOpenAISpec === "function" ? params.tools.toOpenAISpec() : Array.isArray(params.tools) ? params.tools : [];
@@ -1821,6 +1830,7 @@ var applyPatchTool = {
1821
1830
  try {
1822
1831
  mkdirSync3(dirname2(filePath), { recursive: true });
1823
1832
  writeFileSync2(filePath, replacementContent, "utf8");
1833
+ ctx.onFileModified?.(rawPath);
1824
1834
  return { output: `Successfully created new file '${rawPath}'` };
1825
1835
  } catch (err) {
1826
1836
  return {
@@ -1841,7 +1851,31 @@ var applyPatchTool = {
1841
1851
  isError: true
1842
1852
  };
1843
1853
  }
1844
- const firstIndex = originalFileContent.indexOf(targetContent);
1854
+ let targetToFind = targetContent;
1855
+ let replacementToUse = replacementContent;
1856
+ const fileHasCrlf = originalFileContent.includes(`\r
1857
+ `);
1858
+ let firstIndex = originalFileContent.indexOf(targetToFind);
1859
+ if (firstIndex === -1 && fileHasCrlf) {
1860
+ const crlfTarget = targetContent.replace(/\r?\n/g, `\r
1861
+ `);
1862
+ firstIndex = originalFileContent.indexOf(crlfTarget);
1863
+ if (firstIndex !== -1) {
1864
+ targetToFind = crlfTarget;
1865
+ replacementToUse = replacementContent.replace(/\r?\n/g, `\r
1866
+ `);
1867
+ }
1868
+ } else if (firstIndex === -1 && !fileHasCrlf && targetContent.includes(`\r
1869
+ `)) {
1870
+ const lfTarget = targetContent.replace(/\r\n/g, `
1871
+ `);
1872
+ firstIndex = originalFileContent.indexOf(lfTarget);
1873
+ if (firstIndex !== -1) {
1874
+ targetToFind = lfTarget;
1875
+ replacementToUse = replacementContent.replace(/\r\n/g, `
1876
+ `);
1877
+ }
1878
+ }
1845
1879
  if (firstIndex === -1) {
1846
1880
  return {
1847
1881
  output: `Error: targetContent was not found in '${rawPath}'.
@@ -1852,7 +1886,7 @@ var applyPatchTool = {
1852
1886
  isError: true
1853
1887
  };
1854
1888
  }
1855
- const secondIndex = originalFileContent.indexOf(targetContent, firstIndex + 1);
1889
+ const secondIndex = originalFileContent.indexOf(targetToFind, firstIndex + 1);
1856
1890
  if (secondIndex !== -1) {
1857
1891
  return {
1858
1892
  output: `Error: targetContent matched multiple locations in '${rawPath}'.
@@ -1863,8 +1897,9 @@ var applyPatchTool = {
1863
1897
  isError: true
1864
1898
  };
1865
1899
  }
1866
- const newFileContent = originalFileContent.slice(0, firstIndex) + replacementContent + originalFileContent.slice(firstIndex + targetContent.length);
1900
+ const newFileContent = originalFileContent.slice(0, firstIndex) + replacementToUse + originalFileContent.slice(firstIndex + targetToFind.length);
1867
1901
  writeFileSync2(filePath, newFileContent, "utf8");
1902
+ ctx.onFileModified?.(rawPath);
1868
1903
  return {
1869
1904
  output: `Successfully applied patch to '${rawPath}'`
1870
1905
  };
@@ -1876,8 +1911,133 @@ var applyPatchTool = {
1876
1911
  }
1877
1912
  }
1878
1913
  };
1914
+ // src/security/shell-parser.ts
1915
+ function parseShellCommand(commandLine) {
1916
+ const commands = [];
1917
+ const subshellCommands = [];
1918
+ let hasPipes = false;
1919
+ let currentSegment = "";
1920
+ let inSingleQuote = false;
1921
+ let inDoubleQuote = false;
1922
+ let isEscaped = false;
1923
+ const len = commandLine.length;
1924
+ for (let i = 0;i < len; i++) {
1925
+ const char = commandLine[i];
1926
+ if (isEscaped) {
1927
+ currentSegment += char;
1928
+ isEscaped = false;
1929
+ continue;
1930
+ }
1931
+ if (char === "\\") {
1932
+ isEscaped = true;
1933
+ currentSegment += char;
1934
+ continue;
1935
+ }
1936
+ if (char === "'" && !inDoubleQuote) {
1937
+ inSingleQuote = !inSingleQuote;
1938
+ currentSegment += char;
1939
+ continue;
1940
+ }
1941
+ if (char === '"' && !inSingleQuote) {
1942
+ inDoubleQuote = !inDoubleQuote;
1943
+ currentSegment += char;
1944
+ continue;
1945
+ }
1946
+ if (inSingleQuote) {
1947
+ currentSegment += char;
1948
+ continue;
1949
+ }
1950
+ if (char === "`") {
1951
+ let endIdx = -1;
1952
+ for (let j = i + 1;j < len; j++) {
1953
+ if (commandLine[j] === "\\" && j + 1 < len) {
1954
+ j++;
1955
+ continue;
1956
+ }
1957
+ if (commandLine[j] === "`") {
1958
+ endIdx = j;
1959
+ break;
1960
+ }
1961
+ }
1962
+ if (endIdx !== -1) {
1963
+ const innerCmd = commandLine.slice(i + 1, endIdx);
1964
+ if (innerCmd.trim()) {
1965
+ subshellCommands.push(innerCmd.trim());
1966
+ }
1967
+ currentSegment += commandLine.slice(i, endIdx + 1);
1968
+ i = endIdx;
1969
+ continue;
1970
+ }
1971
+ }
1972
+ if (char === "$" && i + 1 < len && commandLine[i + 1] === "(") {
1973
+ let depth = 1;
1974
+ let endIdx = -1;
1975
+ for (let j = i + 2;j < len; j++) {
1976
+ if (commandLine[j] === "\\" && j + 1 < len) {
1977
+ j++;
1978
+ continue;
1979
+ }
1980
+ if (commandLine[j] === "(")
1981
+ depth++;
1982
+ else if (commandLine[j] === ")") {
1983
+ depth--;
1984
+ if (depth === 0) {
1985
+ endIdx = j;
1986
+ break;
1987
+ }
1988
+ }
1989
+ }
1990
+ if (endIdx !== -1) {
1991
+ const innerCmd = commandLine.slice(i + 2, endIdx);
1992
+ if (innerCmd.trim()) {
1993
+ subshellCommands.push(innerCmd.trim());
1994
+ }
1995
+ currentSegment += commandLine.slice(i, endIdx + 1);
1996
+ i = endIdx;
1997
+ continue;
1998
+ }
1999
+ }
2000
+ if (!inDoubleQuote) {
2001
+ if (char === "&" && commandLine[i + 1] === "&" || char === "|" && commandLine[i + 1] === "|") {
2002
+ if (currentSegment.trim()) {
2003
+ commands.push(currentSegment.trim());
2004
+ }
2005
+ currentSegment = "";
2006
+ i++;
2007
+ continue;
2008
+ }
2009
+ if (char === "|") {
2010
+ hasPipes = true;
2011
+ if (currentSegment.trim()) {
2012
+ commands.push(currentSegment.trim());
2013
+ }
2014
+ currentSegment = "";
2015
+ continue;
2016
+ }
2017
+ if (char === ";" || char === `
2018
+ ` || char === "&") {
2019
+ if (currentSegment.trim()) {
2020
+ commands.push(currentSegment.trim());
2021
+ }
2022
+ currentSegment = "";
2023
+ continue;
2024
+ }
2025
+ }
2026
+ currentSegment += char;
2027
+ }
2028
+ if (currentSegment.trim()) {
2029
+ commands.push(currentSegment.trim());
2030
+ }
2031
+ return {
2032
+ commands: commands.filter(Boolean),
2033
+ subshellCommands: subshellCommands.filter(Boolean),
2034
+ hasPipes
2035
+ };
2036
+ }
2037
+
1879
2038
  // src/security/exec-policy.ts
1880
2039
  class ExecPolicy {
2040
+ denyRules = [];
1881
2041
  rules = [];
1882
2042
  mode = "auto";
1883
2043
  constructor(initialMode = "auto") {
@@ -1891,12 +2051,24 @@ class ExecPolicy {
1891
2051
  this.mode = mode;
1892
2052
  }
1893
2053
  initDefaultRules() {
1894
- this.addRule(/^(git\s+(status|log|diff|branch|show|rev-parse))/i, "allow", "Safe git query");
1895
- this.addRule(/^(ls|dir|cat|type|grep|rg|find|pwd|echo|head|tail|wc|which|where)\b/i, "allow", "Safe read-only shell command");
1896
- this.addRule(/^(bun\s+(test|--version|-v)|npm\s+(test|--version|-v)|node\s+-v)\b/i, "allow", "Testing & runtime check");
1897
- this.addRule(/^(rm|del|rmdir|format|mkfs)\b/i, "prompt", "Destructive file removal");
1898
- this.addRule(/^(git\s+(push|reset\s+--hard|clean\s+-fd|rebase))\b/i, "prompt", "Destructive git operation");
1899
- this.addRule(/^(curl|wget|fetch|ssh|scp|ftp)\b/i, "prompt", "Network / remote transfer");
2054
+ this.addDenyRule(/(?:^|[/\\])(mkfs|format|fdisk|parted)\b/i, "Destructive filesystem formatting operation");
2055
+ this.addDenyRule(/^dd\s+.*(of=\/dev\/|\/dev\/sd|\/dev\/nvme)/i, "Raw block device write attempt");
2056
+ this.addDenyRule(/(?:^|[/\\])(reboot|shutdown|poweroff|init\s+0)\b/i, "System power manipulation");
2057
+ this.addRule(/^(sudo|su|doas|runas)\b/i, "prompt", "Privilege escalation attempt");
2058
+ this.addRule(/^(rm|del|rmdir|shred|unlink)\b/i, "prompt", "Destructive file removal");
2059
+ this.addRule(/^(chmod|chown|kill|pkill|killall|systemctl|service|crontab)\b/i, "prompt", "System administration & process control");
2060
+ this.addRule(/^(git\s+(push|reset\s+--hard|clean\s+-fd|rebase|branch\s+-D|checkout\s+-f))\b/i, "prompt", "Destructive git operation");
2061
+ this.addRule(/^(curl|wget|fetch|ssh|scp|sftp|ftp|rsync|nc|ncat|netcat|socat|telnet)\b/i, "prompt", "Network & remote transfer");
2062
+ this.addRule(/^(bash|sh|zsh|dash|ksh|cmd(\.exe)?|powershell(\.exe)?|pwsh)\s+(-c|-command|\/c)\b/i, "prompt", "Arbitrary subshell command execution");
2063
+ this.addRule(/^(python|python3|node|bun|perl|ruby)\s+(-c|-e)\b/i, "prompt", "Inline arbitrary code evaluation");
2064
+ this.addRule(/^(eval|exec)\b/i, "prompt", "Dynamic code execution");
2065
+ this.addRule(/^(npm\s+publish|bun\s+publish|cargo\s+publish)\b/i, "prompt", "Package registry publication");
2066
+ this.addRule(/^(git\s+(status|log|diff|branch|show|rev-parse|tag|remote|describe))\b/i, "allow", "Safe git query");
2067
+ this.addRule(/^(ls|dir|cat|type|grep|rg|find|pwd|echo|head|tail|wc|which|where|stat|file|du|df)\b/i, "allow", "Safe read-only shell command");
2068
+ this.addRule(/^(bun\s+(test|run|--version|-v)|npm\s+(test|run|--version|-v)|npx\s+(tsc|eslint|oxlint)|tsc|cargo\s+(check|test|build)|go\s+(test|vet|build)|pytest|python\s+-m\s+unittest|node\s+(-v|--version|--test))\b/i, "allow", "Testing, typechecking & build verification");
2069
+ }
2070
+ addDenyRule(pattern, description) {
2071
+ this.denyRules.push({ pattern, decision: "deny", description });
1900
2072
  }
1901
2073
  addRule(pattern, decision, description) {
1902
2074
  this.rules.unshift({ pattern, decision, description });
@@ -1919,8 +2091,49 @@ class ExecPolicy {
1919
2091
  }
1920
2092
  evaluate(command) {
1921
2093
  const trimmed = command.trim();
2094
+ if (!trimmed) {
2095
+ return { decision: "allow" };
2096
+ }
2097
+ const parsed = parseShellCommand(trimmed);
2098
+ const subCommands = [...parsed.commands, ...parsed.subshellCommands].map((c) => c.trim()).filter(Boolean);
2099
+ if (subCommands.length > 1 || parsed.subshellCommands.length > 0) {
2100
+ for (const subCmd of subCommands) {
2101
+ const subResult = this.evaluateSingle(subCmd);
2102
+ if (subResult.decision === "deny") {
2103
+ return {
2104
+ decision: "deny",
2105
+ reason: `Chained command contains denied operation: '${subCmd}' (${subResult.reason || "Forbidden"})`
2106
+ };
2107
+ }
2108
+ if (subResult.decision === "prompt") {
2109
+ return {
2110
+ decision: "prompt",
2111
+ reason: `Chained command contains operation requiring confirmation: '${subCmd}' (${subResult.reason || "Requires approval"})`
2112
+ };
2113
+ }
2114
+ }
2115
+ if (parsed.hasPipes && /\|\s*(ba|z|k|c)?sh\b/i.test(trimmed)) {
2116
+ return {
2117
+ decision: "prompt",
2118
+ reason: "Pipeline executes piped input directly into shell interpreter (| sh)"
2119
+ };
2120
+ }
2121
+ return { decision: "allow", reason: "All chained sub-commands are permitted" };
2122
+ }
2123
+ return this.evaluateSingle(trimmed);
2124
+ }
2125
+ evaluateSingle(command) {
2126
+ const trimmed = command.trim();
2127
+ for (const rule of this.denyRules) {
2128
+ if (rule.pattern.test(trimmed)) {
2129
+ return {
2130
+ decision: "deny",
2131
+ reason: rule.description
2132
+ };
2133
+ }
2134
+ }
1922
2135
  if (this.mode === "plan") {
1923
- const isReadOnly = /^(git\s+(status|log|diff|branch|show|rev-parse)|ls|dir|cat|type|grep|rg|find|pwd|which|where)\b/i.test(trimmed);
2136
+ const isReadOnly = /^(git\s+(status|log|diff|branch|show|rev-parse|tag|remote)|ls|dir|cat|type|grep|rg|find|pwd|which|where)\b/i.test(trimmed);
1924
2137
  if (isReadOnly) {
1925
2138
  return { decision: "allow", reason: "Read-only inspection allowed in Plan mode" };
1926
2139
  }
@@ -1936,7 +2149,7 @@ class ExecPolicy {
1936
2149
  };
1937
2150
  }
1938
2151
  if (this.mode === "accept-edits") {
1939
- const isReadOnly = /^(git\s+(status|log|diff|branch|show)|ls|dir|cat|type|grep|rg|find|pwd|bun\s+test|npm\s+test)\b/i.test(trimmed);
2152
+ const isReadOnly = /^(git\s+(status|log|diff|branch|show|rev-parse)|ls|dir|cat|type|grep|rg|find|pwd|bun\s+test|npm\s+test)\b/i.test(trimmed);
1940
2153
  if (isReadOnly) {
1941
2154
  return { decision: "allow", reason: "Safe read-only command in accept-edits mode" };
1942
2155
  }
@@ -1953,11 +2166,21 @@ class ExecPolicy {
1953
2166
  };
1954
2167
  }
1955
2168
  }
2169
+ if (this.isRecognizedSafeDevCommand(trimmed)) {
2170
+ return {
2171
+ decision: "allow",
2172
+ reason: "Safe development workspace command"
2173
+ };
2174
+ }
2175
+ const firstWord = trimmed.split(/\s+/)[0] || trimmed;
1956
2176
  return {
1957
- decision: "allow",
1958
- reason: "Auto mode allows execution"
2177
+ decision: "prompt",
2178
+ reason: `Command '${firstWord}' is unclassified and requires confirmation in auto mode`
1959
2179
  };
1960
2180
  }
2181
+ isRecognizedSafeDevCommand(command) {
2182
+ return /^(git\s+(checkout|add|commit|stash|merge|pull|init)|bun\s+(install|add|remove)|npm\s+(install|i|add|remove)|yarn\s+(add|remove)|pnpm\s+(add|remove|install)|mkdir|touch|cp|copy|mv|move|clear|cls|echo|printf|node|bun|python|python3|cargo|go)\b/i.test(command);
2183
+ }
1961
2184
  }
1962
2185
 
1963
2186
  // src/security/kernel/windows.ts
@@ -2060,8 +2283,12 @@ class LinuxSandbox {
2060
2283
  }
2061
2284
  this.hasBwrap = existsSync4("/usr/bin/bwrap") || existsSync4("/bin/bwrap") || existsSync4("/usr/local/bin/bwrap");
2062
2285
  }
2063
- wrapCommand(cmd, profile) {
2064
- if (!this.hasBwrap || profile.kind === "danger-unrestricted") {
2286
+ wrapCommand(cmd, profile, onWarning) {
2287
+ if (profile.kind === "danger-unrestricted") {
2288
+ return cmd;
2289
+ }
2290
+ if (!this.hasBwrap) {
2291
+ onWarning?.("Linux kernel sandbox (Bubblewrap / bwrap) is not available. Command will execute without OS-level namespace isolation.");
2065
2292
  return cmd;
2066
2293
  }
2067
2294
  const bwrapArgs = [
@@ -2077,6 +2304,23 @@ class LinuxSandbox {
2077
2304
  "--tmpfs",
2078
2305
  "/tmp"
2079
2306
  ];
2307
+ const homeDir = process.env.HOME || "/root";
2308
+ const sensitivePaths = [
2309
+ `${homeDir}/.ssh`,
2310
+ `${homeDir}/.aws`,
2311
+ `${homeDir}/.gnupg`,
2312
+ `${homeDir}/.azure`,
2313
+ `${homeDir}/.kube`,
2314
+ `${homeDir}/.docker`,
2315
+ `${homeDir}/.netrc`,
2316
+ "/etc/shadow",
2317
+ "/etc/sudoers"
2318
+ ];
2319
+ for (const sensitive of sensitivePaths) {
2320
+ if (existsSync4(sensitive)) {
2321
+ bwrapArgs.push("--tmpfs", sensitive);
2322
+ }
2323
+ }
2080
2324
  for (const writableRoot of profile.writableRoots) {
2081
2325
  bwrapArgs.push("--bind", writableRoot, writableRoot);
2082
2326
  }
@@ -2126,11 +2370,21 @@ class MacOSSandbox {
2126
2370
  } else {
2127
2371
  rules.push("(deny network*)");
2128
2372
  }
2373
+ const home = process.env.HOME || "/Users/Shared";
2374
+ rules.push(`(deny file-read* (subpath "${home}/.ssh"))`);
2375
+ rules.push(`(deny file-read* (subpath "${home}/.aws"))`);
2376
+ rules.push(`(deny file-read* (subpath "${home}/.gnupg"))`);
2377
+ rules.push(`(deny file-read* (subpath "${home}/.kube"))`);
2378
+ rules.push(`(deny file-read* (subpath "${home}/.docker"))`);
2129
2379
  return rules.join(`
2130
2380
  `);
2131
2381
  }
2132
- wrapCommand(cmd, profile) {
2133
- if (!this.hasSandboxExec || profile.kind === "danger-unrestricted") {
2382
+ wrapCommand(cmd, profile, onWarning) {
2383
+ if (profile.kind === "danger-unrestricted") {
2384
+ return cmd;
2385
+ }
2386
+ if (!this.hasSandboxExec) {
2387
+ onWarning?.("macOS Seatbelt (sandbox-exec) is not available. Command will execute without OS-level namespace isolation.");
2134
2388
  return cmd;
2135
2389
  }
2136
2390
  const policy = this.generateProfile(profile);
@@ -2163,7 +2417,7 @@ class KernelSandboxManager {
2163
2417
  isSandboxingActive: this.windowsSandbox.isSupported() || this.linuxSandbox.isSupported() || this.macOsSandbox.isSupported()
2164
2418
  };
2165
2419
  }
2166
- buildDefaultProfile(cwd, allowNetwork = true) {
2420
+ buildDefaultProfile(cwd, allowNetwork = false) {
2167
2421
  const normCwd = normalize(resolve4(cwd));
2168
2422
  return {
2169
2423
  kind: "workspace-write",
@@ -2178,11 +2432,15 @@ class KernelSandboxManager {
2178
2432
  }
2179
2433
  };
2180
2434
  }
2181
- wrapCommand(cmd, profile) {
2435
+ wrapCommand(cmd, profile, onWarning) {
2182
2436
  if (process.platform === "linux") {
2183
- return this.linuxSandbox.wrapCommand(cmd, profile);
2437
+ return this.linuxSandbox.wrapCommand(cmd, profile, onWarning);
2184
2438
  } else if (process.platform === "darwin") {
2185
- return this.macOsSandbox.wrapCommand(cmd, profile);
2439
+ return this.macOsSandbox.wrapCommand(cmd, profile, onWarning);
2440
+ } else if (process.platform === "win32") {
2441
+ if (!this.windowsSandbox.isSupported() && profile.kind !== "danger-unrestricted") {
2442
+ onWarning?.("Windows JobObject isolation is not available in this environment. Command will execute without OS-level process limits.");
2443
+ }
2186
2444
  }
2187
2445
  return cmd;
2188
2446
  }
@@ -2314,6 +2572,12 @@ class PrefixRulesStore {
2314
2572
  matchesPrefix(cmdTokens, prefixTokens) {
2315
2573
  if (prefixTokens.length > cmdTokens.length)
2316
2574
  return false;
2575
+ const SHELL_OPERATORS = new Set([";", "&&", "||", "|", "&", ";;", "&|"]);
2576
+ for (const token of cmdTokens) {
2577
+ if (SHELL_OPERATORS.has(token) || token.includes(";") || token.includes("&&") || token.includes("||")) {
2578
+ return false;
2579
+ }
2580
+ }
2317
2581
  for (let i = 0;i < prefixTokens.length; i++) {
2318
2582
  const cmd = cmdTokens[i];
2319
2583
  const prefix = prefixTokens[i];
@@ -2439,11 +2703,29 @@ function createShellTool(policy = new ExecPolicy) {
2439
2703
  }
2440
2704
  const args = rawArgs;
2441
2705
  const rulesStore = ctx.prefixRulesStore || globalPrefixRulesStore;
2442
- const cmdTokens = command.split(/\s+/).filter(Boolean);
2706
+ const activePolicy = ctx.execPolicy || policy;
2707
+ const policyDecision = activePolicy.evaluate(command);
2708
+ if (policyDecision.decision === "deny") {
2709
+ return {
2710
+ output: `Error: Command execution denied by policy: ${policyDecision.reason}`,
2711
+ isError: true
2712
+ };
2713
+ }
2714
+ const parsed = parseShellCommand(command);
2715
+ const isCompound = parsed.commands.length > 1 || parsed.subshellCommands.length > 0 || parsed.hasPipes;
2443
2716
  let isEscalated = false;
2444
- if (rulesStore.isApproved(ctx.cwd, cmdTokens)) {
2445
- isEscalated = true;
2446
- } else if (args.sandbox_permissions === "require_escalated") {
2717
+ if (!isCompound) {
2718
+ const cmdTokens = command.split(/\s+/).filter(Boolean);
2719
+ if (rulesStore.isApproved(ctx.cwd, cmdTokens)) {
2720
+ isEscalated = true;
2721
+ }
2722
+ } else {
2723
+ const allSubCommands = [...parsed.commands, ...parsed.subshellCommands];
2724
+ if (allSubCommands.length > 0 && allSubCommands.every((sub) => rulesStore.isApproved(ctx.cwd, sub.split(/\s+/).filter(Boolean)))) {
2725
+ isEscalated = true;
2726
+ }
2727
+ }
2728
+ if (!isEscalated && args.sandbox_permissions === "require_escalated") {
2447
2729
  if (ctx.requestApproval) {
2448
2730
  const promptDesc = args.justification || "Executing command with escalated permissions";
2449
2731
  const approvalResult = await ctx.requestApproval(promptDesc, command, args.prefix_rule);
@@ -2461,16 +2743,8 @@ function createShellTool(policy = new ExecPolicy) {
2461
2743
  isEscalated = true;
2462
2744
  }
2463
2745
  }
2464
- if (!isEscalated) {
2465
- const activePolicy = ctx.execPolicy || policy;
2466
- const policyDecision = activePolicy.evaluate(command);
2467
- if (policyDecision.decision === "deny") {
2468
- return {
2469
- output: `Error: Command execution denied by policy: ${policyDecision.reason}`,
2470
- isError: true
2471
- };
2472
- }
2473
- if (policyDecision.decision === "prompt" && ctx.requestApproval) {
2746
+ if (policyDecision.decision === "prompt" && !isEscalated) {
2747
+ if (ctx.requestApproval) {
2474
2748
  const approvalResult = await ctx.requestApproval(policyDecision.reason || "Executing external command", command);
2475
2749
  const isAllowed = typeof approvalResult === "boolean" ? approvalResult : approvalResult?.allowed;
2476
2750
  if (!isAllowed) {
@@ -2479,17 +2753,22 @@ function createShellTool(policy = new ExecPolicy) {
2479
2753
  isError: true
2480
2754
  };
2481
2755
  }
2756
+ } else {
2757
+ return {
2758
+ output: `Error: Command execution requires user confirmation: '${command}' (${policyDecision.reason || "Requires approval"})`,
2759
+ isError: true
2760
+ };
2482
2761
  }
2483
2762
  }
2484
2763
  const timeoutMs = typeof args.timeoutMs === "number" ? args.timeoutMs : 30000;
2485
2764
  const isWindows = process.platform === "win32";
2486
2765
  const baseCmd = isWindows ? ["cmd.exe", "/d", "/s", "/c", command] : ["/bin/sh", "-c", command];
2487
2766
  const ephemeralScratchpad = globalEphemeralWorkspace.createScratchpad(ctx.turnId);
2488
- const sandboxProfile = globalKernelSandbox.buildDefaultProfile(ctx.cwd);
2489
- if (isEscalated) {
2490
- sandboxProfile.allowNetwork = true;
2491
- }
2492
- const wrappedCmd = globalKernelSandbox.wrapCommand(baseCmd, sandboxProfile);
2767
+ const sandboxProfile = globalKernelSandbox.buildDefaultProfile(ctx.cwd, isEscalated);
2768
+ let sandboxNotice = null;
2769
+ const wrappedCmd = globalKernelSandbox.wrapCommand(baseCmd, sandboxProfile, (w) => {
2770
+ sandboxNotice = w;
2771
+ });
2493
2772
  try {
2494
2773
  const proc = Bun.spawn(wrappedCmd, {
2495
2774
  cwd: ctx.cwd,
@@ -2543,10 +2822,38 @@ ${result.stderr.trim()}`);
2543
2822
  2. If this is a test failure, trace the failure in source code and fix the root cause before re-running.
2544
2823
  3. If this is a missing command/module, install or configure the prerequisite.`);
2545
2824
  }
2546
- const output = outputParts.join(`
2825
+ let rawOutput = outputParts.join(`
2547
2826
  `) || "[Command completed with no output]";
2827
+ const lines = rawOutput.split(`
2828
+ `);
2829
+ const MAX_LINES = 250;
2830
+ const MAX_CHARS = 30000;
2831
+ if (lines.length > MAX_LINES) {
2832
+ const headLines = lines.slice(0, 125).join(`
2833
+ `);
2834
+ const tailLines = lines.slice(-100).join(`
2835
+ `);
2836
+ rawOutput = `${headLines}
2837
+
2838
+ ... [${lines.length - 225} lines truncated for context efficiency] ...
2839
+
2840
+ ${tailLines}`;
2841
+ }
2842
+ if (rawOutput.length > MAX_CHARS) {
2843
+ const headChars = rawOutput.slice(0, 16000);
2844
+ const tailChars = rawOutput.slice(-12000);
2845
+ rawOutput = `${headChars}
2846
+
2847
+ ... [${rawOutput.length - 28000} characters truncated for context efficiency] ...
2848
+
2849
+ ${tailChars}`;
2850
+ }
2851
+ if (sandboxNotice) {
2852
+ rawOutput = `[Sandbox Notice]: ${sandboxNotice}
2853
+ ${rawOutput}`;
2854
+ }
2548
2855
  return {
2549
- output,
2856
+ output: rawOutput,
2550
2857
  isError: result.code !== 0
2551
2858
  };
2552
2859
  } catch (err) {
@@ -2565,7 +2872,7 @@ ${result.stderr.trim()}`);
2565
2872
  }
2566
2873
  var shellTool = createShellTool();
2567
2874
  // src/tools/handlers/file-ops.ts
2568
- import { readdirSync as readdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync8, statSync as statSync3, mkdirSync as mkdirSync6 } from "fs";
2875
+ import { readdirSync as readdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync8, statSync as statSync3, lstatSync, mkdirSync as mkdirSync6 } from "fs";
2569
2876
  import { resolve as resolve6, dirname as dirname4 } from "path";
2570
2877
  var DEFAULT_MAX_UNPAGINATED_LINES = 250;
2571
2878
  var readFileTool = {
@@ -2677,6 +2984,7 @@ var viewFileTool = {
2677
2984
  name: "view_file",
2678
2985
  description: "View file content with surgical line-range support. Alias for read_file matching Antigravity & Claude Code conventions."
2679
2986
  };
2987
+ var DEFAULT_MAX_DIR_ENTRIES = 500;
2680
2988
  var listDirTool = {
2681
2989
  name: "list_dir",
2682
2990
  description: "List contents of a directory with file names and types.",
@@ -2693,13 +3001,31 @@ var listDirTool = {
2693
3001
  }
2694
3002
  try {
2695
3003
  const entries = readdirSync3(dirPath);
2696
- const formatted = entries.map((entry) => {
3004
+ const totalEntries = entries.length;
3005
+ const displayEntries = entries.slice(0, DEFAULT_MAX_DIR_ENTRIES);
3006
+ const formatted = displayEntries.map((entry) => {
2697
3007
  const full = resolve6(dirPath, entry);
2698
- const isDir = statSync3(full).isDirectory();
3008
+ let isDir = false;
3009
+ try {
3010
+ isDir = statSync3(full).isDirectory();
3011
+ } catch {
3012
+ try {
3013
+ const lst = lstatSync(full);
3014
+ if (lst.isSymbolicLink())
3015
+ return `[SYMLINK] ${entry}`;
3016
+ } catch {}
3017
+ return `[FILE] ${entry}`;
3018
+ }
2699
3019
  return `${isDir ? "[DIR]" : "[FILE]"} ${entry}`;
2700
3020
  });
2701
- return { output: formatted.join(`
2702
- `) || "[Empty directory]" };
3021
+ let output = formatted.join(`
3022
+ `) || "[Empty directory]";
3023
+ if (totalEntries > DEFAULT_MAX_DIR_ENTRIES) {
3024
+ output += `
3025
+
3026
+ ... [Truncated: ${totalEntries - DEFAULT_MAX_DIR_ENTRIES} more entries. Showing first ${DEFAULT_MAX_DIR_ENTRIES} of ${totalEntries}]`;
3027
+ }
3028
+ return { output };
2703
3029
  } catch (err) {
2704
3030
  return { output: `Failed to list directory: ${err instanceof Error ? err.message : String(err)}`, isError: true };
2705
3031
  }
@@ -2740,6 +3066,7 @@ var writeFileTool = {
2740
3066
  try {
2741
3067
  mkdirSync6(dirname4(filePath), { recursive: true });
2742
3068
  writeFileSync3(filePath, String(args.content ?? ""), "utf8");
3069
+ ctx.onFileModified?.(rawPath);
2743
3070
  return { output: `Successfully wrote to '${args.path}'` };
2744
3071
  } catch (err) {
2745
3072
  return {
@@ -3233,6 +3560,12 @@ class CodeModeToolsProxy {
3233
3560
  if (result.isError) {
3234
3561
  throw new Error(`Tool '${tool.name}' failed: ${result.output}`);
3235
3562
  }
3563
+ if (normalizedName === "write_file" || normalizedName === "apply_patch" || tool.name === "write_file" || tool.name === "apply_patch") {
3564
+ const pathArg = String(args?.path || "");
3565
+ if (pathArg) {
3566
+ this.context.onFileModified?.(pathArg);
3567
+ }
3568
+ }
3236
3569
  return result.output;
3237
3570
  };
3238
3571
  }
@@ -3306,6 +3639,9 @@ class SandboxedWorkerHost {
3306
3639
  const fetch = undefined;
3307
3640
  const XMLHttpRequest = undefined;
3308
3641
  const WebSocket = undefined;
3642
+ const globalThis = Object.freeze(Object.create(null));
3643
+ const global = undefined;
3644
+ const window = undefined;
3309
3645
 
3310
3646
  return (async () => {
3311
3647
  ${cleanCode}
@@ -4316,14 +4652,39 @@ function compactHistory(history, retainedRecentItems = 6) {
4316
4652
  if (history.length <= retainedRecentItems) {
4317
4653
  return [...history];
4318
4654
  }
4319
- const itemsToCompact = history.slice(0, history.length - retainedRecentItems);
4320
- const recentItems = history.slice(history.length - retainedRecentItems);
4655
+ const targetCutoff = history.length - retainedRecentItems;
4656
+ let bestCutoff = -1;
4657
+ for (let i = targetCutoff;i >= 0; i--) {
4658
+ if (history[i]?.type === "user_message") {
4659
+ bestCutoff = i;
4660
+ break;
4661
+ }
4662
+ }
4663
+ if (bestCutoff <= 0) {
4664
+ for (let i = targetCutoff + 1;i < history.length; i++) {
4665
+ if (history[i]?.type === "user_message") {
4666
+ bestCutoff = i;
4667
+ break;
4668
+ }
4669
+ }
4670
+ }
4671
+ let cutoffIndex = bestCutoff > 0 ? bestCutoff : targetCutoff;
4672
+ while (cutoffIndex < history.length && history[cutoffIndex]?.type === "function_call_output") {
4673
+ cutoffIndex++;
4674
+ }
4675
+ if (cutoffIndex <= 0 || cutoffIndex >= history.length) {
4676
+ return [...history];
4677
+ }
4678
+ const itemsToCompact = history.slice(0, cutoffIndex);
4679
+ const recentItems = history.slice(cutoffIndex);
4321
4680
  const summaryParts = ["### Summary of previous conversation context:"];
4322
4681
  for (const item of itemsToCompact) {
4323
4682
  if (item.type === "user_message") {
4324
4683
  summaryParts.push(`- User: ${item.content.slice(0, 200)}`);
4325
4684
  } else if (item.type === "function_call") {
4326
4685
  summaryParts.push(`- Executed tool: ${item.name}`);
4686
+ } else if (item.type === "function_call_output") {
4687
+ summaryParts.push(` Tool output: ${item.output.slice(0, 100)}`);
4327
4688
  } else if (item.type === "agent_message") {
4328
4689
  summaryParts.push(`- Assistant: ${item.content.slice(0, 200)}`);
4329
4690
  }
@@ -4363,7 +4724,7 @@ class TurnContext {
4363
4724
  }
4364
4725
  }
4365
4726
  // src/verification/verifier.ts
4366
- import { spawnSync } from "child_process";
4727
+ import { spawn, spawnSync } from "child_process";
4367
4728
  import { existsSync as existsSync14, readFileSync as readFileSync9 } from "fs";
4368
4729
  import { join as join8 } from "path";
4369
4730
 
@@ -4740,36 +5101,120 @@ class AutoVerifier {
4740
5101
  this.customCommand = options.customCommand;
4741
5102
  this.timeoutMs = options.timeoutMs ?? 30000;
4742
5103
  }
4743
- resolveVerificationCommand() {
4744
- if (this.customCommand && this.customCommand.trim()) {
4745
- return this.customCommand.trim();
5104
+ findTargetedTests(modifiedFiles = []) {
5105
+ const matchedTests = new Set;
5106
+ for (const rawFile of modifiedFiles) {
5107
+ const normalized = rawFile.replace(/\\/g, "/").replace(/^\.\//, "");
5108
+ if (/\.(test|spec)\.[jt]sx?$/i.test(normalized) || /(^|\/)test_[^/]+\.py$/i.test(normalized) || /_test\.py$/i.test(normalized) || /_test\.go$/i.test(normalized)) {
5109
+ if (existsSync14(join8(this.cwd, normalized))) {
5110
+ matchedTests.add(normalized);
5111
+ }
5112
+ continue;
5113
+ }
5114
+ const extMatch = normalized.match(/\.[^.]+$/);
5115
+ if (!extMatch)
5116
+ continue;
5117
+ const withoutExt = normalized.slice(0, -extMatch[0].length);
5118
+ const parts = withoutExt.split("/");
5119
+ const baseName = parts[parts.length - 1];
5120
+ const subPath = normalized.startsWith("src/") ? normalized.slice(4, -extMatch[0].length) : normalized.startsWith("lib/") ? normalized.slice(4, -extMatch[0].length) : withoutExt;
5121
+ const candidatePaths = [
5122
+ `tests/${subPath}.test.ts`,
5123
+ `tests/${subPath}.test.js`,
5124
+ `tests/${subPath}.test.tsx`,
5125
+ `tests/${subPath}.spec.ts`,
5126
+ `tests/${subPath}.spec.js`,
5127
+ `tests/${baseName}.test.ts`,
5128
+ `tests/${baseName}.test.js`,
5129
+ `tests/${baseName}.spec.ts`,
5130
+ `test/${subPath}.test.ts`,
5131
+ `test/${subPath}.test.js`,
5132
+ `test/${baseName}.test.ts`,
5133
+ `test/${baseName}.test.js`,
5134
+ `src/${subPath}.test.ts`,
5135
+ `src/${subPath}.spec.ts`,
5136
+ `${withoutExt}.test.ts`,
5137
+ `${withoutExt}.spec.ts`,
5138
+ `tests/test_${baseName}.py`,
5139
+ `test_${baseName}.py`,
5140
+ `tests/${baseName}_test.go`
5141
+ ];
5142
+ for (const candidate of candidatePaths) {
5143
+ if (existsSync14(join8(this.cwd, candidate))) {
5144
+ matchedTests.add(candidate);
5145
+ }
5146
+ }
5147
+ }
5148
+ return Array.from(matchedTests);
5149
+ }
5150
+ resolveTargetedTestCommand(testFiles) {
5151
+ if (testFiles.length === 0)
5152
+ return null;
5153
+ const quotedFiles = testFiles.map((f) => f.includes(" ") ? `"${f}"` : f).join(" ");
5154
+ if (existsSync14(join8(this.cwd, "bun.lockb")) || existsSync14(join8(this.cwd, "bun.lock"))) {
5155
+ return `bun test ${quotedFiles}`;
5156
+ }
5157
+ const pkgPath = join8(this.cwd, "package.json");
5158
+ if (existsSync14(pkgPath)) {
5159
+ try {
5160
+ const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
5161
+ const allDeps = { ...pkg.dependencies || {}, ...pkg.devDependencies || {} };
5162
+ if (allDeps.vitest) {
5163
+ return `npx vitest run ${quotedFiles}`;
5164
+ }
5165
+ if (allDeps.jest) {
5166
+ return `npx jest ${quotedFiles}`;
5167
+ }
5168
+ const pm = existsSync14(join8(this.cwd, "pnpm-lock.yaml")) ? "pnpm" : existsSync14(join8(this.cwd, "yarn.lock")) ? "yarn" : "npm";
5169
+ if (pkg.scripts?.test) {
5170
+ if (pkg.scripts.test.includes("bun test")) {
5171
+ return `bun test ${quotedFiles}`;
5172
+ }
5173
+ return `${pm} test -- ${quotedFiles}`;
5174
+ }
5175
+ } catch (err) {
5176
+ console.warn(`[AutoVerifier] Failed to parse package.json for test runner:`, err);
5177
+ }
5178
+ }
5179
+ if (existsSync14(join8(this.cwd, "pyproject.toml")) || existsSync14(join8(this.cwd, "requirements.txt"))) {
5180
+ if (existsSync14(join8(this.cwd, "uv.lock"))) {
5181
+ return `uv run pytest ${quotedFiles}`;
5182
+ }
5183
+ return `pytest ${quotedFiles}`;
4746
5184
  }
5185
+ if (existsSync14(join8(this.cwd, "go.mod"))) {
5186
+ return `go test ${quotedFiles}`;
5187
+ }
5188
+ if (existsSync14(join8(this.cwd, "Cargo.toml"))) {
5189
+ return `cargo test ${quotedFiles}`;
5190
+ }
5191
+ return null;
5192
+ }
5193
+ resolveStaticCommand() {
4747
5194
  try {
4748
5195
  const analyzer = new ProjectAnalyzer(this.cwd);
4749
5196
  const analysis = analyzer.analyze();
4750
5197
  if (analysis.commands.typecheck) {
4751
5198
  return analysis.commands.typecheck;
4752
5199
  }
4753
- if (analysis.commands.lint) {
4754
- return analysis.commands.lint;
4755
- }
4756
- if (analysis.commands.test) {
4757
- return analysis.commands.test;
4758
- }
4759
- } catch {}
5200
+ } catch (err) {
5201
+ console.warn(`[AutoVerifier] ProjectAnalyzer analysis error:`, err);
5202
+ }
4760
5203
  const pkgPath = join8(this.cwd, "package.json");
4761
5204
  if (existsSync14(pkgPath)) {
4762
5205
  try {
4763
5206
  const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
4764
5207
  if (pkg.scripts) {
5208
+ const pm = existsSync14(join8(this.cwd, "bun.lockb")) || existsSync14(join8(this.cwd, "bun.lock")) ? "bun" : existsSync14(join8(this.cwd, "pnpm-lock.yaml")) ? "pnpm" : existsSync14(join8(this.cwd, "yarn.lock")) ? "yarn" : "npm";
5209
+ const runCmd = pm === "bun" || pm === "pnpm" || pm === "yarn" ? `${pm} run` : "npm run";
4765
5210
  if (pkg.scripts.typecheck)
4766
- return "npm run typecheck";
5211
+ return `${runCmd} typecheck`;
4767
5212
  if (pkg.scripts.check)
4768
- return "npm run check";
4769
- if (pkg.scripts.test)
4770
- return "npm test";
5213
+ return `${runCmd} check`;
4771
5214
  }
4772
- } catch {}
5215
+ } catch (err) {
5216
+ console.warn(`[AutoVerifier] Failed to parse package.json for static check:`, err);
5217
+ }
4773
5218
  }
4774
5219
  if (existsSync14(join8(this.cwd, "tsconfig.json"))) {
4775
5220
  return "npx tsc --noEmit";
@@ -4780,17 +5225,76 @@ class AutoVerifier {
4780
5225
  if (existsSync14(join8(this.cwd, "go.mod"))) {
4781
5226
  return "go vet ./...";
4782
5227
  }
4783
- if (existsSync14(join8(this.cwd, "pyproject.toml")) || existsSync14(join8(this.cwd, "setup.py"))) {
4784
- if (existsSync14(join8(this.cwd, "mypy.ini")) || existsSync14(join8(this.cwd, ".mypy.ini"))) {
4785
- return "mypy .";
4786
- }
5228
+ if (existsSync14(join8(this.cwd, "mypy.ini")) || existsSync14(join8(this.cwd, ".mypy.ini"))) {
5229
+ return "mypy .";
5230
+ }
5231
+ return null;
5232
+ }
5233
+ resolveVerificationCommand(modifiedFiles = []) {
5234
+ if (this.customCommand && this.customCommand.trim()) {
5235
+ return this.customCommand.trim();
5236
+ }
5237
+ const targetedTests = this.findTargetedTests(modifiedFiles);
5238
+ const targetedTestCmd = this.resolveTargetedTestCommand(targetedTests);
5239
+ const staticCmd = this.resolveStaticCommand();
5240
+ if (targetedTestCmd && staticCmd) {
5241
+ return `${staticCmd} && ${targetedTestCmd}`;
5242
+ }
5243
+ if (targetedTestCmd) {
5244
+ return targetedTestCmd;
5245
+ }
5246
+ if (staticCmd) {
5247
+ return staticCmd;
5248
+ }
5249
+ try {
5250
+ const analyzer = new ProjectAnalyzer(this.cwd);
5251
+ const analysis = analyzer.analyze();
5252
+ if (analysis.commands.test)
5253
+ return analysis.commands.test;
5254
+ if (analysis.commands.lint)
5255
+ return analysis.commands.lint;
5256
+ } catch {}
5257
+ const pkgPath = join8(this.cwd, "package.json");
5258
+ if (existsSync14(pkgPath)) {
5259
+ try {
5260
+ const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
5261
+ if (pkg.scripts?.test) {
5262
+ const pm = existsSync14(join8(this.cwd, "bun.lockb")) || existsSync14(join8(this.cwd, "bun.lock")) ? "bun" : "npm";
5263
+ return pm === "bun" ? "bun test" : "npm test";
5264
+ }
5265
+ if (pkg.scripts?.lint) {
5266
+ return "npm run lint";
5267
+ }
5268
+ } catch {}
4787
5269
  }
4788
5270
  return null;
4789
5271
  }
4790
- verify(modifiedFiles = []) {
4791
- const command = this.resolveVerificationCommand();
5272
+ resolveVerificationStages(modifiedFiles = []) {
5273
+ if (this.customCommand && this.customCommand.trim()) {
5274
+ return [this.customCommand.trim()];
5275
+ }
5276
+ const stages = [];
5277
+ const staticCmd = this.resolveStaticCommand();
5278
+ const targetedTests = this.findTargetedTests(modifiedFiles);
5279
+ const targetedTestCmd = this.resolveTargetedTestCommand(targetedTests);
5280
+ if (staticCmd) {
5281
+ stages.push(staticCmd);
5282
+ }
5283
+ if (targetedTestCmd) {
5284
+ stages.push(targetedTestCmd);
5285
+ }
5286
+ if (stages.length === 0) {
5287
+ const fallback = this.resolveVerificationCommand(modifiedFiles);
5288
+ if (fallback) {
5289
+ stages.push(fallback);
5290
+ }
5291
+ }
5292
+ return stages;
5293
+ }
5294
+ async verify(modifiedFiles = [], signal) {
5295
+ const stages = this.resolveVerificationStages(modifiedFiles);
4792
5296
  const startTime = performance.now();
4793
- if (!command) {
5297
+ if (stages.length === 0) {
4794
5298
  return {
4795
5299
  command: "none",
4796
5300
  success: true,
@@ -4800,59 +5304,158 @@ class AutoVerifier {
4800
5304
  reason: "NO_VERIFIER_DETECTED"
4801
5305
  };
4802
5306
  }
4803
- try {
4804
- const isWindows = process.platform === "win32";
4805
- const proc = spawnSync(command, {
4806
- cwd: this.cwd,
4807
- shell: true,
4808
- encoding: "utf8",
4809
- timeout: this.timeoutMs,
4810
- maxBuffer: 10 * 1024 * 1024,
4811
- env: {
4812
- ...process.env,
4813
- CI: "true",
4814
- FORCE_COLOR: "0"
5307
+ const combinedOutputs = [];
5308
+ let executedCommands = [];
5309
+ for (const cmd of stages) {
5310
+ if (signal?.aborted) {
5311
+ return {
5312
+ command: cmd,
5313
+ success: false,
5314
+ exitCode: 1,
5315
+ output: "Verification was aborted.",
5316
+ durationMs: Math.round(performance.now() - startTime),
5317
+ reason: "ABORTED"
5318
+ };
5319
+ }
5320
+ executedCommands.push(cmd);
5321
+ const stageResult = await this.executeCommandAsync(cmd, signal);
5322
+ combinedOutputs.push(`[${cmd}]
5323
+ ${stageResult.output}`);
5324
+ if (!stageResult.success) {
5325
+ return {
5326
+ command: cmd,
5327
+ success: false,
5328
+ exitCode: stageResult.exitCode,
5329
+ output: this.truncateOutput(combinedOutputs.join(`
5330
+
5331
+ `)),
5332
+ durationMs: Math.round(performance.now() - startTime)
5333
+ };
5334
+ }
5335
+ }
5336
+ return {
5337
+ command: executedCommands.join(" && "),
5338
+ success: true,
5339
+ exitCode: 0,
5340
+ output: this.truncateOutput(combinedOutputs.join(`
5341
+
5342
+ `)),
5343
+ durationMs: Math.round(performance.now() - startTime)
5344
+ };
5345
+ }
5346
+ executeCommandAsync(command, signal) {
5347
+ return new Promise((resolve) => {
5348
+ let proc = null;
5349
+ let stdoutData = "";
5350
+ let stderrData = "";
5351
+ let isSettled = false;
5352
+ const finish = (success, exitCode, output) => {
5353
+ if (isSettled)
5354
+ return;
5355
+ isSettled = true;
5356
+ cleanup();
5357
+ resolve({ success, exitCode, output: output.trim() });
5358
+ };
5359
+ const killProcessTree = () => {
5360
+ if (!proc || !proc.pid)
5361
+ return;
5362
+ try {
5363
+ if (process.platform === "win32") {
5364
+ spawnSync("taskkill", ["/pid", String(proc.pid), "/T", "/F"], { stdio: "ignore" });
5365
+ } else {
5366
+ proc.kill("SIGTERM");
5367
+ setTimeout(() => {
5368
+ try {
5369
+ proc?.kill("SIGKILL");
5370
+ } catch {}
5371
+ }, 500);
5372
+ }
5373
+ } catch {}
5374
+ };
5375
+ const onAbort = () => {
5376
+ killProcessTree();
5377
+ finish(false, 1, "Verification command aborted by user or session signal.");
5378
+ };
5379
+ if (signal?.aborted) {
5380
+ return finish(false, 1, "Verification aborted before launch.");
5381
+ }
5382
+ if (signal) {
5383
+ signal.addEventListener("abort", onAbort, { once: true });
5384
+ }
5385
+ const timer = setTimeout(() => {
5386
+ killProcessTree();
5387
+ const partial = (stdoutData + `
5388
+ ` + stderrData).trim();
5389
+ const timeoutMsg = `[Verification Timeout Error]: Verification command '${command}' exceeded timeout limit of ${this.timeoutMs}ms and was terminated.`;
5390
+ finish(false, 1, partial ? `${partial}
5391
+
5392
+ ${timeoutMsg}` : timeoutMsg);
5393
+ }, this.timeoutMs);
5394
+ const cleanup = () => {
5395
+ clearTimeout(timer);
5396
+ if (signal) {
5397
+ signal.removeEventListener("abort", onAbort);
4815
5398
  }
4816
- });
4817
- const durationMs = Math.round(performance.now() - startTime);
4818
- const stdout = proc.stdout ? String(proc.stdout) : "";
4819
- const stderr = proc.stderr ? String(proc.stderr) : "";
4820
- let combined = (stdout + `
4821
- ` + stderr).trim();
4822
- if (combined.length > 3000) {
4823
- const lines = combined.split(`
5399
+ };
5400
+ try {
5401
+ proc = spawn(command, {
5402
+ cwd: this.cwd,
5403
+ shell: true,
5404
+ env: {
5405
+ ...process.env,
5406
+ CI: "true",
5407
+ FORCE_COLOR: "0"
5408
+ }
5409
+ });
5410
+ proc.stdout?.on("data", (chunk) => {
5411
+ stdoutData += chunk.toString();
5412
+ });
5413
+ proc.stderr?.on("data", (chunk) => {
5414
+ stderrData += chunk.toString();
5415
+ });
5416
+ proc.on("error", (err) => {
5417
+ const partial = (stdoutData + `
5418
+ ` + stderrData).trim();
5419
+ const spawnMsg = `[Verification Process Spawn Error]: Failed to spawn command '${command}': ${err.message}`;
5420
+ finish(false, 1, partial ? `${partial}
5421
+
5422
+ ${spawnMsg}` : spawnMsg);
5423
+ });
5424
+ proc.on("close", (code) => {
5425
+ const exitCode = code ?? 0;
5426
+ const combined = (stdoutData + `
5427
+ ` + stderrData).trim();
5428
+ if (exitCode === 0) {
5429
+ finish(true, 0, combined || "Verification passed cleanly.");
5430
+ } else {
5431
+ const errorFallback = `[Verification Failure]: Command '${command}' exited with code ${exitCode} and produced no output.`;
5432
+ finish(false, exitCode, combined ? `${combined}
5433
+
5434
+ [Process exited with non-zero code ${exitCode}]` : errorFallback);
5435
+ }
5436
+ });
5437
+ } catch (err) {
5438
+ finish(false, 1, `[Verification Execution Exception]: ${err.message || String(err)}`);
5439
+ }
5440
+ });
5441
+ }
5442
+ truncateOutput(output) {
5443
+ if (output.length > 4000) {
5444
+ const lines = output.split(`
4824
5445
  `);
4825
- if (lines.length > 60) {
4826
- const head = lines.slice(0, 30).join(`
5446
+ if (lines.length > 70) {
5447
+ const head = lines.slice(0, 35).join(`
4827
5448
  `);
4828
- const tail = lines.slice(-25).join(`
5449
+ const tail = lines.slice(-30).join(`
4829
5450
  `);
4830
- combined = `${head}
5451
+ return `${head}
4831
5452
 
4832
- ... [${lines.length - 55} lines truncated for context efficiency] ...
5453
+ ... [${lines.length - 65} lines truncated for context efficiency] ...
4833
5454
 
4834
5455
  ${tail}`;
4835
- }
4836
5456
  }
4837
- const exitCode = proc.status ?? (proc.error ? 1 : 0);
4838
- const success = exitCode === 0;
4839
- return {
4840
- command,
4841
- success,
4842
- exitCode,
4843
- output: combined || (success ? "Verification succeeded cleanly." : "Command failed with empty output."),
4844
- durationMs
4845
- };
4846
- } catch (err) {
4847
- const durationMs = Math.round(performance.now() - startTime);
4848
- return {
4849
- command,
4850
- success: false,
4851
- exitCode: 1,
4852
- output: `Verification execution error: ${err.message || String(err)}`,
4853
- durationMs
4854
- };
4855
5457
  }
5458
+ return output;
4856
5459
  }
4857
5460
  }
4858
5461
  // src/session/turn.ts
@@ -4976,6 +5579,7 @@ async function runTurn(session, turnContext, input) {
4976
5579
  });
4977
5580
  }
4978
5581
  if (toolCallRequests.length > 0) {
5582
+ const validCalls = [];
4979
5583
  for (const toolCall of toolCallRequests) {
4980
5584
  if (!toolCall.name || !toolCall.name.trim())
4981
5585
  continue;
@@ -4999,6 +5603,14 @@ async function runTurn(session, turnContext, input) {
4999
5603
  toolName: toolCall.name,
5000
5604
  arguments: toolCall.arguments
5001
5605
  });
5606
+ validCalls.push({
5607
+ callId: toolCall.callId,
5608
+ name: toolCall.name,
5609
+ arguments: toolCall.arguments,
5610
+ functionCallItem
5611
+ });
5612
+ }
5613
+ for (const toolCall of validCalls) {
5002
5614
  const toolResult = await turnContext.tools.execute(toolCall.name, toolCall.arguments, {
5003
5615
  cwd: turnContext.environment.cwd,
5004
5616
  turnId,
@@ -5006,6 +5618,12 @@ async function runTurn(session, turnContext, input) {
5006
5618
  execPolicy: session.execPolicy,
5007
5619
  mode: session.collaborationMode,
5008
5620
  permissionMode: session.permissionMode,
5621
+ onFileModified: (p) => {
5622
+ if (p) {
5623
+ modifiedFiles.add(p);
5624
+ hasRunVerification = false;
5625
+ }
5626
+ },
5009
5627
  onPlanUpdate: (plan, explanation) => {
5010
5628
  session.emitEvent({
5011
5629
  type: "PlanUpdated",
@@ -5021,7 +5639,8 @@ async function runTurn(session, turnContext, input) {
5021
5639
  turnId,
5022
5640
  toolName: toolCall.name,
5023
5641
  description,
5024
- command
5642
+ command,
5643
+ prefixRule
5025
5644
  });
5026
5645
  },
5027
5646
  requestInput: async (question, options) => {
@@ -5072,7 +5691,7 @@ async function runTurn(session, turnContext, input) {
5072
5691
  cwd,
5073
5692
  customCommand: session.autoVerificationCommand
5074
5693
  });
5075
- const command = verifier.resolveVerificationCommand();
5694
+ const command = verifier.resolveVerificationCommand(Array.from(modifiedFiles));
5076
5695
  if (command) {
5077
5696
  session.emitEvent({
5078
5697
  type: "VerificationStarted",
@@ -5080,7 +5699,7 @@ async function runTurn(session, turnContext, input) {
5080
5699
  command,
5081
5700
  modifiedFiles: Array.from(modifiedFiles)
5082
5701
  });
5083
- const vResult = verifier.verify(Array.from(modifiedFiles));
5702
+ const vResult = await verifier.verify(Array.from(modifiedFiles), signal);
5084
5703
  session.emitEvent({
5085
5704
  type: "VerificationCompleted",
5086
5705
  turnId,
@@ -5373,7 +5992,8 @@ class Session {
5373
5992
  turnId: params.turnId,
5374
5993
  toolName: params.toolName,
5375
5994
  description: params.description,
5376
- command: params.command
5995
+ command: params.command,
5996
+ prefixRule: params.prefixRule
5377
5997
  });
5378
5998
  this.emitEvent({
5379
5999
  type: "StatusChanged",
@@ -9128,7 +9748,11 @@ class AgentGraphStore {
9128
9748
  if (dbPath !== ":memory:") {
9129
9749
  const dir = resolve19(dbPath, "..");
9130
9750
  if (!existsSync19(dir)) {
9131
- mkdirSync10(dir, { recursive: true });
9751
+ try {
9752
+ mkdirSync10(dir, { recursive: true });
9753
+ } catch (err) {
9754
+ console.warn(`[AgentGraphStore] Failed to create database directory '${dir}':`, err);
9755
+ }
9132
9756
  }
9133
9757
  }
9134
9758
  this.db = new Database3(dbPath);
@@ -9210,24 +9834,44 @@ class AgentGraphStore {
9210
9834
  close() {
9211
9835
  try {
9212
9836
  this.db.close();
9213
- } catch {}
9837
+ } catch (err) {
9838
+ console.warn("[AgentGraphStore] Failed to close database cleanly:", err);
9839
+ }
9214
9840
  }
9215
9841
  }
9216
9842
  // src/agents/spawner.ts
9217
9843
  class AgentSpawner {
9218
9844
  parentSession;
9845
+ depth;
9219
9846
  subAgents = new Map;
9220
9847
  nextAgentId = 1;
9221
9848
  roleRegistry;
9222
9849
  parentIdentity;
9223
9850
  graphStore;
9224
- constructor(parentSession, roleRegistry, parentIdentity, graphStore) {
9851
+ maxConcurrentAgents;
9852
+ maxDepth;
9853
+ maxRetainedCompleted;
9854
+ defaultTokenBudget;
9855
+ constructor(parentSession, roleRegistry, parentIdentity, graphStore, options, depth = 0) {
9225
9856
  this.parentSession = parentSession;
9857
+ this.depth = depth;
9226
9858
  this.roleRegistry = roleRegistry || new AgentRoleRegistry;
9227
9859
  this.parentIdentity = parentIdentity || createAgentIdentity(undefined, "groupy-main");
9228
9860
  this.graphStore = graphStore || new AgentGraphStore;
9861
+ const envMax = process.env.PIKAA_MAX_SUBAGENTS ? parseInt(process.env.PIKAA_MAX_SUBAGENTS, 10) : NaN;
9862
+ this.maxConcurrentAgents = options?.maxConcurrentAgents ?? (!isNaN(envMax) && envMax > 0 ? envMax : 5);
9863
+ this.maxDepth = options?.maxDepth ?? 2;
9864
+ this.maxRetainedCompleted = options?.maxRetainedCompleted ?? 20;
9865
+ this.defaultTokenBudget = options?.defaultTokenBudget ?? 50000;
9229
9866
  }
9230
9867
  async spawnAgent(params) {
9868
+ if (this.depth >= this.maxDepth) {
9869
+ throw new GroupyError(`Recursion limit exceeded: Maximum sub-agent nesting depth (${this.maxDepth}) reached.`);
9870
+ }
9871
+ const activeRunningCount = Array.from(this.subAgents.values()).filter((h) => h.status === "running").length;
9872
+ if (activeRunningCount >= this.maxConcurrentAgents) {
9873
+ throw new GroupyError(`Resource limit exceeded: Maximum concurrent sub-agents limit (${this.maxConcurrentAgents}) reached. Please wait for running sub-agents to complete or close them.`);
9874
+ }
9231
9875
  const roleName = params.role || "default";
9232
9876
  const roleConfig = this.roleRegistry.getRole(roleName);
9233
9877
  const agentIndex = this.nextAgentId++;
@@ -9239,7 +9883,18 @@ class AgentSpawner {
9239
9883
 
9240
9884
  Your nickname is ${nickname}. Your assigned task is: '${params.taskName}'. Focus strictly on this task.` : `You are a specialized sub-agent named ${nickname} tasked with: '${params.taskName}'. Focus strictly on this task.`);
9241
9885
  const baseTools = params.tools || this.parentSession.tools;
9242
- const effectiveTools = this.roleRegistry.filterRouterForRole(baseTools, roleName);
9886
+ let effectiveTools = this.roleRegistry.filterRouterForRole(baseTools, roleName);
9887
+ if (this.depth + 1 >= this.maxDepth) {
9888
+ const sanitizedRouter = new ToolRouter;
9889
+ for (const t of effectiveTools.list()) {
9890
+ if (t.name !== "spawn_agent") {
9891
+ sanitizedRouter.register(t);
9892
+ }
9893
+ }
9894
+ effectiveTools = sanitizedRouter;
9895
+ }
9896
+ const tokenBudget = params.maxTokens ?? this.defaultTokenBudget;
9897
+ let accumulatedTokens = 0;
9243
9898
  const childSession = new Session({
9244
9899
  threadId: agentId,
9245
9900
  model: effectiveModel,
@@ -9263,45 +9918,87 @@ Your nickname is ${nickname}. Your assigned task is: '${params.taskName}'. Focus
9263
9918
  createdAt: Date.now(),
9264
9919
  identity: childIdentity,
9265
9920
  session: childSession,
9266
- promise: taskPromise
9921
+ promise: taskPromise,
9922
+ depth: this.depth + 1,
9923
+ tokenBudget,
9924
+ totalTokens: 0
9267
9925
  };
9268
9926
  try {
9269
9927
  this.graphStore.upsertEdge(this.parentSession.threadId, agentId, "open");
9270
- } catch {}
9928
+ } catch (err) {
9929
+ console.warn(`[AgentSpawner] Failed to record edge in graph store for agent '${agentId}':`, err);
9930
+ }
9271
9931
  let collectedAgentText = "";
9272
9932
  childSession.onEvent((event) => {
9273
9933
  if (event.msg.type === "AgentMessageDelta") {
9274
9934
  collectedAgentText += event.msg.delta;
9935
+ accumulatedTokens += Math.ceil(event.msg.delta.length / 4);
9936
+ handle.totalTokens = accumulatedTokens;
9937
+ if (accumulatedTokens > tokenBudget && handle.status === "running") {
9938
+ handle.status = "error";
9939
+ handle.error = `Token budget limit exceeded (${tokenBudget} tokens).`;
9940
+ childSession.interrupt();
9941
+ try {
9942
+ this.graphStore.setEdgeStatus(agentId, "closed");
9943
+ } catch (err) {
9944
+ console.warn(`[AgentSpawner] Failed to update edge status for agent '${agentId}':`, err);
9945
+ }
9946
+ rejectPromise(new Error(handle.error));
9947
+ }
9275
9948
  } else if (event.msg.type === "TurnCompleted") {
9276
- handle.status = "completed";
9277
- handle.lastOutput = collectedAgentText.trim();
9278
- try {
9279
- this.graphStore.setEdgeStatus(agentId, "closed");
9280
- } catch {}
9281
- resolvePromise(handle.lastOutput);
9949
+ if (handle.status === "running") {
9950
+ handle.status = "completed";
9951
+ handle.lastOutput = collectedAgentText.trim();
9952
+ handle.totalTokens = accumulatedTokens;
9953
+ try {
9954
+ this.graphStore.setEdgeStatus(agentId, "closed");
9955
+ } catch (err) {
9956
+ console.warn(`[AgentSpawner] Failed to update edge status for agent '${agentId}':`, err);
9957
+ }
9958
+ this.pruneCompletedAgents();
9959
+ resolvePromise(handle.lastOutput);
9960
+ }
9282
9961
  } else if (event.msg.type === "Error") {
9283
- handle.status = "error";
9284
- handle.error = event.msg.message;
9285
- try {
9286
- this.graphStore.setEdgeStatus(agentId, "closed");
9287
- } catch {}
9288
- rejectPromise(new Error(event.msg.message));
9962
+ if (handle.status === "running") {
9963
+ handle.status = "error";
9964
+ handle.error = event.msg.message;
9965
+ handle.totalTokens = accumulatedTokens;
9966
+ try {
9967
+ this.graphStore.setEdgeStatus(agentId, "closed");
9968
+ } catch (err) {
9969
+ console.warn(`[AgentSpawner] Failed to update edge status for agent '${agentId}':`, err);
9970
+ }
9971
+ this.pruneCompletedAgents();
9972
+ rejectPromise(new Error(event.msg.message));
9973
+ }
9289
9974
  } else if (event.msg.type === "StatusChanged" && event.msg.status === "interrupted") {
9290
- handle.status = "interrupted";
9291
- try {
9292
- this.graphStore.setEdgeStatus(agentId, "closed");
9293
- } catch {}
9294
- resolvePromise(collectedAgentText.trim() || "[Task was interrupted]");
9975
+ if (handle.status === "running") {
9976
+ handle.status = "interrupted";
9977
+ handle.totalTokens = accumulatedTokens;
9978
+ try {
9979
+ this.graphStore.setEdgeStatus(agentId, "closed");
9980
+ } catch (err) {
9981
+ console.warn(`[AgentSpawner] Failed to update edge status for agent '${agentId}':`, err);
9982
+ }
9983
+ this.pruneCompletedAgents();
9984
+ resolvePromise(collectedAgentText.trim() || "[Task was interrupted]");
9985
+ }
9295
9986
  }
9296
9987
  });
9297
9988
  this.subAgents.set(agentId, handle);
9298
9989
  childSession.prompt(params.message).catch((err) => {
9299
- handle.status = "error";
9300
- handle.error = err instanceof Error ? err.message : String(err);
9301
- try {
9302
- this.graphStore.setEdgeStatus(agentId, "closed");
9303
- } catch {}
9304
- rejectPromise(err);
9990
+ if (handle.status === "running") {
9991
+ handle.status = "error";
9992
+ handle.error = err instanceof Error ? err.message : String(err);
9993
+ handle.totalTokens = accumulatedTokens;
9994
+ try {
9995
+ this.graphStore.setEdgeStatus(agentId, "closed");
9996
+ } catch (storeErr) {
9997
+ console.warn(`[AgentSpawner] Failed to update edge status for agent '${agentId}':`, storeErr);
9998
+ }
9999
+ this.pruneCompletedAgents();
10000
+ rejectPromise(err);
10001
+ }
9305
10002
  });
9306
10003
  return {
9307
10004
  id: handle.id,
@@ -9310,9 +10007,22 @@ Your nickname is ${nickname}. Your assigned task is: '${params.taskName}'. Focus
9310
10007
  role: handle.role,
9311
10008
  status: handle.status,
9312
10009
  createdAt: handle.createdAt,
9313
- agentRuntimeId: childIdentity.agentRuntimeId
10010
+ agentRuntimeId: childIdentity.agentRuntimeId,
10011
+ depth: handle.depth,
10012
+ tokenBudget: handle.tokenBudget,
10013
+ totalTokens: handle.totalTokens
9314
10014
  };
9315
10015
  }
10016
+ pruneCompletedAgents() {
10017
+ const finishedHandles = Array.from(this.subAgents.values()).filter((h) => h.status !== "running");
10018
+ if (finishedHandles.length > this.maxRetainedCompleted) {
10019
+ finishedHandles.sort((a, b) => a.createdAt - b.createdAt);
10020
+ const toRemove = finishedHandles.slice(0, finishedHandles.length - this.maxRetainedCompleted);
10021
+ for (const h of toRemove) {
10022
+ this.subAgents.delete(h.id);
10023
+ }
10024
+ }
10025
+ }
9316
10026
  async waitAgent(agentIdOrTaskName, timeoutMs = 60000) {
9317
10027
  if (!agentIdOrTaskName) {
9318
10028
  const handles = Array.from(this.subAgents.values());
@@ -9366,9 +10076,36 @@ Your nickname is ${nickname}. Your assigned task is: '${params.taskName}'. Focus
9366
10076
  handle.status = "interrupted";
9367
10077
  try {
9368
10078
  this.graphStore.setEdgeStatus(agentId, "closed");
9369
- } catch {}
10079
+ } catch (err) {
10080
+ console.warn(`[AgentSpawner] Failed to close edge for agent '${agentId}':`, err);
10081
+ }
10082
+ this.pruneCompletedAgents();
9370
10083
  return `Sub-agent ${handle.nickname} (${agentId}) interrupted and closed.`;
9371
10084
  }
10085
+ removeAgent(agentId) {
10086
+ const handle = this.subAgents.get(agentId);
10087
+ if (!handle)
10088
+ return false;
10089
+ if (handle.status === "running") {
10090
+ handle.session.interrupt();
10091
+ }
10092
+ try {
10093
+ this.graphStore.setEdgeStatus(agentId, "closed");
10094
+ } catch (err) {
10095
+ console.warn(`[AgentSpawner] Failed to close edge on removal for '${agentId}':`, err);
10096
+ }
10097
+ return this.subAgents.delete(agentId);
10098
+ }
10099
+ clearCompleted() {
10100
+ let cleared = 0;
10101
+ for (const [id, handle] of this.subAgents.entries()) {
10102
+ if (handle.status !== "running") {
10103
+ this.subAgents.delete(id);
10104
+ cleared++;
10105
+ }
10106
+ }
10107
+ return cleared;
10108
+ }
9372
10109
  listAgents() {
9373
10110
  const list = [];
9374
10111
  for (const handle of this.subAgents.values()) {
@@ -9380,7 +10117,10 @@ Your nickname is ${nickname}. Your assigned task is: '${params.taskName}'. Focus
9380
10117
  status: handle.status,
9381
10118
  createdAt: handle.createdAt,
9382
10119
  agentRuntimeId: handle.identity.agentRuntimeId,
9383
- lastOutput: handle.lastOutput
10120
+ lastOutput: handle.lastOutput,
10121
+ depth: handle.depth,
10122
+ tokenBudget: handle.tokenBudget,
10123
+ totalTokens: handle.totalTokens
9384
10124
  });
9385
10125
  }
9386
10126
  return list;
@@ -9413,6 +10153,10 @@ function createMultiAgentTools(spawner) {
9413
10153
  model: {
9414
10154
  type: "string",
9415
10155
  description: "Optional model override. Defaults to inheriting parent model."
10156
+ },
10157
+ max_tokens: {
10158
+ type: "number",
10159
+ description: "Optional token budget limit for the sub-agent (default: 50000)."
9416
10160
  }
9417
10161
  },
9418
10162
  required: ["task_name", "message"]
@@ -9422,8 +10166,9 @@ function createMultiAgentTools(spawner) {
9422
10166
  const message = String(args.message || "");
9423
10167
  const role = args.role ? String(args.role) : undefined;
9424
10168
  const model = args.model ? String(args.model) : undefined;
10169
+ const maxTokens = typeof args.max_tokens === "number" ? args.max_tokens : undefined;
9425
10170
  try {
9426
- const handle = await spawner.spawnAgent({ taskName, message, role, model });
10171
+ const handle = await spawner.spawnAgent({ taskName, message, role, model, maxTokens });
9427
10172
  return {
9428
10173
  output: `Successfully spawned sub-agent '${handle.id}' with role '${handle.role}' for task '${handle.taskName}' (status: ${handle.status}). Use 'wait_agent' to collect results.`
9429
10174
  };
@@ -9770,7 +10515,8 @@ class SessionPersistenceManager {
9770
10515
  loadSession(threadId) {
9771
10516
  try {
9772
10517
  return this.store.restoreSession(threadId);
9773
- } catch {
10518
+ } catch (err) {
10519
+ console.warn(`[SessionPersistenceManager] Failed to restore session '${threadId}':`, err);
9774
10520
  return null;
9775
10521
  }
9776
10522
  }
@@ -9778,7 +10524,9 @@ class SessionPersistenceManager {
9778
10524
  for (const unsub of this.unsubscribers) {
9779
10525
  try {
9780
10526
  unsub();
9781
- } catch {}
10527
+ } catch (err) {
10528
+ console.warn("[SessionPersistenceManager] Error in unbindSession callback:", err);
10529
+ }
9782
10530
  }
9783
10531
  this.unsubscribers = [];
9784
10532
  }
@@ -9805,7 +10553,9 @@ class SessionPersistenceManager {
9805
10553
  for (const unsub of this.unsubscribers) {
9806
10554
  try {
9807
10555
  unsub();
9808
- } catch {}
10556
+ } catch (err) {
10557
+ console.warn("[SessionPersistenceManager] Error closing session persistence listener:", err);
10558
+ }
9809
10559
  }
9810
10560
  this.unsubscribers = [];
9811
10561
  this.store.close();
@@ -10070,7 +10820,9 @@ class MemoryStore {
10070
10820
  if (!existsSync22(dir)) {
10071
10821
  try {
10072
10822
  mkdirSync12(dir, { recursive: true });
10073
- } catch {}
10823
+ } catch (err) {
10824
+ console.warn(`[MemoryStore] Failed to create custom memory directory '${dir}':`, err);
10825
+ }
10074
10826
  }
10075
10827
  return dir;
10076
10828
  }
@@ -10079,7 +10831,9 @@ class MemoryStore {
10079
10831
  if (!existsSync22(dir)) {
10080
10832
  try {
10081
10833
  mkdirSync12(dir, { recursive: true });
10082
- } catch {}
10834
+ } catch (err) {
10835
+ console.warn(`[MemoryStore] Failed to create project memory directory '${dir}':`, err);
10836
+ }
10083
10837
  }
10084
10838
  return dir;
10085
10839
  }
@@ -10223,7 +10977,9 @@ class MemoryStore {
10223
10977
  `)[0] || parsed.name,
10224
10978
  file: f
10225
10979
  });
10226
- } catch {}
10980
+ } catch (err) {
10981
+ console.warn(`[MemoryStore] Failed to parse topic memory file '${f}':`, err);
10982
+ }
10227
10983
  }
10228
10984
  const indexLines = [
10229
10985
  "# Project Auto-Memory Index",
@@ -10265,7 +11021,9 @@ class MemoryStore {
10265
11021
  try {
10266
11022
  const full = join13(memoryDir, f);
10267
11023
  list.push(this.parseTopicFile(readFileSync13(full, "utf8"), full));
10268
- } catch {}
11024
+ } catch (err) {
11025
+ console.warn(`[MemoryStore] Failed to read topic memory file '${f}':`, err);
11026
+ }
10269
11027
  }
10270
11028
  return list;
10271
11029
  }
@@ -10785,7 +11543,7 @@ function ClaudeLogo({
10785
11543
  }, undefined, false, undefined, this);
10786
11544
  }
10787
11545
  function ClaudeHeader({
10788
- version = "v0.3.2",
11546
+ version = "v0.4.0",
10789
11547
  user = "Developer",
10790
11548
  model = "Claude 3.7 Sonnet (Thinking)",
10791
11549
  plan = "Pro",
@@ -12234,6 +12992,7 @@ export {
12234
12992
  CredentialsStore,
12235
12993
  DEFAULT_AUTO_COMPACT_THRESHOLD_TOKENS,
12236
12994
  DEFAULT_MAX_CONTEXT_TOKENS,
12995
+ DEFAULT_MAX_DIR_ENTRIES,
12237
12996
  DEFAULT_MAX_UNPAGINATED_LINES,
12238
12997
  DEFAULT_WORKTREE_KEEP_COUNT,
12239
12998
  DefaultModelClientSession,