@pikaa-ai/pikaa 0.3.28 → 0.4.1

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}`;
5184
+ }
5185
+ if (existsSync14(join8(this.cwd, "go.mod"))) {
5186
+ return `go test ${quotedFiles}`;
4746
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 .";
4787
5230
  }
4788
5231
  return null;
4789
5232
  }
4790
- verify(modifiedFiles = []) {
4791
- const command = this.resolveVerificationCommand();
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 {}
5269
+ }
5270
+ return null;
5271
+ }
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",
@@ -9008,7 +9628,30 @@ class AgentRoleRegistry {
9008
9628
  name: "default",
9009
9629
  description: "Generalist autonomous developer agent capable of coding, testing, and debugging.",
9010
9630
  systemPrompt: "You are Groupy, an expert autonomous software engineer. Think carefully, use tools surgically, and verify every step.",
9011
- nicknameCandidates: ["Pikaa", "Heca", "Bankli", "Moli"]
9631
+ nicknameCandidates: ["Pikaa", "Heca", "Bankli", "Moli"],
9632
+ allowedToolNames: [
9633
+ "read_file",
9634
+ "view_file",
9635
+ "write_file",
9636
+ "apply_patch",
9637
+ "list_dir",
9638
+ "shell",
9639
+ "update_plan",
9640
+ "ask_question",
9641
+ "request_user_input",
9642
+ "remember",
9643
+ "list_memories",
9644
+ "read_memory",
9645
+ "save_memory",
9646
+ "spawn_agent",
9647
+ "wait_agent",
9648
+ "send_input",
9649
+ "close_agent",
9650
+ "list_agents",
9651
+ "create_worktree",
9652
+ "list_worktrees",
9653
+ "merge_worktree"
9654
+ ]
9012
9655
  });
9013
9656
  this.registerRole({
9014
9657
  name: "reviewer",
@@ -9105,7 +9748,13 @@ class AgentRoleRegistry {
9105
9748
  if (!role || !role.allowedToolNames)
9106
9749
  return sourceRouter;
9107
9750
  const filtered = sourceRouter.list().filter((t) => {
9108
- return role.allowedToolNames.some((allowed) => t.name === allowed || t.name.startsWith(`mcp__`));
9751
+ return role.allowedToolNames.some((allowed) => {
9752
+ if (t.name === allowed)
9753
+ return true;
9754
+ if (allowed.endsWith("*") && t.name.startsWith(allowed.slice(0, -1)))
9755
+ return true;
9756
+ return false;
9757
+ });
9109
9758
  });
9110
9759
  const newRouter = new sourceRouter.constructor;
9111
9760
  for (const tool of filtered) {
@@ -9128,7 +9777,11 @@ class AgentGraphStore {
9128
9777
  if (dbPath !== ":memory:") {
9129
9778
  const dir = resolve19(dbPath, "..");
9130
9779
  if (!existsSync19(dir)) {
9131
- mkdirSync10(dir, { recursive: true });
9780
+ try {
9781
+ mkdirSync10(dir, { recursive: true });
9782
+ } catch (err) {
9783
+ console.warn(`[AgentGraphStore] Failed to create database directory '${dir}':`, err);
9784
+ }
9132
9785
  }
9133
9786
  }
9134
9787
  this.db = new Database3(dbPath);
@@ -9210,24 +9863,44 @@ class AgentGraphStore {
9210
9863
  close() {
9211
9864
  try {
9212
9865
  this.db.close();
9213
- } catch {}
9866
+ } catch (err) {
9867
+ console.warn("[AgentGraphStore] Failed to close database cleanly:", err);
9868
+ }
9214
9869
  }
9215
9870
  }
9216
9871
  // src/agents/spawner.ts
9217
9872
  class AgentSpawner {
9218
9873
  parentSession;
9874
+ depth;
9219
9875
  subAgents = new Map;
9220
9876
  nextAgentId = 1;
9221
9877
  roleRegistry;
9222
9878
  parentIdentity;
9223
9879
  graphStore;
9224
- constructor(parentSession, roleRegistry, parentIdentity, graphStore) {
9880
+ maxConcurrentAgents;
9881
+ maxDepth;
9882
+ maxRetainedCompleted;
9883
+ defaultTokenBudget;
9884
+ constructor(parentSession, roleRegistry, parentIdentity, graphStore, options, depth = 0) {
9225
9885
  this.parentSession = parentSession;
9886
+ this.depth = depth;
9226
9887
  this.roleRegistry = roleRegistry || new AgentRoleRegistry;
9227
9888
  this.parentIdentity = parentIdentity || createAgentIdentity(undefined, "groupy-main");
9228
9889
  this.graphStore = graphStore || new AgentGraphStore;
9890
+ const envMax = process.env.PIKAA_MAX_SUBAGENTS ? parseInt(process.env.PIKAA_MAX_SUBAGENTS, 10) : NaN;
9891
+ this.maxConcurrentAgents = options?.maxConcurrentAgents ?? (!isNaN(envMax) && envMax > 0 ? envMax : 5);
9892
+ this.maxDepth = options?.maxDepth ?? 2;
9893
+ this.maxRetainedCompleted = options?.maxRetainedCompleted ?? 20;
9894
+ this.defaultTokenBudget = options?.defaultTokenBudget ?? 50000;
9229
9895
  }
9230
9896
  async spawnAgent(params) {
9897
+ if (this.depth >= this.maxDepth) {
9898
+ throw new GroupyError(`Recursion limit exceeded: Maximum sub-agent nesting depth (${this.maxDepth}) reached.`);
9899
+ }
9900
+ const activeRunningCount = Array.from(this.subAgents.values()).filter((h) => h.status === "running").length;
9901
+ if (activeRunningCount >= this.maxConcurrentAgents) {
9902
+ 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.`);
9903
+ }
9231
9904
  const roleName = params.role || "default";
