@pikaa-ai/pikaa 0.3.27 → 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,31 +2872,119 @@ ${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";
2877
+ var DEFAULT_MAX_UNPAGINATED_LINES = 250;
2570
2878
  var readFileTool = {
2571
2879
  name: "read_file",
2572
- description: "Read the full text content of a file.",
2880
+ description: "Read file content with surgical line-range support. Supports start_line and end_line to inspect specific sections of large files without exhausting token context.",
2573
2881
  parameters: {
2574
2882
  type: "object",
2575
2883
  properties: {
2576
- path: { type: "string", description: "Relative or absolute path to the file." }
2884
+ path: {
2885
+ type: "string",
2886
+ description: "Relative or absolute path to the file."
2887
+ },
2888
+ start_line: {
2889
+ type: "number",
2890
+ description: "Optional 1-indexed line number to start reading from (e.g. 120)."
2891
+ },
2892
+ end_line: {
2893
+ type: "number",
2894
+ description: "Optional 1-indexed line number to end reading at, inclusive (e.g. 180)."
2895
+ },
2896
+ offset: {
2897
+ type: "number",
2898
+ description: "Alias for start_line (1-indexed)."
2899
+ },
2900
+ limit: {
2901
+ type: "number",
2902
+ description: "Maximum number of lines to read."
2903
+ },
2904
+ line_numbers: {
2905
+ type: "boolean",
2906
+ description: "Whether to include line number prefixes ('<line>: <content>'). Defaults to true for range reads."
2907
+ }
2577
2908
  },
2578
2909
  required: ["path"]
2579
2910
  },
2580
2911
  async execute(args, ctx) {
2581
- const filePath = resolve6(ctx.cwd, String(args.path || ""));
2912
+ const rawPath = String(args.path || "");
2913
+ const filePath = resolve6(ctx.cwd, rawPath);
2582
2914
  if (!existsSync8(filePath)) {
2583
- return { output: `Error: File not found: '${args.path}'`, isError: true };
2915
+ return { output: `Error: File not found: '${rawPath}'`, isError: true };
2584
2916
  }
2585
2917
  try {
2586
2918
  const content = readFileSync3(filePath, "utf8");
2587
- return { output: content };
2919
+ const lines = content.split(/\r?\n/);
2920
+ const totalLines = lines.length;
2921
+ const hasRange = args.start_line !== undefined || args.end_line !== undefined || args.startLine !== undefined || args.endLine !== undefined || args.offset !== undefined || args.limit !== undefined;
2922
+ if (!hasRange) {
2923
+ if (args.line_numbers === true) {
2924
+ const formatted = lines.map((l, idx) => `${idx + 1}: ${l}`).join(`
2925
+ `);
2926
+ return { output: formatted };
2927
+ }
2928
+ if (totalLines <= DEFAULT_MAX_UNPAGINATED_LINES) {
2929
+ return { output: content };
2930
+ }
2931
+ const truncated = lines.slice(0, DEFAULT_MAX_UNPAGINATED_LINES);
2932
+ const formatted = truncated.map((l, idx) => `${idx + 1}: ${l}`).join(`
2933
+ `);
2934
+ return {
2935
+ output: `[Showing lines 1 to ${DEFAULT_MAX_UNPAGINATED_LINES} of ${totalLines} in '${rawPath}']
2936
+ ${formatted}
2937
+
2938
+ [Truncated: ${totalLines - DEFAULT_MAX_UNPAGINATED_LINES} more lines. Use start_line=${DEFAULT_MAX_UNPAGINATED_LINES + 1} to continue reading.]`
2939
+ };
2940
+ }
2941
+ const startArg = args.start_line ?? args.startLine ?? args.offset;
2942
+ const start = Math.max(1, typeof startArg === "number" ? Math.floor(startArg) : 1);
2943
+ let end;
2944
+ const endArg = args.end_line ?? args.endLine;
2945
+ if (typeof endArg === "number") {
2946
+ end = Math.min(totalLines, Math.floor(endArg));
2947
+ } else if (typeof args.limit === "number") {
2948
+ end = Math.min(totalLines, start + Math.floor(args.limit) - 1);
2949
+ } else {
2950
+ end = Math.min(totalLines, start + DEFAULT_MAX_UNPAGINATED_LINES - 1);
2951
+ }
2952
+ if (start > totalLines) {
2953
+ return {
2954
+ output: `Error: start_line (${start}) exceeds total lines in file (${totalLines}).`,
2955
+ isError: true
2956
+ };
2957
+ }
2958
+ if (end < start) {
2959
+ return {
2960
+ output: `Error: end_line (${end}) cannot be less than start_line (${start}).`,
2961
+ isError: true
2962
+ };
2963
+ }
2964
+ const sliced = lines.slice(start - 1, end);
2965
+ const withNums = args.line_numbers !== false;
2966
+ const rendered = withNums ? sliced.map((l, idx) => `${start + idx}: ${l}`).join(`
2967
+ `) : sliced.join(`
2968
+ `);
2969
+ let notice = `[Showing lines ${start} to ${end} of ${totalLines} in '${rawPath}']
2970
+ ${rendered}`;
2971
+ if (end < totalLines) {
2972
+ notice += `
2973
+
2974
+ [File has ${totalLines} lines. To read further, use start_line=${end + 1}.]`;
2975
+ }
2976
+ return { output: notice };
2588
2977
  } catch (err) {
2589
2978
  return { output: `Failed to read file: ${err instanceof Error ? err.message : String(err)}`, isError: true };
2590
2979
  }
2591
2980
  }
2592
2981
  };
2982
+ var viewFileTool = {
2983
+ ...readFileTool,
2984
+ name: "view_file",
2985
+ description: "View file content with surgical line-range support. Alias for read_file matching Antigravity & Claude Code conventions."
2986
+ };
2987
+ var DEFAULT_MAX_DIR_ENTRIES = 500;
2593
2988
  var listDirTool = {
2594
2989
  name: "list_dir",
2595
2990
  description: "List contents of a directory with file names and types.",
@@ -2606,13 +3001,31 @@ var listDirTool = {
2606
3001
  }
2607
3002
  try {
2608
3003
  const entries = readdirSync3(dirPath);
2609
- 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) => {
2610
3007
  const full = resolve6(dirPath, entry);
2611
- 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
+ }
2612
3019
  return `${isDir ? "[DIR]" : "[FILE]"} ${entry}`;
2613
3020
  });
2614
- return { output: formatted.join(`
2615
- `) || "[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 };
2616
3029
  } catch (err) {
2617
3030
  return { output: `Failed to list directory: ${err instanceof Error ? err.message : String(err)}`, isError: true };
2618
3031
  }
