@pikaa-ai/pikaa 0.3.28 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -244,6 +244,7 @@ class DefaultModelClientSession {
244
244
  systemMessage.cache_control = { type: "ephemeral" };
245
245
  }
246
246
  const messages = [systemMessage];
247
+ const declaredToolCallIds = new Set;
247
248
  for (let i = 0;i < params.history.length; i++) {
248
249
  const item = params.history[i];
249
250
  if (item.type === "user_message") {
@@ -268,6 +269,9 @@ class DefaultModelClientSession {
268
269
  j++;
269
270
  }
270
271
  if (toolCalls.length > 0) {
272
+ for (const tc of toolCalls) {
273
+ declaredToolCallIds.add(tc.id);
274
+ }
271
275
  messages.push({
272
276
  role: "assistant",
273
277
  content: cleanedContent || null,
@@ -304,6 +308,9 @@ class DefaultModelClientSession {
304
308
  });
305
309
  j++;
306
310
  }
311
+ for (const tc of toolCalls) {
312
+ declaredToolCallIds.add(tc.id);
313
+ }
307
314
  messages.push({
308
315
  role: "assistant",
309
316
  content: null,
@@ -311,11 +318,13 @@ class DefaultModelClientSession {
311
318
  });
312
319
  i = j - 1;
313
320
  } else if (item.type === "function_call_output") {
314
- messages.push({
315
- role: "tool",
316
- tool_call_id: item.callId,
317
- content: item.output
318
- });
321
+ if (declaredToolCallIds.has(item.callId)) {
322
+ messages.push({
323
+ role: "tool",
324
+ tool_call_id: item.callId,
325
+ content: item.output
326
+ });
327
+ }
319
328
  }
320
329
  }
321
330
  const toolsPayload = typeof params.tools?.toModelToolsSchema === "function" ? params.tools.toModelToolsSchema() : typeof params.tools?.toOpenAISpec === "function" ? params.tools.toOpenAISpec() : Array.isArray(params.tools) ? params.tools : [];
@@ -705,14 +714,39 @@ function compactHistory(history, retainedRecentItems = 6) {
705
714
  if (history.length <= retainedRecentItems) {
706
715
  return [...history];
707
716
  }
708
- const itemsToCompact = history.slice(0, history.length - retainedRecentItems);
709
- const recentItems = history.slice(history.length - retainedRecentItems);
717
+ const targetCutoff = history.length - retainedRecentItems;
718
+ let bestCutoff = -1;
719
+ for (let i = targetCutoff;i >= 0; i--) {
720
+ if (history[i]?.type === "user_message") {
721
+ bestCutoff = i;
722
+ break;
723
+ }
724
+ }
725
+ if (bestCutoff <= 0) {
726
+ for (let i = targetCutoff + 1;i < history.length; i++) {
727
+ if (history[i]?.type === "user_message") {
728
+ bestCutoff = i;
729
+ break;
730
+ }
731
+ }
732
+ }
733
+ let cutoffIndex = bestCutoff > 0 ? bestCutoff : targetCutoff;
734
+ while (cutoffIndex < history.length && history[cutoffIndex]?.type === "function_call_output") {
735
+ cutoffIndex++;
736
+ }
737
+ if (cutoffIndex <= 0 || cutoffIndex >= history.length) {
738
+ return [...history];
739
+ }
740
+ const itemsToCompact = history.slice(0, cutoffIndex);
741
+ const recentItems = history.slice(cutoffIndex);
710
742
  const summaryParts = ["### Summary of previous conversation context:"];
711
743
  for (const item of itemsToCompact) {
712
744
  if (item.type === "user_message") {
713
745
  summaryParts.push(`- User: ${item.content.slice(0, 200)}`);
714
746
  } else if (item.type === "function_call") {
715
747
  summaryParts.push(`- Executed tool: ${item.name}`);
748
+ } else if (item.type === "function_call_output") {
749
+ summaryParts.push(` Tool output: ${item.output.slice(0, 100)}`);
716
750
  } else if (item.type === "agent_message") {
717
751
  summaryParts.push(`- Assistant: ${item.content.slice(0, 200)}`);
718
752
  }
@@ -1110,7 +1144,7 @@ class EphemeralWorkspaceManager {
1110
1144
  }
1111
1145
  var globalEphemeralWorkspace = new EphemeralWorkspaceManager;
1112
1146
  // src/verification/verifier.ts
1113
- import { spawnSync } from "child_process";
1147
+ import { spawn, spawnSync } from "child_process";
1114
1148
  import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
1115
1149
  import { join as join6 } from "path";
1116
1150
 
@@ -1487,36 +1521,120 @@ class AutoVerifier {
1487
1521
  this.customCommand = options.customCommand;
1488
1522
  this.timeoutMs = options.timeoutMs ?? 30000;
1489
1523
  }
1490
- resolveVerificationCommand() {
1491
- if (this.customCommand && this.customCommand.trim()) {
1492
- return this.customCommand.trim();
1524
+ findTargetedTests(modifiedFiles = []) {
1525
+ const matchedTests = new Set;
1526
+ for (const rawFile of modifiedFiles) {
1527
+ const normalized = rawFile.replace(/\\/g, "/").replace(/^\.\//, "");
1528
+ if (/\.(test|spec)\.[jt]sx?$/i.test(normalized) || /(^|\/)test_[^/]+\.py$/i.test(normalized) || /_test\.py$/i.test(normalized) || /_test\.go$/i.test(normalized)) {
1529
+ if (existsSync7(join6(this.cwd, normalized))) {
1530
+ matchedTests.add(normalized);
1531
+ }
1532
+ continue;
1533
+ }
1534
+ const extMatch = normalized.match(/\.[^.]+$/);
1535
+ if (!extMatch)
1536
+ continue;
1537
+ const withoutExt = normalized.slice(0, -extMatch[0].length);
1538
+ const parts = withoutExt.split("/");
1539
+ const baseName = parts[parts.length - 1];
1540
+ const subPath = normalized.startsWith("src/") ? normalized.slice(4, -extMatch[0].length) : normalized.startsWith("lib/") ? normalized.slice(4, -extMatch[0].length) : withoutExt;
1541
+ const candidatePaths = [
1542
+ `tests/${subPath}.test.ts`,
1543
+ `tests/${subPath}.test.js`,
1544
+ `tests/${subPath}.test.tsx`,
1545
+ `tests/${subPath}.spec.ts`,
1546
+ `tests/${subPath}.spec.js`,
1547
+ `tests/${baseName}.test.ts`,
1548
+ `tests/${baseName}.test.js`,
1549
+ `tests/${baseName}.spec.ts`,
1550
+ `test/${subPath}.test.ts`,
1551
+ `test/${subPath}.test.js`,
1552
+ `test/${baseName}.test.ts`,
1553
+ `test/${baseName}.test.js`,
1554
+ `src/${subPath}.test.ts`,
1555
+ `src/${subPath}.spec.ts`,
1556
+ `${withoutExt}.test.ts`,
1557
+ `${withoutExt}.spec.ts`,
1558
+ `tests/test_${baseName}.py`,
1559
+ `test_${baseName}.py`,
1560
+ `tests/${baseName}_test.go`
1561
+ ];
1562
+ for (const candidate of candidatePaths) {
1563
+ if (existsSync7(join6(this.cwd, candidate))) {
1564
+ matchedTests.add(candidate);
1565
+ }
1566
+ }
1567
+ }
1568
+ return Array.from(matchedTests);
1569
+ }
1570
+ resolveTargetedTestCommand(testFiles) {
1571
+ if (testFiles.length === 0)
1572
+ return null;
1573
+ const quotedFiles = testFiles.map((f) => f.includes(" ") ? `"${f}"` : f).join(" ");
1574
+ if (existsSync7(join6(this.cwd, "bun.lockb")) || existsSync7(join6(this.cwd, "bun.lock"))) {
1575
+ return `bun test ${quotedFiles}`;
1576
+ }
1577
+ const pkgPath = join6(this.cwd, "package.json");
1578
+ if (existsSync7(pkgPath)) {
1579
+ try {
1580
+ const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
1581
+ const allDeps = { ...pkg.dependencies || {}, ...pkg.devDependencies || {} };
1582
+ if (allDeps.vitest) {
1583
+ return `npx vitest run ${quotedFiles}`;
1584
+ }
1585
+ if (allDeps.jest) {
1586
+ return `npx jest ${quotedFiles}`;
1587
+ }
1588
+ const pm = existsSync7(join6(this.cwd, "pnpm-lock.yaml")) ? "pnpm" : existsSync7(join6(this.cwd, "yarn.lock")) ? "yarn" : "npm";
1589
+ if (pkg.scripts?.test) {
1590
+ if (pkg.scripts.test.includes("bun test")) {
1591
+ return `bun test ${quotedFiles}`;
1592
+ }
1593
+ return `${pm} test -- ${quotedFiles}`;
1594
+ }
1595
+ } catch (err) {
1596
+ console.warn(`[AutoVerifier] Failed to parse package.json for test runner:`, err);
1597
+ }
1598
+ }
1599
+ if (existsSync7(join6(this.cwd, "pyproject.toml")) || existsSync7(join6(this.cwd, "requirements.txt"))) {
1600
+ if (existsSync7(join6(this.cwd, "uv.lock"))) {
1601
+ return `uv run pytest ${quotedFiles}`;
1602
+ }
1603
+ return `pytest ${quotedFiles}`;
1493
1604
  }
1605
+ if (existsSync7(join6(this.cwd, "go.mod"))) {
1606
+ return `go test ${quotedFiles}`;
1607
+ }
1608
+ if (existsSync7(join6(this.cwd, "Cargo.toml"))) {
1609
+ return `cargo test ${quotedFiles}`;
1610
+ }
1611
+ return null;
1612
+ }
1613
+ resolveStaticCommand() {
1494
1614
  try {
1495
1615
  const analyzer = new ProjectAnalyzer(this.cwd);
1496
1616
  const analysis = analyzer.analyze();
1497
1617
  if (analysis.commands.typecheck) {
1498
1618
  return analysis.commands.typecheck;
1499
1619
  }
1500
- if (analysis.commands.lint) {
1501
- return analysis.commands.lint;
1502
- }
1503
- if (analysis.commands.test) {
1504
- return analysis.commands.test;
1505
- }
1506
- } catch {}
1620
+ } catch (err) {
1621
+ console.warn(`[AutoVerifier] ProjectAnalyzer analysis error:`, err);
1622
+ }
1507
1623
  const pkgPath = join6(this.cwd, "package.json");
1508
1624
  if (existsSync7(pkgPath)) {
1509
1625
  try {
1510
1626
  const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
1511
1627
  if (pkg.scripts) {
1628
+ const pm = existsSync7(join6(this.cwd, "bun.lockb")) || existsSync7(join6(this.cwd, "bun.lock")) ? "bun" : existsSync7(join6(this.cwd, "pnpm-lock.yaml")) ? "pnpm" : existsSync7(join6(this.cwd, "yarn.lock")) ? "yarn" : "npm";
1629
+ const runCmd = pm === "bun" || pm === "pnpm" || pm === "yarn" ? `${pm} run` : "npm run";
1512
1630
  if (pkg.scripts.typecheck)
1513
- return "npm run typecheck";
1631
+ return `${runCmd} typecheck`;
1514
1632
  if (pkg.scripts.check)
1515
- return "npm run check";
1516
- if (pkg.scripts.test)
1517
- return "npm test";
1633
+ return `${runCmd} check`;
1518
1634
  }
1519
- } catch {}
1635
+ } catch (err) {
1636
+ console.warn(`[AutoVerifier] Failed to parse package.json for static check:`, err);
1637
+ }
1520
1638
  }
1521
1639
  if (existsSync7(join6(this.cwd, "tsconfig.json"))) {
1522
1640
  return "npx tsc --noEmit";
@@ -1527,17 +1645,76 @@ class AutoVerifier {
1527
1645
  if (existsSync7(join6(this.cwd, "go.mod"))) {
1528
1646
  return "go vet ./...";
1529
1647
  }
1530
- if (existsSync7(join6(this.cwd, "pyproject.toml")) || existsSync7(join6(this.cwd, "setup.py"))) {
1531
- if (existsSync7(join6(this.cwd, "mypy.ini")) || existsSync7(join6(this.cwd, ".mypy.ini"))) {
1532
- return "mypy .";
1533
- }
1648
+ if (existsSync7(join6(this.cwd, "mypy.ini")) || existsSync7(join6(this.cwd, ".mypy.ini"))) {
1649
+ return "mypy .";
1534
1650
  }
1535
1651
  return null;
1536
1652
  }
1537
- verify(modifiedFiles = []) {
1538
- const command = this.resolveVerificationCommand();
1653
+ resolveVerificationCommand(modifiedFiles = []) {
1654
+ if (this.customCommand && this.customCommand.trim()) {
1655
+ return this.customCommand.trim();
1656
+ }
1657
+ const targetedTests = this.findTargetedTests(modifiedFiles);
1658
+ const targetedTestCmd = this.resolveTargetedTestCommand(targetedTests);
1659
+ const staticCmd = this.resolveStaticCommand();
1660
+ if (targetedTestCmd && staticCmd) {
1661
+ return `${staticCmd} && ${targetedTestCmd}`;
1662
+ }
1663
+ if (targetedTestCmd) {
1664
+ return targetedTestCmd;
1665
+ }
1666
+ if (staticCmd) {
1667
+ return staticCmd;
1668
+ }
1669
+ try {
1670
+ const analyzer = new ProjectAnalyzer(this.cwd);
1671
+ const analysis = analyzer.analyze();
1672
+ if (analysis.commands.test)
1673
+ return analysis.commands.test;
1674
+ if (analysis.commands.lint)
1675
+ return analysis.commands.lint;
1676
+ } catch {}
1677
+ const pkgPath = join6(this.cwd, "package.json");
1678
+ if (existsSync7(pkgPath)) {
1679
+ try {
1680
+ const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
1681
+ if (pkg.scripts?.test) {
1682
+ const pm = existsSync7(join6(this.cwd, "bun.lockb")) || existsSync7(join6(this.cwd, "bun.lock")) ? "bun" : "npm";
1683
+ return pm === "bun" ? "bun test" : "npm test";
1684
+ }
1685
+ if (pkg.scripts?.lint) {
1686
+ return "npm run lint";
1687
+ }
1688
+ } catch {}
1689
+ }
1690
+ return null;
1691
+ }
1692
+ resolveVerificationStages(modifiedFiles = []) {
1693
+ if (this.customCommand && this.customCommand.trim()) {
1694
+ return [this.customCommand.trim()];
1695
+ }
1696
+ const stages = [];
1697
+ const staticCmd = this.resolveStaticCommand();
1698
+ const targetedTests = this.findTargetedTests(modifiedFiles);
1699
+ const targetedTestCmd = this.resolveTargetedTestCommand(targetedTests);
1700
+ if (staticCmd) {
1701
+ stages.push(staticCmd);
1702
+ }
1703
+ if (targetedTestCmd) {
1704
+ stages.push(targetedTestCmd);
1705
+ }
1706
+ if (stages.length === 0) {
1707
+ const fallback = this.resolveVerificationCommand(modifiedFiles);
1708
+ if (fallback) {
1709
+ stages.push(fallback);
1710
+ }
1711
+ }
1712
+ return stages;
1713
+ }
1714
+ async verify(modifiedFiles = [], signal) {
1715
+ const stages = this.resolveVerificationStages(modifiedFiles);
1539
1716
  const startTime = performance.now();
1540
- if (!command) {
1717
+ if (stages.length === 0) {
1541
1718
  return {
1542
1719
  command: "none",
1543
1720
  success: true,
@@ -1547,59 +1724,158 @@ class AutoVerifier {
1547
1724
  reason: "NO_VERIFIER_DETECTED"
1548
1725
  };
1549
1726
  }
1550
- try {
1551
- const isWindows = process.platform === "win32";
1552
- const proc = spawnSync(command, {
1553
- cwd: this.cwd,
1554
- shell: true,
1555
- encoding: "utf8",
1556
- timeout: this.timeoutMs,
1557
- maxBuffer: 10 * 1024 * 1024,
1558
- env: {
1559
- ...process.env,
1560
- CI: "true",
1561
- FORCE_COLOR: "0"
1727
+ const combinedOutputs = [];
1728
+ let executedCommands = [];
1729
+ for (const cmd of stages) {
1730
+ if (signal?.aborted) {
1731
+ return {
1732
+ command: cmd,
1733
+ success: false,
1734
+ exitCode: 1,
1735
+ output: "Verification was aborted.",
1736
+ durationMs: Math.round(performance.now() - startTime),
1737
+ reason: "ABORTED"
1738
+ };
1739
+ }
1740
+ executedCommands.push(cmd);
1741
+ const stageResult = await this.executeCommandAsync(cmd, signal);
1742
+ combinedOutputs.push(`[${cmd}]
1743
+ ${stageResult.output}`);
1744
+ if (!stageResult.success) {
1745
+ return {
1746
+ command: cmd,
1747
+ success: false,
1748
+ exitCode: stageResult.exitCode,
1749
+ output: this.truncateOutput(combinedOutputs.join(`
1750
+
1751
+ `)),
1752
+ durationMs: Math.round(performance.now() - startTime)
1753
+ };
1754
+ }
1755
+ }
1756
+ return {
1757
+ command: executedCommands.join(" && "),
1758
+ success: true,
1759
+ exitCode: 0,
1760
+ output: this.truncateOutput(combinedOutputs.join(`
1761
+
1762
+ `)),
1763
+ durationMs: Math.round(performance.now() - startTime)
1764
+ };
1765
+ }
1766
+ executeCommandAsync(command, signal) {
1767
+ return new Promise((resolve) => {
1768
+ let proc = null;
1769
+ let stdoutData = "";
1770
+ let stderrData = "";
1771
+ let isSettled = false;
1772
+ const finish = (success, exitCode, output) => {
1773
+ if (isSettled)
1774
+ return;
1775
+ isSettled = true;
1776
+ cleanup();
1777
+ resolve({ success, exitCode, output: output.trim() });
1778
+ };
1779
+ const killProcessTree = () => {
1780
+ if (!proc || !proc.pid)
1781
+ return;
1782
+ try {
1783
+ if (process.platform === "win32") {
1784
+ spawnSync("taskkill", ["/pid", String(proc.pid), "/T", "/F"], { stdio: "ignore" });
1785
+ } else {
1786
+ proc.kill("SIGTERM");
1787
+ setTimeout(() => {
1788
+ try {
1789
+ proc?.kill("SIGKILL");
1790
+ } catch {}
1791
+ }, 500);
1792
+ }
1793
+ } catch {}
1794
+ };
1795
+ const onAbort = () => {
1796
+ killProcessTree();
1797
+ finish(false, 1, "Verification command aborted by user or session signal.");
1798
+ };
1799
+ if (signal?.aborted) {
1800
+ return finish(false, 1, "Verification aborted before launch.");
1801
+ }
1802
+ if (signal) {
1803
+ signal.addEventListener("abort", onAbort, { once: true });
1804
+ }
1805
+ const timer = setTimeout(() => {
1806
+ killProcessTree();
1807
+ const partial = (stdoutData + `
1808
+ ` + stderrData).trim();
1809
+ const timeoutMsg = `[Verification Timeout Error]: Verification command '${command}' exceeded timeout limit of ${this.timeoutMs}ms and was terminated.`;
1810
+ finish(false, 1, partial ? `${partial}
1811
+
1812
+ ${timeoutMsg}` : timeoutMsg);
1813
+ }, this.timeoutMs);
1814
+ const cleanup = () => {
1815
+ clearTimeout(timer);
1816
+ if (signal) {
1817
+ signal.removeEventListener("abort", onAbort);
1562
1818
  }
1563
- });
1564
- const durationMs = Math.round(performance.now() - startTime);
1565
- const stdout = proc.stdout ? String(proc.stdout) : "";
1566
- const stderr = proc.stderr ? String(proc.stderr) : "";
1567
- let combined = (stdout + `
1568
- ` + stderr).trim();
1569
- if (combined.length > 3000) {
1570
- const lines = combined.split(`
1819
+ };
1820
+ try {
1821
+ proc = spawn(command, {
1822
+ cwd: this.cwd,
1823
+ shell: true,
1824
+ env: {
1825
+ ...process.env,
1826
+ CI: "true",
1827
+ FORCE_COLOR: "0"
1828
+ }
1829
+ });
1830
+ proc.stdout?.on("data", (chunk) => {
1831
+ stdoutData += chunk.toString();
1832
+ });
1833
+ proc.stderr?.on("data", (chunk) => {
1834
+ stderrData += chunk.toString();
1835
+ });
1836
+ proc.on("error", (err) => {
1837
+ const partial = (stdoutData + `
1838
+ ` + stderrData).trim();
1839
+ const spawnMsg = `[Verification Process Spawn Error]: Failed to spawn command '${command}': ${err.message}`;
1840
+ finish(false, 1, partial ? `${partial}
1841
+
1842
+ ${spawnMsg}` : spawnMsg);
1843
+ });
1844
+ proc.on("close", (code) => {
1845
+ const exitCode = code ?? 0;
1846
+ const combined = (stdoutData + `
1847
+ ` + stderrData).trim();
1848
+ if (exitCode === 0) {
1849
+ finish(true, 0, combined || "Verification passed cleanly.");
1850
+ } else {
1851
+ const errorFallback = `[Verification Failure]: Command '${command}' exited with code ${exitCode} and produced no output.`;
1852
+ finish(false, exitCode, combined ? `${combined}
1853
+
1854
+ [Process exited with non-zero code ${exitCode}]` : errorFallback);
1855
+ }
1856
+ });
1857
+ } catch (err) {
1858
+ finish(false, 1, `[Verification Execution Exception]: ${err.message || String(err)}`);
1859
+ }
1860
+ });
1861
+ }
1862
+ truncateOutput(output) {
1863
+ if (output.length > 4000) {
1864
+ const lines = output.split(`
1571
1865
  `);
1572
- if (lines.length > 60) {
1573
- const head = lines.slice(0, 30).join(`
1866
+ if (lines.length > 70) {
1867
+ const head = lines.slice(0, 35).join(`
1574
1868
  `);
1575
- const tail = lines.slice(-25).join(`
1869
+ const tail = lines.slice(-30).join(`
1576
1870
  `);
1577
- combined = `${head}
1871
+ return `${head}
1578
1872
 
1579
- ... [${lines.length - 55} lines truncated for context efficiency] ...
1873
+ ... [${lines.length - 65} lines truncated for context efficiency] ...
1580
1874
 
1581
1875
  ${tail}`;
1582
- }
1583
1876
  }
1584
- const exitCode = proc.status ?? (proc.error ? 1 : 0);
1585
- const success = exitCode === 0;
1586
- return {
1587
- command,
1588
- success,
1589
- exitCode,
1590
- output: combined || (success ? "Verification succeeded cleanly." : "Command failed with empty output."),
1591
- durationMs
1592
- };
1593
- } catch (err) {
1594
- const durationMs = Math.round(performance.now() - startTime);
1595
- return {
1596
- command,
1597
- success: false,
1598
- exitCode: 1,
1599
- output: `Verification execution error: ${err.message || String(err)}`,
1600
- durationMs
1601
- };
1602
1877
  }
1878
+ return output;
1603
1879
  }
1604
1880
  }
1605
1881
  // src/session/turn.ts
@@ -1723,6 +1999,7 @@ async function runTurn(session, turnContext, input) {
1723
1999
  });
1724
2000
  }
1725
2001
  if (toolCallRequests.length > 0) {
2002
+ const validCalls = [];
1726
2003
  for (const toolCall of toolCallRequests) {
1727
2004
  if (!toolCall.name || !toolCall.name.trim())
1728
2005
  continue;
@@ -1746,6 +2023,14 @@ async function runTurn(session, turnContext, input) {
1746
2023
  toolName: toolCall.name,
1747
2024
  arguments: toolCall.arguments
1748
2025
  });
2026
+ validCalls.push({
2027
+ callId: toolCall.callId,
2028
+ name: toolCall.name,
2029
+ arguments: toolCall.arguments,
2030
+ functionCallItem
2031
+ });
2032
+ }
2033
+ for (const toolCall of validCalls) {
1749
2034
  const toolResult = await turnContext.tools.execute(toolCall.name, toolCall.arguments, {
1750
2035
  cwd: turnContext.environment.cwd,
1751
2036
  turnId,
@@ -1753,6 +2038,12 @@ async function runTurn(session, turnContext, input) {
1753
2038
  execPolicy: session.execPolicy,
1754
2039
  mode: session.collaborationMode,
1755
2040
  permissionMode: session.permissionMode,
2041
+ onFileModified: (p) => {
2042
+ if (p) {
2043
+ modifiedFiles.add(p);
2044
+ hasRunVerification = false;
2045
+ }
2046
+ },
1756
2047
  onPlanUpdate: (plan, explanation) => {
1757
2048
  session.emitEvent({
1758
2049
  type: "PlanUpdated",
@@ -1768,7 +2059,8 @@ async function runTurn(session, turnContext, input) {
1768
2059
  turnId,
1769
2060
  toolName: toolCall.name,
1770
2061
  description,
1771
- command
2062
+ command,
2063
+ prefixRule
1772
2064
  });
1773
2065
  },
1774
2066
  requestInput: async (question, options) => {
@@ -1819,7 +2111,7 @@ async function runTurn(session, turnContext, input) {
1819
2111
  cwd,
1820
2112
  customCommand: session.autoVerificationCommand
1821
2113
  });
1822
- const command = verifier.resolveVerificationCommand();
2114
+ const command = verifier.resolveVerificationCommand(Array.from(modifiedFiles));
1823
2115
  if (command) {
1824
2116
  session.emitEvent({
1825
2117
  type: "VerificationStarted",
@@ -1827,7 +2119,7 @@ async function runTurn(session, turnContext, input) {
1827
2119
  command,
1828
2120
  modifiedFiles: Array.from(modifiedFiles)
1829
2121
  });
1830
- const vResult = verifier.verify(Array.from(modifiedFiles));
2122
+ const vResult = await verifier.verify(Array.from(modifiedFiles), signal);
1831
2123
  session.emitEvent({
1832
2124
  type: "VerificationCompleted",
1833
2125
  turnId,
@@ -1990,8 +2282,133 @@ async function submissionLoop(session, queue) {
1990
2282
  }
1991
2283
  }
1992
2284
 
2285
+ // src/security/shell-parser.ts
2286
+ function parseShellCommand(commandLine) {
2287
+ const commands = [];
2288
+ const subshellCommands = [];
2289
+ let hasPipes = false;
2290
+ let currentSegment = "";
2291
+ let inSingleQuote = false;
2292
+ let inDoubleQuote = false;
2293
+ let isEscaped = false;
2294
+ const len = commandLine.length;
2295
+ for (let i = 0;i < len; i++) {
2296
+ const char = commandLine[i];
2297
+ if (isEscaped) {
2298
+ currentSegment += char;
2299
+ isEscaped = false;
2300
+ continue;
2301
+ }
2302
+ if (char === "\\") {
2303
+ isEscaped = true;
2304
+ currentSegment += char;
2305
+ continue;
2306
+ }
2307
+ if (char === "'" && !inDoubleQuote) {
2308
+ inSingleQuote = !inSingleQuote;
2309
+ currentSegment += char;
2310
+ continue;
2311
+ }
2312
+ if (char === '"' && !inSingleQuote) {
2313
+ inDoubleQuote = !inDoubleQuote;
2314
+ currentSegment += char;
2315
+ continue;
2316
+ }
2317
+ if (inSingleQuote) {
2318
+ currentSegment += char;
2319
+ continue;
2320
+ }
2321
+ if (char === "`") {
2322
+ let endIdx = -1;
2323
+ for (let j = i + 1;j < len; j++) {
2324
+ if (commandLine[j] === "\\" && j + 1 < len) {
2325
+ j++;
2326
+ continue;
2327
+ }
2328
+ if (commandLine[j] === "`") {
2329
+ endIdx = j;
2330
+ break;
2331
+ }
2332
+ }
2333
+ if (endIdx !== -1) {
2334
+ const innerCmd = commandLine.slice(i + 1, endIdx);
2335
+ if (innerCmd.trim()) {
2336
+ subshellCommands.push(innerCmd.trim());
2337
+ }
2338
+ currentSegment += commandLine.slice(i, endIdx + 1);
2339
+ i = endIdx;
2340
+ continue;
2341
+ }
2342
+ }
2343
+ if (char === "$" && i + 1 < len && commandLine[i + 1] === "(") {
2344
+ let depth = 1;
2345
+ let endIdx = -1;
2346
+ for (let j = i + 2;j < len; j++) {
2347
+ if (commandLine[j] === "\\" && j + 1 < len) {
2348
+ j++;
2349
+ continue;
2350
+ }
2351
+ if (commandLine[j] === "(")
2352
+ depth++;
2353
+ else if (commandLine[j] === ")") {
2354
+ depth--;
2355
+ if (depth === 0) {
2356
+ endIdx = j;
2357
+ break;
2358
+ }
2359
+ }
2360
+ }
2361
+ if (endIdx !== -1) {
2362
+ const innerCmd = commandLine.slice(i + 2, endIdx);
2363
+ if (innerCmd.trim()) {
2364
+ subshellCommands.push(innerCmd.trim());
2365
+ }
2366
+ currentSegment += commandLine.slice(i, endIdx + 1);
2367
+ i = endIdx;
2368
+ continue;
2369
+ }
2370
+ }
2371
+ if (!inDoubleQuote) {
2372
+ if (char === "&" && commandLine[i + 1] === "&" || char === "|" && commandLine[i + 1] === "|") {
2373
+ if (currentSegment.trim()) {
2374
+ commands.push(currentSegment.trim());
2375
+ }
2376
+ currentSegment = "";
2377
+ i++;
2378
+ continue;
2379
+ }
2380
+ if (char === "|") {
2381
+ hasPipes = true;
2382
+ if (currentSegment.trim()) {
2383
+ commands.push(currentSegment.trim());
2384
+ }
2385
+ currentSegment = "";
2386
+ continue;
2387
+ }
2388
+ if (char === ";" || char === `
2389
+ ` || char === "&") {
2390
+ if (currentSegment.trim()) {
2391
+ commands.push(currentSegment.trim());
2392
+ }
2393
+ currentSegment = "";
2394
+ continue;
2395
+ }
2396
+ }
2397
+ currentSegment += char;
2398
+ }
2399
+ if (currentSegment.trim()) {
2400
+ commands.push(currentSegment.trim());
2401
+ }
2402
+ return {
2403
+ commands: commands.filter(Boolean),
2404
+ subshellCommands: subshellCommands.filter(Boolean),
2405
+ hasPipes
2406
+ };
2407
+ }
2408
+
1993
2409
  // src/security/exec-policy.ts
1994
2410
  class ExecPolicy {
2411
+ denyRules = [];
1995
2412
  rules = [];
1996
2413
  mode = "auto";
1997
2414
  constructor(initialMode = "auto") {
@@ -2005,12 +2422,24 @@ class ExecPolicy {
2005
2422
  this.mode = mode;
2006
2423
  }
2007
2424
  initDefaultRules() {
2008
- this.addRule(/^(git\s+(status|log|diff|branch|show|rev-parse))/i, "allow", "Safe git query");
2009
- this.addRule(/^(ls|dir|cat|type|grep|rg|find|pwd|echo|head|tail|wc|which|where)\b/i, "allow", "Safe read-only shell command");
2010
- this.addRule(/^(bun\s+(test|--version|-v)|npm\s+(test|--version|-v)|node\s+-v)\b/i, "allow", "Testing & runtime check");
2011
- this.addRule(/^(rm|del|rmdir|format|mkfs)\b/i, "prompt", "Destructive file removal");
2012
- this.addRule(/^(git\s+(push|reset\s+--hard|clean\s+-fd|rebase))\b/i, "prompt", "Destructive git operation");
2013
- this.addRule(/^(curl|wget|fetch|ssh|scp|ftp)\b/i, "prompt", "Network / remote transfer");
2425
+ this.addDenyRule(/(?:^|[/\\])(mkfs|format|fdisk|parted)\b/i, "Destructive filesystem formatting operation");
2426
+ this.addDenyRule(/^dd\s+.*(of=\/dev\/|\/dev\/sd|\/dev\/nvme)/i, "Raw block device write attempt");
2427
+ this.addDenyRule(/(?:^|[/\\])(reboot|shutdown|poweroff|init\s+0)\b/i, "System power manipulation");
2428
+ this.addRule(/^(sudo|su|doas|runas)\b/i, "prompt", "Privilege escalation attempt");
2429
+ this.addRule(/^(rm|del|rmdir|shred|unlink)\b/i, "prompt", "Destructive file removal");
2430
+ this.addRule(/^(chmod|chown|kill|pkill|killall|systemctl|service|crontab)\b/i, "prompt", "System administration & process control");
2431
+ this.addRule(/^(git\s+(push|reset\s+--hard|clean\s+-fd|rebase|branch\s+-D|checkout\s+-f))\b/i, "prompt", "Destructive git operation");
2432
+ this.addRule(/^(curl|wget|fetch|ssh|scp|sftp|ftp|rsync|nc|ncat|netcat|socat|telnet)\b/i, "prompt", "Network & remote transfer");
2433
+ this.addRule(/^(bash|sh|zsh|dash|ksh|cmd(\.exe)?|powershell(\.exe)?|pwsh)\s+(-c|-command|\/c)\b/i, "prompt", "Arbitrary subshell command execution");
2434
+ this.addRule(/^(python|python3|node|bun|perl|ruby)\s+(-c|-e)\b/i, "prompt", "Inline arbitrary code evaluation");
2435
+ this.addRule(/^(eval|exec)\b/i, "prompt", "Dynamic code execution");
2436
+ this.addRule(/^(npm\s+publish|bun\s+publish|cargo\s+publish)\b/i, "prompt", "Package registry publication");
2437
+ this.addRule(/^(git\s+(status|log|diff|branch|show|rev-parse|tag|remote|describe))\b/i, "allow", "Safe git query");
2438
+ 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");
2439
+ 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");
2440
+ }
2441
+ addDenyRule(pattern, description) {
2442
+ this.denyRules.push({ pattern, decision: "deny", description });
2014
2443
  }
2015
2444
  addRule(pattern, decision, description) {
2016
2445
  this.rules.unshift({ pattern, decision, description });
@@ -2033,8 +2462,49 @@ class ExecPolicy {
2033
2462
  }
2034
2463
  evaluate(command) {
2035
2464
  const trimmed = command.trim();
2465
+ if (!trimmed) {
2466
+ return { decision: "allow" };
2467
+ }
2468
+ const parsed = parseShellCommand(trimmed);
2469
+ const subCommands = [...parsed.commands, ...parsed.subshellCommands].map((c) => c.trim()).filter(Boolean);
2470
+ if (subCommands.length > 1 || parsed.subshellCommands.length > 0) {
2471
+ for (const subCmd of subCommands) {
2472
+ const subResult = this.evaluateSingle(subCmd);
2473
+ if (subResult.decision === "deny") {
2474
+ return {
2475
+ decision: "deny",
2476
+ reason: `Chained command contains denied operation: '${subCmd}' (${subResult.reason || "Forbidden"})`
2477
+ };
2478
+ }
2479
+ if (subResult.decision === "prompt") {
2480
+ return {
2481
+ decision: "prompt",
2482
+ reason: `Chained command contains operation requiring confirmation: '${subCmd}' (${subResult.reason || "Requires approval"})`
2483
+ };
2484
+ }
2485
+ }
2486
+ if (parsed.hasPipes && /\|\s*(ba|z|k|c)?sh\b/i.test(trimmed)) {
2487
+ return {
2488
+ decision: "prompt",
2489
+ reason: "Pipeline executes piped input directly into shell interpreter (| sh)"
2490
+ };
2491
+ }
2492
+ return { decision: "allow", reason: "All chained sub-commands are permitted" };
2493
+ }
2494
+ return this.evaluateSingle(trimmed);
2495
+ }
2496
+ evaluateSingle(command) {
2497
+ const trimmed = command.trim();
2498
+ for (const rule of this.denyRules) {
2499
+ if (rule.pattern.test(trimmed)) {
2500
+ return {
2501
+ decision: "deny",
2502
+ reason: rule.description
2503
+ };
2504
+ }
2505
+ }
2036
2506
  if (this.mode === "plan") {
2037
- 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);
2507
+ 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);
2038
2508
  if (isReadOnly) {
2039
2509
  return { decision: "allow", reason: "Read-only inspection allowed in Plan mode" };
2040
2510
  }
@@ -2050,7 +2520,7 @@ class ExecPolicy {
2050
2520
  };
2051
2521
  }
2052
2522
  if (this.mode === "accept-edits") {
2053
- 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);
2523
+ 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);
2054
2524
  if (isReadOnly) {
2055
2525
  return { decision: "allow", reason: "Safe read-only command in accept-edits mode" };
2056
2526
  }
@@ -2067,11 +2537,21 @@ class ExecPolicy {
2067
2537
  };
2068
2538
  }
2069
2539
  }
2540
+ if (this.isRecognizedSafeDevCommand(trimmed)) {
2541
+ return {
2542
+ decision: "allow",
2543
+ reason: "Safe development workspace command"
2544
+ };
2545
+ }
2546
+ const firstWord = trimmed.split(/\s+/)[0] || trimmed;
2070
2547
  return {
2071
- decision: "allow",
2072
- reason: "Auto mode allows execution"
2548
+ decision: "prompt",
2549
+ reason: `Command '${firstWord}' is unclassified and requires confirmation in auto mode`
2073
2550
  };
2074
2551
  }
2552
+ isRecognizedSafeDevCommand(command) {
2553
+ 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);
2554
+ }
2075
2555
  }
2076
2556
 
2077
2557
  // src/session/session.ts
@@ -2204,7 +2684,8 @@ class Session {
2204
2684
  turnId: params.turnId,
2205
2685
  toolName: params.toolName,
2206
2686
  description: params.description,
2207
- command: params.command
2687
+ command: params.command,
2688
+ prefixRule: params.prefixRule
2208
2689
  });
2209
2690
  this.emitEvent({
2210
2691
  type: "StatusChanged",
@@ -2382,6 +2863,7 @@ var applyPatchTool = {
2382
2863
  try {
2383
2864
  mkdirSync4(dirname3(filePath), { recursive: true });
2384
2865
  writeFileSync2(filePath, replacementContent, "utf8");
2866
+ ctx.onFileModified?.(rawPath);
2385
2867
  return { output: `Successfully created new file '${rawPath}'` };
2386
2868
  } catch (err) {
2387
2869
  return {
@@ -2402,7 +2884,31 @@ var applyPatchTool = {
2402
2884
  isError: true
2403
2885
  };
2404
2886
  }
2405
- const firstIndex = originalFileContent.indexOf(targetContent);
2887
+ let targetToFind = targetContent;
2888
+ let replacementToUse = replacementContent;
2889
+ const fileHasCrlf = originalFileContent.includes(`\r
2890
+ `);
2891
+ let firstIndex = originalFileContent.indexOf(targetToFind);
2892
+ if (firstIndex === -1 && fileHasCrlf) {
2893
+ const crlfTarget = targetContent.replace(/\r?\n/g, `\r
2894
+ `);
2895
+ firstIndex = originalFileContent.indexOf(crlfTarget);
2896
+ if (firstIndex !== -1) {
2897
+ targetToFind = crlfTarget;
2898
+ replacementToUse = replacementContent.replace(/\r?\n/g, `\r
2899
+ `);
2900
+ }
2901
+ } else if (firstIndex === -1 && !fileHasCrlf && targetContent.includes(`\r
2902
+ `)) {
2903
+ const lfTarget = targetContent.replace(/\r\n/g, `
2904
+ `);
2905
+ firstIndex = originalFileContent.indexOf(lfTarget);
2906
+ if (firstIndex !== -1) {
2907
+ targetToFind = lfTarget;
2908
+ replacementToUse = replacementContent.replace(/\r\n/g, `
2909
+ `);
2910
+ }
2911
+ }
2406
2912
  if (firstIndex === -1) {
2407
2913
  return {
2408
2914
  output: `Error: targetContent was not found in '${rawPath}'.
@@ -2413,7 +2919,7 @@ var applyPatchTool = {
2413
2919
  isError: true
2414
2920
  };
2415
2921
  }
2416
- const secondIndex = originalFileContent.indexOf(targetContent, firstIndex + 1);
2922
+ const secondIndex = originalFileContent.indexOf(targetToFind, firstIndex + 1);
2417
2923
  if (secondIndex !== -1) {
2418
2924
  return {
2419
2925
  output: `Error: targetContent matched multiple locations in '${rawPath}'.
@@ -2424,8 +2930,9 @@ var applyPatchTool = {
2424
2930
  isError: true
2425
2931
  };
2426
2932
  }
2427
- const newFileContent = originalFileContent.slice(0, firstIndex) + replacementContent + originalFileContent.slice(firstIndex + targetContent.length);
2933
+ const newFileContent = originalFileContent.slice(0, firstIndex) + replacementToUse + originalFileContent.slice(firstIndex + targetToFind.length);
2428
2934
  writeFileSync2(filePath, newFileContent, "utf8");
2935
+ ctx.onFileModified?.(rawPath);
2429
2936
  return {
2430
2937
  output: `Successfully applied patch to '${rawPath}'`
2431
2938
  };
@@ -2537,8 +3044,12 @@ class LinuxSandbox {
2537
3044
  }
2538
3045
  this.hasBwrap = existsSync9("/usr/bin/bwrap") || existsSync9("/bin/bwrap") || existsSync9("/usr/local/bin/bwrap");
2539
3046
  }
2540
- wrapCommand(cmd, profile) {
2541
- if (!this.hasBwrap || profile.kind === "danger-unrestricted") {
3047
+ wrapCommand(cmd, profile, onWarning) {
3048
+ if (profile.kind === "danger-unrestricted") {
3049
+ return cmd;
3050
+ }
3051
+ if (!this.hasBwrap) {
3052
+ onWarning?.("Linux kernel sandbox (Bubblewrap / bwrap) is not available. Command will execute without OS-level namespace isolation.");
2542
3053
  return cmd;
2543
3054
  }
2544
3055
  const bwrapArgs = [
@@ -2554,6 +3065,23 @@ class LinuxSandbox {
2554
3065
  "--tmpfs",
2555
3066
  "/tmp"
2556
3067
  ];
3068
+ const homeDir = process.env.HOME || "/root";
3069
+ const sensitivePaths = [
3070
+ `${homeDir}/.ssh`,
3071
+ `${homeDir}/.aws`,
3072
+ `${homeDir}/.gnupg`,
3073
+ `${homeDir}/.azure`,
3074
+ `${homeDir}/.kube`,
3075
+ `${homeDir}/.docker`,
3076
+ `${homeDir}/.netrc`,
3077
+ "/etc/shadow",
3078
+ "/etc/sudoers"
3079
+ ];
3080
+ for (const sensitive of sensitivePaths) {
3081
+ if (existsSync9(sensitive)) {
3082
+ bwrapArgs.push("--tmpfs", sensitive);
3083
+ }
3084
+ }
2557
3085
  for (const writableRoot of profile.writableRoots) {
2558
3086
  bwrapArgs.push("--bind", writableRoot, writableRoot);
2559
3087
  }
@@ -2603,11 +3131,21 @@ class MacOSSandbox {
2603
3131
  } else {
2604
3132
  rules.push("(deny network*)");
2605
3133
  }
3134
+ const home = process.env.HOME || "/Users/Shared";
3135
+ rules.push(`(deny file-read* (subpath "${home}/.ssh"))`);
3136
+ rules.push(`(deny file-read* (subpath "${home}/.aws"))`);
3137
+ rules.push(`(deny file-read* (subpath "${home}/.gnupg"))`);
3138
+ rules.push(`(deny file-read* (subpath "${home}/.kube"))`);
3139
+ rules.push(`(deny file-read* (subpath "${home}/.docker"))`);
2606
3140
  return rules.join(`
2607
3141
  `);
2608
3142
  }
2609
- wrapCommand(cmd, profile) {
2610
- if (!this.hasSandboxExec || profile.kind === "danger-unrestricted") {
3143
+ wrapCommand(cmd, profile, onWarning) {
3144
+ if (profile.kind === "danger-unrestricted") {
3145
+ return cmd;
3146
+ }
3147
+ if (!this.hasSandboxExec) {
3148
+ onWarning?.("macOS Seatbelt (sandbox-exec) is not available. Command will execute without OS-level namespace isolation.");
2611
3149
  return cmd;
2612
3150
  }
2613
3151
  const policy = this.generateProfile(profile);
@@ -2640,7 +3178,7 @@ class KernelSandboxManager {
2640
3178
  isSandboxingActive: this.windowsSandbox.isSupported() || this.linuxSandbox.isSupported() || this.macOsSandbox.isSupported()
2641
3179
  };
2642
3180
  }
2643
- buildDefaultProfile(cwd, allowNetwork = true) {
3181
+ buildDefaultProfile(cwd, allowNetwork = false) {
2644
3182
  const normCwd = normalize(resolve6(cwd));
2645
3183
  return {
2646
3184
  kind: "workspace-write",
@@ -2655,11 +3193,15 @@ class KernelSandboxManager {
2655
3193
  }
2656
3194
  };
2657
3195
  }
2658
- wrapCommand(cmd, profile) {
3196
+ wrapCommand(cmd, profile, onWarning) {
2659
3197
  if (process.platform === "linux") {
2660
- return this.linuxSandbox.wrapCommand(cmd, profile);
3198
+ return this.linuxSandbox.wrapCommand(cmd, profile, onWarning);
2661
3199
  } else if (process.platform === "darwin") {
2662
- return this.macOsSandbox.wrapCommand(cmd, profile);
3200
+ return this.macOsSandbox.wrapCommand(cmd, profile, onWarning);
3201
+ } else if (process.platform === "win32") {
3202
+ if (!this.windowsSandbox.isSupported() && profile.kind !== "danger-unrestricted") {
3203
+ onWarning?.("Windows JobObject isolation is not available in this environment. Command will execute without OS-level process limits.");
3204
+ }
2663
3205
  }
2664
3206
  return cmd;
2665
3207
  }
@@ -2791,6 +3333,12 @@ class PrefixRulesStore {
2791
3333
  matchesPrefix(cmdTokens, prefixTokens) {
2792
3334
  if (prefixTokens.length > cmdTokens.length)
2793
3335
  return false;
3336
+ const SHELL_OPERATORS = new Set([";", "&&", "||", "|", "&", ";;", "&|"]);
3337
+ for (const token of cmdTokens) {
3338
+ if (SHELL_OPERATORS.has(token) || token.includes(";") || token.includes("&&") || token.includes("||")) {
3339
+ return false;
3340
+ }
3341
+ }
2794
3342
  for (let i = 0;i < prefixTokens.length; i++) {
2795
3343
  const cmd = cmdTokens[i];
2796
3344
  const prefix = prefixTokens[i];
@@ -2846,11 +3394,29 @@ function createShellTool(policy = new ExecPolicy) {
2846
3394
  }
2847
3395
  const args = rawArgs;
2848
3396
  const rulesStore = ctx.prefixRulesStore || globalPrefixRulesStore;
2849
- const cmdTokens = command.split(/\s+/).filter(Boolean);
3397
+ const activePolicy = ctx.execPolicy || policy;
3398
+ const policyDecision = activePolicy.evaluate(command);
3399
+ if (policyDecision.decision === "deny") {
3400
+ return {
3401
+ output: `Error: Command execution denied by policy: ${policyDecision.reason}`,
3402
+ isError: true
3403
+ };
3404
+ }
3405
+ const parsed = parseShellCommand(command);
3406
+ const isCompound = parsed.commands.length > 1 || parsed.subshellCommands.length > 0 || parsed.hasPipes;
2850
3407
  let isEscalated = false;
2851
- if (rulesStore.isApproved(ctx.cwd, cmdTokens)) {
2852
- isEscalated = true;
2853
- } else if (args.sandbox_permissions === "require_escalated") {
3408
+ if (!isCompound) {
3409
+ const cmdTokens = command.split(/\s+/).filter(Boolean);
3410
+ if (rulesStore.isApproved(ctx.cwd, cmdTokens)) {
3411
+ isEscalated = true;
3412
+ }
3413
+ } else {
3414
+ const allSubCommands = [...parsed.commands, ...parsed.subshellCommands];
3415
+ if (allSubCommands.length > 0 && allSubCommands.every((sub) => rulesStore.isApproved(ctx.cwd, sub.split(/\s+/).filter(Boolean)))) {
3416
+ isEscalated = true;
3417
+ }
3418
+ }
3419
+ if (!isEscalated && args.sandbox_permissions === "require_escalated") {
2854
3420
  if (ctx.requestApproval) {
2855
3421
  const promptDesc = args.justification || "Executing command with escalated permissions";
2856
3422
  const approvalResult = await ctx.requestApproval(promptDesc, command, args.prefix_rule);
@@ -2868,16 +3434,8 @@ function createShellTool(policy = new ExecPolicy) {
2868
3434
  isEscalated = true;
2869
3435
  }
2870
3436
  }
2871
- if (!isEscalated) {
2872
- const activePolicy = ctx.execPolicy || policy;
2873
- const policyDecision = activePolicy.evaluate(command);
2874
- if (policyDecision.decision === "deny") {
2875
- return {
2876
- output: `Error: Command execution denied by policy: ${policyDecision.reason}`,
2877
- isError: true
2878
- };
2879
- }
2880
- if (policyDecision.decision === "prompt" && ctx.requestApproval) {
3437
+ if (policyDecision.decision === "prompt" && !isEscalated) {
3438
+ if (ctx.requestApproval) {
2881
3439
  const approvalResult = await ctx.requestApproval(policyDecision.reason || "Executing external command", command);
2882
3440
  const isAllowed = typeof approvalResult === "boolean" ? approvalResult : approvalResult?.allowed;
2883
3441
  if (!isAllowed) {
@@ -2886,17 +3444,22 @@ function createShellTool(policy = new ExecPolicy) {
2886
3444
  isError: true
2887
3445
  };
2888
3446
  }
3447
+ } else {
3448
+ return {
3449
+ output: `Error: Command execution requires user confirmation: '${command}' (${policyDecision.reason || "Requires approval"})`,
3450
+ isError: true
3451
+ };
2889
3452
  }
2890
3453
  }
2891
3454
  const timeoutMs = typeof args.timeoutMs === "number" ? args.timeoutMs : 30000;
2892
3455
  const isWindows = process.platform === "win32";
2893
3456
  const baseCmd = isWindows ? ["cmd.exe", "/d", "/s", "/c", command] : ["/bin/sh", "-c", command];
2894
3457
  const ephemeralScratchpad = globalEphemeralWorkspace.createScratchpad(ctx.turnId);
2895
- const sandboxProfile = globalKernelSandbox.buildDefaultProfile(ctx.cwd);
2896
- if (isEscalated) {
2897
- sandboxProfile.allowNetwork = true;
2898
- }
2899
- const wrappedCmd = globalKernelSandbox.wrapCommand(baseCmd, sandboxProfile);
3458
+ const sandboxProfile = globalKernelSandbox.buildDefaultProfile(ctx.cwd, isEscalated);
3459
+ let sandboxNotice = null;
3460
+ const wrappedCmd = globalKernelSandbox.wrapCommand(baseCmd, sandboxProfile, (w) => {
3461
+ sandboxNotice = w;
3462
+ });
2900
3463
  try {
2901
3464
  const proc = Bun.spawn(wrappedCmd, {
2902
3465
  cwd: ctx.cwd,
@@ -2950,10 +3513,38 @@ ${result.stderr.trim()}`);
2950
3513
  2. If this is a test failure, trace the failure in source code and fix the root cause before re-running.
2951
3514
  3. If this is a missing command/module, install or configure the prerequisite.`);
2952
3515
  }
2953
- const output = outputParts.join(`
3516
+ let rawOutput = outputParts.join(`
2954
3517
  `) || "[Command completed with no output]";
3518
+ const lines = rawOutput.split(`
3519
+ `);
3520
+ const MAX_LINES = 250;
3521
+ const MAX_CHARS = 30000;
3522
+ if (lines.length > MAX_LINES) {
3523
+ const headLines = lines.slice(0, 125).join(`
3524
+ `);
3525
+ const tailLines = lines.slice(-100).join(`
3526
+ `);
3527
+ rawOutput = `${headLines}
3528
+
3529
+ ... [${lines.length - 225} lines truncated for context efficiency] ...
3530
+
3531
+ ${tailLines}`;
3532
+ }
3533
+ if (rawOutput.length > MAX_CHARS) {
3534
+ const headChars = rawOutput.slice(0, 16000);
3535
+ const tailChars = rawOutput.slice(-12000);
3536
+ rawOutput = `${headChars}
3537
+
3538
+ ... [${rawOutput.length - 28000} characters truncated for context efficiency] ...
3539
+
3540
+ ${tailChars}`;
3541
+ }
3542
+ if (sandboxNotice) {
3543
+ rawOutput = `[Sandbox Notice]: ${sandboxNotice}
3544
+ ${rawOutput}`;
3545
+ }
2955
3546
  return {
2956
- output,
3547
+ output: rawOutput,
2957
3548
  isError: result.code !== 0
2958
3549
  };
2959
3550
  } catch (err) {
@@ -2972,7 +3563,7 @@ ${result.stderr.trim()}`);
2972
3563
  }
2973
3564
  var shellTool = createShellTool();
2974
3565
  // src/tools/handlers/file-ops.ts
2975
- import { readdirSync as readdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync3, existsSync as existsSync12, statSync as statSync4, mkdirSync as mkdirSync6 } from "fs";
3566
+ import { readdirSync as readdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync3, existsSync as existsSync12, statSync as statSync4, lstatSync, mkdirSync as mkdirSync6 } from "fs";
2976
3567
  import { resolve as resolve8, dirname as dirname5 } from "path";
2977
3568
  var DEFAULT_MAX_UNPAGINATED_LINES = 250;
2978
3569
  var readFileTool = {
@@ -3084,6 +3675,7 @@ var viewFileTool = {
3084
3675
  name: "view_file",
3085
3676
  description: "View file content with surgical line-range support. Alias for read_file matching Antigravity & Claude Code conventions."
3086
3677
  };
3678
+ var DEFAULT_MAX_DIR_ENTRIES = 500;
3087
3679
  var listDirTool = {
3088
3680
  name: "list_dir",
3089
3681
  description: "List contents of a directory with file names and types.",
@@ -3100,13 +3692,31 @@ var listDirTool = {
3100
3692
  }
3101
3693
  try {
3102
3694
  const entries = readdirSync4(dirPath);
3103
- const formatted = entries.map((entry) => {
3695
+ const totalEntries = entries.length;
3696
+ const displayEntries = entries.slice(0, DEFAULT_MAX_DIR_ENTRIES);
3697
+ const formatted = displayEntries.map((entry) => {
3104
3698
  const full = resolve8(dirPath, entry);
3105
- const isDir = statSync4(full).isDirectory();
3699
+ let isDir = false;
3700
+ try {
3701
+ isDir = statSync4(full).isDirectory();
3702
+ } catch {
3703
+ try {
3704
+ const lst = lstatSync(full);
3705
+ if (lst.isSymbolicLink())
3706
+ return `[SYMLINK] ${entry}`;
3707
+ } catch {}
3708
+ return `[FILE] ${entry}`;
3709
+ }
3106
3710
  return `${isDir ? "[DIR]" : "[FILE]"} ${entry}`;
3107
3711
  });
3108
- return { output: formatted.join(`
3109
- `) || "[Empty directory]" };
3712
+ let output = formatted.join(`
3713
+ `) || "[Empty directory]";
3714
+ if (totalEntries > DEFAULT_MAX_DIR_ENTRIES) {
3715
+ output += `
3716
+
3717
+ ... [Truncated: ${totalEntries - DEFAULT_MAX_DIR_ENTRIES} more entries. Showing first ${DEFAULT_MAX_DIR_ENTRIES} of ${totalEntries}]`;
3718
+ }
3719
+ return { output };
3110
3720
  } catch (err) {
3111
3721
  return { output: `Failed to list directory: ${err instanceof Error ? err.message : String(err)}`, isError: true };
3112
3722
  }
@@ -3147,6 +3757,7 @@ var writeFileTool = {
3147
3757
  try {
3148
3758
  mkdirSync6(dirname5(filePath), { recursive: true });
3149
3759
  writeFileSync3(filePath, String(args.content ?? ""), "utf8");
3760
+ ctx.onFileModified?.(rawPath);
3150
3761
  return { output: `Successfully wrote to '${args.path}'` };
3151
3762
  } catch (err) {
3152
3763
  return {
@@ -3640,6 +4251,12 @@ class CodeModeToolsProxy {
3640
4251
  if (result.isError) {
3641
4252
  throw new Error(`Tool '${tool.name}' failed: ${result.output}`);
3642
4253
  }
4254
+ if (normalizedName === "write_file" || normalizedName === "apply_patch" || tool.name === "write_file" || tool.name === "apply_patch") {
4255
+ const pathArg = String(args?.path || "");
4256
+ if (pathArg) {
4257
+ this.context.onFileModified?.(pathArg);
4258
+ }
4259
+ }
3643
4260
  return result.output;
3644
4261
  };
3645
4262
  }
@@ -3713,6 +4330,9 @@ class SandboxedWorkerHost {
3713
4330
  const fetch = undefined;
3714
4331
  const XMLHttpRequest = undefined;
3715
4332
  const WebSocket = undefined;
4333
+ const globalThis = Object.freeze(Object.create(null));
4334
+ const global = undefined;
4335
+ const window = undefined;
3716
4336
 
3717
4337
  return (async () => {
3718
4338
  ${cleanCode}
@@ -4307,7 +4927,11 @@ class AgentGraphStore {
4307
4927
  if (dbPath !== ":memory:") {
4308
4928
  const dir = resolve11(dbPath, "..");
4309
4929
  if (!existsSync15(dir)) {
4310
- mkdirSync7(dir, { recursive: true });
4930
+ try {
4931
+ mkdirSync7(dir, { recursive: true });
4932
+ } catch (err) {
4933
+ console.warn(`[AgentGraphStore] Failed to create database directory '${dir}':`, err);
4934
+ }
4311
4935
  }
4312
4936
  }
4313
4937
  this.db = new Database2(dbPath);
@@ -4389,25 +5013,45 @@ class AgentGraphStore {
4389
5013
  close() {
4390
5014
  try {
4391
5015
  this.db.close();
4392
- } catch {}
5016
+ } catch (err) {
5017
+ console.warn("[AgentGraphStore] Failed to close database cleanly:", err);
5018
+ }
4393
5019
  }
4394
5020
  }
4395
5021
 
4396
5022
  // src/agents/spawner.ts
4397
5023
  class AgentSpawner {
4398
5024
  parentSession;
5025
+ depth;
4399
5026
  subAgents = new Map;
4400
5027
  nextAgentId = 1;
4401
5028
  roleRegistry;
4402
5029
  parentIdentity;
4403
5030
  graphStore;
4404
- constructor(parentSession, roleRegistry, parentIdentity, graphStore) {
5031
+ maxConcurrentAgents;
5032
+ maxDepth;
5033
+ maxRetainedCompleted;
5034
+ defaultTokenBudget;
5035
+ constructor(parentSession, roleRegistry, parentIdentity, graphStore, options, depth = 0) {
4405
5036
  this.parentSession = parentSession;
5037
+ this.depth = depth;
4406
5038
  this.roleRegistry = roleRegistry || new AgentRoleRegistry;
4407
5039
  this.parentIdentity = parentIdentity || createAgentIdentity(undefined, "groupy-main");
4408
5040
  this.graphStore = graphStore || new AgentGraphStore;
5041
+ const envMax = process.env.PIKAA_MAX_SUBAGENTS ? parseInt(process.env.PIKAA_MAX_SUBAGENTS, 10) : NaN;
5042
+ this.maxConcurrentAgents = options?.maxConcurrentAgents ?? (!isNaN(envMax) && envMax > 0 ? envMax : 5);
5043
+ this.maxDepth = options?.maxDepth ?? 2;
5044
+ this.maxRetainedCompleted = options?.maxRetainedCompleted ?? 20;
5045
+ this.defaultTokenBudget = options?.defaultTokenBudget ?? 50000;
4409
5046
  }
4410
5047
  async spawnAgent(params) {
5048
+ if (this.depth >= this.maxDepth) {
5049
+ throw new GroupyError(`Recursion limit exceeded: Maximum sub-agent nesting depth (${this.maxDepth}) reached.`);
5050
+ }
5051
+ const activeRunningCount = Array.from(this.subAgents.values()).filter((h) => h.status === "running").length;
5052
+ if (activeRunningCount >= this.maxConcurrentAgents) {
5053
+ 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.`);
5054
+ }
4411
5055
  const roleName = params.role || "default";
4412
5056
  const roleConfig = this.roleRegistry.getRole(roleName);
4413
5057
  const agentIndex = this.nextAgentId++;
@@ -4419,7 +5063,18 @@ class AgentSpawner {
4419
5063
 
4420
5064
  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.`);
4421
5065
  const baseTools = params.tools || this.parentSession.tools;
4422
- const effectiveTools = this.roleRegistry.filterRouterForRole(baseTools, roleName);
5066
+ let effectiveTools = this.roleRegistry.filterRouterForRole(baseTools, roleName);
5067
+ if (this.depth + 1 >= this.maxDepth) {
5068
+ const sanitizedRouter = new ToolRouter;
5069
+ for (const t of effectiveTools.list()) {
5070
+ if (t.name !== "spawn_agent") {
5071
+ sanitizedRouter.register(t);
5072
+ }
5073
+ }
5074
+ effectiveTools = sanitizedRouter;
5075
+ }
5076
+ const tokenBudget = params.maxTokens ?? this.defaultTokenBudget;
5077
+ let accumulatedTokens = 0;
4423
5078
  const childSession = new Session({
4424
5079
  threadId: agentId,
4425
5080
  model: effectiveModel,
@@ -4443,45 +5098,87 @@ Your nickname is ${nickname}. Your assigned task is: '${params.taskName}'. Focus
4443
5098
  createdAt: Date.now(),
4444
5099
  identity: childIdentity,
4445
5100
  session: childSession,
4446
- promise: taskPromise
5101
+ promise: taskPromise,
5102
+ depth: this.depth + 1,
5103
+ tokenBudget,
5104
+ totalTokens: 0
4447
5105
  };
4448
5106
  try {
4449
5107
  this.graphStore.upsertEdge(this.parentSession.threadId, agentId, "open");
4450
- } catch {}
5108
+ } catch (err) {
5109
+ console.warn(`[AgentSpawner] Failed to record edge in graph store for agent '${agentId}':`, err);
5110
+ }
4451
5111
  let collectedAgentText = "";
4452
5112
  childSession.onEvent((event) => {
4453
5113
  if (event.msg.type === "AgentMessageDelta") {
4454
5114
  collectedAgentText += event.msg.delta;
5115
+ accumulatedTokens += Math.ceil(event.msg.delta.length / 4);
5116
+ handle.totalTokens = accumulatedTokens;
5117
+ if (accumulatedTokens > tokenBudget && handle.status === "running") {
5118
+ handle.status = "error";
5119
+ handle.error = `Token budget limit exceeded (${tokenBudget} tokens).`;
5120
+ childSession.interrupt();
5121
+ try {
5122
+ this.graphStore.setEdgeStatus(agentId, "closed");
5123
+ } catch (err) {
5124
+ console.warn(`[AgentSpawner] Failed to update edge status for agent '${agentId}':`, err);
5125
+ }
5126
+ rejectPromise(new Error(handle.error));
5127
+ }
4455
5128
  } else if (event.msg.type === "TurnCompleted") {
4456
- handle.status = "completed";
4457
- handle.lastOutput = collectedAgentText.trim();
4458
- try {
4459
- this.graphStore.setEdgeStatus(agentId, "closed");
4460
- } catch {}
4461
- resolvePromise(handle.lastOutput);
5129
+ if (handle.status === "running") {
5130
+ handle.status = "completed";
5131
+ handle.lastOutput = collectedAgentText.trim();
5132
+ handle.totalTokens = accumulatedTokens;
5133
+ try {
5134
+ this.graphStore.setEdgeStatus(agentId, "closed");
5135
+ } catch (err) {
5136
+ console.warn(`[AgentSpawner] Failed to update edge status for agent '${agentId}':`, err);
5137
+ }
5138
+ this.pruneCompletedAgents();
5139
+ resolvePromise(handle.lastOutput);
5140
+ }
4462
5141
  } else if (event.msg.type === "Error") {
4463
- handle.status = "error";
4464
- handle.error = event.msg.message;
4465
- try {
4466
- this.graphStore.setEdgeStatus(agentId, "closed");
4467
- } catch {}
4468
- rejectPromise(new Error(event.msg.message));
5142
+ if (handle.status === "running") {
5143
+ handle.status = "error";
5144
+ handle.error = event.msg.message;
5145
+ handle.totalTokens = accumulatedTokens;
5146
+ try {
5147
+ this.graphStore.setEdgeStatus(agentId, "closed");
5148
+ } catch (err) {
5149
+ console.warn(`[AgentSpawner] Failed to update edge status for agent '${agentId}':`, err);
5150
+ }
5151
+ this.pruneCompletedAgents();
5152
+ rejectPromise(new Error(event.msg.message));
5153
+ }
4469
5154
  } else if (event.msg.type === "StatusChanged" && event.msg.status === "interrupted") {
4470
- handle.status = "interrupted";
4471
- try {
4472
- this.graphStore.setEdgeStatus(agentId, "closed");
4473
- } catch {}
4474
- resolvePromise(collectedAgentText.trim() || "[Task was interrupted]");
5155
+ if (handle.status === "running") {
5156
+ handle.status = "interrupted";
5157
+ handle.totalTokens = accumulatedTokens;
5158
+ try {
5159
+ this.graphStore.setEdgeStatus(agentId, "closed");
5160
+ } catch (err) {
5161
+ console.warn(`[AgentSpawner] Failed to update edge status for agent '${agentId}':`, err);
5162
+ }
5163
+ this.pruneCompletedAgents();
5164
+ resolvePromise(collectedAgentText.trim() || "[Task was interrupted]");
5165
+ }
4475
5166
  }
4476
5167
  });
4477
5168
  this.subAgents.set(agentId, handle);
4478
5169
  childSession.prompt(params.message).catch((err) => {
4479
- handle.status = "error";
4480
- handle.error = err instanceof Error ? err.message : String(err);
4481
- try {
4482
- this.graphStore.setEdgeStatus(agentId, "closed");
4483
- } catch {}
4484
- rejectPromise(err);
5170
+ if (handle.status === "running") {
5171
+ handle.status = "error";
5172
+ handle.error = err instanceof Error ? err.message : String(err);
5173
+ handle.totalTokens = accumulatedTokens;
5174
+ try {
5175
+ this.graphStore.setEdgeStatus(agentId, "closed");
5176
+ } catch (storeErr) {
5177
+ console.warn(`[AgentSpawner] Failed to update edge status for agent '${agentId}':`, storeErr);
5178
+ }
5179
+ this.pruneCompletedAgents();
5180
+ rejectPromise(err);
5181
+ }
4485
5182
  });
4486
5183
  return {
4487
5184
  id: handle.id,
@@ -4490,9 +5187,22 @@ Your nickname is ${nickname}. Your assigned task is: '${params.taskName}'. Focus
4490
5187
  role: handle.role,
4491
5188
  status: handle.status,
4492
5189
  createdAt: handle.createdAt,
4493
- agentRuntimeId: childIdentity.agentRuntimeId
5190
+ agentRuntimeId: childIdentity.agentRuntimeId,
5191
+ depth: handle.depth,
5192
+ tokenBudget: handle.tokenBudget,
5193
+ totalTokens: handle.totalTokens
4494
5194
  };
4495
5195
  }
5196
+ pruneCompletedAgents() {
5197
+ const finishedHandles = Array.from(this.subAgents.values()).filter((h) => h.status !== "running");
5198
+ if (finishedHandles.length > this.maxRetainedCompleted) {
5199
+ finishedHandles.sort((a, b) => a.createdAt - b.createdAt);
5200
+ const toRemove = finishedHandles.slice(0, finishedHandles.length - this.maxRetainedCompleted);
5201
+ for (const h of toRemove) {
5202
+ this.subAgents.delete(h.id);
5203
+ }
5204
+ }
5205
+ }
4496
5206
  async waitAgent(agentIdOrTaskName, timeoutMs = 60000) {
4497
5207
  if (!agentIdOrTaskName) {
4498
5208
  const handles = Array.from(this.subAgents.values());
@@ -4546,9 +5256,36 @@ Your nickname is ${nickname}. Your assigned task is: '${params.taskName}'. Focus
4546
5256
  handle.status = "interrupted";
4547
5257
  try {
4548
5258
  this.graphStore.setEdgeStatus(agentId, "closed");
4549
- } catch {}
5259
+ } catch (err) {
5260
+ console.warn(`[AgentSpawner] Failed to close edge for agent '${agentId}':`, err);
5261
+ }
5262
+ this.pruneCompletedAgents();
4550
5263
  return `Sub-agent ${handle.nickname} (${agentId}) interrupted and closed.`;
4551
5264
  }
5265
+ removeAgent(agentId) {
5266
+ const handle = this.subAgents.get(agentId);
5267
+ if (!handle)
5268
+ return false;
5269
+ if (handle.status === "running") {
5270
+ handle.session.interrupt();
5271
+ }
5272
+ try {
5273
+ this.graphStore.setEdgeStatus(agentId, "closed");
5274
+ } catch (err) {
5275
+ console.warn(`[AgentSpawner] Failed to close edge on removal for '${agentId}':`, err);
5276
+ }
5277
+ return this.subAgents.delete(agentId);
5278
+ }
5279
+ clearCompleted() {
5280
+ let cleared = 0;
5281
+ for (const [id, handle] of this.subAgents.entries()) {
5282
+ if (handle.status !== "running") {
5283
+ this.subAgents.delete(id);
5284
+ cleared++;
5285
+ }
5286
+ }
5287
+ return cleared;
5288
+ }
4552
5289
  listAgents() {
4553
5290
  const list = [];
4554
5291
  for (const handle of this.subAgents.values()) {
@@ -4560,7 +5297,10 @@ Your nickname is ${nickname}. Your assigned task is: '${params.taskName}'. Focus
4560
5297
  status: handle.status,
4561
5298
  createdAt: handle.createdAt,
4562
5299
  agentRuntimeId: handle.identity.agentRuntimeId,
4563
- lastOutput: handle.lastOutput
5300
+ lastOutput: handle.lastOutput,
5301
+ depth: handle.depth,
5302
+ tokenBudget: handle.tokenBudget,
5303
+ totalTokens: handle.totalTokens
4564
5304
  });
4565
5305
  }
4566
5306
  return list;
@@ -4594,6 +5334,10 @@ function createMultiAgentTools(spawner) {
4594
5334
  model: {
4595
5335
  type: "string",
4596
5336
  description: "Optional model override. Defaults to inheriting parent model."
5337
+ },
5338
+ max_tokens: {
5339
+ type: "number",
5340
+ description: "Optional token budget limit for the sub-agent (default: 50000)."
4597
5341
  }
4598
5342
  },
4599
5343
  required: ["task_name", "message"]
@@ -4603,8 +5347,9 @@ function createMultiAgentTools(spawner) {
4603
5347
  const message = String(args.message || "");
4604
5348
  const role = args.role ? String(args.role) : undefined;
4605
5349
  const model = args.model ? String(args.model) : undefined;
5350
+ const maxTokens = typeof args.max_tokens === "number" ? args.max_tokens : undefined;
4606
5351
  try {
4607
- const handle = await spawner.spawnAgent({ taskName, message, role, model });
5352
+ const handle = await spawner.spawnAgent({ taskName, message, role, model, maxTokens });
4608
5353
  return {
4609
5354
  output: `Successfully spawned sub-agent '${handle.id}' with role '${handle.role}' for task '${handle.taskName}' (status: ${handle.status}). Use 'wait_agent' to collect results.`
4610
5355
  };
@@ -5898,7 +6643,8 @@ class SessionPersistenceManager {
5898
6643
  loadSession(threadId) {
5899
6644
  try {
5900
6645
  return this.store.restoreSession(threadId);
5901
- } catch {
6646
+ } catch (err) {
6647
+ console.warn(`[SessionPersistenceManager] Failed to restore session '${threadId}':`, err);
5902
6648
  return null;
5903
6649
  }
5904
6650
  }
@@ -5906,7 +6652,9 @@ class SessionPersistenceManager {
5906
6652
  for (const unsub of this.unsubscribers) {
5907
6653
  try {
5908
6654
  unsub();
5909
- } catch {}
6655
+ } catch (err) {
6656
+ console.warn("[SessionPersistenceManager] Error in unbindSession callback:", err);
6657
+ }
5910
6658
  }
5911
6659
  this.unsubscribers = [];
5912
6660
  }
@@ -5933,7 +6681,9 @@ class SessionPersistenceManager {
5933
6681
  for (const unsub of this.unsubscribers) {
5934
6682
  try {
5935
6683
  unsub();
5936
- } catch {}
6684
+ } catch (err) {
6685
+ console.warn("[SessionPersistenceManager] Error closing session persistence listener:", err);
6686
+ }
5937
6687
  }
5938
6688
  this.unsubscribers = [];
5939
6689
  this.store.close();
@@ -6268,7 +7018,9 @@ class MemoryStore {
6268
7018
  if (!existsSync20(dir)) {
6269
7019
  try {
6270
7020
  mkdirSync11(dir, { recursive: true });
6271
- } catch {}
7021
+ } catch (err) {
7022
+ console.warn(`[MemoryStore] Failed to create custom memory directory '${dir}':`, err);
7023
+ }
6272
7024
  }
6273
7025
  return dir;
6274
7026
  }
@@ -6277,7 +7029,9 @@ class MemoryStore {
6277
7029
  if (!existsSync20(dir)) {
6278
7030
  try {
6279
7031
  mkdirSync11(dir, { recursive: true });
6280
- } catch {}
7032
+ } catch (err) {
7033
+ console.warn(`[MemoryStore] Failed to create project memory directory '${dir}':`, err);
7034
+ }
6281
7035
  }
6282
7036
  return dir;
6283
7037
  }
@@ -6421,7 +7175,9 @@ class MemoryStore {
6421
7175
  `)[0] || parsed.name,
6422
7176
  file: f
6423
7177
  });
6424
- } catch {}
7178
+ } catch (err) {
7179
+ console.warn(`[MemoryStore] Failed to parse topic memory file '${f}':`, err);
7180
+ }
6425
7181
  }
6426
7182
  const indexLines = [
6427
7183
  "# Project Auto-Memory Index",
@@ -6463,7 +7219,9 @@ class MemoryStore {
6463
7219
  try {
6464
7220
  const full = join12(memoryDir, f);
6465
7221
  list.push(this.parseTopicFile(readFileSync12(full, "utf8"), full));
6466
- } catch {}
7222
+ } catch (err) {
7223
+ console.warn(`[MemoryStore] Failed to read topic memory file '${f}':`, err);
7224
+ }
6467
7225
  }
6468
7226
  return list;
6469
7227
  }
@@ -7397,7 +8155,7 @@ function parsePatch(oldSrc, newSrc, contextLines = 3) {
7397
8155
  // package.json
7398
8156
  var package_default = {
7399
8157
  name: "@pikaa-ai/pikaa",
7400
- version: "0.3.28",
8158
+ version: "0.4.0",
7401
8159
  description: "PIKAA CLI - AI coding agent that runs locally in your terminal.",
7402
8160
  main: "./dist/index.js",
7403
8161
  module: "./dist/index.js",
@@ -7429,14 +8187,14 @@ var package_default = {
7429
8187
  prepublishOnly: "bun run build:js"
7430
8188
  },
7431
8189
  optionalDependencies: {
7432
- "@pikaa-ai/pikaa-linux-x64": "0.3.28",
7433
- "@pikaa-ai/pikaa-linux-x64-musl": "0.3.28",
7434
- "@pikaa-ai/pikaa-linux-arm64": "0.3.28",
7435
- "@pikaa-ai/pikaa-linux-arm64-musl": "0.3.28",
7436
- "@pikaa-ai/pikaa-darwin-x64": "0.3.28",
7437
- "@pikaa-ai/pikaa-darwin-arm64": "0.3.28",
7438
- "@pikaa-ai/pikaa-windows-x64": "0.3.28",
7439
- "@pikaa-ai/pikaa-windows-arm64": "0.3.28"
8190
+ "@pikaa-ai/pikaa-linux-x64": "0.4.0",
8191
+ "@pikaa-ai/pikaa-linux-x64-musl": "0.4.0",
8192
+ "@pikaa-ai/pikaa-linux-arm64": "0.4.0",
8193
+ "@pikaa-ai/pikaa-linux-arm64-musl": "0.4.0",
8194
+ "@pikaa-ai/pikaa-darwin-x64": "0.4.0",
8195
+ "@pikaa-ai/pikaa-darwin-arm64": "0.4.0",
8196
+ "@pikaa-ai/pikaa-windows-x64": "0.4.0",
8197
+ "@pikaa-ai/pikaa-windows-arm64": "0.4.0"
7440
8198
  },
7441
8199
  keywords: [
7442
8200
  "ai",
@@ -11068,9 +11826,15 @@ class CliRepl {
11068
11826
  try {
11069
11827
  const decision = await promptToolApproval(msg);
11070
11828
  if (decision === "always") {
11071
- this.session.execPolicy.addRule(/.*/, "allow", "User allowed all actions for this session");
11072
- this.session.resolveApproval(msg.approvalId, true);
11073
- this.spinner.start(`Executing approved action (auto-approved for session)...`, this.turnStartTime);
11829
+ if (msg.prefixRule && Array.isArray(msg.prefixRule) && msg.prefixRule.length > 0) {
11830
+ globalPrefixRulesStore.addRule(this.session.cwd, msg.prefixRule);
11831
+ this.session.resolveApproval(msg.approvalId, { allowed: true, rememberPrefix: true });
11832
+ this.spinner.start(`Executing approved action (saved prefix rule)...`, this.turnStartTime);
11833
+ } else {
11834
+ this.session.execPolicy.addRule(/.*/, "allow", "User allowed all standard actions for this session");
11835
+ this.session.resolveApproval(msg.approvalId, true);
11836
+ this.spinner.start(`Executing approved action (auto-approved for session)...`, this.turnStartTime);
11837
+ }
11074
11838
  } else if (decision === "yes") {
11075
11839
  this.session.resolveApproval(msg.approvalId, true);
11076
11840
  this.spinner.start(`Executing approved action...`, this.turnStartTime);