9232
9905
  const roleConfig = this.roleRegistry.getRole(roleName);
9233
9906
  const agentIndex = this.nextAgentId++;
@@ -9239,7 +9912,18 @@ class AgentSpawner {
9239
9912
 
9240
9913
  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
9914
  const baseTools = params.tools || this.parentSession.tools;
9242
- const effectiveTools = this.roleRegistry.filterRouterForRole(baseTools, roleName);
9915
+ let effectiveTools = this.roleRegistry.filterRouterForRole(baseTools, roleName);
9916
+ if (this.depth + 1 >= this.maxDepth) {
9917
+ const sanitizedRouter = new ToolRouter;
9918
+ for (const t of effectiveTools.list()) {
9919
+ if (t.name !== "spawn_agent") {
9920
+ sanitizedRouter.register(t);
9921
+ }
9922
+ }
9923
+ effectiveTools = sanitizedRouter;
9924
+ }
9925
+ const tokenBudget = params.maxTokens ?? this.defaultTokenBudget;
9926
+ let accumulatedTokens = 0;
9243
9927
  const childSession = new Session({
9244
9928
  threadId: agentId,
9245
9929
  model: effectiveModel,
@@ -9263,45 +9947,87 @@ Your nickname is ${nickname}. Your assigned task is: '${params.taskName}'. Focus
9263
9947
  createdAt: Date.now(),
9264
9948
  identity: childIdentity,
9265
9949
  session: childSession,
9266
- promise: taskPromise
9950
+ promise: taskPromise,
9951
+ depth: this.depth + 1,
9952
+ tokenBudget,
9953
+ totalTokens: 0
9267
9954
  };
9268
9955
  try {
9269
9956
  this.graphStore.upsertEdge(this.parentSession.threadId, agentId, "open");
9270
- } catch {}
9957
+ } catch (err) {
9958
+ console.warn(`[AgentSpawner] Failed to record edge in graph store for agent '${agentId}':`, err);
9959
+ }
9271
9960
  let collectedAgentText = "";
9272
9961
  childSession.onEvent((event) => {
9273
9962
  if (event.msg.type === "AgentMessageDelta") {
9274
9963
  collectedAgentText += event.msg.delta;
9964
+ accumulatedTokens += Math.ceil(event.msg.delta.length / 4);
9965
+ handle.totalTokens = accumulatedTokens;
9966
+ if (accumulatedTokens > tokenBudget && handle.status === "running") {
9967
+ handle.status = "error";
9968
+ handle.error = `Token budget limit exceeded (${tokenBudget} tokens).`;
9969
+ childSession.interrupt();
9970
+ try {
9971
+ this.graphStore.setEdgeStatus(agentId, "closed");
9972
+ } catch (err) {
9973
+ console.warn(`[AgentSpawner] Failed to update edge status for agent '${agentId}':`, err);
9974
+ }
9975
+ rejectPromise(new Error(handle.error));
9976
+ }
9275
9977
  } 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);
9978
+ if (handle.status === "running") {
9979
+ handle.status = "completed";
9980
+ handle.lastOutput = collectedAgentText.trim();
9981
+ handle.totalTokens = accumulatedTokens;
9982
+ try {
9983
+ this.graphStore.setEdgeStatus(agentId, "closed");
9984
+ } catch (err) {
9985
+ console.warn(`[AgentSpawner] Failed to update edge status for agent '${agentId}':`, err);
9986
+ }
9987
+ this.pruneCompletedAgents();
9988
+ resolvePromise(handle.lastOutput);
9989
+ }
9282
9990
  } 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));