@@ -2653,6 +3066,7 @@ var writeFileTool = {
2653
3066
  try {
2654
3067
  mkdirSync6(dirname4(filePath), { recursive: true });
2655
3068
  writeFileSync3(filePath, String(args.content ?? ""), "utf8");
3069
+ ctx.onFileModified?.(rawPath);
2656
3070
  return { output: `Successfully wrote to '${args.path}'` };
2657
3071
  } catch (err) {
2658
3072
  return {
@@ -3103,7 +3517,13 @@ function createFileSearchTools(engine = new FileSearchEngine) {
3103
3517
  `) };
3104
3518
  }
3105
3519
  };
3106
- return [grepSearchTool, findFilesTool];
3520
+ const findByNameTool = {
3521
+ name: "find_by_name",
3522
+ description: "Search for files and directories across the workspace matching a name or glob pattern. Alias for find_files matching Antigravity & Claude Code conventions.",
3523
+ parameters: findFilesTool.parameters,
3524
+ execute: findFilesTool.execute
3525
+ };
3526
+ return [grepSearchTool, findFilesTool, findByNameTool];
3107
3527
  }
3108
3528
 
3109
3529
  // src/code-mode/tools-proxy.ts
@@ -3140,6 +3560,12 @@ class CodeModeToolsProxy {
3140
3560
  if (result.isError) {
3141
3561
  throw new Error(`Tool '${tool.name}' failed: ${result.output}`);
3142
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
+ }
3143
3569
  return result.output;
3144
3570
  };
3145
3571
  }
@@ -3213,6 +3639,9 @@ class SandboxedWorkerHost {
3213
3639
  const fetch = undefined;
3214
3640
  const XMLHttpRequest = undefined;
3215
3641
  const WebSocket = undefined;
3642
+ const globalThis = Object.freeze(Object.create(null));
3643
+ const global = undefined;
3644
+ const window = undefined;
3216
3645
 
3217
3646
  return (async () => {
3218
3647
  ${cleanCode}
@@ -3622,6 +4051,7 @@ function createDefaultTools(options = {}) {
3622
4051
  router2.register(applyPatchTool);
3623
4052
  router2.register(shellTool);
3624
4053
  router2.register(readFileTool);
4054
+ router2.register(viewFileTool);
3625
4055
  router2.register(writeFileTool);
3626
4056
  router2.register(listDirTool);
3627
4057
  router2.register(requestUserInputTool);
@@ -4222,14 +4652,39 @@ function compactHistory(history, retainedRecentItems = 6) {
4222
4652
  if (history.length <= retainedRecentItems) {
4223
4653
  return [...history];
4224
4654
  }
4225
- const itemsToCompact = history.slice(0, history.length - retainedRecentItems);
4226
- 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);
4227
4680
  const summaryParts = ["### Summary of previous conversation context:"];
4228
4681
  for (const item of itemsToCompact) {
4229
4682
  if (item.type === "user_message") {
4230
4683
  summaryParts.push(`- User: ${item.content.slice(0, 200)}`);
4231
4684
  } else if (item.type === "function_call") {
4232
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)}`);
4233
4688
  } else if (item.type === "agent_message") {
4234
4689
  summaryParts.push(`- Assistant: ${item.content.slice(0, 200)}`);
4235
4690
  }
@@ -4268,7 +4723,741 @@ class TurnContext {
4268
4723
  this.abortController.abort(new Error(reason));
4269
4724
  }
4270
4725
  }
4726
+ // src/verification/verifier.ts
4727
+ import { spawn, spawnSync } from "child_process";
4728
+ import { existsSync as existsSync14, readFileSync as readFileSync9 } from "fs";
4729
+ import { join as join8 } from "path";
4271
4730
 
4731
+ // src/init/project-analyzer.ts
4732
+ import { existsSync as existsSync13, readFileSync as readFileSync8, readdirSync as readdirSync6 } from "fs";
4733
+ import { join as join7, basename } from "path";
4734
+
4735
+ class ProjectAnalyzer {
4736
+ cwd;
4737
+ constructor(cwd = process.cwd()) {
4738
+ this.cwd = cwd;
4739
+ }
4740
+ analyze() {
4741
+ const readmeInfo = this.extractReadmeMetadata();
4742
+ const projectName = readmeInfo.title || this.detectProjectName();
4743
+ const languages = this.detectLanguages();
4744
+ const packageManager = this.detectPackageManager();
4745
+ const frameworks = [];
4746
+ const infrastructure = [];
4747
+ const commands = {};
4748
+ const architectureNotes = [];
4749
+ const codeConventions = [];
4750
+ let description = readmeInfo.description;
4751
+ const pkgPath = join7(this.cwd, "package.json");
4752
+ if (existsSync13(pkgPath)) {
4753
+ try {
4754
+ const pkg = JSON.parse(readFileSync8(pkgPath, "utf8"));
4755
+ if (!description && pkg.description)
4756
+ description = pkg.description;
4757
+ const pm = packageManager || "npm";
4758
+ const runPrefix = pm === "bun" || pm === "yarn" || pm === "pnpm" ? `${pm} run` : "npm run";
4759
+ const testPrefix = pm === "bun" ? "bun test" : pm === "pnpm" ? "pnpm test" : pm === "yarn" ? "yarn test" : "npm test";
4760
+ if (pkg.scripts) {
4761
+ if (pkg.scripts.dev)
4762
+ commands.dev = `${runPrefix} dev`;
4763
+ else if (pkg.scripts.start)
4764
+ commands.dev = `${runPrefix} start`;
4765
+ if (pkg.scripts.build)
4766
+ commands.build = `${runPrefix} build`;
4767
+ if (pkg.scripts.test)
4768
+ commands.test = pkg.scripts.test === "bun test" ? "bun test" : testPrefix;
4769
+ if (pkg.scripts.typecheck)
4770
+ commands.typecheck = `${runPrefix} typecheck`;
4771
+ else if (pkg.scripts.check)
4772
+ commands.typecheck = `${runPrefix} check`;
4773
+ if (pkg.scripts.lint)
4774
+ commands.lint = `${runPrefix} lint`;
4775
+ if (pkg.scripts.format)
4776
+ commands.format = `${runPrefix} format`;
4777
+ }
4778
+ const allDeps = {
4779
+ ...pkg.dependencies || {},
4780
+ ...pkg.devDependencies || {}
4781
+ };
4782
+ if (allDeps.next)
4783
+ frameworks.push("Next.js");
4784
+ if (allDeps.react)
4785
+ frameworks.push("React");
4786
+ if (allDeps.vue)
4787
+ frameworks.push("Vue.js");
4788
+ if (allDeps.svelte || allDeps["@sveltejs/kit"])
4789
+ frameworks.push("Svelte");
4790
+ if (allDeps.astro)
4791
+ frameworks.push("Astro");
4792
+ if (allDeps.vite)
4793
+ frameworks.push("Vite");
4794
+ if (allDeps.express)
4795
+ frameworks.push("Express");
4796
+ if (allDeps.hono)
4797
+ frameworks.push("Hono");
4798
+ if (allDeps.fastify)
4799
+ frameworks.push("Fastify");
4800
+ if (allDeps["@nestjs/core"])
4801
+ frameworks.push("NestJS");
4802
+ if (allDeps.tailwindcss)
4803
+ frameworks.push("TailwindCSS");
4804
+ if (allDeps["lucide-react"] || allDeps.lucide)
4805
+ frameworks.push("Lucide Icons");
4806
+ if (allDeps.zustand)
4807
+ frameworks.push("Zustand");
4808
+ if (allDeps["@tanstack/react-query"])
4809
+ frameworks.push("TanStack Query");
4810
+ if (allDeps.oxlint)
4811
+ frameworks.push("Oxlint");
4812
+ if (allDeps.eslint)
4813
+ frameworks.push("ESLint");
4814
+ if (allDeps.vitest)
4815
+ frameworks.push("Vitest");
4816
+ if (allDeps.jest)
4817
+ frameworks.push("Jest");
4818
+ if (allDeps.playwright || allDeps["@playwright/test"])
4819
+ frameworks.push("Playwright");
4820
+ if (pkg.type === "module") {
4821
+ codeConventions.push("Use ES modules (`import/export`), not CommonJS (`require`).");
4822
+ }
4823
+ } catch {}
4824
+ }
4825
+ const tsconfigPath = join7(this.cwd, "tsconfig.json");
4826
+ if (existsSync13(tsconfigPath)) {
4827
+ try {
4828
+ const tsconfig = JSON.parse(readFileSync8(tsconfigPath, "utf8"));
4829
+ if (tsconfig.compilerOptions?.strict) {
4830
+ codeConventions.push("TypeScript strict mode enabled.");
4831
+ }
4832
+ if (!commands.typecheck) {
4833
+ commands.typecheck = "tsc --noEmit";
4834
+ }
4835
+ } catch {}
4836
+ }
4837
+ const cargoPath = join7(this.cwd, "Cargo.toml");
4838
+ if (existsSync13(cargoPath)) {
4839
+ try {
4840
+ commands.dev = commands.dev || "cargo run";
4841
+ commands.build = commands.build || "cargo build";
4842
+ commands.test = commands.test || "cargo test";
4843
+ commands.lint = commands.lint || "cargo clippy";
4844
+ frameworks.push("Rust Cargo");
4845
+ } catch {}
4846
+ }
4847
+ const goModPath = join7(this.cwd, "go.mod");
4848
+ if (existsSync13(goModPath)) {
4849
+ try {
4850
+ commands.dev = commands.dev || "go run .";
4851
+ commands.build = commands.build || "go build ./...";
4852
+ commands.test = commands.test || "go test ./...";
4853
+ commands.lint = commands.lint || "golangci-lint run";
4854
+ frameworks.push("Go Modules");
4855
+ } catch {}
4856
+ }
4857
+ const pyprojectPath = join7(this.cwd, "pyproject.toml");
4858
+ const requirementsPath = join7(this.cwd, "requirements.txt");
4859
+ if (existsSync13(pyprojectPath) || existsSync13(requirementsPath)) {
4860
+ commands.test = commands.test || "pytest";
4861
+ commands.lint = commands.lint || "ruff check .";
4862
+ if (existsSync13(join7(this.cwd, "uv.lock"))) {
4863
+ frameworks.push("uv");
4864
+ commands.test = "uv run pytest";
4865
+ } else if (existsSync13(join7(this.cwd, "poetry.lock"))) {
4866
+ frameworks.push("Poetry");
4867
+ commands.test = "poetry run pytest";
4868
+ }
4869
+ }
4870
+ if (existsSync13(join7(this.cwd, "Dockerfile"))) {
4871
+ infrastructure.push("Docker");
4872
+ const sanitizedName = projectName.toLowerCase().replace(/[^a-z0-9_-]/g, "-").replace(/^-+|-+$/g, "");
4873
+ commands.dockerBuild = `docker build -t ${sanitizedName || "app"} .`;
4874
+ }
4875
+ if (existsSync13(join7(this.cwd, "nginx.conf"))) {
4876
+ infrastructure.push("Nginx");
4877
+ }
4878
+ if (existsSync13(join7(this.cwd, "src/api.ts")) || existsSync13(join7(this.cwd, "src/api"))) {
4879
+ architectureNotes.push("Backend API endpoints and network client logic are centralized in `src/api`.");
4880
+ }
4881
+ if (existsSync13(join7(this.cwd, "src/components"))) {
4882
+ architectureNotes.push("Reusable UI presentation components live in `src/components/`.");
4883
+ }
4884
+ if (existsSync13(join7(this.cwd, "src/types.ts")) || existsSync13(join7(this.cwd, "src/types"))) {
4885
+ architectureNotes.push("Shared TypeScript data models and interfaces are defined in `src/types`.");
4886
+ }
4887
+ if (existsSync13(join7(this.cwd, ".env.example"))) {
4888
+ architectureNotes.push("Environment configuration template is in `.env.example`.");
4889
+ }
4890
+ if (commands.typecheck || commands.lint || commands.test) {
4891
+ const checks = [];
4892
+ if (commands.typecheck)
4893
+ checks.push(`typecheck (\`${commands.typecheck}\`)`);
4894
+ if (commands.lint)
4895
+ checks.push(`lint (\`${commands.lint}\`)`);
4896
+ if (commands.test)
4897
+ checks.push(`tests (\`${commands.test}\`)`);
4898
+ codeConventions.push(`Run ${checks.join(" and ")} before concluding any major code edits.`);
4899
+ }
4900
+ const instructionFiles = ["AGENTS.md", "CLAUDE.md", ".agents.md", "AGENTS.override.md"];
4901
+ let hasExistingInstructions = false;
4902
+ let existingInstructionFile;
4903
+ for (const f of instructionFiles) {
4904
+ if (existsSync13(join7(this.cwd, f))) {
4905
+ hasExistingInstructions = true;
4906
+ existingInstructionFile = f;
4907
+ break;
4908
+ }
4909
+ }
4910
+ return {
4911
+ projectName,
4912
+ description,
4913
+ languages,
4914
+ packageManager,
4915
+ frameworks,
4916
+ infrastructure,
4917
+ commands,
4918
+ architectureNotes,
4919
+ codeConventions,
4920
+ hasExistingInstructions,
4921
+ existingInstructionFile
4922
+ };
4923
+ }
4924
+ generateAgentsMarkdown(analysis) {
4925
+ const lines = [];
4926
+ lines.push(`# ${analysis.projectName}`);
4927
+ lines.push("");
4928
+ if (analysis.description) {
4929
+ lines.push(`> ${analysis.description}`);
4930
+ lines.push("");
4931
+ }
4932
+ lines.push("## Commands");
4933
+ lines.push("");
4934
+ if (Object.keys(analysis.commands).length > 0) {
4935
+ if (analysis.commands.dev)
4936
+ lines.push(`- **Dev Server**: \`${analysis.commands.dev}\``);
4937
+ if (analysis.commands.build)
4938
+ lines.push(`- **Build**: \`${analysis.commands.build}\``);
4939
+ if (analysis.commands.test)
4940
+ lines.push(`- **Test**: \`${analysis.commands.test}\``);
4941
+ if (analysis.commands.typecheck)
4942
+ lines.push(`- **Typecheck**: \`${analysis.commands.typecheck}\``);
4943
+ if (analysis.commands.lint)
4944
+ lines.push(`- **Lint**: \`${analysis.commands.lint}\``);
4945
+ if (analysis.commands.format)
4946
+ lines.push(`- **Format**: \`${analysis.commands.format}\``);
4947
+ if (analysis.commands.dockerBuild)
4948
+ lines.push(`- **Docker Build**: \`${analysis.commands.dockerBuild}\``);
4949
+ } else {
4950
+ lines.push("- *No standard build/test commands detected.*");
4951
+ }
4952
+ lines.push("");
4953
+ lines.push("## Architecture & Stack");
4954
+ lines.push("");
4955
+ const stackItems = [];
4956
+ if (analysis.languages.length > 0)
4957
+ stackItems.push(analysis.languages.join(", "));
4958
+ if (analysis.frameworks.length > 0)
4959
+ stackItems.push(analysis.frameworks.join(", "));
4960
+ if (analysis.infrastructure.length > 0)
4961
+ stackItems.push(analysis.infrastructure.join(", "));
4962
+ if (stackItems.length > 0) {
4963
+ lines.push(`- **Core Stack**: ${stackItems.join(" \u2022 ")}`);
4964
+ }
4965
+ for (const note of analysis.architectureNotes) {
4966
+ lines.push(`- ${note}`);
4967
+ }
4968
+ lines.push("");
4969
+ lines.push("## Workflow & Code Guidelines");
4970
+ lines.push("");
4971
+ if (analysis.codeConventions.length > 0) {
4972
+ for (const conv of analysis.codeConventions) {
4973
+ lines.push(`- ${conv}`);
4974
+ }
4975
+ }
4976
+ lines.push("- Prefer targeted edits over whole-file rewrites.");
4977
+ lines.push("- When fixing errors, address the root cause rather than suppressing compiler warnings.");
4978
+ lines.push("");
4979
+ return lines.join(`
4980
+ `);
4981
+ }
4982
+ extractReadmeMetadata() {
4983
+ const readmeFiles = ["README.md", "readme.md", "README.MD"];
4984
+ for (const file of readmeFiles) {
4985
+ const fullPath = join7(this.cwd, file);
4986
+ if (existsSync13(fullPath)) {
4987
+ try {
4988
+ const content = readFileSync8(fullPath, "utf8");
4989
+ const lines = content.split(`
4990
+ `);
4991
+ let title;
4992
+ let description;
4993
+ for (const line of lines) {
4994
+ const trimmed = line.trim();
4995
+ if (!title && trimmed.startsWith("# ")) {
4996
+ title = trimmed.replace(/^#\s+/, "").trim();
4997
+ continue;
4998
+ }
4999
+ if (title && !description && trimmed.length > 0 && !trimmed.startsWith("#") && !trimmed.startsWith("```") && !trimmed.startsWith("[")) {
5000
+ description = trimmed;
5001
+ break;
5002
+ }
5003
+ }
5004
+ return { title, description };
5005
+ } catch {}
5006
+ }
5007
+ }
5008
+ return {};
5009
+ }
5010
+ detectProjectName() {
5011
+ const pkgPath = join7(this.cwd, "package.json");
5012
+ if (existsSync13(pkgPath)) {
5013
+ try {
5014
+ const pkg = JSON.parse(readFileSync8(pkgPath, "utf8"));
5015
+ if (pkg.name && pkg.name !== "frontend" && pkg.name !== "backend" && pkg.name !== "app") {
5016
+ return pkg.name.startsWith("@") ? pkg.name.split("/")[1] || pkg.name : pkg.name;
5017
+ }
5018
+ } catch {}
5019
+ }
5020
+ const cargoPath = join7(this.cwd, "Cargo.toml");
5021
+ if (existsSync13(cargoPath)) {
5022
+ try {
5023
+ const match = readFileSync8(cargoPath, "utf8").match(/name\s*=\s*"([^"]+)"/);
5024
+ if (match?.[1])
5025
+ return match[1];
5026
+ } catch {}
5027
+ }
5028
+ const goModPath = join7(this.cwd, "go.mod");
5029
+ if (existsSync13(goModPath)) {
5030
+ try {
5031
+ const match = readFileSync8(goModPath, "utf8").match(/module\s+([^\s]+)/);
5032
+ if (match?.[1])
5033
+ return basename(match[1]);
5034
+ } catch {}
5035
+ }
5036
+ return basename(this.cwd);
5037
+ }
5038
+ detectLanguages() {
5039
+ const langs = new Set;
5040
+ if (existsSync13(join7(this.cwd, "tsconfig.json")) || this.hasFileWithExtension(".ts", ".tsx")) {
5041
+ langs.add("TypeScript");
5042
+ }
5043
+ if (existsSync13(join7(this.cwd, "package.json")) || this.hasFileWithExtension(".js", ".jsx", ".mjs")) {
5044
+ langs.add("JavaScript");
5045
+ }
5046
+ if (existsSync13(join7(this.cwd, "Cargo.toml")) || this.hasFileWithExtension(".rs")) {
5047
+ langs.add("Rust");
5048
+ }
5049
+ if (existsSync13(join7(this.cwd, "go.mod")) || this.hasFileWithExtension(".go")) {
5050
+ langs.add("Go");
5051
+ }
5052
+ if (existsSync13(join7(this.cwd, "pyproject.toml")) || existsSync13(join7(this.cwd, "requirements.txt")) || this.hasFileWithExtension(".py")) {
5053
+ langs.add("Python");
5054
+ }
5055
+ if (existsSync13(join7(this.cwd, "pom.xml")) || existsSync13(join7(this.cwd, "build.gradle")) || this.hasFileWithExtension(".java")) {
5056
+ langs.add("Java");
5057
+ }
5058
+ if (existsSync13(join7(this.cwd, "CMakeLists.txt")) || this.hasFileWithExtension(".cpp", ".c", ".h", ".hpp")) {
5059
+ langs.add("C/C++");
5060
+ }
5061
+ return Array.from(langs);
5062
+ }
5063
+ detectPackageManager() {
5064
+ if (existsSync13(join7(this.cwd, "bun.lockb")) || existsSync13(join7(this.cwd, "bun.lock")))
5065
+ return "bun";
5066
+ if (existsSync13(join7(this.cwd, "pnpm-lock.yaml")))
5067
+ return "pnpm";
5068
+ if (existsSync13(join7(this.cwd, "yarn.lock")))
5069
+ return "yarn";
5070
+ if (existsSync13(join7(this.cwd, "package-lock.json")))
5071
+ return "npm";
5072
+ if (existsSync13(join7(this.cwd, "Cargo.lock")) || existsSync13(join7(this.cwd, "Cargo.toml")))
5073
+ return "cargo";
5074
+ if (existsSync13(join7(this.cwd, "uv.lock")))
5075
+ return "uv";
5076
+ if (existsSync13(join7(this.cwd, "poetry.lock")))
5077
+ return "poetry";
5078
+ if (existsSync13(join7(this.cwd, "go.sum")) || existsSync13(join7(this.cwd, "go.mod")))
5079
+ return "go";
5080
+ if (existsSync13(join7(this.cwd, "package.json")))
5081
+ return "npm";
5082
+ return;
5083
+ }
5084
+ hasFileWithExtension(...exts) {
5085
+ try {
5086
+ const entries = readdirSync6(this.cwd);
5087
+ return entries.some((e) => exts.some((ext) => e.endsWith(ext)));
5088
+ } catch {
5089
+ return false;
5090
+ }
5091
+ }
5092
+ }
5093
+
5094
+ // src/verification/verifier.ts
5095
+ class AutoVerifier {
5096
+ cwd;
5097
+ customCommand;
5098
+ timeoutMs;
5099
+ constructor(options) {
5100
+ this.cwd = options.cwd;
5101
+ this.customCommand = options.customCommand;
5102
+ this.timeoutMs = options.timeoutMs ?? 30000;
5103
+ }
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}`;
5187
+ }
5188
+ if (existsSync14(join8(this.cwd, "Cargo.toml"))) {
5189
+ return `cargo test ${quotedFiles}`;
5190
+ }
5191
+ return null;
5192
+ }
5193
+ resolveStaticCommand() {
5194
+ try {
5195
+ const analyzer = new ProjectAnalyzer(this.cwd);
5196
+ const analysis = analyzer.analyze();
5197
+ if (analysis.commands.typecheck) {
5198
+ return analysis.commands.typecheck;
5199
+ }
5200
+ } catch (err) {
5201
+ console.warn(`[AutoVerifier] ProjectAnalyzer analysis error:`, err);
5202
+ }
5203
+ const pkgPath = join8(this.cwd, "package.json");
5204
+ if (existsSync14(pkgPath)) {
5205
+ try {
5206
+ const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
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";
5210
+ if (pkg.scripts.typecheck)
5211
+ return `${runCmd} typecheck`;
5212
+ if (pkg.scripts.check)
5213
+ return `${runCmd} check`;
5214
+ }
5215
+ } catch (err) {
5216
+ console.warn(`[AutoVerifier] Failed to parse package.json for static check:`, err);
5217
+ }
5218
+ }
5219
+ if (existsSync14(join8(this.cwd, "tsconfig.json"))) {
5220
+ return "npx tsc --noEmit";
5221
+ }
5222
+ if (existsSync14(join8(this.cwd, "Cargo.toml"))) {
5223
+ return "cargo check";
5224
+ }
5225
+ if (existsSync14(join8(this.cwd, "go.mod"))) {
5226
+ return "go vet ./...";
5227
+ }
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 {}
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);
5296
+ const startTime = performance.now();
5297
+ if (stages.length === 0) {
5298
+ return {
5299
+ command: "none",
5300
+ success: true,
5301
+ exitCode: 0,
5302
+ output: "No automated verification command configured or detected for this workspace.",
5303
+ durationMs: 0,
5304
+ reason: "NO_VERIFIER_DETECTED"
5305
+ };
5306
+ }
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);
5398
+ }
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(`
5445
+ `);
5446
+ if (lines.length > 70) {
5447
+ const head = lines.slice(0, 35).join(`
5448
+ `);
5449
+ const tail = lines.slice(-30).join(`
5450
+ `);
5451
+ return `${head}
5452
+
5453
+ ... [${lines.length - 65} lines truncated for context efficiency] ...
5454
+
5455
+ ${tail}`;
5456
+ }
5457
+ }
5458
+ return output;
5459
+ }
5460
+ }
4272
5461
  // src/session/turn.ts
4273
5462
  async function runTurn(session, turnContext, input) {
4274
5463
  const { turnId, signal } = turnContext;
@@ -4319,6 +5508,9 @@ async function runTurn(session, turnContext, input) {
4319
5508
  let accumulatedOutputTokens = 0;
4320
5509
  let accumulatedCachedTokens = 0;
4321
5510
  const clientSession = session.modelClient.newSession();
5511
+ const modifiedFiles = new Set;
5512
+ let selfHealingAttempts = 0;
5513
+ let hasRunVerification = false;
4322
5514
  try {
4323
5515
  while (iteration < turnContext.maxIterations) {
4324
5516
  if (signal.aborted) {
@@ -4387,6 +5579,7 @@ async function runTurn(session, turnContext, input) {
4387
5579
  });
4388
5580
  }
4389
5581
  if (toolCallRequests.length > 0) {
5582
+ const validCalls = [];
4390
5583
  for (const toolCall of toolCallRequests) {
4391
5584
  if (!toolCall.name || !toolCall.name.trim())
4392
5585
  continue;
@@ -4410,6 +5603,14 @@ async function runTurn(session, turnContext, input) {
4410
5603
  toolName: toolCall.name,
4411
5604
  arguments: toolCall.arguments
4412
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) {
4413
5614
  const toolResult = await turnContext.tools.execute(toolCall.name, toolCall.arguments, {
4414
5615
  cwd: turnContext.environment.cwd,
4415
5616
  turnId,
@@ -4417,6 +5618,12 @@ async function runTurn(session, turnContext, input) {
4417
5618
  execPolicy: session.execPolicy,
4418
5619
  mode: session.collaborationMode,
4419
5620
  permissionMode: session.permissionMode,
5621
+ onFileModified: (p) => {
5622
+ if (p) {
5623
+ modifiedFiles.add(p);
5624
+ hasRunVerification = false;
5625
+ }
5626
+ },
4420
5627
  onPlanUpdate: (plan, explanation) => {
4421
5628
  session.emitEvent({
4422
5629
  type: "PlanUpdated",
@@ -4432,7 +5639,8 @@ async function runTurn(session, turnContext, input) {
4432
5639
  turnId,
4433
5640
  toolName: toolCall.name,
4434
5641
  description,
4435
- command
5642
+ command,
5643
+ prefixRule
4436
5644
  });
4437
5645
  },
4438
5646
  requestInput: async (question, options) => {
@@ -4453,6 +5661,15 @@ async function runTurn(session, turnContext, input) {
4453
5661
  isError: toolResult.isError,
4454
5662
  createdAt: Date.now()
4455
5663
  };
5664
+ if (!toolResult.isError) {
5665
+ if (toolCall.name === "apply_patch" || toolCall.name === "write_file") {
5666
+ const p = String(toolCall.arguments?.path || "");
5667
+ if (p) {
5668
+ modifiedFiles.add(p);
5669
+ hasRunVerification = false;
5670
+ }
5671
+ }
5672
+ }
4456
5673
  session.addHistoryItem(functionOutputItem);
4457
5674
  session.emitEvent({
4458
5675
  type: "ToolCallFinished",
@@ -4469,6 +5686,70 @@ async function runTurn(session, turnContext, input) {
4469
5686
  }
4470
5687
  continue;
4471
5688
  }
5689
+ if (session.autoVerification && modifiedFiles.size > 0 && !hasRunVerification && iteration < turnContext.maxIterations) {
5690
+ const verifier = new AutoVerifier({
5691
+ cwd,
5692
+ customCommand: session.autoVerificationCommand
5693
+ });
5694
+ const command = verifier.resolveVerificationCommand(Array.from(modifiedFiles));
5695
+ if (command) {
5696
+ session.emitEvent({
5697
+ type: "VerificationStarted",
5698
+ turnId,
5699
+ command,
5700
+ modifiedFiles: Array.from(modifiedFiles)
5701
+ });
5702
+ const vResult = await verifier.verify(Array.from(modifiedFiles), signal);
5703
+ session.emitEvent({
5704
+ type: "VerificationCompleted",
5705
+ turnId,
5706
+ command: vResult.command,
5707
+ success: vResult.success,
5708
+ output: vResult.output,
5709
+ durationMs: vResult.durationMs
5710
+ });
5711
+ if (!vResult.success) {
5712
+ if (selfHealingAttempts < session.maxSelfHealingAttempts) {
5713
+ selfHealingAttempts++;
5714
+ session.emitEvent({
5715
+ type: "SelfHealingStarted",
5716
+ turnId,
5717
+ attempt: selfHealingAttempts,
5718
+ maxAttempts: session.maxSelfHealingAttempts,
5719
+ command: vResult.command,
5720
+ error: vResult.output
5721
+ });
5722
+ const feedbackMsg = `[Automated Self-Verification Failure]
5723
+ Verification command '${vResult.command}' failed with exit code ${vResult.exitCode}.
5724
+
5725
+ Error trace / compiler output:
5726
+ ${vResult.output}
5727
+
5728
+ Modified file(s) in this turn: ${Array.from(modifiedFiles).join(", ")}
5729
+
5730
+ Self-Healing Directive (Attempt ${selfHealingAttempts} of ${session.maxSelfHealingAttempts}):
5731
+ 1. Review the error trace above carefully and locate the exact root cause.
5732
+ 2. Formulate and apply the necessary surgical fix using 'apply_patch' or 'write_file'.
5733
+ 3. Do NOT conclude the turn or report to the user until this error is resolved and verification passes cleanly.`;
5734
+ session.addHistoryItem({
5735
+ id: `msg_heal_${Date.now()}`,
5736
+ type: "user_message",
5737
+ content: feedbackMsg,
5738
+ createdAt: Date.now()
5739
+ });
5740
+ continue;
5741
+ } else {
5742
+ session.emitEvent({
5743
+ type: "Warning",
5744
+ message: `Auto-verification failed after ${selfHealingAttempts} self-healing attempts for command: ${vResult.command}`
5745
+ });
5746
+ hasRunVerification = true;
5747
+ }
5748
+ } else {
5749
+ hasRunVerification = true;
5750
+ }
5751
+ }
5752
+ }
4472
5753
  if (!currentAgentText.trim() && toolCallRequests.length === 0) {
4473
5754
  if (iteration === 1 && iteration < turnContext.maxIterations) {
4474
5755
  session.addHistoryItem({
@@ -4594,6 +5875,9 @@ class Session {
4594
5875
  mcpManager;
4595
5876
  execPolicy;
4596
5877
  collaborationMode = "default";
5878
+ autoVerification;
5879
+ autoVerificationCommand;
5880
+ maxSelfHealingAttempts;
4597
5881
  get permissionMode() {
4598
5882
  return this.execPolicy.getMode();
4599
5883
  }
@@ -4626,6 +5910,9 @@ class Session {
4626
5910
  this.mcpManager = options.mcpManager;
4627
5911
  this.execPolicy = options.execPolicy || new ExecPolicy;
4628
5912
  this.collaborationMode = options.collaborationMode || "default";
5913
+ this.autoVerification = options.autoVerification ?? (process.env.PIKAA_AUTO_VERIFY !== "0" && process.env.PIKAA_AUTO_VERIFY !== "false");
5914
+ this.autoVerificationCommand = options.autoVerificationCommand;
5915
+ this.maxSelfHealingAttempts = options.maxSelfHealingAttempts ?? 3;
4629
5916
  this.history = options.initialHistory ? [...options.initialHistory] : [];
4630
5917
  if (options.onEvent) {
4631
5918
  this.eventListeners.push(options.onEvent);
@@ -4705,7 +5992,8 @@ class Session {
4705
5992
  turnId: params.turnId,
4706
5993
  toolName: params.toolName,
4707
5994
  description: params.description,
4708
- command: params.command
5995
+ command: params.command,
5996
+ prefixRule: params.prefixRule
4709
5997
  });
4710
5998
  this.emitEvent({
4711
5999
  type: "StatusChanged",
@@ -4873,7 +6161,7 @@ class ThreadManager {
4873
6161
  }
4874
6162
  }
4875
6163
  // src/mcp/process-killer.ts
4876
- import { spawnSync } from "child_process";
6164
+ import { spawnSync as spawnSync2 } from "child_process";
4877
6165
 
4878
6166
  class GlobalProcessRegistry {
4879
6167
  static trackedProcesses = new Map;
@@ -4883,7 +6171,7 @@ class GlobalProcessRegistry {
4883
6171
  return;
4884
6172
  if (process.platform === "win32") {
4885
6173
  try {
4886
- spawnSync("taskkill", ["/pid", String(pid), "/T", "/F"], {
6174
+ spawnSync2("taskkill", ["/pid", String(pid), "/T", "/F"], {
4887
6175
  stdio: "ignore",
4888
6176
  windowsHide: true
4889
6177
  });
@@ -5450,8 +6738,8 @@ class McpClient {
5450
6738
  }
5451
6739
  }
5452
6740
  // src/mcp/manager.ts
5453
- import { existsSync as existsSync13, readFileSync as readFileSync8, writeFileSync as writeFileSync4, mkdirSync as mkdirSync7 } from "fs";
5454
- import { resolve as resolve12, dirname as dirname6, join as join7 } from "path";
6741
+ import { existsSync as existsSync15, readFileSync as readFileSync10, writeFileSync as writeFileSync4, mkdirSync as mkdirSync7 } from "fs";
6742
+ import { resolve as resolve12, dirname as dirname6, join as join9 } from "path";
5455
6743
  class McpManager {
5456
6744
  clients = new Map;
5457
6745
  serverConfigs = new Map;
@@ -5486,11 +6774,11 @@ class McpManager {
5486
6774
  }
5487
6775
  async loadConfigFile(filePath) {
5488
6776
  const fullPath = resolve12(filePath);
5489
- if (!existsSync13(fullPath))
6777
+ if (!existsSync15(fullPath))
5490
6778
  return;
5491
6779
  this.loadedConfigFiles.add(fullPath);
5492
6780
  try {
5493
- const content = readFileSync8(fullPath, "utf8");
6781
+ const content = readFileSync10(fullPath, "utf8");
5494
6782
  const parsed = JSON.parse(content);
5495
6783
  if (parsed.mcpServers) {
5496
6784
  await this.loadConfig(parsed);
@@ -5741,13 +7029,13 @@ class McpManager {
5741
7029
  saveServerToConfigFile(filePath, name, config) {
5742
7030
  const fullPath = resolve12(filePath);
5743
7031
  const dir = dirname6(fullPath);
5744
- if (!existsSync13(dir)) {
7032
+ if (!existsSync15(dir)) {
5745
7033
  mkdirSync7(dir, { recursive: true });
5746
7034
  }
5747
7035
  let existing = { mcpServers: {} };
5748
- if (existsSync13(fullPath)) {
7036
+ if (existsSync15(fullPath)) {
5749
7037
  try {
5750
- const content = readFileSync8(fullPath, "utf8");
7038
+ const content = readFileSync10(fullPath, "utf8");
5751
7039
  existing = JSON.parse(content);
5752
7040
  if (!existing.mcpServers)
5753
7041
  existing.mcpServers = {};
@@ -5760,10 +7048,10 @@ class McpManager {
5760
7048
  }
5761
7049
  removeServerFromConfigFile(filePath, name) {
5762
7050
  const fullPath = resolve12(filePath);
5763
- if (!existsSync13(fullPath))
7051
+ if (!existsSync15(fullPath))
5764
7052
  return false;
5765
7053
  try {
5766
- const content = readFileSync8(fullPath, "utf8");
7054
+ const content = readFileSync10(fullPath, "utf8");
5767
7055
  const existing = JSON.parse(content);
5768
7056
  if (existing.mcpServers && existing.mcpServers[name]) {
5769
7057
  delete existing.mcpServers[name];
@@ -5790,11 +7078,11 @@ class McpManager {
5790
7078
  }
5791
7079
  }
5792
7080
  getDefaultConfigFile(cwd = process.cwd()) {
5793
- const workspaceConfig = join7(cwd, ".mcp.json");
5794
- if (existsSync13(workspaceConfig))
7081
+ const workspaceConfig = join9(cwd, ".mcp.json");
7082
+ if (existsSync15(workspaceConfig))
5795
7083
  return workspaceConfig;
5796
- const altConfig = join7(cwd, "mcp_config.json");
5797
- if (existsSync13(altConfig))
7084
+ const altConfig = join9(cwd, "mcp_config.json");
7085
+ if (existsSync15(altConfig))
5798
7086
  return altConfig;
5799
7087
  return workspaceConfig;
5800
7088
  }
@@ -5816,8 +7104,8 @@ class McpManager {
5816
7104
  import { resolve as resolve14 } from "path";
5817
7105
 
5818
7106
  // src/mcp/servers/chrome-devtools/launcher.ts
5819
- import { existsSync as existsSync14, mkdirSync as mkdirSync8, rmSync as rmSync2 } from "fs";
5820
- import { join as join8 } from "path";
7107
+ import { existsSync as existsSync16, mkdirSync as mkdirSync8, rmSync as rmSync2 } from "fs";
7108
+ import { join as join10 } from "path";
5821
7109
  import { tmpdir as tmpdir2 } from "os";
5822
7110
  class BrowserLauncher {
5823
7111
  proc = null;
@@ -5825,21 +7113,21 @@ class BrowserLauncher {
5825
7113
  wsDebuggerUrl = null;
5826
7114
  port = 0;
5827
7115
  static findBrowserExecutable() {
5828
- if (process.env.CHROME_PATH && existsSync14(process.env.CHROME_PATH)) {
7116
+ if (process.env.CHROME_PATH && existsSync16(process.env.CHROME_PATH)) {
5829
7117
  return process.env.CHROME_PATH;
5830
7118
  }
5831
7119
  if (process.platform === "win32") {
5832
7120
  const candidates = [
5833
7121
  "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
5834
7122
  "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
5835
- join8(process.env.LOCALAPPDATA || "", "Google\\Chrome\\Application\\chrome.exe"),
7123
+ join10(process.env.LOCALAPPDATA || "", "Google\\Chrome\\Application\\chrome.exe"),
5836
7124
  "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe",
5837
7125
  "C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
5838
7126
  "C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe",
5839
- join8(process.env.LOCALAPPDATA || "", "BraveSoftware\\Brave-Browser\\Application\\brave.exe")
7127
+ join10(process.env.LOCALAPPDATA || "", "BraveSoftware\\Brave-Browser\\Application\\brave.exe")
5840
7128
  ];
5841
7129
  for (const path of candidates) {
5842
- if (path && existsSync14(path))
7130
+ if (path && existsSync16(path))
5843
7131
  return path;
5844
7132
  }
5845
7133
  } else if (process.platform === "darwin") {
@@ -5850,7 +7138,7 @@ class BrowserLauncher {
5850
7138
  "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
5851
7139
  ];
5852
7140
  for (const path of candidates) {
5853
- if (existsSync14(path))
7141
+ if (existsSync16(path))
5854
7142
  return path;
5855
7143
  }
5856
7144
  } else {
@@ -5863,7 +7151,7 @@ class BrowserLauncher {
5863
7151
  "/usr/bin/microsoft-edge"
5864
7152
  ];
5865
7153
  for (const path of candidates) {
5866
- if (existsSync14(path))
7154
+ if (existsSync16(path))
5867
7155
  return path;
5868
7156
  }
5869
7157
  }
@@ -5875,7 +7163,7 @@ class BrowserLauncher {
5875
7163
  throw new Error("No supported browser (Google Chrome, Chromium, MS Edge, Brave) found on this machine. Please install Chrome or specify CHROME_PATH.");
5876
7164
  }
5877
7165
  this.port = options.port || 9200 + Math.floor(Math.random() * 500);
5878
- this.tempUserDataDir = options.userDataDir || join8(tmpdir2(), `groupy_chrome_${Date.now()}_${Math.random().toString(36).slice(2)}`);
7166
+ this.tempUserDataDir = options.userDataDir || join10(tmpdir2(), `groupy_chrome_${Date.now()}_${Math.random().toString(36).slice(2)}`);
5879
7167
  mkdirSync8(this.tempUserDataDir, { recursive: true });
5880
7168
  const isHeadless = options.headless ?? true;
5881
7169
  const launchArgs = [
@@ -5953,7 +7241,7 @@ class BrowserLauncher {
5953
7241
  }
5954
7242
  this.proc = null;
5955
7243
  }
5956
- if (this.tempUserDataDir && existsSync14(this.tempUserDataDir)) {
7244
+ if (this.tempUserDataDir && existsSync16(this.tempUserDataDir)) {
5957
7245
  try {
5958
7246
  rmSync2(this.tempUserDataDir, { recursive: true, force: true });
5959
7247
  } catch {}
@@ -7793,7 +9081,7 @@ import { resolve as resolve17 } from "path";
7793
9081
  // src/mcp/servers/sqlite/db-engine.ts
7794
9082
  import { Database as Database2 } from "bun:sqlite";
7795
9083
  import { resolve as resolve16, isAbsolute } from "path";
7796
- import { readdirSync as readdirSync6 } from "fs";
9084
+ import { readdirSync as readdirSync7 } from "fs";
7797
9085
 
7798
9086
  class SqliteEngine {
7799
9087
  connections = new Map;
@@ -7823,7 +9111,7 @@ class SqliteEngine {
7823
9111
  }
7824
9112
  autoDiscoverDatabase() {
7825
9113
  try {
7826
- const files = readdirSync6(process.cwd());
9114
+ const files = readdirSync7(process.cwd());
7827
9115
  const dbFile = files.find((f) => f.endsWith(".sqlite") || f.endsWith(".sqlite3") || f.endsWith(".db"));
7828
9116
  return dbFile ? resolve16(process.cwd(), dbFile) : null;
7829
9117
  } catch {
@@ -8327,8 +9615,8 @@ function verifyTaskAction(assertion, payload) {
8327
9615
  }
8328
9616
  }
8329
9617
  // src/agents/roles.ts
8330
- import { existsSync as existsSync16, readdirSync as readdirSync7, readFileSync as readFileSync9 } from "fs";
8331
- import { resolve as resolve18, join as join9 } from "path";
9618
+ import { existsSync as existsSync18, readdirSync as readdirSync8, readFileSync as readFileSync11 } from "fs";
9619
+ import { resolve as resolve18, join as join11 } from "path";
8332
9620
 
8333
9621
  class AgentRoleRegistry {
8334
9622
  roles = new Map;
@@ -8413,13 +9701,13 @@ class AgentRoleRegistry {
8413
9701
  }
8414
9702
  loadRolesFromDir(dirPath) {
8415
9703
  const fullPath = resolve18(dirPath);
8416
- if (!existsSync16(fullPath))
9704
+ if (!existsSync18(fullPath))
8417
9705
  return;
8418
- const entries = readdirSync7(fullPath);
9706
+ const entries = readdirSync8(fullPath);
8419
9707
  for (const entry of entries) {
8420
9708
  if (entry.endsWith(".json")) {
8421
9709
  try {
8422
- const content = readFileSync9(join9(fullPath, entry), "utf8");
9710
+ const content = readFileSync11(join11(fullPath, entry), "utf8");
8423
9711
  const parsed = JSON.parse(content);
8424
9712
  if (parsed.name && parsed.systemPrompt) {
8425
9713
  this.registerRole(parsed);
@@ -8449,7 +9737,7 @@ class AgentRoleRegistry {
8449
9737
  // src/agents/graph-store.ts
8450
9738
  import { Database as Database3 } from "bun:sqlite";
8451
9739
  import { resolve as resolve19 } from "path";
8452
- import { existsSync as existsSync17, mkdirSync as mkdirSync10 } from "fs";
9740
+ import { existsSync as existsSync19, mkdirSync as mkdirSync10 } from "fs";
8453
9741
  class AgentGraphStore {
8454
9742
  db;
8455
9743
  constructor(dbPathOrDb) {
@@ -8459,8 +9747,12 @@ class AgentGraphStore {
8459
9747
  const dbPath = dbPathOrDb || getAgentGraphDbPath();
8460
9748
  if (dbPath !== ":memory:") {
8461
9749
  const dir = resolve19(dbPath, "..");
8462
- if (!existsSync17(dir)) {
8463
- mkdirSync10(dir, { recursive: true });
9750
+ if (!existsSync19(dir)) {
9751
+ try {
9752
+ mkdirSync10(dir, { recursive: true });
9753
+ } catch (err) {
9754
+ console.warn(`[AgentGraphStore] Failed to create database directory '${dir}':`, err);
9755
+ }
8464
9756
  }
8465
9757
  }
8466
9758
  this.db = new Database3(dbPath);
@@ -8542,24 +9834,44 @@ class AgentGraphStore {
8542
9834
  close() {
8543
9835
  try {
8544
9836
  this.db.close();
8545
- } catch {}
9837
+ } catch (err) {
9838
+ console.warn("[AgentGraphStore] Failed to close database cleanly:", err);
9839
+ }
8546
9840
  }
8547
9841
  }
8548
9842
  // src/agents/spawner.ts
8549
9843
  class AgentSpawner {
8550
9844
  parentSession;
9845
+ depth;
8551
9846
  subAgents = new Map;
8552
9847
  nextAgentId = 1;
8553
9848
  roleRegistry;
8554
9849
  parentIdentity;
8555
9850
  graphStore;
8556
- constructor(parentSession, roleRegistry, parentIdentity, graphStore) {
9851
+ maxConcurrentAgents;
9852
+ maxDepth;
9853
+ maxRetainedCompleted;
9854
+ defaultTokenBudget;
9855
+ constructor(parentSession, roleRegistry, parentIdentity, graphStore, options, depth = 0) {
8557
9856
  this.parentSession = parentSession;
9857
+ this.depth = depth;
8558
9858
  this.roleRegistry = roleRegistry || new AgentRoleRegistry;
8559
9859
  this.parentIdentity = parentIdentity || createAgentIdentity(undefined, "groupy-main");
8560
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;
8561
9866
  }
8562
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
+ }
8563
9875
  const roleName = params.role || "default";
8564
9876
  const roleConfig = this.roleRegistry.getRole(roleName);
8565
9877
  const agentIndex = this.nextAgentId++;
@@ -8571,7 +9883,18 @@ class AgentSpawner {
8571
9883
 
8572
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.`);
8573
9885
  const baseTools = params.tools || this.parentSession.tools;
