@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/bin/pikaa.js +5 -2
- package/dist/cli.js +8166 -7078
- package/dist/index.js +1613 -183
- package/package.json +9 -9
- package/templates/base/groupy_prompt.md +22 -19
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
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
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
|
-
|
|
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(
|
|
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) +
|
|
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.
|
|
1895
|
-
this.
|
|
1896
|
-
this.
|
|
1897
|
-
this.addRule(/^(
|
|
1898
|
-
this.addRule(/^(
|
|
1899
|
-
this.addRule(/^(
|
|
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: "
|
|
1958
|
-
reason:
|
|
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 (
|
|
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 (
|
|
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 =
|
|
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
|
|
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 (
|
|
2445
|
-
|
|
2446
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
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
|
-
|
|
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
|
|
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: {
|
|
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
|
|
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: '${
|
|
2915
|
+
return { output: `Error: File not found: '${rawPath}'`, isError: true };
|
|
2584
2916
|
}
|
|
2585
2917
|
try {
|
|
2586
2918
|
const content = readFileSync3(filePath, "utf8");
|
|
2587
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
4226
|
-
|
|
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
|
-
|
|
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
|
|
5454
|
-
import { resolve as resolve12, dirname as dirname6, join as
|
|
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 (!
|
|
6777
|
+
if (!existsSync15(fullPath))
|
|
5490
6778
|
return;
|
|
5491
6779
|
this.loadedConfigFiles.add(fullPath);
|
|
5492
6780
|
try {
|
|
5493
|
-
const content =
|
|
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 (!
|
|
7032
|
+
if (!existsSync15(dir)) {
|
|
5745
7033
|
mkdirSync7(dir, { recursive: true });
|
|
5746
7034
|
}
|
|
5747
7035
|
let existing = { mcpServers: {} };
|
|
5748
|
-
if (
|
|
7036
|
+
if (existsSync15(fullPath)) {
|
|
5749
7037
|
try {
|
|
5750
|
-
const content =
|
|
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 (!
|
|
7051
|
+
if (!existsSync15(fullPath))
|
|
5764
7052
|
return false;
|
|
5765
7053
|
try {
|
|
5766
|
-
const content =
|
|
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 =
|
|
5794
|
-
if (
|
|
7081
|
+
const workspaceConfig = join9(cwd, ".mcp.json");
|
|
7082
|
+
if (existsSync15(workspaceConfig))
|
|
5795
7083
|
return workspaceConfig;
|
|
5796
|
-
const altConfig =
|
|
5797
|
-
if (
|
|
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
|
|
5820
|
-
import { join as
|
|
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 &&
|
|
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
|
-
|
|
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
|
-
|
|
7127
|
+
join10(process.env.LOCALAPPDATA || "", "BraveSoftware\\Brave-Browser\\Application\\brave.exe")
|
|
5840
7128
|
];
|
|
5841
7129
|
for (const path of candidates) {
|
|
5842
|
-
if (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 (
|
|
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 (
|
|
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 ||
|
|
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 &&
|
|
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
|
|
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 =
|
|
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
|
|
8331
|
-
import { resolve as resolve18, join as
|
|
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 (!
|
|
9704
|
+
if (!existsSync18(fullPath))
|
|
8417
9705
|
return;
|
|
8418
|
-
const entries =
|
|
9706
|
+
const entries = readdirSync8(fullPath);
|
|
8419
9707
|
for (const entry of entries) {
|
|
8420
9708
|
if (entry.endsWith(".json")) {
|
|
8421
9709
|
try {
|
|
8422
|
-
const content =
|
|
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
|
|
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 (!
|
|
8463
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
8609
|
-
|
|
8610
|
-
|
|
8611
|
-
|
|
8612
|
-
|
|
8613
|
-
|
|
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
|
|
8616
|
-
|
|
8617
|
-
|
|
8618
|
-
|
|
8619
|
-
|
|
8620
|
-
|
|
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
|
|
8623
|
-
|
|
8624
|
-
|
|
8625
|
-
|
|
8626
|
-
|
|
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
|
|
8632
|
-
|
|
8633
|
-
|
|
8634
|
-
|
|
8635
|
-
|
|
8636
|
-
|
|
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
|
|
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 (!
|
|
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
|
|
9148
|
-
import { resolve as resolve20, join as
|
|
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 (
|
|
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) =>
|
|
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 =
|
|
10661
|
+
const entries = readdirSync9(root, { withFileTypes: true });
|
|
9244
10662
|
for (const entry of entries) {
|
|
9245
10663
|
if (entry.isDirectory()) {
|
|
9246
|
-
const skillDir =
|
|
9247
|
-
const skillFilePath =
|
|
9248
|
-
if (
|
|
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 =
|
|
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 =
|
|
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
|
|
9371
|
-
import { resolve as resolve21, join as
|
|
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 (
|
|
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 =
|
|
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 (!
|
|
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 =
|
|
9411
|
-
if (!
|
|
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
|
|
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 =
|
|
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 =
|
|
9474
|
-
if (!
|
|
10895
|
+
let targetPath = join13(memoryDir, topicNameOrFile);
|
|
10896
|
+
if (!existsSync22(targetPath)) {
|
|
9475
10897
|
if (!topicNameOrFile.endsWith(".md")) {
|
|
9476
|
-
targetPath =
|
|
10898
|
+
targetPath = join13(memoryDir, `${topicNameOrFile}.md`);
|
|
9477
10899
|
}
|
|
9478
10900
|
}
|
|
9479
|
-
if (!
|
|
9480
|
-
const files =
|
|
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 =
|
|
10905
|
+
targetPath = join13(memoryDir, match);
|
|
9484
10906
|
} else {
|
|
9485
10907
|
return null;
|
|
9486
10908
|
}
|
|
9487
10909
|
}
|
|
9488
10910
|
try {
|
|
9489
|
-
const raw =
|
|
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 =
|
|
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 =
|
|
9545
|
-
const files =
|
|
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 =
|
|
9550
|
-
const parsed = this.parseTopicFile(
|
|
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 (!
|
|
11000
|
+
if (!existsSync22(indexPath))
|
|
9577
11001
|
return "";
|
|
9578
11002
|
try {
|
|
9579
|
-
const raw =
|
|
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 (!
|
|
11016
|
+
if (!existsSync22(memoryDir))
|
|
9593
11017
|
return [];
|
|
9594
|
-
const files =
|
|
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 =
|
|
9599
|
-
list.push(this.parseTopicFile(
|
|
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
|
|
9777
|
-
import { existsSync as
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
9836
|
-
if (
|
|
11261
|
+
const metaPath = join14(wt.path, "groupy-thread.json");
|
|
11262
|
+
if (existsSync23(metaPath)) {
|
|
9837
11263
|
try {
|
|
9838
|
-
const raw = JSON.parse(
|
|
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.
|
|
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
|
};
|