9991
+ if (handle.status === "running") {
9992
+ handle.status = "error";
9993
+ handle.error = event.msg.message;
9994
+ handle.totalTokens = accumulatedTokens;
9995
+ try {
9996
+ this.graphStore.setEdgeStatus(agentId, "closed");
9997
+ } catch (err) {
9998
+ console.warn(`[AgentSpawner] Failed to update edge status for agent '${agentId}':`, err);
9999
+ }
10000
+ this.pruneCompletedAgents();
10001
+ rejectPromise(new Error(event.msg.message));
10002
+ }
9289
10003
  } 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]");
10004
+ if (handle.status === "running") {
10005
+ handle.status = "interrupted";
10006
+ handle.totalTokens = accumulatedTokens;
10007
+ try {
10008
+ this.graphStore.setEdgeStatus(agentId, "closed");
10009
+ } catch (err) {
10010
+ console.warn(`[AgentSpawner] Failed to update edge status for agent '${agentId}':`, err);
10011
+ }
10012
+ this.pruneCompletedAgents();
10013
+ resolvePromise(collectedAgentText.trim() || "[Task was interrupted]");
10014
+ }
9295
10015
  }
9296
10016
  });
9297
10017
  this.subAgents.set(agentId, handle);
9298
10018
  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);
10019
+ if (handle.status === "running") {
10020
+ handle.status = "error";
10021
+ handle.error = err instanceof Error ? err.message : String(err);
10022
+ handle.totalTokens = accumulatedTokens;
10023
+ try {
10024
+ this.graphStore.setEdgeStatus(agentId, "closed");
10025
+ } catch (storeErr) {
10026
+ console.warn(`[AgentSpawner] Failed to update edge status for agent '${agentId}':`, storeErr);
10027
+ }
10028
+ this.pruneCompletedAgents();
10029
+ rejectPromise(err);
10030
+ }
9305
10031
  });
9306
10032
  return {
9307
10033
  id: handle.id,
@@ -9310,9 +10036,22 @@ Your nickname is ${nickname}. Your assigned task is: '${params.taskName}'. Focus
9310
10036
  role: handle.role,
9311
10037
  status: handle.status,
9312
10038
  createdAt: handle.createdAt,
9313
- agentRuntimeId: childIdentity.agentRuntimeId
10039
+ agentRuntimeId: childIdentity.agentRuntimeId,
10040
+ depth: handle.depth,
10041
+ tokenBudget: handle.tokenBudget,
10042
+ totalTokens: handle.totalTokens
9314
10043
  };
9315
10044
  }