8574
- 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;
8575
9898
  const childSession = new Session({
8576
9899
  threadId: agentId,
8577
9900
  model: effectiveModel,
@@ -8595,45 +9918,87 @@ Your nickname is ${nickname}. Your assigned task is: '${params.taskName}'. Focus
8595
9918
  createdAt: Date.now(),
8596
9919
  identity: childIdentity,
8597
9920
  session: childSession,
8598
- promise: taskPromise
9921
+ promise: taskPromise,
9922
+ depth: this.depth + 1,
9923
+ tokenBudget,
9924
+ totalTokens: 0
8599
9925
  };
8600
9926
  try {
8601
9927
  this.graphStore.upsertEdge(this.parentSession.threadId, agentId, "open");
8602
- } catch {}
9928
+ } catch (err) {
9929
+ console.warn(`[AgentSpawner] Failed to record edge in graph store for agent '${agentId}':`, err);
9930
+ }
8603
9931
  let collectedAgentText = "";
8604
9932
  childSession.onEvent((event) => {
8605
9933
  if (event.msg.type === "AgentMessageDelta") {
8606
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
+ }
8607
9948
  } else if (event.msg.type === "TurnCompleted") {
8608
- handle.status = "completed";
8609
- handle.lastOutput = collectedAgentText.trim();
8610
- try {
8611
- this.graphStore.setEdgeStatus(agentId, "closed");
8612
- } catch {}
8613
- 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
+ }
8614
9961
  } else if (event.msg.type === "Error") {
8615
- handle.status = "error";
8616
- handle.error = event.msg.message;
8617
- try {
8618
- this.graphStore.setEdgeStatus(agentId, "closed");
8619
- } catch {}
8620
- 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
+ }
8621
9974
  } else if (event.msg.type === "StatusChanged" && event.msg.status === "interrupted") {
8622
- handle.status = "interrupted";
8623
- try {
8624
- this.graphStore.setEdgeStatus(agentId, "closed");
8625
- } catch {}
8626
- 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
+ }
8627
9986
  }
