@openape/ape-agent 2.7.0 → 2.7.2

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.
Files changed (2) hide show
  1. package/dist/bridge.mjs +454 -73
  2. package/package.json +4 -4
package/dist/bridge.mjs CHANGED
@@ -333,7 +333,7 @@ async function prompt(message, opts = {}) {
333
333
  }
334
334
  throw new Error(`Unknown prompt type: ${opts.type}`);
335
335
  }
336
- var src, hasRequiredSrc, srcExports, picocolors, hasRequiredPicocolors, picocolorsExports, e, Q, P$1, X, DD, uD, FD, m, L$1, N, I, r, tD, eD, iD, v, CD, w$1, W$1, rD, R, y, V$1, z, ED, _, nD, oD, aD, c, S, AD, pD, h, x, fD, bD, mD, Y, wD, SD, $D, q, jD, PD, V, u, le, L, W, C, o, d, k, P, A, T, F, w, B, he, ye, ve, fe, kCancel;
336
+ var src, hasRequiredSrc, srcExports, picocolors, hasRequiredPicocolors, picocolorsExports, e, Q, P$1, X, DD, uD, FD, m, L$1, N, I, r, tD, eD, iD, v, CD, w$1, W$1, rD, R, y, V$1, z, ED, _, nD, oD, aD, c, S, AD, pD, h, x, fD, bD, mD, Y, wD, SD, $D, q2, jD, PD, V, u, le, L, W, C, o, d, k, P, A, T, F, w, B, he, ye, ve, fe, kCancel;
337
337
  var init_prompt = __esm({
338
338
  "../../node_modules/.pnpm/consola@3.4.2/node_modules/consola/dist/chunks/prompt.mjs"() {
339
339
  "use strict";
@@ -609,10 +609,10 @@ var init_prompt = __esm({
609
609
  };
610
610
  SD = Object.defineProperty;
611
611
  $D = (t2, u3, F3) => u3 in t2 ? SD(t2, u3, { enumerable: true, configurable: true, writable: true, value: F3 }) : t2[u3] = F3;
612
- q = (t2, u3, F3) => ($D(t2, typeof u3 != "symbol" ? u3 + "" : u3, F3), F3);
612
+ q2 = (t2, u3, F3) => ($D(t2, typeof u3 != "symbol" ? u3 + "" : u3, F3), F3);
613
613
  jD = class extends x {
614
614
  constructor(u3) {
615
- super(u3, false), q(this, "options"), q(this, "cursor", 0), this.options = u3.options, this.cursor = this.options.findIndex(({ value: F3 }) => F3 === u3.initialValue), this.cursor === -1 && (this.cursor = 0), this.changeValue(), this.on("cursor", (F3) => {
615
+ super(u3, false), q2(this, "options"), q2(this, "cursor", 0), this.options = u3.options, this.cursor = this.options.findIndex(({ value: F3 }) => F3 === u3.initialValue), this.cursor === -1 && (this.cursor = 0), this.changeValue(), this.on("cursor", (F3) => {
616
616
  switch (F3) {
617
617
  case "left":
618
618
  case "up":
@@ -1036,7 +1036,7 @@ var require_shell_quote = __commonJS({
1036
1036
  import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
1037
1037
  import { homedir as homedir9 } from "os";
1038
1038
  import { join as join9 } from "path";
1039
- import process2 from "process";
1039
+ import process3 from "process";
1040
1040
 
1041
1041
  // ../../packages/cli-auth/dist/index.js
1042
1042
  import { ofetch } from "ofetch";
@@ -1510,11 +1510,14 @@ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as read
1510
1510
  import { homedir as homedir6 } from "os";
1511
1511
  import { join as join6 } from "path";
1512
1512
 
1513
- // ../../packages/apes/dist/chunk-L2V3CW5B.js
1513
+ // ../../packages/apes/dist/chunk-AFTJZVOQ.js
1514
1514
  import { spawn } from "child_process";
1515
1515
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
1516
1516
  import { homedir as homedir3 } from "os";
1517
1517
  import { dirname, normalize, resolve } from "path";
1518
+ import { homedir as homedir22 } from "os";
1519
+ import { resolve as resolve2 } from "path";
1520
+ import process2 from "process";
1518
1521
  import { execFileSync } from "child_process";
1519
1522
  import { execFileSync as execFileSync2 } from "child_process";
1520
1523
  var DEFAULT_TIMEOUT_MS = 5 * 60 * 1e3;
@@ -1526,6 +1529,57 @@ function capStdio(s2) {
1526
1529
  return `${buf.subarray(0, MAX_STDIO_BYTES).toString("utf8")}
1527
1530
  [truncated to ${MAX_STDIO_BYTES} bytes]`;
1528
1531
  }
1532
+ function runApeShell(cmd, timeoutMs = DEFAULT_TIMEOUT_MS) {
1533
+ return new Promise((resolveResult) => {
1534
+ const child = spawn(BIN, ["-c", cmd], {
1535
+ env: { ...process.env, APE_WAIT: "1" },
1536
+ stdio: ["ignore", "pipe", "pipe"]
1537
+ });
1538
+ let stdout2 = "";
1539
+ let stderr = "";
1540
+ let timedOut = false;
1541
+ let spawnError = null;
1542
+ child.stdout.on("data", (chunk) => {
1543
+ stdout2 += chunk.toString("utf8");
1544
+ });
1545
+ child.stderr.on("data", (chunk) => {
1546
+ stderr += chunk.toString("utf8");
1547
+ });
1548
+ child.on("error", (err) => {
1549
+ spawnError = err;
1550
+ });
1551
+ const timer = setTimeout(() => {
1552
+ timedOut = true;
1553
+ child.kill("SIGTERM");
1554
+ setTimeout(() => {
1555
+ try {
1556
+ child.kill("SIGKILL");
1557
+ } catch {
1558
+ }
1559
+ }, 5e3);
1560
+ }, timeoutMs);
1561
+ child.on("close", (code) => {
1562
+ clearTimeout(timer);
1563
+ if (spawnError) {
1564
+ resolveResult({
1565
+ stdout: "",
1566
+ stderr: "",
1567
+ exit_code: -1,
1568
+ error: spawnError.message,
1569
+ hint: `Could not exec '${BIN}'. The agent host needs @openape/apes installed globally so ape-shell is on PATH.`
1570
+ });
1571
+ return;
1572
+ }
1573
+ resolveResult({
1574
+ stdout: capStdio(stdout2),
1575
+ stderr: capStdio(stderr),
1576
+ exit_code: code ?? -1,
1577
+ ...timedOut ? { timed_out: true } : {}
1578
+ });
1579
+ });
1580
+ });
1581
+ }
1582
+ var DEFAULTS = { DEFAULT_TIMEOUT_MS };
1529
1583
  var bashTools = [
1530
1584
  {
1531
1585
  name: "bash",
@@ -1549,52 +1603,8 @@ var bashTools = [
1549
1603
  if (typeof a2.cmd !== "string" || a2.cmd.trim() === "") {
1550
1604
  throw new Error("cmd must be a non-empty string");
1551
1605
  }
1552
- const timeout = typeof a2.timeout_ms === "number" && a2.timeout_ms > 0 ? a2.timeout_ms : DEFAULT_TIMEOUT_MS;
1553
- return await new Promise((resolveResult) => {
1554
- const child = spawn(BIN, ["-c", a2.cmd], {
1555
- env: { ...process.env, APE_WAIT: "1" },
1556
- stdio: ["ignore", "pipe", "pipe"]
1557
- });
1558
- let stdout2 = "";
1559
- let stderr = "";
1560
- let timedOut = false;
1561
- let spawnError = null;
1562
- child.stdout.on("data", (chunk) => {
1563
- stdout2 += chunk.toString("utf8");
1564
- });
1565
- child.stderr.on("data", (chunk) => {
1566
- stderr += chunk.toString("utf8");
1567
- });
1568
- child.on("error", (err) => {
1569
- spawnError = err;
1570
- });
1571
- const timer = setTimeout(() => {
1572
- timedOut = true;
1573
- child.kill("SIGTERM");
1574
- setTimeout(() => {
1575
- try {
1576
- child.kill("SIGKILL");
1577
- } catch {
1578
- }
1579
- }, 5e3);
1580
- }, timeout);
1581
- child.on("close", (code) => {
1582
- clearTimeout(timer);
1583
- if (spawnError) {
1584
- resolveResult({
1585
- error: spawnError.message,
1586
- hint: `Could not exec '${BIN}'. The agent host needs @openape/apes installed globally so ape-shell is on PATH.`
1587
- });
1588
- return;
1589
- }
1590
- resolveResult({
1591
- stdout: capStdio(stdout2),
1592
- stderr: capStdio(stderr),
1593
- exit_code: code ?? -1,
1594
- ...timedOut ? { timed_out: true } : {}
1595
- });
1596
- });
1597
- });
1606
+ const timeout = typeof a2.timeout_ms === "number" && a2.timeout_ms > 0 ? a2.timeout_ms : DEFAULTS.DEFAULT_TIMEOUT_MS;
1607
+ return await runApeShell(a2.cmd, timeout);
1598
1608
  }
1599
1609
  }
1600
1610
  ];
@@ -1653,6 +1663,337 @@ var fileTools = [
1653
1663
  writeFileSync2(p, a2.content, { encoding: "utf8" });
1654
1664
  return { path: p, bytes: Buffer.byteLength(a2.content, "utf8") };
1655
1665
  }
1666
+ },
1667
+ {
1668
+ name: "file.edit",
1669
+ description: "Replace an exact substring in a file under the agent's home directory. Prefer this over file.write for edits \u2014 it touches only the changed region instead of rewriting the whole file. `old_string` must appear exactly once unless `replace_all` is true. Path traversal blocked, 1MB max.",
1670
+ parameters: {
1671
+ type: "object",
1672
+ properties: {
1673
+ path: { type: "string", description: "Path relative to $HOME (or absolute under $HOME)." },
1674
+ old_string: { type: "string", description: "Exact text to replace. Include enough surrounding context to be unique unless replace_all is set." },
1675
+ new_string: { type: "string", description: "Replacement text. Must differ from old_string." },
1676
+ replace_all: { type: "boolean", description: "Replace every occurrence instead of requiring a unique match. Default false." }
1677
+ },
1678
+ required: ["path", "old_string", "new_string"]
1679
+ },
1680
+ execute: async (args) => {
1681
+ const a2 = args;
1682
+ if (typeof a2.old_string !== "string" || a2.old_string === "") {
1683
+ throw new Error("old_string must be a non-empty string");
1684
+ }
1685
+ if (typeof a2.new_string !== "string") {
1686
+ throw new TypeError("new_string must be a string");
1687
+ }
1688
+ if (a2.old_string === a2.new_string) {
1689
+ throw new Error("old_string and new_string are identical \u2014 nothing to change");
1690
+ }
1691
+ const replaceAll = a2.replace_all === true;
1692
+ const p = jailPath(a2.path);
1693
+ const before = readFileSync3(p, "utf8");
1694
+ const occurrences = before.split(a2.old_string).length - 1;
1695
+ if (occurrences === 0) {
1696
+ throw new Error("old_string not found in file");
1697
+ }
1698
+ if (occurrences > 1 && !replaceAll) {
1699
+ throw new Error(`old_string occurs ${occurrences} times \u2014 pass replace_all:true or add surrounding context to make it unique`);
1700
+ }
1701
+ const after = replaceAll ? before.split(a2.old_string).join(a2.new_string) : before.replace(a2.old_string, a2.new_string);
1702
+ if (Buffer.byteLength(after, "utf8") > MAX_BYTES) {
1703
+ throw new Error(`result exceeds ${MAX_BYTES} byte cap`);
1704
+ }
1705
+ writeFileSync2(p, after, { encoding: "utf8" });
1706
+ return { path: p, replacements: replaceAll ? occurrences : 1 };
1707
+ }
1708
+ }
1709
+ ];
1710
+ var BRANCH_RE = /^[\w./-]{1,200}$/;
1711
+ var ID_RE = /^\d{1,12}$/;
1712
+ function shq(s2) {
1713
+ return `'${String(s2).replace(/'/g, "'\\''")}'`;
1714
+ }
1715
+ function assertBranch(v2) {
1716
+ if (typeof v2 !== "string" || !BRANCH_RE.test(v2)) {
1717
+ throw new Error("branch must match ^[A-Za-z0-9._/-]{1,200}$");
1718
+ }
1719
+ return v2;
1720
+ }
1721
+ function assertId(v2) {
1722
+ if (typeof v2 !== "string" && typeof v2 !== "number") throw new Error("id required");
1723
+ const s2 = String(v2);
1724
+ if (!ID_RE.test(s2)) throw new Error("id must be a number");
1725
+ return s2;
1726
+ }
1727
+ var githubAdapter = {
1728
+ id: "github",
1729
+ matchesRemote: (url) => /github\.com/i.test(url),
1730
+ prCreate: (i2) => {
1731
+ const head = assertBranch(i2.head);
1732
+ const parts = ["gh", "pr", "create", "--title", shq(i2.title), "--body", shq(i2.body), "--head", shq(head)];
1733
+ if (i2.base !== void 0) parts.push("--base", shq(assertBranch(i2.base)));
1734
+ return parts.join(" ");
1735
+ },
1736
+ prMerge: (i2) => {
1737
+ const ref = String(i2.ref);
1738
+ const refTok = ID_RE.test(ref) ? ref : assertBranch(ref);
1739
+ const parts = ["gh", "pr", "merge", shq(refTok)];
1740
+ if (i2.squash === true) parts.push("--squash");
1741
+ if (i2.auto) parts.push("--auto");
1742
+ if (i2.deleteBranch) parts.push("--delete-branch");
1743
+ return parts.join(" ");
1744
+ },
1745
+ prStatus: (ref) => {
1746
+ const r3 = String(ref);
1747
+ const refTok = ID_RE.test(r3) ? r3 : assertBranch(r3);
1748
+ return `gh pr view ${shq(refTok)} --json state,mergeStateStatus,statusCheckRollup,reviewDecision`;
1749
+ },
1750
+ issueGet: (ref) => `gh issue view ${assertId(ref)} --json number,title,body,labels`
1751
+ };
1752
+ var azureAdapter = {
1753
+ id: "azure",
1754
+ matchesRemote: (url) => /dev\.azure\.com|visualstudio\.com/i.test(url),
1755
+ prCreate: (i2) => {
1756
+ const head = assertBranch(i2.head);
1757
+ const parts = ["az", "repos", "pr", "create", "--title", shq(i2.title), "--description", shq(i2.body), "--source-branch", shq(head)];
1758
+ if (i2.base !== void 0) parts.push("--target-branch", shq(assertBranch(i2.base)));
1759
+ return parts.join(" ");
1760
+ },
1761
+ prMerge: (i2) => {
1762
+ const id = assertId(i2.ref);
1763
+ const parts = ["az", "repos", "pr", "update", "--id", id];
1764
+ if (i2.auto) parts.push("--auto-complete", "true");
1765
+ else parts.push("--status", "completed");
1766
+ if (i2.squash === true) parts.push("--merge-commit-message-style", "squash");
1767
+ if (i2.deleteBranch) parts.push("--delete-source-branch", "true");
1768
+ return parts.join(" ");
1769
+ },
1770
+ prStatus: (ref) => `az repos pr show --id ${assertId(ref)}`,
1771
+ issueGet: (ref) => `az boards work-item show --id ${assertId(ref)}`
1772
+ };
1773
+ var registry = /* @__PURE__ */ new Map([
1774
+ [githubAdapter.id, githubAdapter],
1775
+ [azureAdapter.id, azureAdapter]
1776
+ ]);
1777
+ function listForges() {
1778
+ return [...registry.keys()];
1779
+ }
1780
+ function getForge(id) {
1781
+ const a2 = registry.get(id);
1782
+ if (!a2) {
1783
+ throw new Error(`unknown forge '${id}'. Registered: ${listForges().join(", ")}. Add one with registerForge().`);
1784
+ }
1785
+ return a2;
1786
+ }
1787
+ function detectForge(remoteUrl) {
1788
+ if (typeof remoteUrl !== "string" || remoteUrl === "") {
1789
+ throw new Error("remote URL required to detect forge");
1790
+ }
1791
+ for (const a2 of registry.values()) {
1792
+ if (a2.matchesRemote(remoteUrl)) return a2.id;
1793
+ }
1794
+ throw new Error(`no forge adapter matches remote: ${remoteUrl}. Registered: ${listForges().join(", ")}. Register one with registerForge() (e.g. GitLab/Bitbucket/Gitea).`);
1795
+ }
1796
+ function buildPrCreate(input) {
1797
+ return getForge(input.forge).prCreate(input);
1798
+ }
1799
+ function buildPrMerge(input) {
1800
+ return getForge(input.forge).prMerge(input);
1801
+ }
1802
+ function buildPrStatus(forge, ref) {
1803
+ return getForge(forge).prStatus(ref);
1804
+ }
1805
+ function buildIssueGet(forge, ref) {
1806
+ return getForge(forge).issueGet(ref);
1807
+ }
1808
+ function resolveForge(a2) {
1809
+ if (typeof a2.forge === "string" && a2.forge !== "") return a2.forge;
1810
+ if (typeof a2.remote === "string") return detectForge(a2.remote);
1811
+ throw new Error("provide a forge id (e.g. github, azure, or a registered adapter) or a remote URL to detect it");
1812
+ }
1813
+ var forgeParam = { type: "string", description: "Target forge id (github, azure, or a registered adapter). Omit to auto-detect from `remote`." };
1814
+ var remoteParam = { type: "string", description: "git remote URL \u2014 used to auto-detect the forge when `forge` is omitted." };
1815
+ var forgeTools = [
1816
+ {
1817
+ name: "forge.pr.create",
1818
+ description: "Open a pull request on GitHub (gh) or Azure DevOps (az). Gated via the DDISA grant cycle. Provider chosen by `forge` or auto-detected from `remote`.",
1819
+ parameters: {
1820
+ type: "object",
1821
+ properties: {
1822
+ forge: forgeParam,
1823
+ remote: remoteParam,
1824
+ title: { type: "string", description: "PR title." },
1825
+ body: { type: "string", description: "PR description / body." },
1826
+ head: { type: "string", description: "Source branch." },
1827
+ base: { type: "string", description: "Target branch. Omit for the repo default." }
1828
+ },
1829
+ required: ["title", "body", "head"]
1830
+ },
1831
+ execute: async (args) => {
1832
+ const a2 = args;
1833
+ const cmd = buildPrCreate({ forge: resolveForge(a2), title: a2.title, body: a2.body, head: a2.head, base: a2.base });
1834
+ return await runApeShell(cmd);
1835
+ }
1836
+ },
1837
+ {
1838
+ name: "forge.pr.merge",
1839
+ description: 'Merge a PR \u2014 or with auto=true, arm "merge when checks pass" (gh --auto / az auto-complete) so the platform merges only on green CI. Gated. Never bypasses required checks (branch protection is the server-side gate).',
1840
+ parameters: {
1841
+ type: "object",
1842
+ properties: {
1843
+ forge: forgeParam,
1844
+ remote: remoteParam,
1845
+ ref: { type: "string", description: "GitHub: PR number or branch. Azure: PR id." },
1846
+ auto: { type: "boolean", description: "Arm merge-when-green instead of immediate merge. Recommended." },
1847
+ squash: { type: "boolean", description: "Squash-merge. Default true." },
1848
+ delete_branch: { type: "boolean", description: "Delete the source branch after merge." }
1849
+ },
1850
+ required: ["ref"]
1851
+ },
1852
+ execute: async (args) => {
1853
+ const a2 = args;
1854
+ const cmd = buildPrMerge({ forge: resolveForge(a2), ref: a2.ref, auto: a2.auto, squash: a2.squash, deleteBranch: a2.delete_branch });
1855
+ return await runApeShell(cmd);
1856
+ }
1857
+ },
1858
+ {
1859
+ name: "forge.pr.status",
1860
+ description: "Fetch a PR's state + checks + review decision. Gated (read).",
1861
+ parameters: {
1862
+ type: "object",
1863
+ properties: { forge: forgeParam, remote: remoteParam, ref: { type: "string", description: "PR number/branch (GitHub) or id (Azure)." } },
1864
+ required: ["ref"]
1865
+ },
1866
+ execute: async (args) => {
1867
+ const a2 = args;
1868
+ return await runApeShell(buildPrStatus(resolveForge(a2), a2.ref));
1869
+ }
1870
+ },
1871
+ {
1872
+ name: "forge.issue.get",
1873
+ description: "Fetch an issue (GitHub) or work-item (Azure) \u2014 title, body, labels. Gated (read). Use to turn an assigned task into a coding run.",
1874
+ parameters: {
1875
+ type: "object",
1876
+ properties: { forge: forgeParam, remote: remoteParam, ref: { type: "string", description: "Issue number (GitHub) or work-item id (Azure)." } },
1877
+ required: ["ref"]
1878
+ },
1879
+ execute: async (args) => {
1880
+ const a2 = args;
1881
+ return await runApeShell(buildIssueGet(resolveForge(a2), a2.ref));
1882
+ }
1883
+ }
1884
+ ];
1885
+ function jailedRoot(envVar, fallbackName) {
1886
+ const home = homedir22();
1887
+ const raw = process2.env[envVar];
1888
+ const dir = raw ? resolve2(raw) : resolve2(home, fallbackName);
1889
+ if (dir !== home && !dir.startsWith(`${home}/`)) {
1890
+ throw new Error(`${envVar} (${dir}) must resolve inside the agent's home`);
1891
+ }
1892
+ return dir;
1893
+ }
1894
+ function workRoot() {
1895
+ return jailedRoot("OPENAPE_CODING_WORK_DIR", "work");
1896
+ }
1897
+ function reposRoot() {
1898
+ return jailedRoot("OPENAPE_CODING_REPOS_DIR", "repos");
1899
+ }
1900
+ var TASK_ID_RE = /^[\w.-]{1,64}$/;
1901
+ var BRANCH_RE2 = /^[\w./-]{1,128}$/;
1902
+ var URL_RE = /^(?:https:\/\/|git@)[\w@:/.-]{3,256}$/;
1903
+ function assertTaskId(v2) {
1904
+ if (typeof v2 !== "string" || !TASK_ID_RE.test(v2)) {
1905
+ throw new Error("task_id must match ^[a-zA-Z0-9._-]{1,64}$");
1906
+ }
1907
+ return v2;
1908
+ }
1909
+ function assertBranch2(v2) {
1910
+ if (typeof v2 !== "string" || !BRANCH_RE2.test(v2)) {
1911
+ throw new Error("branch must match ^[A-Za-z0-9._/-]{1,128}$");
1912
+ }
1913
+ return v2;
1914
+ }
1915
+ function resolveRepo(repo) {
1916
+ if (typeof repo !== "string" || repo === "") {
1917
+ throw new Error("repo must be a non-empty string (URL or path under $HOME)");
1918
+ }
1919
+ const home = homedir22();
1920
+ if (URL_RE.test(repo)) {
1921
+ const tail = repo.replace(/\.git$/, "").replace(/[/:]+$/, "");
1922
+ const parts = tail.split(/[/:]/).filter(Boolean).slice(-2);
1923
+ const base = parts.join("-").replace(/[^\w.-]/g, "");
1924
+ if (!base) throw new Error("could not derive a clone name from repo URL");
1925
+ return { source: repo, baseDir: resolve2(reposRoot(), base), isUrl: true };
1926
+ }
1927
+ const candidate = repo.startsWith("~/") ? resolve2(home, repo.slice(2)) : resolve2(home, repo);
1928
+ if (candidate !== home && !candidate.startsWith(`${home}/`)) {
1929
+ throw new Error(`repo path "${repo}" resolves outside the agent's home`);
1930
+ }
1931
+ return { source: candidate, baseDir: candidate, isUrl: false };
1932
+ }
1933
+ function worktreePathFor(taskId) {
1934
+ return resolve2(workRoot(), assertTaskId(taskId));
1935
+ }
1936
+ var q = (s2) => `'${s2}'`;
1937
+ function buildCreateCommand(repo, taskId, branch) {
1938
+ const id = assertTaskId(taskId);
1939
+ const br = assertBranch2(branch);
1940
+ const { source, baseDir, isUrl } = resolveRepo(repo);
1941
+ const wt = worktreePathFor(id);
1942
+ const clone = isUrl ? `if [ ! -d ${q(baseDir)}/.git ]; then git clone ${q(source)} ${q(baseDir)}; fi` : `test -d ${q(baseDir)}/.git`;
1943
+ return [
1944
+ `mkdir -p ${q(reposRoot())} ${q(workRoot())}`,
1945
+ clone,
1946
+ `git -C ${q(baseDir)} fetch --quiet || true`,
1947
+ `git -C ${q(baseDir)} worktree add -b ${q(br)} ${q(wt)}`,
1948
+ `echo ${q(wt)}`
1949
+ ].join(" && ");
1950
+ }
1951
+ function buildRemoveCommand(repo, taskId) {
1952
+ const id = assertTaskId(taskId);
1953
+ const { baseDir } = resolveRepo(repo);
1954
+ const wt = worktreePathFor(id);
1955
+ return `git -C ${q(baseDir)} worktree remove --force ${q(wt)} && git -C ${q(baseDir)} worktree prune`;
1956
+ }
1957
+ function buildListCommand() {
1958
+ return `ls -1 ${q(workRoot())} 2>/dev/null || true`;
1959
+ }
1960
+ var gitWorktreeTools = [
1961
+ {
1962
+ name: "git.worktree",
1963
+ description: "Manage isolated git worktrees for coding tasks. action=create clones the repo (cached under ~/repos) and adds a fresh worktree under ~/work/<task_id> on a new branch. action=remove tears it down. action=list shows current task worktrees. Git operations go through the DDISA grant cycle (git-shape).",
1964
+ parameters: {
1965
+ type: "object",
1966
+ properties: {
1967
+ action: { type: "string", enum: ["create", "remove", "list"], description: "create | remove | list" },
1968
+ repo: { type: "string", description: "For create/remove: git remote URL (https/git@) or a path under $HOME to an existing clone." },
1969
+ task_id: { type: "string", description: "For create/remove: identifier for the worktree, ^[a-zA-Z0-9._-]{1,64}$. The worktree lands at ~/work/<task_id>." },
1970
+ branch: { type: "string", description: "For create: new branch name, ^[A-Za-z0-9._/-]{1,128}$." }
1971
+ },
1972
+ required: ["action"]
1973
+ },
1974
+ execute: async (args) => {
1975
+ const a2 = args;
1976
+ let cmd;
1977
+ if (a2.action === "create") {
1978
+ if (typeof a2.branch !== "string") throw new Error("branch is required for action=create");
1979
+ cmd = buildCreateCommand(a2.repo, assertTaskId(a2.task_id), a2.branch);
1980
+ } else if (a2.action === "remove") {
1981
+ cmd = buildRemoveCommand(a2.repo, assertTaskId(a2.task_id));
1982
+ } else if (a2.action === "list") {
1983
+ cmd = buildListCommand();
1984
+ } else {
1985
+ throw new Error("action must be one of: create, remove, list");
1986
+ }
1987
+ const res = await runApeShell(cmd);
1988
+ return {
1989
+ action: a2.action,
1990
+ ...a2.action !== "list" ? { worktree: worktreePathFor(assertTaskId(a2.task_id)) } : {},
1991
+ stdout: res.stdout,
1992
+ stderr: res.stderr,
1993
+ exit_code: res.exit_code,
1994
+ ...res.error ? { error: res.error, hint: res.hint } : {}
1995
+ };
1996
+ }
1656
1997
  }
1657
1998
  ];
1658
1999
  var MAX_BYTES2 = 1024 * 1024;
@@ -1883,13 +2224,53 @@ var timeTools = [
1883
2224
  }
1884
2225
  }
1885
2226
  ];
2227
+ var CWD_RE = /^[\w./-]{1,256}$/;
2228
+ async function runVerify(cwd, command, timeoutMs) {
2229
+ if (typeof cwd !== "string" || !CWD_RE.test(cwd)) {
2230
+ throw new Error("cwd must match ^[A-Za-z0-9._/-]{1,256}$");
2231
+ }
2232
+ if (typeof command !== "string" || command.trim() === "") {
2233
+ throw new Error("verify command must be a non-empty string");
2234
+ }
2235
+ const res = await runApeShell(`cd '${cwd}' && ${command}`, timeoutMs);
2236
+ return {
2237
+ passed: res.exit_code === 0,
2238
+ exit_code: res.exit_code,
2239
+ stdout: res.stdout,
2240
+ stderr: res.stderr,
2241
+ ...res.timed_out ? { timed_out: true } : {}
2242
+ };
2243
+ }
2244
+ var verifyTools = [
2245
+ {
2246
+ name: "verify",
2247
+ description: "Run the verification command (tests/build/lint) in a worktree and report pass/fail. The coding loop must NOT open or merge a PR when this fails. Runs through the DDISA grant cycle (same as bash). Returns { passed, exit_code, stdout, stderr }.",
2248
+ parameters: {
2249
+ type: "object",
2250
+ properties: {
2251
+ cwd: { type: "string", description: "Worktree path to run in (e.g. ~/work/issue-42)." },
2252
+ command: { type: "string", description: "Verification command, e.g. `pnpm test` or `npm run build && npm test`." },
2253
+ timeout_ms: { type: "number", description: "Wall-clock cap incl. approval wait. Default 300000." }
2254
+ },
2255
+ required: ["cwd", "command"]
2256
+ },
2257
+ execute: async (args) => {
2258
+ const a2 = args;
2259
+ const timeout = typeof a2.timeout_ms === "number" && a2.timeout_ms > 0 ? a2.timeout_ms : void 0;
2260
+ return await runVerify(a2.cwd, a2.command, timeout);
2261
+ }
2262
+ }
2263
+ ];
1886
2264
  var ALL_TOOLS = [
1887
2265
  ...timeTools,
1888
2266
  ...httpTools,
1889
2267
  ...fileTools,
1890
2268
  ...tasksTools,
1891
2269
  ...mailTools,
1892
- ...bashTools
2270
+ ...bashTools,
2271
+ ...gitWorktreeTools,
2272
+ ...verifyTools,
2273
+ ...forgeTools
1893
2274
  ];
1894
2275
  var TOOLS = Object.fromEntries(
1895
2276
  ALL_TOOLS.map((t2) => [t2.name, t2])
@@ -3890,7 +4271,7 @@ function shouldAutoAccept(peerEmail, identity, allowlist) {
3890
4271
  import { execFileSync as execFileSync3 } from "child_process";
3891
4272
  import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync7, statSync } from "fs";
3892
4273
  import { homedir as homedir8 } from "os";
3893
- import { dirname as dirname2, join as join8, resolve as resolve2 } from "path";
4274
+ import { dirname as dirname2, join as join8, resolve as resolve3 } from "path";
3894
4275
  import { fileURLToPath } from "url";
3895
4276
  import { parse as parseYaml } from "yaml";
3896
4277
  var SKILLS_SUBDIR = [".openape", "agent", "skills"];
@@ -3999,7 +4380,7 @@ function scanSkillsDir(dir) {
3999
4380
  }
4000
4381
  function defaultSkillsDir() {
4001
4382
  const here = dirname2(fileURLToPath(import.meta.url));
4002
- return resolve2(here, "..", "default-skills");
4383
+ return resolve3(here, "..", "default-skills");
4003
4384
  }
4004
4385
  function composeSkills(home, enabledTools) {
4005
4386
  const enabled = new Set(enabledTools);
@@ -4063,7 +4444,7 @@ function readDefaultPersona() {
4063
4444
  if (_defaultPersonaCache !== void 0) return _defaultPersonaCache;
4064
4445
  try {
4065
4446
  const here = dirname2(fileURLToPath(import.meta.url));
4066
- const path = resolve2(here, "..", "default-persona.md");
4447
+ const path = resolve3(here, "..", "default-persona.md");
4067
4448
  if (!existsSync6(path)) {
4068
4449
  _defaultPersonaCache = null;
4069
4450
  return null;
@@ -4345,8 +4726,8 @@ function loadBridgeEnvFile() {
4345
4726
  const key = trimmed.slice(0, eq).trim();
4346
4727
  const value = trimmed.slice(eq + 1).trim().replace(/^["']|["']$/g, "");
4347
4728
  if (!key) continue;
4348
- if (process2.env[key] === void 0) {
4349
- process2.env[key] = value;
4729
+ if (process3.env[key] === void 0) {
4730
+ process3.env[key] = value;
4350
4731
  }
4351
4732
  }
4352
4733
  } catch {
@@ -4354,24 +4735,24 @@ function loadBridgeEnvFile() {
4354
4735
  }
4355
4736
  function readConfig() {
4356
4737
  loadBridgeEnvFile();
4357
- const toolsRaw = process2.env.APE_CHAT_BRIDGE_TOOLS ?? "";
4738
+ const toolsRaw = process3.env.APE_CHAT_BRIDGE_TOOLS ?? "";
4358
4739
  const tools = toolsRaw.split(",").map((s2) => s2.trim()).filter(Boolean);
4359
- const maxStepsRaw = process2.env.APE_CHAT_BRIDGE_MAX_STEPS;
4740
+ const maxStepsRaw = process3.env.APE_CHAT_BRIDGE_MAX_STEPS;
4360
4741
  const maxSteps = maxStepsRaw ? Number.parseInt(maxStepsRaw, 10) : DEFAULT_MAX_STEPS;
4361
- const model = process2.env.APE_CHAT_BRIDGE_MODEL;
4742
+ const model = process3.env.APE_CHAT_BRIDGE_MODEL;
4362
4743
  if (!model) {
4363
4744
  throw new Error(
4364
4745
  "APE_CHAT_BRIDGE_MODEL is not set. Set it in the bridge .env (usually `~/Library/Application Support/openape/bridge/.env` on macOS) or globally in `~/litellm/.env` so resolveBridgeConfig picks it up at spawn time. Common values: `gpt-5.4` (ChatGPT-only LiteLLM proxy), `claude-haiku-4-5` (Anthropic-only)."
4365
4746
  );
4366
4747
  }
4367
4748
  return {
4368
- endpoint: (process2.env.APE_CHAT_ENDPOINT ?? DEFAULT_ENDPOINT).replace(/\/$/, ""),
4369
- apesBin: process2.env.APE_CHAT_BRIDGE_APES_BIN ?? DEFAULT_APES_BIN,
4749
+ endpoint: (process3.env.APE_CHAT_ENDPOINT ?? DEFAULT_ENDPOINT).replace(/\/$/, ""),
4750
+ apesBin: process3.env.APE_CHAT_BRIDGE_APES_BIN ?? DEFAULT_APES_BIN,
4370
4751
  model,
4371
- systemPrompt: process2.env.APE_CHAT_BRIDGE_SYSTEM_PROMPT ?? DEFAULT_SYSTEM_PROMPT,
4752
+ systemPrompt: process3.env.APE_CHAT_BRIDGE_SYSTEM_PROMPT ?? DEFAULT_SYSTEM_PROMPT,
4372
4753
  tools,
4373
4754
  maxSteps: Number.isFinite(maxSteps) && maxSteps > 0 ? maxSteps : DEFAULT_MAX_STEPS,
4374
- roomFilter: process2.env.APE_CHAT_BRIDGE_ROOM
4755
+ roomFilter: process3.env.APE_CHAT_BRIDGE_ROOM
4375
4756
  };
4376
4757
  }
4377
4758
  async function getIdentity() {
@@ -4383,11 +4764,11 @@ async function getIdentity() {
4383
4764
  return { email: claims.sub };
4384
4765
  }
4385
4766
  function log(line) {
4386
- process2.stderr.write(`${(/* @__PURE__ */ new Date()).toISOString()} ${line}
4767
+ process3.stderr.write(`${(/* @__PURE__ */ new Date()).toISOString()} ${line}
4387
4768
  `);
4388
4769
  }
4389
4770
  function sleep(ms) {
4390
- return new Promise((resolve3) => setTimeout(resolve3, ms));
4771
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
4391
4772
  }
4392
4773
  function truncate(s2, n2) {
4393
4774
  return s2.length <= n2 ? s2 : `${s2.slice(0, n2 - 1)}\u2026`;
@@ -4413,7 +4794,7 @@ var Bridge = class {
4413
4794
  chat: this.chat,
4414
4795
  ownerEmail: this.ownerEmail,
4415
4796
  log,
4416
- troopUrl: (process2.env.OPENAPE_TROOP_URL ?? "https://troop.openape.ai").replace(/\/$/, ""),
4797
+ troopUrl: (process3.env.OPENAPE_TROOP_URL ?? "https://troop.openape.ai").replace(/\/$/, ""),
4417
4798
  bearer: this.bearer
4418
4799
  });
4419
4800
  this.cron.start();
@@ -4438,8 +4819,8 @@ var Bridge = class {
4438
4819
  * whole process lifetime.
4439
4820
  */
4440
4821
  runtimeConfig() {
4441
- const apiBase = (process2.env.LITELLM_BASE_URL ?? "http://127.0.0.1:4000/v1").replace(/\/$/, "");
4442
- const apiKey = process2.env.LITELLM_API_KEY ?? process2.env.LITELLM_MASTER_KEY ?? "";
4822
+ const apiBase = (process3.env.LITELLM_BASE_URL ?? "http://127.0.0.1:4000/v1").replace(/\/$/, "");
4823
+ const apiKey = process3.env.LITELLM_API_KEY ?? process3.env.LITELLM_MASTER_KEY ?? "";
4443
4824
  if (!apiKey) {
4444
4825
  throw new Error("LITELLM_API_KEY (or LITELLM_MASTER_KEY) must be set in the bridge env.");
4445
4826
  }
@@ -4549,7 +4930,7 @@ var Bridge = class {
4549
4930
  const bearer = await this.bearer();
4550
4931
  const wsUrl = `${this.cfg.endpoint.replace(/^http/, "ws")}/api/ws?token=${encodeURIComponent(bearer.replace(/^Bearer\s+/i, ""))}`;
4551
4932
  const ws = new WebSocket(wsUrl);
4552
- return new Promise((resolve3, reject) => {
4933
+ return new Promise((resolve4, reject) => {
4553
4934
  let pingTimer;
4554
4935
  let allowlistTimer;
4555
4936
  ws.on("open", () => {
@@ -4582,7 +4963,7 @@ var Bridge = class {
4582
4963
  ws.on("close", () => {
4583
4964
  if (pingTimer) clearInterval(pingTimer);
4584
4965
  if (allowlistTimer) clearInterval(allowlistTimer);
4585
- resolve3();
4966
+ resolve4();
4586
4967
  });
4587
4968
  ws.on("error", (err) => {
4588
4969
  if (pingTimer) clearInterval(pingTimer);
@@ -4621,7 +5002,7 @@ async function main() {
4621
5002
  }
4622
5003
  main().catch((err) => {
4623
5004
  const msg = err instanceof Error ? err.message : String(err);
4624
- process2.stderr.write(`fatal: ${msg}
5005
+ process3.stderr.write(`fatal: ${msg}
4625
5006
  `);
4626
- process2.exit(1);
5007
+ process3.exit(1);
4627
5008
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openape/ape-agent",
3
- "version": "2.7.0",
3
+ "version": "2.7.2",
4
4
  "description": "OpenApe agent runtime: per-agent process that connects to chat.openape.ai, runs the LLM loop with tools + cron tasks, and streams replies back to owners.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -23,9 +23,9 @@
23
23
  "ofetch": "^1.4.1",
24
24
  "ws": "^8.18.0",
25
25
  "yaml": "^2.8.0",
26
- "@openape/apes": "1.25.1",
27
- "@openape/prompt-injection-detector": "0.1.0",
28
- "@openape/cli-auth": "0.4.1"
26
+ "@openape/apes": "1.27.0",
27
+ "@openape/cli-auth": "0.4.1",
28
+ "@openape/prompt-injection-detector": "0.1.0"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@antfu/eslint-config": "^7.6.1",