10045
+ pruneCompletedAgents() {
10046
+ const finishedHandles = Array.from(this.subAgents.values()).filter((h) => h.status !== "running");
10047
+ if (finishedHandles.length > this.maxRetainedCompleted) {
10048
+ finishedHandles.sort((a, b) => a.createdAt - b.createdAt);
10049
+ const toRemove = finishedHandles.slice(0, finishedHandles.length - this.maxRetainedCompleted);
10050
+ for (const h of toRemove) {
10051
+ this.subAgents.delete(h.id);
10052
+ }
10053
+ }
10054
+ }
9316
10055
  async waitAgent(agentIdOrTaskName, timeoutMs = 60000) {
9317
10056
  if (!agentIdOrTaskName) {
9318
10057
  const handles = Array.from(this.subAgents.values());
@@ -9366,9 +10105,36 @@ Your nickname is ${nickname}. Your assigned task is: '${params.taskName}'. Focus
9366
10105
  handle.status = "interrupted";
9367
10106
  try {
9368
10107
  this.graphStore.setEdgeStatus(agentId, "closed");
9369
- } catch {}
10108
+ } catch (err) {
10109
+ console.warn(`[AgentSpawner] Failed to close edge for agent '${agentId}':`, err);
10110
+ }
10111
+ this.pruneCompletedAgents();
9370
10112
  return `Sub-agent ${handle.nickname} (${agentId}) interrupted and closed.`;
9371
10113
  }
10114
+ removeAgent(agentId) {
10115
+ const handle = this.subAgents.get(agentId);
10116
+ if (!handle)
10117
+ return false;
10118
+ if (handle.status === "running") {
10119
+ handle.session.interrupt();
10120
+ }
10121
+ try {
10122
+ this.graphStore.setEdgeStatus(agentId, "closed");
10123
+ } catch (err) {
10124
+ console.warn(`[AgentSpawner] Failed to close edge on removal for '${agentId}':`, err);
10125
+ }
10126
+ return this.subAgents.delete(agentId);
10127
+ }
10128
+ clearCompleted() {
10129
+ let cleared = 0;
10130
+ for (const [id, handle] of this.subAgents.entries()) {
10131
+ if (handle.status !== "running") {
10132
+ this.subAgents.delete(id);
10133
+ cleared++;
10134
+ }
10135
+ }
10136
+ return cleared;
10137
+ }
9372
10138
  listAgents() {
9373
10139
  const list = [];
9374
10140
  for (const handle of this.subAgents.values()) {
@@ -9380,7 +10146,10 @@ Your nickname is ${nickname}. Your assigned task is: '${params.taskName}'. Focus
9380
10146
  status: handle.status,
9381
10147
  createdAt: handle.createdAt,
9382
10148
  agentRuntimeId: handle.identity.agentRuntimeId,
9383
- lastOutput: handle.lastOutput
10149
+ lastOutput: handle.lastOutput,
10150
+ depth: handle.depth,
10151
+ tokenBudget: handle.tokenBudget,
10152
+ totalTokens: handle.totalTokens
9384
10153
  });
9385
10154
  }
9386
10155
  return list;
@@ -9413,6 +10182,10 @@ function createMultiAgentTools(spawner) {
9413
10182
  model: {
9414
10183
  type: "string",
9415
10184
  description: "Optional model override. Defaults to inheriting parent model."
10185
+ },
10186
+ max_tokens: {
10187
+ type: "number",
10188
+ description: "Optional token budget limit for the sub-agent (default: 50000)."
9416
10189
  }
9417
10190
  },
9418
10191
  required: ["task_name", "message"]