8628
9987
  });
8629
9988
  this.subAgents.set(agentId, handle);
8630
9989
  childSession.prompt(params.message).catch((err) => {
8631
- handle.status = "error";
8632
- handle.error = err instanceof Error ? err.message : String(err);
8633
- try {
8634
- this.graphStore.setEdgeStatus(agentId, "closed");
8635
- } catch {}
8636
- 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
+ }
8637
10002
  });
8638
10003
  return {
8639
10004
  id: handle.id,
@@ -8642,9 +10007,22 @@ Your nickname is ${nickname}. Your assigned task is: '${params.taskName}'. Focus
8642
10007
  role: handle.role,
8643
10008
  status: handle.status,
8644
10009
  createdAt: handle.createdAt,
8645
- agentRuntimeId: childIdentity.agentRuntimeId
10010
+ agentRuntimeId: childIdentity.agentRuntimeId,
10011
+ depth: handle.depth,
10012
+ tokenBudget: handle.tokenBudget,
10013
+ totalTokens: handle.totalTokens
8646
10014
  };
8647
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
+ }
8648
10026
  async waitAgent(agentIdOrTaskName, timeoutMs = 60000) {
8649
10027
  if (!agentIdOrTaskName) {
8650
10028
  const handles = Array.from(this.subAgents.values());
@@ -8698,9 +10076,36 @@ Your nickname is ${nickname}. Your assigned task is: '${params.taskName}'. Focus
8698
10076
  handle.status = "interrupted";
8699
10077
  try {
8700
10078
  this.graphStore.setEdgeStatus(agentId, "closed");
8701
- } catch {}
10079
+ } catch (err) {
10080
+ console.warn(`[AgentSpawner] Failed to close edge for agent '${agentId}':`, err);
10081
+ }
10082
+ this.pruneCompletedAgents();
8702
10083
  return `Sub-agent ${handle.nickname} (${agentId}) interrupted and closed.`;
8703
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
+ }
8704
10109
  listAgents() {
8705
10110
  const list = [];
8706
10111
  for (const handle of this.subAgents.values()) {
@@ -8712,7 +10117,10 @@ Your nickname is ${nickname}. Your assigned task is: '${params.taskName}'. Focus
8712
10117
  status: handle.status,
8713
10118
  createdAt: handle.createdAt,
8714
10119
  agentRuntimeId: handle.identity.agentRuntimeId,
8715
- lastOutput: handle.lastOutput
10120
+ lastOutput: handle.lastOutput,
10121
+ depth: handle.depth,
10122
+ tokenBudget: handle.tokenBudget,
10123
+ totalTokens: handle.totalTokens
8716
10124
  });
8717
10125
  }
8718
10126
  return list;
@@ -8745,6 +10153,10 @@ function createMultiAgentTools(spawner) {
8745
10153
  model: {
8746
10154
  type: "string",
8747
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)."
8748
10160
  }
8749
10161
  },
8750
10162
  required: ["task_name", "message"]
@@ -8754,8 +10166,9 @@ function createMultiAgentTools(spawner) {
8754
10166
  const message = String(args.message || "");
8755
10167
  const role = args.role ? String(args.role) : undefined;
8756
10168
  const model = args.model ? String(args.model) : undefined;
10169
+ const maxTokens = typeof args.max_tokens === "number" ? args.max_tokens : undefined;
8757
10170
  try {
8758
- const handle = await spawner.spawnAgent({ taskName, message, role, model });
10171
+ const handle = await spawner.spawnAgent({ taskName, message, role, model, maxTokens });
8759
10172
  return {
8760
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.`
8761
10174
  };
@@ -8882,7 +10295,7 @@ function registerMultiAgentTools(router, spawner) {
8882
10295
  }
8883
10296
  // src/storage/sqlite-store.ts
8884
10297
  import { Database as Database4 } from "bun:sqlite";
8885
- import { existsSync as existsSync18, mkdirSync as mkdirSync11 } from "fs";
10298
+ import { existsSync as existsSync20, mkdirSync as mkdirSync11 } from "fs";
8886
10299
  import { dirname as dirname8 } from "path";
8887
10300
  class SqliteThreadStore {
8888
10301
  db;
@@ -8890,7 +10303,7 @@ class SqliteThreadStore {
8890
10303
  const effectivePath = dbPath || this.getDefaultDbPath();
8891
10304
  if (effectivePath !== ":memory:") {
8892
10305
  const dir = dirname8(effectivePath);
8893
- if (!existsSync18(dir)) {
10306
+ if (!existsSync20(dir)) {
8894
10307
  mkdirSync11(dir, { recursive: true });
8895
10308
  }
8896
10309
  }
@@ -9102,7 +10515,8 @@ class SessionPersistenceManager {
9102
10515
  loadSession(threadId) {
9103
10516
  try {
9104
10517
  return this.store.restoreSession(threadId);
9105
- } catch {
10518
+ } catch (err) {
10519
+ console.warn(`[SessionPersistenceManager] Failed to restore session '${threadId}':`, err);
9106
10520
  return null;
9107
10521
  }
9108
10522
  }
@@ -9110,7 +10524,9 @@ class SessionPersistenceManager {
9110
10524
  for (const unsub of this.unsubscribers) {
9111
10525
  try {
9112
10526
  unsub();
9113
- } catch {}
10527
+ } catch (err) {
10528
+ console.warn("[SessionPersistenceManager] Error in unbindSession callback:", err);
10529
+ }
9114
10530
  }
9115
10531
  this.unsubscribers = [];
9116
10532
  }
@@ -9137,15 +10553,17 @@ class SessionPersistenceManager {
9137
10553
  for (const unsub of this.unsubscribers) {
9138
10554
  try {
9139
10555
  unsub();
9140
- } catch {}
10556
+ } catch (err) {
10557
+ console.warn("[SessionPersistenceManager] Error closing session persistence listener:", err);
10558
+ }
9141
10559
  }
9142
10560
  this.unsubscribers = [];
9143
10561
  this.store.close();
9144
10562
  }
9145
10563
  }
9146
10564
  // src/skills/loader.ts
9147
- import { existsSync as existsSync19, readdirSync as readdirSync8, readFileSync as readFileSync10 } from "fs";
9148
- import { resolve as resolve20, join as join10 } from "path";
10565
+ import { existsSync as existsSync21, readdirSync as readdirSync9, readFileSync as readFileSync12 } from "fs";
10566
+ import { resolve as resolve20, join as join12 } from "path";
9149
10567
  var __dirname = "/home/runner/work/agent-cli/agent-cli/src/skills";
9150
10568
 
9151
10569
  class SkillsLoader {
@@ -9215,7 +10633,7 @@ class SkillsLoader {
9215
10633
  resolve20(cwd, "skills")
9216
10634
  ];
9217
10635
  for (const cand of candidates) {
9218
- if (existsSync19(cand) && !roots.includes(cand)) {
10636
+ if (existsSync21(cand) && !roots.includes(cand)) {
9219
10637
  roots.push(cand);
9220
10638
  }
9221
10639
  }
@@ -9224,7 +10642,7 @@ class SkillsLoader {
9224
10642
  roots.push(getGlobalSkillsDir());
9225
10643
  }
9226
10644
  roots.push(...this.customRoots.map((r) => resolve20(r)));
9227
- return roots.filter((r) => existsSync19(r));
10645
+ return roots.filter((r) => existsSync21(r));
9228
10646
  }
9229
10647
  discoverSkills(cwd, options) {
9230
10648
  return this.listSkills(cwd, options);
@@ -9240,12 +10658,12 @@ class SkillsLoader {
9240
10658
  const discovered = new Map;
9241
10659
  for (const root of roots) {
9242
10660
  try {
9243
- const entries = readdirSync8(root, { withFileTypes: true });
10661
+ const entries = readdirSync9(root, { withFileTypes: true });
9244
10662
  for (const entry of entries) {
9245
10663
  if (entry.isDirectory()) {
9246
- const skillDir = join10(root, entry.name);
9247
- const skillFilePath = join10(skillDir, "SKILL.md");
9248
- if (existsSync19(skillFilePath)) {
10664
+ const skillDir = join12(root, entry.name);
10665
+ const skillFilePath = join12(skillDir, "SKILL.md");
10666
+ if (existsSync21(skillFilePath)) {
9249
10667
  const meta = this.parseSkillFrontmatter(skillFilePath, entry.name, root, cwd);
9250
10668
  if (meta && !discovered.has(meta.name)) {
9251
10669
  meta.enabled = !this.isSkillDisabled(meta.name);
@@ -9274,7 +10692,7 @@ class SkillsLoader {
9274
10692
  if (!meta)
9275
10693
  return null;
9276
10694
  try {
9277
- const raw = readFileSync10(meta.path, "utf8");
10695
+ const raw = readFileSync12(meta.path, "utf8");
9278
10696
  const { body } = this.extractFrontmatterAndBody(raw);
9279
10697
  return {
9280
10698
  ...meta,
@@ -9286,7 +10704,7 @@ class SkillsLoader {
9286
10704
  }
9287
10705
  parseSkillFrontmatter(filePath, dirName, root, cwd) {
9288
10706
  try {
9289
- const raw = readFileSync10(filePath, "utf8");
10707
+ const raw = readFileSync12(filePath, "utf8");
9290
10708
  const { attributes } = this.extractFrontmatterAndBody(raw);
9291
10709
  let scope = "global";
9292
10710
  const normPath = filePath.toLowerCase().replace(/\\/g, "/");
@@ -9367,8 +10785,8 @@ When tackling complex specialized tasks that match any of these skills, autonomo
9367
10785
  }
9368
10786
  }
9369
10787
  // src/memories/store.ts
9370
- import { existsSync as existsSync20, readFileSync as readFileSync11, writeFileSync as writeFileSync6, mkdirSync as mkdirSync12, readdirSync as readdirSync9 } from "fs";
9371
- import { resolve as resolve21, join as join11, basename, dirname as dirname9 } from "path";
10788
+ import { existsSync as existsSync22, readFileSync as readFileSync13, writeFileSync as writeFileSync6, mkdirSync as mkdirSync12, readdirSync as readdirSync10 } from "fs";
10789
+ import { resolve as resolve21, join as join13, basename as basename2, dirname as dirname9 } from "path";
9372
10790
  import { createHash } from "crypto";
9373
10791
  class MemoryStore {
9374
10792
  globalPath;
@@ -9380,7 +10798,7 @@ class MemoryStore {
9380
10798
  findProjectRoot(cwd) {
9381
10799
  let current = resolve21(cwd);
9382
10800
  while (true) {
9383
- if (existsSync20(join11(current, ".git"))) {
10801
+ if (existsSync22(join13(current, ".git"))) {
9384
10802
  return current;
9385
10803
  }
9386
10804
  const parent = dirname9(current);
@@ -9392,31 +10810,35 @@ class MemoryStore {
9392
10810
  }
9393
10811
  getProjectSlug(cwd) {
9394
10812
  const root = this.findProjectRoot(cwd);
9395
- const folderName = basename(root).toLowerCase().replace(/[^a-z0-9_-]/g, "-") || "project";
10813
+ const folderName = basename2(root).toLowerCase().replace(/[^a-z0-9_-]/g, "-") || "project";
9396
10814
  const hash = createHash("sha256").update(resolve21(root)).digest("hex").slice(0, 6);
9397
10815
  return `${folderName}-${hash}`;
9398
10816
  }
9399
10817
  getProjectMemoryDir(cwd) {
9400
10818
  if (this.customWorkspacePath) {
9401
10819
  const dir = resolve21(this.customWorkspacePath);
9402
- if (!existsSync20(dir)) {
10820
+ if (!existsSync22(dir)) {
9403
10821
  try {
9404
10822
  mkdirSync12(dir, { recursive: true });
9405
- } catch {}
10823
+ } catch (err) {
10824
+ console.warn(`[MemoryStore] Failed to create custom memory directory '${dir}':`, err);
10825
+ }
9406
10826
  }
9407
10827
  return dir;
9408
10828
  }
9409
10829
  const slug = this.getProjectSlug(cwd);
9410
- const dir = join11(getProjectsDir(), slug, "memory");
9411
- if (!existsSync20(dir)) {
10830
+ const dir = join13(getProjectsDir(), slug, "memory");
10831
+ if (!existsSync22(dir)) {
9412
10832
  try {
9413
10833
  mkdirSync12(dir, { recursive: true });
9414
- } catch {}
10834
+ } catch (err) {
10835
+ console.warn(`[MemoryStore] Failed to create project memory directory '${dir}':`, err);
10836
+ }
9415
10837
  }
9416
10838
  return dir;
9417
10839
  }
9418
10840
  getMemoryIndexPath(cwd) {
9419
- return join11(this.getProjectMemoryDir(cwd), "MEMORY.md");
10841
+ return join13(this.getProjectMemoryDir(cwd), "MEMORY.md");
9420
10842
  }
9421
10843
  normalizeCategory(raw) {
9422
10844
  const cat = raw.toLowerCase().trim();
@@ -9435,7 +10857,7 @@ class MemoryStore {
9435
10857
  const sanitizedName = params.name.toLowerCase().trim().replace(/[^a-z0-9_-]/g, "_").replace(/^_+|_+$/g, "") || `note_${Date.now()}`;
9436
10858
  const memoryDir = this.getProjectMemoryDir(params.cwd);
9437
10859
  const fileName = `${type}_${sanitizedName}.md`;
9438
- const filePath = join11(memoryDir, fileName);
10860
+ const filePath = join13(memoryDir, fileName);
9439
10861
  const nowIso = new Date().toISOString();
9440
10862
  const cleanContent = params.content.trim();
9441
10863
  const desc = (params.description || cleanContent.split(`
@@ -9470,23 +10892,23 @@ class MemoryStore {
9470
10892
  }
9471
10893
  readTopicMemory(topicNameOrFile, cwd) {
9472
10894
  const memoryDir = this.getProjectMemoryDir(cwd);
9473
- let targetPath = join11(memoryDir, topicNameOrFile);
9474
- if (!existsSync20(targetPath)) {
10895
+ let targetPath = join13(memoryDir, topicNameOrFile);
10896
+ if (!existsSync22(targetPath)) {
9475
10897
  if (!topicNameOrFile.endsWith(".md")) {
9476
- targetPath = join11(memoryDir, `${topicNameOrFile}.md`);
10898
+ targetPath = join13(memoryDir, `${topicNameOrFile}.md`);
9477
10899
  }
9478
10900
  }
9479
- if (!existsSync20(targetPath)) {
9480
- const files = readdirSync9(memoryDir);
10901
+ if (!existsSync22(targetPath)) {
10902
+ const files = readdirSync10(memoryDir);
9481
10903
  const match = files.find((f) => f.includes(topicNameOrFile));
9482
10904
  if (match) {
9483
- targetPath = join11(memoryDir, match);
10905
+ targetPath = join13(memoryDir, match);
9484
10906
  } else {
9485
10907
  return null;
9486
10908
  }
9487
10909
  }
9488
10910
  try {
9489
- const raw = readFileSync11(targetPath, "utf8");
10911
+ const raw = readFileSync13(targetPath, "utf8");
9490
10912
  return this.parseTopicFile(raw, targetPath);
9491
10913
  } catch {
9492
10914
  return null;
@@ -9497,7 +10919,7 @@ class MemoryStore {
9497
10919
  `);
9498
10920
  let inFm = false;
9499
10921
  let type = "project";
9500
- let name = basename(filePath, ".md");
10922
+ let name = basename2(filePath, ".md");
9501
10923
  let description;
9502
10924
  let modified = new Date().toISOString();
9503
10925
  const bodyLines = [];
@@ -9541,13 +10963,13 @@ class MemoryStore {
9541
10963
  }
9542
10964
  syncMemoryIndex(cwd) {
9543
10965
  const memoryDir = this.getProjectMemoryDir(cwd);
9544
- const indexPath = join11(memoryDir, "MEMORY.md");
9545
- const files = existsSync20(memoryDir) ? readdirSync9(memoryDir).filter((f) => f.endsWith(".md") && f !== "MEMORY.md") : [];
10966
+ const indexPath = join13(memoryDir, "MEMORY.md");
10967
+ const files = existsSync22(memoryDir) ? readdirSync10(memoryDir).filter((f) => f.endsWith(".md") && f !== "MEMORY.md") : [];
9546
10968
  const items = [];
9547
10969
  for (const f of files) {
9548
10970
  try {
9549
- const full = join11(memoryDir, f);
9550
- const parsed = this.parseTopicFile(readFileSync11(full, "utf8"), full);
10971
+ const full = join13(memoryDir, f);
10972
+ const parsed = this.parseTopicFile(readFileSync13(full, "utf8"), full);
9551
10973
  items.push({
9552
10974
  type: parsed.type,
9553
10975
  name: parsed.name,
@@ -9555,7 +10977,9 @@ class MemoryStore {
9555
10977
  `)[0] || parsed.name,
9556
10978
  file: f
9557
10979
  });
9558
- } catch {}
10980
+ } catch (err) {
10981
+ console.warn(`[MemoryStore] Failed to parse topic memory file '${f}':`, err);
10982
+ }
9559
10983
  }
9560
10984
  const indexLines = [
9561
10985
  "# Project Auto-Memory Index",
@@ -9573,10 +10997,10 @@ class MemoryStore {
9573
10997
  }
9574
10998
  loadMemoryIndex(cwd) {
9575
10999
  const indexPath = this.getMemoryIndexPath(cwd);
9576
- if (!existsSync20(indexPath))
11000
+ if (!existsSync22(indexPath))
9577
11001
  return "";
9578
11002
  try {
9579
- const raw = readFileSync11(indexPath, "utf8");
11003
+ const raw = readFileSync13(indexPath, "utf8");
9580
11004
  const byteLimit = 25 * 1024;
9581
11005
  const sliced = raw.length > byteLimit ? raw.slice(0, byteLimit) : raw;
9582
11006
  const lines = sliced.split(`
@@ -9589,15 +11013,17 @@ class MemoryStore {
9589
11013
  }
9590
11014
  listProjectMemories(cwd) {
9591
11015
  const memoryDir = this.getProjectMemoryDir(cwd);
9592
- if (!existsSync20(memoryDir))
11016
+ if (!existsSync22(memoryDir))
9593
11017
  return [];
9594
- const files = readdirSync9(memoryDir).filter((f) => f.endsWith(".md") && f !== "MEMORY.md");
11018
+ const files = readdirSync10(memoryDir).filter((f) => f.endsWith(".md") && f !== "MEMORY.md");
9595
11019
  const list = [];
9596
11020
  for (const f of files) {
9597
11021
  try {
9598
- const full = join11(memoryDir, f);
9599
- list.push(this.parseTopicFile(readFileSync11(full, "utf8"), full));
9600
- } catch {}
11022
+ const full = join13(memoryDir, f);
11023
+ list.push(this.parseTopicFile(readFileSync13(full, "utf8"), full));
11024
+ } catch (err) {
11025
+ console.warn(`[MemoryStore] Failed to read topic memory file '${f}':`, err);
11026
+ }
9601
11027
  }
9602
11028
  return list;
9603
11029
  }
@@ -9773,8 +11199,8 @@ async function removeWorktreeGit(repoRoot, worktreePath, deleteBranch = false) {
9773
11199
  return { success: true };
9774
11200
  }
9775
11201
  // src/worktree/manager.ts
9776
- import { resolve as resolve23, join as join12 } from "path";
9777
- import { existsSync as existsSync21, mkdirSync as mkdirSync13, writeFileSync as writeFileSync7, readFileSync as readFileSync12 } from "fs";
11202
+ import { resolve as resolve23, join as join14 } from "path";
11203
+ import { existsSync as existsSync23, mkdirSync as mkdirSync13, writeFileSync as writeFileSync7, readFileSync as readFileSync14 } from "fs";
9778
11204
  var DEFAULT_WORKTREE_KEEP_COUNT = 15;
9779
11205
 
9780
11206
  class WorktreeManager {
@@ -9798,7 +11224,7 @@ class WorktreeManager {
9798
11224
  const branchName = options.branch || `groupy/${taskId}`;
9799
11225
  const targetDir = options.worktreePath || (this.baseStorageDir ? resolve23(this.baseStorageDir, branchName.replace(/\//g, "_")) : resolve23(repoRoot, ".groupy", "worktrees", branchName.replace(/\//g, "_")));
9800
11226
  const worktreeParent = resolve23(targetDir, "..");
9801
- if (!existsSync21(worktreeParent)) {
11227
+ if (!existsSync23(worktreeParent)) {
9802
11228
  mkdirSync13(worktreeParent, { recursive: true });
9803
11229
  }
9804
11230
  const baseBranch = options.baseBranch || await getCurrentBranch(repoRoot);
@@ -9806,7 +11232,7 @@ class WorktreeManager {
9806
11232
  if (!result.success) {
9807
11233
  throw new Error(`Failed to create git worktree: ${result.error}`);
9808
11234
  }
9809
- const metaPath = join12(targetDir, "groupy-thread.json");
11235
+ const metaPath = join14(targetDir, "groupy-thread.json");
9810
11236
  try {
9811
11237
  writeFileSync7(metaPath, JSON.stringify({
9812
11238
  version: 1,
@@ -9832,10 +11258,10 @@ class WorktreeManager {
9832
11258
  return [];
9833
11259
  const worktrees = await listWorktreesGit(repoRoot);
9834
11260
  return worktrees.map((wt) => {
9835
- const metaPath = join12(wt.path, "groupy-thread.json");
9836
- if (existsSync21(metaPath)) {
11261
+ const metaPath = join14(wt.path, "groupy-thread.json");
11262
+ if (existsSync23(metaPath)) {
9837
11263
  try {
9838
- const raw = JSON.parse(readFileSync12(metaPath, "utf8"));
11264
+ const raw = JSON.parse(readFileSync14(metaPath, "utf8"));
9839
11265
  return { ...wt, threadId: raw.ownerThreadId || raw.threadId };
9840
11266
  } catch {}
9841
11267
  }
@@ -10117,7 +11543,7 @@ function ClaudeLogo({
10117
11543
  }, undefined, false, undefined, this);
10118
11544
  }
10119
11545
  function ClaudeHeader({
10120
- version = "v0.3.2",
11546
+ version = "v0.4.0",
10121
11547
  user = "Developer",
10122
11548
  model = "Claude 3.7 Sonnet (Thinking)",
10123
11549
  plan = "Pro",
@@ -11544,6 +12970,7 @@ export {
11544
12970
  AgentRoleRegistry,
11545
12971
  AgentSpawner,
11546
12972
  AuthClient,
12973
+ AutoVerifier,
11547
12974
  BrowserLauncher,
11548
12975
  CHROME_DEVTOOLS_MCP_SERVER_PATH,
11549
12976
  CLAUDE_GLYPHS,
@@ -11565,6 +12992,8 @@ export {
11565
12992
  CredentialsStore,
11566
12993
  DEFAULT_AUTO_COMPACT_THRESHOLD_TOKENS,
11567
12994
  DEFAULT_MAX_CONTEXT_TOKENS,
12995
+ DEFAULT_MAX_DIR_ENTRIES,
12996
+ DEFAULT_MAX_UNPAGINATED_LINES,
11568
12997
  DEFAULT_WORKTREE_KEEP_COUNT,
11569
12998
  DefaultModelClientSession,
11570
12999
  DomSnapshotEngine,
@@ -11660,5 +13089,6 @@ export {
11660
13089
  submissionLoop,
11661
13090
  updatePlanTool,
11662
13091
  verifyTaskAction,
13092
+ viewFileTool,
11663
13093
  writeFileTool
11664
13094
  };