@@ -9422,8 +10195,9 @@ function createMultiAgentTools(spawner) {
9422
10195
  const message = String(args.message || "");
9423
10196
  const role = args.role ? String(args.role) : undefined;
9424
10197
  const model = args.model ? String(args.model) : undefined;
10198
+ const maxTokens = typeof args.max_tokens === "number" ? args.max_tokens : undefined;
9425
10199
  try {
9426
- const handle = await spawner.spawnAgent({ taskName, message, role, model });
10200
+ const handle = await spawner.spawnAgent({ taskName, message, role, model, maxTokens });
9427
10201
  return {
9428
10202
  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
10203
  };
@@ -9770,7 +10544,8 @@ class SessionPersistenceManager {
9770
10544
  loadSession(threadId) {
9771
10545
  try {
9772
10546
  return this.store.restoreSession(threadId);
9773
- } catch {
10547
+ } catch (err) {
10548
+ console.warn(`[SessionPersistenceManager] Failed to restore session '${threadId}':`, err);
9774
10549
  return null;
9775
10550
  }
9776
10551
  }
@@ -9778,7 +10553,9 @@ class SessionPersistenceManager {
9778
10553
  for (const unsub of this.unsubscribers) {
9779
10554
  try {
9780
10555
  unsub();
9781
- } catch {}
10556
+ } catch (err) {
10557
+ console.warn("[SessionPersistenceManager] Error in unbindSession callback:", err);
10558
+ }
9782
10559
  }
9783
10560
  this.unsubscribers = [];
9784
10561
  }
@@ -9805,7 +10582,9 @@ class SessionPersistenceManager {
9805
10582
  for (const unsub of this.unsubscribers) {
9806
10583
  try {
9807
10584
  unsub();
9808
- } catch {}
10585
+ } catch (err) {
10586
+ console.warn("[SessionPersistenceManager] Error closing session persistence listener:", err);
10587
+ }
9809
10588
  }
9810
10589
  this.unsubscribers = [];
9811
10590
  this.store.close();
@@ -10070,7 +10849,9 @@ class MemoryStore {
10070
10849
  if (!existsSync22(dir)) {
10071
10850
  try {
10072
10851
  mkdirSync12(dir, { recursive: true });
10073
- } catch {}
10852
+ } catch (err) {
10853
+ console.warn(`[MemoryStore] Failed to create custom memory directory '${dir}':`, err);
10854
+ }
10074
10855
  }
10075
10856
  return dir;
10076
10857
  }
@@ -10079,7 +10860,9 @@ class MemoryStore {
10079
10860
  if (!existsSync22(dir)) {
10080
10861
  try {
10081
10862
  mkdirSync12(dir, { recursive: true });
10082
- } catch {}
10863
+ } catch (err) {
10864
+ console.warn(`[MemoryStore] Failed to create project memory directory '${dir}':`, err);
10865
+ }
10083
10866
  }
10084
10867
  return dir;
10085
10868
  }
@@ -10223,7 +11006,9 @@ class MemoryStore {
10223
11006
  `)[0] || parsed.name,
10224
11007
  file: f
10225
11008
  });
10226
- } catch {}
11009
+ } catch (err) {
11010
+ console.warn(`[MemoryStore] Failed to parse topic memory file '${f}':`, err);
11011
+ }
10227
11012
  }
10228
11013
  const indexLines = [
10229
11014
  "# Project Auto-Memory Index",
@@ -10265,7 +11050,9 @@ class MemoryStore {
10265
11050
  try {
10266
11051
  const full = join13(memoryDir, f);
10267
11052
  list.push(this.parseTopicFile(readFileSync13(full, "utf8"), full));
10268
- } catch {}
11053
+ } catch (err) {
11054
+ console.warn(`[MemoryStore] Failed to read topic memory file '${f}':`, err);
11055
+ }
10269
11056
  }
10270
11057
  return list;
10271
11058
  }
@@ -10785,7 +11572,7 @@ function ClaudeLogo({
10785
11572
  }, undefined, false, undefined, this);
10786
11573
  }
10787
11574
  function ClaudeHeader({
10788
- version = "v0.3.2",
11575
+ version = "v0.4.0",
10789
11576
  user = "Developer",
10790
11577
  model = "Claude 3.7 Sonnet (Thinking)",
10791
11578
  plan = "Pro",
@@ -12234,6 +13021,7 @@ export {
12234
13021
  CredentialsStore,
12235
13022
  DEFAULT_AUTO_COMPACT_THRESHOLD_TOKENS,
12236
13023
  DEFAULT_MAX_CONTEXT_TOKENS,
13024
+ DEFAULT_MAX_DIR_ENTRIES,
12237
13025
  DEFAULT_MAX_UNPAGINATED_LINES,
12238
13026
  DEFAULT_WORKTREE_KEEP_COUNT,
12239
13027
  DefaultModelClientSession,