amness 0.0.0-stage → 0.1.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 (235) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +237 -2
  3. package/dist/cli/acpcli.d.ts +7 -0
  4. package/dist/cli/acpcli.js +14 -0
  5. package/dist/cli/amness.d.ts +2 -0
  6. package/dist/cli/amness.js +694 -0
  7. package/dist/cli/benchcli.d.ts +5 -0
  8. package/dist/cli/benchcli.js +160 -0
  9. package/dist/cli/config.d.ts +79 -0
  10. package/dist/cli/config.js +111 -0
  11. package/dist/cli/demo.d.ts +13 -0
  12. package/dist/cli/demo.js +44 -0
  13. package/dist/cli/detect.d.ts +2 -0
  14. package/dist/cli/detect.js +20 -0
  15. package/dist/cli/evalcli.d.ts +15 -0
  16. package/dist/cli/evalcli.js +198 -0
  17. package/dist/cli/format.d.ts +5 -0
  18. package/dist/cli/format.js +55 -0
  19. package/dist/cli/headless.d.ts +2 -0
  20. package/dist/cli/headless.js +169 -0
  21. package/dist/cli/keystore.d.ts +9 -0
  22. package/dist/cli/keystore.js +108 -0
  23. package/dist/cli/model.d.ts +11 -0
  24. package/dist/cli/model.js +19 -0
  25. package/dist/cli/plugincli.d.ts +2 -0
  26. package/dist/cli/plugincli.js +176 -0
  27. package/dist/cli/schedulecli.d.ts +2 -0
  28. package/dist/cli/schedulecli.js +66 -0
  29. package/dist/cli/state.d.ts +11 -0
  30. package/dist/cli/state.js +19 -0
  31. package/dist/cli/tui/ansi.d.ts +38 -0
  32. package/dist/cli/tui/ansi.js +142 -0
  33. package/dist/cli/tui/app.d.ts +328 -0
  34. package/dist/cli/tui/app.js +1358 -0
  35. package/dist/cli/tui/blocks.d.ts +151 -0
  36. package/dist/cli/tui/blocks.js +128 -0
  37. package/dist/cli/tui/brand.d.ts +9 -0
  38. package/dist/cli/tui/brand.js +16 -0
  39. package/dist/cli/tui/canvas.d.ts +92 -0
  40. package/dist/cli/tui/canvas.js +199 -0
  41. package/dist/cli/tui/cards.d.ts +44 -0
  42. package/dist/cli/tui/cards.js +215 -0
  43. package/dist/cli/tui/clipboard.d.ts +5 -0
  44. package/dist/cli/tui/clipboard.js +29 -0
  45. package/dist/cli/tui/commands.d.ts +10 -0
  46. package/dist/cli/tui/commands.js +650 -0
  47. package/dist/cli/tui/complete.d.ts +39 -0
  48. package/dist/cli/tui/complete.js +113 -0
  49. package/dist/cli/tui/custom.d.ts +9 -0
  50. package/dist/cli/tui/custom.js +34 -0
  51. package/dist/cli/tui/diffview.d.ts +25 -0
  52. package/dist/cli/tui/diffview.js +130 -0
  53. package/dist/cli/tui/editor.d.ts +60 -0
  54. package/dist/cli/tui/editor.js +230 -0
  55. package/dist/cli/tui/full.d.ts +86 -0
  56. package/dist/cli/tui/full.js +630 -0
  57. package/dist/cli/tui/keys.d.ts +31 -0
  58. package/dist/cli/tui/keys.js +134 -0
  59. package/dist/cli/tui/markdown.d.ts +37 -0
  60. package/dist/cli/tui/markdown.js +155 -0
  61. package/dist/cli/tui/mentions.d.ts +15 -0
  62. package/dist/cli/tui/mentions.js +48 -0
  63. package/dist/cli/tui/modal.d.ts +26 -0
  64. package/dist/cli/tui/modal.js +107 -0
  65. package/dist/cli/tui/overlays.d.ts +98 -0
  66. package/dist/cli/tui/overlays.js +237 -0
  67. package/dist/cli/tui/pager.d.ts +15 -0
  68. package/dist/cli/tui/pager.js +66 -0
  69. package/dist/cli/tui/pricing.d.ts +9 -0
  70. package/dist/cli/tui/pricing.js +23 -0
  71. package/dist/cli/tui/rich.d.ts +16 -0
  72. package/dist/cli/tui/rich.js +72 -0
  73. package/dist/cli/tui/screen.d.ts +30 -0
  74. package/dist/cli/tui/screen.js +62 -0
  75. package/dist/cli/tui/session.d.ts +77 -0
  76. package/dist/cli/tui/session.js +103 -0
  77. package/dist/cli/tui/sidebar.d.ts +84 -0
  78. package/dist/cli/tui/sidebar.js +114 -0
  79. package/dist/cli/tui/text.d.ts +15 -0
  80. package/dist/cli/tui/text.js +94 -0
  81. package/dist/cli/tui/theme.d.ts +59 -0
  82. package/dist/cli/tui/theme.js +148 -0
  83. package/dist/cli/tui/themes.d.ts +56 -0
  84. package/dist/cli/tui/themes.js +80 -0
  85. package/dist/cli/tui/tokens.d.ts +39 -0
  86. package/dist/cli/tui/tokens.js +71 -0
  87. package/dist/cli/tui/view.d.ts +60 -0
  88. package/dist/cli/tui/view.js +178 -0
  89. package/dist/cli/ui.d.ts +34 -0
  90. package/dist/cli/ui.js +224 -0
  91. package/dist/src/acp.d.ts +28 -0
  92. package/dist/src/acp.js +111 -0
  93. package/dist/src/agents.d.ts +23 -0
  94. package/dist/src/agents.js +71 -0
  95. package/dist/src/anthropic.d.ts +30 -0
  96. package/dist/src/anthropic.js +91 -0
  97. package/dist/src/approver.d.ts +15 -0
  98. package/dist/src/approver.js +40 -0
  99. package/dist/src/bench.d.ts +67 -0
  100. package/dist/src/bench.js +201 -0
  101. package/dist/src/bundle.d.ts +36 -0
  102. package/dist/src/bundle.js +55 -0
  103. package/dist/src/chaos.d.ts +15 -0
  104. package/dist/src/chaos.js +26 -0
  105. package/dist/src/code.d.ts +187 -0
  106. package/dist/src/code.js +590 -0
  107. package/dist/src/codeindex.d.ts +65 -0
  108. package/dist/src/codeindex.js +300 -0
  109. package/dist/src/deep.d.ts +62 -0
  110. package/dist/src/deep.js +177 -0
  111. package/dist/src/diff.d.ts +8 -0
  112. package/dist/src/diff.js +74 -0
  113. package/dist/src/dynamic.d.ts +42 -0
  114. package/dist/src/dynamic.js +133 -0
  115. package/dist/src/embed.d.ts +8 -0
  116. package/dist/src/embed.js +25 -0
  117. package/dist/src/evalcases.d.ts +11 -0
  118. package/dist/src/evalcases.js +196 -0
  119. package/dist/src/evals.d.ts +197 -0
  120. package/dist/src/evals.js +340 -0
  121. package/dist/src/gitflow.d.ts +27 -0
  122. package/dist/src/gitflow.js +56 -0
  123. package/dist/src/grade.d.ts +42 -0
  124. package/dist/src/grade.js +96 -0
  125. package/dist/src/guidance.d.ts +15 -0
  126. package/dist/src/guidance.js +68 -0
  127. package/dist/src/hardcases.d.ts +2 -0
  128. package/dist/src/hardcases.js +94 -0
  129. package/dist/src/harness.d.ts +411 -0
  130. package/dist/src/harness.js +725 -0
  131. package/dist/src/hooks.d.ts +44 -0
  132. package/dist/src/hooks.js +116 -0
  133. package/dist/src/images.d.ts +13 -0
  134. package/dist/src/images.js +27 -0
  135. package/dist/src/index.d.ts +57 -0
  136. package/dist/src/index.js +57 -0
  137. package/dist/src/ledger.d.ts +58 -0
  138. package/dist/src/ledger.js +78 -0
  139. package/dist/src/live.d.ts +16 -0
  140. package/dist/src/live.js +80 -0
  141. package/dist/src/lsp.d.ts +31 -0
  142. package/dist/src/lsp.js +147 -0
  143. package/dist/src/memory.d.ts +128 -0
  144. package/dist/src/memory.js +390 -0
  145. package/dist/src/metrics.d.ts +3 -0
  146. package/dist/src/metrics.js +42 -0
  147. package/dist/src/mining.d.ts +49 -0
  148. package/dist/src/mining.js +126 -0
  149. package/dist/src/openai.d.ts +50 -0
  150. package/dist/src/openai.js +307 -0
  151. package/dist/src/parallel.d.ts +66 -0
  152. package/dist/src/parallel.js +135 -0
  153. package/dist/src/patch.d.ts +45 -0
  154. package/dist/src/patch.js +219 -0
  155. package/dist/src/permissions.d.ts +46 -0
  156. package/dist/src/permissions.js +245 -0
  157. package/dist/src/plugins.d.ts +148 -0
  158. package/dist/src/plugins.js +225 -0
  159. package/dist/src/pluginscan.d.ts +8 -0
  160. package/dist/src/pluginscan.js +20 -0
  161. package/dist/src/procs.d.ts +41 -0
  162. package/dist/src/procs.js +94 -0
  163. package/dist/src/profiles.d.ts +36 -0
  164. package/dist/src/profiles.js +61 -0
  165. package/dist/src/progress.d.ts +29 -0
  166. package/dist/src/progress.js +75 -0
  167. package/dist/src/prompts.d.ts +10 -0
  168. package/dist/src/prompts.js +28 -0
  169. package/dist/src/ratelimit.d.ts +34 -0
  170. package/dist/src/ratelimit.js +116 -0
  171. package/dist/src/replay.d.ts +25 -0
  172. package/dist/src/replay.js +99 -0
  173. package/dist/src/review.d.ts +20 -0
  174. package/dist/src/review.js +49 -0
  175. package/dist/src/route.d.ts +9 -0
  176. package/dist/src/route.js +30 -0
  177. package/dist/src/router.d.ts +61 -0
  178. package/dist/src/router.js +126 -0
  179. package/dist/src/sandbox.d.ts +13 -0
  180. package/dist/src/sandbox.js +68 -0
  181. package/dist/src/schedule.d.ts +20 -0
  182. package/dist/src/schedule.js +59 -0
  183. package/dist/src/schema.d.ts +13 -0
  184. package/dist/src/schema.js +36 -0
  185. package/dist/src/secrets.d.ts +10 -0
  186. package/dist/src/secrets.js +41 -0
  187. package/dist/src/shell.d.ts +34 -0
  188. package/dist/src/shell.js +95 -0
  189. package/dist/src/skills.d.ts +20 -0
  190. package/dist/src/skills.js +112 -0
  191. package/dist/src/snapshot.d.ts +20 -0
  192. package/dist/src/snapshot.js +93 -0
  193. package/dist/src/stats.d.ts +24 -0
  194. package/dist/src/stats.js +62 -0
  195. package/dist/src/strategies.d.ts +16 -0
  196. package/dist/src/strategies.js +35 -0
  197. package/dist/src/suites.d.ts +30 -0
  198. package/dist/src/suites.js +112 -0
  199. package/dist/src/trace.d.ts +63 -0
  200. package/dist/src/trace.js +153 -0
  201. package/dist/src/tune.d.ts +38 -0
  202. package/dist/src/tune.js +49 -0
  203. package/dist/src/web.d.ts +19 -0
  204. package/dist/src/web.js +56 -0
  205. package/dist/src/worktree.d.ts +24 -0
  206. package/dist/src/worktree.js +85 -0
  207. package/docs/agent-loop.md +222 -0
  208. package/docs/architecture.md +228 -0
  209. package/docs/bench.md +292 -0
  210. package/docs/budgets.md +185 -0
  211. package/docs/build-your-own-harness.md +424 -0
  212. package/docs/cookbook.md +375 -0
  213. package/docs/development.md +150 -0
  214. package/docs/evaluation.md +450 -0
  215. package/docs/features.md +1320 -0
  216. package/docs/getting-started.md +205 -0
  217. package/docs/headless.md +188 -0
  218. package/docs/images/approval.png +0 -0
  219. package/docs/images/chat.png +0 -0
  220. package/docs/images/daylight.png +0 -0
  221. package/docs/images/loop.png +0 -0
  222. package/docs/images/palette.png +0 -0
  223. package/docs/images/running.png +0 -0
  224. package/docs/images/subagent-done.png +0 -0
  225. package/docs/images/subagent-running.png +0 -0
  226. package/docs/images/welcome.png +0 -0
  227. package/docs/index.md +25 -0
  228. package/docs/library.md +267 -0
  229. package/docs/plugins.md +691 -0
  230. package/docs/providers.md +175 -0
  231. package/docs/safety.md +250 -0
  232. package/docs/usage.md +348 -0
  233. package/docs/why.md +155 -0
  234. package/docs/writing-guide.md +39 -0
  235. package/package.json +66 -4
package/dist/cli/ui.js ADDED
@@ -0,0 +1,224 @@
1
+ // Tiny zero-dependency terminal UI: colors, boxes, arrow-key menus, prompts, spinner. Falls back to plain line input when piped.
2
+ import { createInterface, emitKeypressEvents } from "node:readline";
3
+ const color = (!!process.stdout.isTTY || !!process.env.FORCE_COLOR) && !process.env.NO_COLOR;
4
+ const sgr = (code) => (s) => (color ? `\x1b[${code}m${s}\x1b[0m` : s);
5
+ export const bold = sgr("1"), dim = sgr("2"), red = sgr("31"), green = sgr("32"), yellow = sgr("33"), blue = sgr("34"), magenta = sgr("35"), cyan = sgr("36"), gray = sgr("90");
6
+ export const strip = (s) => s.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, "");
7
+ export const width = () => Math.min(process.stdout.columns || 80, 100);
8
+ /** Word-wrap one line (ANSI-aware) to `w` visible columns; continuation lines are indented 2 spaces. */
9
+ export function wrap(line, w) {
10
+ if (strip(line).length <= w)
11
+ return [line];
12
+ const reset = color ? "\x1b[0m" : "";
13
+ const out = [];
14
+ let cur = "", vis = 0, lastSpace = -1;
15
+ for (const t of line.match(/\x1b\[[0-9;?]*[A-Za-z]|[^\x1b]/g) ?? []) {
16
+ if (t[0] === "\x1b") {
17
+ cur += t;
18
+ continue;
19
+ }
20
+ if (vis >= w) {
21
+ const cut = lastSpace > 0 ? lastSpace : cur.length;
22
+ out.push(cur.slice(0, cut) + reset);
23
+ cur = " " + cur.slice(lastSpace > 0 ? cut + 1 : cut);
24
+ vis = strip(cur).length;
25
+ lastSpace = -1;
26
+ }
27
+ if (t === " ")
28
+ lastSpace = cur.length;
29
+ cur += t;
30
+ vis++;
31
+ }
32
+ out.push(cur);
33
+ return out;
34
+ }
35
+ export function box(input, paint = cyan, title = "") {
36
+ const w = Math.min(Math.max(...input.map((l) => strip(l).length), strip(title).length + 2), width() - 4);
37
+ const lines = input.flatMap((l) => wrap(l, w));
38
+ const top = title ? `╭─ ${title} ${"─".repeat(Math.max(0, w - strip(title).length - 1))}╮` : `╭${"─".repeat(w + 2)}╮`;
39
+ return [paint(top), ...lines.map((l) => paint("│ ") + l + " ".repeat(Math.max(0, w - strip(l).length)) + paint(" │")), paint(`╰${"─".repeat(w + 2)}╯`)].join("\n");
40
+ }
41
+ // ---- line input shared by all prompts when stdin is not a TTY (piped / tests)
42
+ let lineIter;
43
+ const nextLine = async () => ((lineIter ??= createInterface({ input: process.stdin })[Symbol.asyncIterator]()), (await lineIter.next()).value);
44
+ const isTTY = () => !!process.stdin.isTTY && !!process.stdout.isTTY;
45
+ /** Thrown when the user presses Esc inside a text prompt; screens let it bubble up to the menu. */
46
+ export class Cancel extends Error {
47
+ constructor() { super("cancelled"); }
48
+ }
49
+ /**
50
+ * Arrow-key menu. ↑/↓ or j/k move, 1-9 jump, enter selects, esc goes back, "/" filters long lists, "?" toggles help.
51
+ * Long lists scroll inside the terminal height. Returns undefined on back.
52
+ */
53
+ export async function select(title, items, initial = 0) {
54
+ if (!isTTY()) {
55
+ console.log(`\n${bold(title)}`);
56
+ items.forEach((it, i) => console.log(` ${i + 1}. ${it.label}${it.hint ? " " + dim(it.hint) : ""}`));
57
+ const a = (await nextLine())?.trim();
58
+ return a ? items[Number(a) - 1]?.value : undefined;
59
+ }
60
+ const out = process.stdout, inp = process.stdin;
61
+ let cur = Math.min(initial, items.length - 1), top = 0, drawn = 0, filter = "", filtering = false, help = false;
62
+ const vis = () => { const f = filter.toLowerCase(); return f ? items.filter((i) => `${i.label} ${i.hint ?? ""}`.toLowerCase().includes(f)) : items; };
63
+ const draw = () => {
64
+ if (drawn)
65
+ out.write(`\x1b[${drawn}A\x1b[J`);
66
+ const list = vis(), room = Math.max(4, (out.rows || 24) - 7);
67
+ cur = Math.max(0, Math.min(cur, list.length - 1));
68
+ if (cur < top)
69
+ top = cur;
70
+ if (cur >= top + room)
71
+ top = cur - room + 1;
72
+ const slice = list.slice(top, top + room), labelW = Math.max(0, ...slice.map((i) => strip(i.label).length));
73
+ const rows = [
74
+ bold(title) + (filtering || filter ? " " + cyan("/") + filter + (filtering ? cyan("▌") : "") : ""),
75
+ ...(top > 0 ? [dim(` ↑ ${top} more`)] : []),
76
+ ...slice.map((it, k) => {
77
+ const on = top + k === cur, pad = " ".repeat(labelW - strip(it.label).length);
78
+ return (on ? cyan("❯ ") + bold(cyan(it.label)) : " " + it.label) + pad + (it.hint ? " " + (on ? gray(it.hint) : dim(it.hint)) : "");
79
+ }),
80
+ ...(top + room < list.length ? [dim(` ↓ ${list.length - top - room} more`)] : []),
81
+ ...(list.length ? [] : [dim(" no matches")]),
82
+ dim(help ? " ↑/↓ j/k move · 1-9 jump · enter select · esc back · / filter · ? hide help · ctrl-c quit"
83
+ : filtering ? " type to filter · enter select · esc clear filter" : " ↑/↓ move · enter select · esc back" + (items.length > 6 ? " · / filter" : "") + " · ? help"),
84
+ ];
85
+ out.write(rows.map((r) => r.slice(0, Math.max(20, (out.columns || 80) + 20))).join("\n") + "\n");
86
+ drawn = rows.length;
87
+ };
88
+ emitKeypressEvents(inp);
89
+ inp.setRawMode(true);
90
+ inp.resume();
91
+ out.write("\x1b[?25l");
92
+ draw();
93
+ return new Promise((resolve) => {
94
+ const done = (v) => {
95
+ inp.off("keypress", onKey);
96
+ inp.setRawMode(false);
97
+ inp.pause();
98
+ out.write("\x1b[?25h");
99
+ out.write(`\x1b[${drawn}A\x1b[J${bold(title)} ${v === undefined ? dim("(back)") : cyan(items.find((i) => i.value === v).label)}\n`);
100
+ resolve(v);
101
+ };
102
+ const onKey = (s, k) => {
103
+ const list = vis();
104
+ if (k.ctrl && k.name === "c") {
105
+ inp.setRawMode(false);
106
+ out.write("\x1b[?25h\n");
107
+ process.exit(130);
108
+ }
109
+ if (filtering) {
110
+ if (k.name === "return")
111
+ return list.length ? done(list[cur].value) : draw();
112
+ if (k.name === "escape") {
113
+ filtering = false;
114
+ filter = "";
115
+ cur = 0;
116
+ }
117
+ else if (k.name === "backspace")
118
+ filter = filter.slice(0, -1);
119
+ else if (k.name === "up")
120
+ cur--;
121
+ else if (k.name === "down")
122
+ cur++;
123
+ else if (s && s >= " " && !k.ctrl) {
124
+ filter += s;
125
+ cur = 0;
126
+ }
127
+ return draw();
128
+ }
129
+ if (k.name === "up" || s === "k")
130
+ cur = (cur + list.length - 1) % list.length;
131
+ else if (k.name === "down" || s === "j")
132
+ cur = (cur + 1) % list.length;
133
+ else if (k.name === "return")
134
+ return done(list[cur]?.value);
135
+ else if (k.name === "escape" || s === "q") {
136
+ if (filter) {
137
+ filter = "";
138
+ cur = 0;
139
+ }
140
+ else
141
+ return done(undefined);
142
+ }
143
+ else if (s === "/" && items.length > 6) {
144
+ filtering = true;
145
+ filter = "";
146
+ }
147
+ else if (s === "?")
148
+ help = !help;
149
+ else if (s && /^[1-9]$/.test(s) && list[Number(s) - 1])
150
+ return done(list[Number(s) - 1].value);
151
+ draw();
152
+ };
153
+ inp.on("keypress", onKey);
154
+ });
155
+ }
156
+ /**
157
+ * Text prompt. Esc cancels (throws Cancel), up/down recall `history`, and pasted multi-line text arrives as ONE answer
158
+ * instead of leaking into later prompts. With `multiline`, newlines in a paste are kept; otherwise they become spaces.
159
+ */
160
+ export async function ask(prompt, def = "", o = {}) {
161
+ const q = `${cyan("?")} ${bold(prompt)}${def ? " " + dim(`(${o.secret ? "set" : def})`) : ""} ${cyan("›")} `;
162
+ if (!isTTY()) {
163
+ process.stdout.write(q + "\n");
164
+ return (await nextLine())?.trim() || def;
165
+ }
166
+ const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true, escapeCodeTimeout: 40, history: o.history ? [...o.history] : undefined, historySize: 100 });
167
+ if (o.secret) {
168
+ const w = rl._writeToOutput;
169
+ rl._writeToOutput = (s) => { if (s.includes(q))
170
+ w.call(rl, s); };
171
+ }
172
+ return new Promise((resolve, reject) => {
173
+ const lines = [];
174
+ let timer, settled = false;
175
+ const finish = (err) => {
176
+ if (settled)
177
+ return;
178
+ settled = true;
179
+ clearTimeout(timer); // settle first: rl.close() fires the "close" handler below
180
+ process.stdin.off("keypress", onKey);
181
+ rl.close();
182
+ if (o.secret)
183
+ process.stdout.write("\n");
184
+ err ? reject(err) : resolve(lines.join(o.multiline ? "\n" : " ").trim() || def);
185
+ };
186
+ const onKey = (_s, k) => { if (k?.name === "escape") {
187
+ process.stdout.write("\n");
188
+ finish(new Cancel());
189
+ } };
190
+ process.stdin.on("keypress", onKey);
191
+ // a paste delivers several lines in one tick; collect them before answering
192
+ rl.on("line", (l) => { lines.push(l); clearTimeout(timer); timer = setTimeout(() => finish(), 25); });
193
+ rl.on("close", () => finish()); // e.g. Ctrl-D: answer with what we have
194
+ rl.setPrompt(q);
195
+ rl.prompt();
196
+ });
197
+ }
198
+ export const confirm = async (q, def = false) => (await select(q, [{ label: "Yes", value: true }, { label: "No", value: false }], def ? 0 : 1)) ?? false;
199
+ export const pause = async () => { try {
200
+ await ask(dim("press enter to continue"), "");
201
+ }
202
+ catch (e) {
203
+ if (!(e instanceof Cancel))
204
+ throw e;
205
+ } };
206
+ export const clearScreen = () => { if (process.stdout.isTTY)
207
+ process.stdout.write("\x1b[2J\x1b[H"); };
208
+ // ---- trace output + ref-counted spinner (parallel model calls share one spinner). The label may be a live function.
209
+ const frames = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏";
210
+ let spinners = 0, timer, frame = 0, label = "";
211
+ export function log(line = "") { process.stderr.write(process.stderr.isTTY ? `\r\x1b[2K${line}\n` : line + "\n"); }
212
+ export function spin(text) {
213
+ if (!process.stderr.isTTY)
214
+ return () => { };
215
+ label = text;
216
+ if (spinners++ === 0)
217
+ timer = setInterval(() => process.stderr.write(`\r\x1b[2K${cyan(frames[frame++ % frames.length])} ${dim(typeof label === "function" ? label() : label)}`), 100);
218
+ let stopped = false;
219
+ return () => { if (stopped)
220
+ return; stopped = true; if (--spinners === 0) {
221
+ clearInterval(timer);
222
+ process.stderr.write("\r\x1b[2K");
223
+ } };
224
+ }
@@ -0,0 +1,28 @@
1
+ import type { Readable, Writable } from "node:stream";
2
+ import type { Approve, Event, Message } from "./harness.ts";
3
+ export type AcpAgent = {
4
+ chat: (history: Message[], text: string, extra?: {
5
+ images?: {
6
+ mime: string;
7
+ data: string;
8
+ }[];
9
+ }) => Promise<{
10
+ messages: Message[];
11
+ text: string;
12
+ stopped?: string;
13
+ finished: boolean;
14
+ }>;
15
+ };
16
+ export type AcpOptions = {
17
+ input: Readable;
18
+ output: Writable;
19
+ version: string;
20
+ /** Build an agent for a session. `onEvent` and `approve` must be wired into it; `signal` is aborted by session/cancel. */
21
+ makeAgent: (s: {
22
+ cwd: string;
23
+ onEvent: (e: Event) => void;
24
+ approve: Approve;
25
+ signal: () => AbortSignal | undefined;
26
+ }) => AcpAgent;
27
+ };
28
+ export declare function serveAcp(o: AcpOptions): Promise<void>;
@@ -0,0 +1,111 @@
1
+ // Agent Client Protocol server: lets an editor (Zed, JetBrains, ...) host Amness as its coding agent. JSON-RPC 2.0, one JSON message per line over stdio.
2
+ // Implemented: initialize, session/new, session/prompt (streams text and tool calls back as session/update), session/cancel, and session/request_permission for risky calls.
3
+ // Not implemented: session/load, authenticate, fs/* and terminal/* (Amness reads and runs things itself), non-text prompt content other than images and embedded resources.
4
+ import { createInterface } from "node:readline";
5
+ const KIND = { read_file: "read", list_dir: "read", grep: "search", find_refs: "search", repo_map: "search", write_file: "edit", edit_file: "edit", apply_patch: "edit", sh: "execute", sh_start: "execute", web_fetch: "fetch", web_search: "fetch" };
6
+ export function serveAcp(o) {
7
+ const send = (m) => o.output.write(JSON.stringify({ jsonrpc: "2.0", ...m }) + "\n");
8
+ const sessions = new Map();
9
+ const waiting = new Map();
10
+ let nextId = 1, n = 0;
11
+ const call = (method, params) => new Promise((res) => { const id = nextId++; waiting.set(id, res); send({ id, method, params }); });
12
+ const update = (sessionId, u) => send({ method: "session/update", params: { sessionId, update: u } });
13
+ const newSession = (cwd) => {
14
+ const id = `sess_${Date.now().toString(36)}${(++n).toString(36)}`, s = { cwd, history: [], agent: undefined };
15
+ const onEvent = (e) => {
16
+ if (e.kind === "text")
17
+ update(id, { sessionUpdate: "agent_message_chunk", content: { type: "text", text: e.delta } });
18
+ else if (e.kind === "thinking")
19
+ update(id, { sessionUpdate: "agent_thought_chunk", content: { type: "text", text: e.delta } });
20
+ else if (e.kind === "toolstart")
21
+ update(id, { sessionUpdate: "tool_call", toolCallId: e.id, title: `${e.name} ${e.args.slice(0, 80)}`, kind: KIND[e.name] ?? "other", status: "in_progress" });
22
+ else if (e.kind === "tool" && e.id)
23
+ update(id, { sessionUpdate: "tool_call_update", toolCallId: e.id, status: /^(ERROR|DENIED|REJECTED)/.test(e.result) ? "failed" : "completed", content: [{ type: "content", content: { type: "text", text: e.result.slice(0, 4000) } }] });
24
+ else if (e.kind === "todos")
25
+ update(id, { sessionUpdate: "plan", entries: e.todos.map((t) => ({ content: t.content, priority: "medium", status: t.status === "done" ? "completed" : t.status })) });
26
+ };
27
+ const approve = async (req) => {
28
+ const r = await call("session/request_permission", { sessionId: id, toolCall: { toolCallId: req.id ?? `perm_${++n}`, title: `${req.name}: ${req.reason ?? "needs approval"}`, kind: KIND[req.name] ?? "other", status: "pending", rawInput: req.args, ...(req.preview && { content: [{ type: "content", content: { type: "text", text: req.preview.slice(0, 4000) } }] }) },
29
+ options: [{ optionId: "allow", name: "Allow", kind: "allow_once" }, { optionId: "reject", name: "Reject", kind: "reject_once" }] });
30
+ const out = r?.outcome;
31
+ return out?.outcome === "selected" && out.optionId === "allow" ? true : "the user rejected this in the editor";
32
+ };
33
+ s.agent = o.makeAgent({ cwd, onEvent, approve, signal: () => s.ac?.signal });
34
+ sessions.set(id, s);
35
+ return id;
36
+ };
37
+ const toText = (blocks) => {
38
+ const parts = [], images = [];
39
+ for (const b of blocks ?? []) {
40
+ if (b.type === "text")
41
+ parts.push(b.text);
42
+ else if (b.type === "image" && b.data)
43
+ images.push({ mime: b.mimeType ?? "image/png", data: b.data });
44
+ else if (b.type === "resource" && b.resource?.text !== undefined)
45
+ parts.push(`<file uri="${b.resource.uri}">\n${b.resource.text}\n</file>`);
46
+ else if (b.type === "resource_link" && b.uri)
47
+ parts.push(`(see ${b.uri})`);
48
+ }
49
+ return { text: parts.join("\n\n"), images };
50
+ };
51
+ const handle = async (m) => {
52
+ const ok = (result) => send({ id: m.id, result }), fail = (code, message) => send({ id: m.id, error: { code, message } });
53
+ try {
54
+ switch (m.method) {
55
+ case "initialize": return ok({ protocolVersion: 1, agentCapabilities: { loadSession: false, promptCapabilities: { image: true, embeddedContext: true }, mcpCapabilities: { http: false, sse: false } }, agentInfo: { name: "amness", title: "Amness", version: o.version }, authMethods: [] });
56
+ case "session/new": return ok({ sessionId: newSession(m.params?.cwd || process.cwd()) });
57
+ case "session/prompt": {
58
+ const s = sessions.get(m.params?.sessionId);
59
+ if (!s)
60
+ return fail(-32602, "unknown session");
61
+ if (s.ac)
62
+ return fail(-32000, "a turn is already running in this session");
63
+ const { text, images } = toText(m.params.prompt);
64
+ s.ac = new AbortController();
65
+ try {
66
+ const r = await s.agent.chat(s.history, text, { images });
67
+ s.history = r.messages;
68
+ const stop = s.ac.signal.aborted ? "cancelled" : r.stopped === "budget" || r.stopped === "maxSteps" ? "max_turn_requests" : "end_turn";
69
+ return ok({ stopReason: stop });
70
+ }
71
+ finally {
72
+ s.ac = undefined;
73
+ }
74
+ }
75
+ case "session/cancel":
76
+ sessions.get(m.params?.sessionId)?.ac?.abort();
77
+ return; // a notification: no reply
78
+ case "authenticate": return ok({});
79
+ default: if (m.id !== undefined)
80
+ return fail(-32601, `method not found: ${m.method}`);
81
+ }
82
+ }
83
+ catch (e) {
84
+ if (m.id !== undefined)
85
+ fail(-32603, e.message);
86
+ }
87
+ };
88
+ return new Promise((resolve) => {
89
+ const rl = createInterface({ input: o.input });
90
+ rl.on("line", (line) => {
91
+ if (!line.trim())
92
+ return;
93
+ let m;
94
+ try {
95
+ m = JSON.parse(line);
96
+ }
97
+ catch {
98
+ return send({ id: null, error: { code: -32700, message: "parse error" } });
99
+ }
100
+ if (m.method)
101
+ void handle(m);
102
+ else if (m.id !== undefined) {
103
+ waiting.get(m.id)?.(m.result ?? { error: m.error });
104
+ waiting.delete(m.id);
105
+ }
106
+ });
107
+ rl.on("close", () => { for (const s of sessions.values())
108
+ s.ac?.abort(); for (const w of waiting.values())
109
+ w({}); resolve(); });
110
+ });
111
+ }
@@ -0,0 +1,23 @@
1
+ export type AgentDef = {
2
+ name: string;
3
+ description: string;
4
+ tools: string[];
5
+ model?: string;
6
+ isolation?: "worktree";
7
+ maxSteps: number;
8
+ prompt: string;
9
+ scope: "project" | "user";
10
+ source: string;
11
+ file: string;
12
+ };
13
+ export declare const READ_TOOLS: string[];
14
+ /** Tools an agent definition may name. Everything else (task, background processes, memory) stays with the lead. */
15
+ export declare const AGENT_TOOLS: string[];
16
+ export declare const WRITE_TOOLS: Set<string>;
17
+ export declare function loadAgents(cwd: string, o?: {
18
+ userHome?: string;
19
+ }): {
20
+ agents: AgentDef[];
21
+ warnings: string[];
22
+ };
23
+ export declare const isWriter: (a: Pick<AgentDef, "tools">) => boolean;
@@ -0,0 +1,71 @@
1
+ // Custom subagents: a markdown file per agent (front matter + the agent's system prompt), in .amness/agents/ or .claude/agents/ (project and home).
2
+ // ---
3
+ // name: reviewer
4
+ // description: reviews diffs for bugs
5
+ // tools: read_file, grep, list_files (default: the read-only set)
6
+ // model: fast (a key of the `agentModels` option; default: the main model)
7
+ // isolation: worktree (a writing agent works in its own git worktree and hands back a diff)
8
+ // max-steps: 20
9
+ // ---
10
+ // You are a careful reviewer. ...
11
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
12
+ import { homedir } from "node:os";
13
+ import { join, resolve } from "node:path";
14
+ export const READ_TOOLS = ["list_files", "read_file", "grep"];
15
+ /** Tools an agent definition may name. Everything else (task, background processes, memory) stays with the lead. */
16
+ export const AGENT_TOOLS = ["list_files", "read_file", "grep", "write_file", "edit_file", "apply_patch", "sh", "skill"];
17
+ export const WRITE_TOOLS = new Set(["write_file", "edit_file", "apply_patch", "sh"]);
18
+ const CONVENTIONS = [".amness/agents", ".claude/agents"];
19
+ export function loadAgents(cwd, o = {}) {
20
+ const agents = [], warnings = [], seen = new Set(["explore"]), home = o.userHome ?? (process.env.AMNESS_HOME ? resolve(process.env.AMNESS_HOME, "..") : homedir());
21
+ for (const [base, scope] of [[cwd, "project"], [home, "user"]])
22
+ for (const conv of CONVENTIONS) {
23
+ const dir = join(base, conv);
24
+ if (!existsSync(dir))
25
+ continue;
26
+ let files = [];
27
+ try {
28
+ files = readdirSync(dir).filter((f) => f.endsWith(".md")).sort();
29
+ }
30
+ catch {
31
+ continue;
32
+ }
33
+ for (const f of files) {
34
+ const text = readFileSync(join(dir, f), "utf8"), m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(text);
35
+ if (!m) {
36
+ warnings.push(`${conv}/${f}: skipped, it needs front matter with a name and description`);
37
+ continue;
38
+ }
39
+ const meta = {};
40
+ for (const line of m[1].split(/\r?\n/)) {
41
+ const kv = /^([\w-]+):\s*(.*)$/.exec(line);
42
+ if (kv)
43
+ meta[kv[1]] = kv[2].replace(/^["']|["']$/g, "").trim();
44
+ }
45
+ const name = (meta.name || f.replace(/\.md$/, "")).trim();
46
+ if (!/^[\w][\w.-]{0,40}$/.test(name)) {
47
+ warnings.push(`${conv}/${f}: skipped, "${name}" is not a usable agent name`);
48
+ continue;
49
+ }
50
+ if (!meta.description) {
51
+ warnings.push(`${conv}/${f}: skipped, it needs a "description:" line`);
52
+ continue;
53
+ }
54
+ if (seen.has(name)) {
55
+ if (name === "explore")
56
+ warnings.push(`${conv}/${f}: "explore" is built in; pick another name`);
57
+ continue;
58
+ }
59
+ seen.add(name);
60
+ const asked = (meta.tools ? meta.tools.split(/[,\s]+/).filter(Boolean) : READ_TOOLS), tools = asked.filter((t) => AGENT_TOOLS.includes(t));
61
+ for (const t of asked)
62
+ if (!AGENT_TOOLS.includes(t))
63
+ warnings.push(`${conv}/${f}: tool "${t}" is not available to subagents (allowed: ${AGENT_TOOLS.join(", ")})`);
64
+ if (!/\b(report|return|summar|answer|finish)/i.test(m[2]))
65
+ warnings.push(`${conv}/${f}: advice: say what the agent should report back (a short summary with file paths); subagents that do not know tend to ramble or stop without an answer`);
66
+ agents.push({ name, description: meta.description.slice(0, 200), tools: tools.length ? tools : READ_TOOLS, model: meta.model || undefined, isolation: meta.isolation === "worktree" ? "worktree" : undefined, maxSteps: Math.min(60, Math.max(3, Number(meta["max-steps"]) || 20)), prompt: m[2].trim(), scope, source: conv, file: join(dir, f) });
67
+ }
68
+ }
69
+ return { agents, warnings };
70
+ }
71
+ export const isWriter = (a) => a.tools.some((t) => WRITE_TOOLS.has(t));
@@ -0,0 +1,30 @@
1
+ import type { Message, Model } from "./harness.ts";
2
+ /**
3
+ * Claude backend via the official Anthropic SDK (an optional dependency, installed with the package: `npm i @anthropic-ai/sdk` if it is missing).
4
+ * Defaults follow Anthropic's guidance for agent loops: adaptive thinking, auto prompt caching, streaming,
5
+ * server-side refusal fallbacks. Credentials: `apiKey`, ANTHROPIC_API_KEY, or an `ant auth login` profile.
6
+ */
7
+ export type AnthropicOptions = {
8
+ /** Advisory token budget for each agentic turn (Claude's `task_budget`, beta): the model sees a running countdown and wraps up gracefully. At least 20000; only sent to models that support it. Set once; it is not changed between requests (that would break the prompt cache). */
9
+ taskBudget?: number;
10
+ model?: string;
11
+ apiKey?: string;
12
+ baseUrl?: string;
13
+ effort?: "low" | "medium" | "high" | "xhigh" | "max";
14
+ maxTokens?: number;
15
+ /** Re-run a safety-declined request on Anthropic's recommended fallback model. Default: on where the model supports it. */
16
+ fallbacks?: boolean;
17
+ /** Adaptive thinking. Default: on for models that support it. */
18
+ thinking?: boolean;
19
+ /** Inject a client (tests, proxies, Bedrock/Vertex clients). */
20
+ client?: any;
21
+ };
22
+ export declare const DEFAULT_CLAUDE = "claude-opus-5-5";
23
+ /** Our transcript -> Anthropic `system` + `messages`. Parallel tool results are merged into ONE user message, as the API wants. */
24
+ export declare function toAnthropic(msgs: Message[]): {
25
+ system?: string;
26
+ messages: any[];
27
+ };
28
+ /** Anthropic response -> our Message, keeping the native blocks (thinking!) in `raw`. */
29
+ export declare function fromAnthropic(r: any): Message;
30
+ export declare function anthropic(o?: AnthropicOptions): Model;
@@ -0,0 +1,91 @@
1
+ export const DEFAULT_CLAUDE = "claude-opus-5-5";
2
+ const ADAPTIVE = /^claude-(opus-(4-[6-8]|5)|sonnet-(4-6|5)|fable|mythos|haiku-5)/;
3
+ const FALLBACK_OK = /^claude-(opus-5|fable-|sonnet-5-5|mythos-)/;
4
+ const isError = (s) => /^(ERROR|DENIED|REJECTED)/.test(s ?? "");
5
+ const parse = (s) => { try {
6
+ return JSON.parse(s || "{}");
7
+ }
8
+ catch {
9
+ return {};
10
+ } };
11
+ /** Our transcript -> Anthropic `system` + `messages`. Parallel tool results are merged into ONE user message, as the API wants. */
12
+ export function toAnthropic(msgs) {
13
+ let system;
14
+ const out = [];
15
+ const push = (role, blocks) => {
16
+ const last = out.at(-1);
17
+ if (last?.role === role)
18
+ last.content.push(...blocks);
19
+ else
20
+ out.push({ role, content: blocks });
21
+ };
22
+ for (const m of msgs) {
23
+ if (m.role === "system")
24
+ system = (system ? system + "\n\n" : "") + (m.content ?? "");
25
+ else if (m.role === "user")
26
+ push("user", [...(m.images ?? []).map((i) => ({ type: "image", source: { type: "base64", media_type: i.mime, data: i.data } })), { type: "text", text: m.content || "(continue)" }]);
27
+ else if (m.role === "tool")
28
+ push("user", [{ type: "tool_result", tool_use_id: m.tool_call_id, content: m.content ?? "", ...(isError(m.content) && { is_error: true }) }]);
29
+ else {
30
+ const blocks = Array.isArray(m.raw) ? m.raw : [
31
+ ...(m.content ? [{ type: "text", text: m.content }] : []),
32
+ ...(m.tool_calls ?? []).map((c) => ({ type: "tool_use", id: c.id, name: c.function.name, input: parse(c.function.arguments) })),
33
+ ];
34
+ push("assistant", blocks.length ? blocks : [{ type: "text", text: "(no output)" }]);
35
+ }
36
+ }
37
+ return { system, messages: out };
38
+ }
39
+ /** Anthropic response -> our Message, keeping the native blocks (thinking!) in `raw`. */
40
+ export function fromAnthropic(r) {
41
+ const blocks = r.content ?? [];
42
+ const text = blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
43
+ let calls = blocks.filter((b) => b.type === "tool_use").map((b) => ({ id: b.id, type: "function", function: { name: b.name, arguments: JSON.stringify(b.input ?? {}) } }));
44
+ let content = text || null, raw = blocks;
45
+ if (r.stop_reason === "refusal" && !calls.length)
46
+ content = `[model declined: ${r.stop_details?.category ?? "policy"}] ${text}`.trim();
47
+ if (r.stop_reason === "max_tokens" && calls.length) {
48
+ // A tool call cut off mid-JSON must never run. Drop it and nudge the model to go smaller (the "I'll" trips the harness nudge).
49
+ calls = [];
50
+ raw = blocks.filter((b) => b.type !== "tool_use");
51
+ content = `${text}\n[output hit the max_tokens limit; I'll redo that in smaller steps]`.trim();
52
+ }
53
+ const u = r.usage ?? {}, input = (u.input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
54
+ return {
55
+ role: "assistant", content, raw, ...(calls.length && { tool_calls: calls }),
56
+ usage: { prompt_tokens: input, completion_tokens: u.output_tokens ?? 0, total_tokens: input + (u.output_tokens ?? 0), cached_tokens: u.cache_read_input_tokens ?? 0 },
57
+ };
58
+ }
59
+ const toTool = (t) => ({
60
+ name: t.name, ...(t.description && { description: t.description }),
61
+ input_schema: t.parameters.type === "object" ? t.parameters : { type: "object", properties: {} },
62
+ });
63
+ /** Models that accept `task_budget` (beta). Not Sonnet 5, Opus 4.6, Sonnet 4.6 or Haiku 4.5. */
64
+ const TASK_BUDGET_OK = /claude-(fable|mythos|opus-(4-7|4-8|5)|sonnet-5-5|haiku-5-5)/;
65
+ export function anthropic(o = {}) {
66
+ const model = o.model ?? process.env.AMNESS_MODEL ?? DEFAULT_CLAUDE;
67
+ const fallbacks = o.fallbacks ?? FALLBACK_OK.test(model);
68
+ const thinking = o.thinking ?? ADAPTIVE.test(model);
69
+ const taskBudget = o.taskBudget && o.taskBudget >= 20_000 && TASK_BUDGET_OK.test(model) ? o.taskBudget : undefined;
70
+ let client = o.client;
71
+ const call = async (messages, tools, opts) => {
72
+ client ??= await import("@anthropic-ai/sdk").then((m) => new m.default({ ...(o.apiKey && { apiKey: o.apiKey }), ...(o.baseUrl && { baseURL: o.baseUrl }) }), () => { throw new Error("Claude support needs the SDK: npm install @anthropic-ai/sdk"); });
73
+ const { system, messages: msgs } = toAnthropic(messages);
74
+ const req = {
75
+ model, max_tokens: opts?.maxTokens ?? o.maxTokens ?? 32_000, ...(system && { system }), messages: msgs,
76
+ ...(tools.length && { tools: tools.map(toTool) }),
77
+ ...((thinking || taskBudget) && { ...(thinking && { thinking: { type: "adaptive" } }), output_config: { ...(thinking && { effort: o.effort ?? "high" }), ...(taskBudget && { task_budget: { type: "tokens", total: taskBudget } }) } }),
78
+ cache_control: { type: "ephemeral" }, // the transcript is append-only, so each turn reads the previous one from cache
79
+ };
80
+ // Streaming avoids HTTP timeouts on long outputs; we only need the assembled message.
81
+ const betas = [...(fallbacks ? ["server-side-fallback-2026-07-01"] : []), ...(taskBudget ? ["task-budgets-2026-03-13"] : [])];
82
+ const stream = betas.length
83
+ ? client.beta.messages.stream({ ...req, betas, ...(fallbacks && { fallbacks: "default" }) }, { signal: opts?.signal })
84
+ : client.messages.stream(req, { signal: opts?.signal });
85
+ if (opts?.onText)
86
+ stream.on?.("text", (delta) => opts.onText(delta));
87
+ return fromAnthropic(await stream.finalMessage());
88
+ };
89
+ call.appendOnly = true;
90
+ return call;
91
+ }
@@ -0,0 +1,15 @@
1
+ import type { Approve, Model } from "./harness.ts";
2
+ export type ReviewDecision = {
3
+ decision: "approve" | "escalate";
4
+ reason: string;
5
+ };
6
+ export declare function parseDecision(text: string): ReviewDecision;
7
+ /** Wrap `inner` (what asks the user) so a reviewer model gets the first look. `task` is the user's request, for context. */
8
+ export declare function reviewedApprove(inner: Approve, o: {
9
+ model: Model;
10
+ task?: () => string | undefined;
11
+ onDecision?: (d: ReviewDecision & {
12
+ tool: string;
13
+ }) => void;
14
+ timeoutMs?: number;
15
+ }): Approve;
@@ -0,0 +1,40 @@
1
+ const SYSTEM = `You review ONE tool call an autonomous coding agent wants to make, on behalf of the developer who owns the machine. Decide if it is clearly safe to run without asking them.
2
+ Approve only when ALL hold: it does what the task plausibly needs; it stays inside the project; it is easy to undo or only reads; it sends nothing off the machine; it deletes or overwrites nothing that was not just created.
3
+ Escalate (let the developer decide) for anything else, and whenever you are unsure. Network writes, publishing, installing global packages, credentials, deleting data, force operations, and anything you do not fully understand are always escalated.
4
+ Reply with JSON only: {"decision":"approve"|"escalate","reason":"<one short sentence>"}`;
5
+ const clip = (s, n) => (s.length > n ? s.slice(0, n) + "…" : s);
6
+ export function parseDecision(text) {
7
+ const m = /\{[\s\S]*\}/.exec(text);
8
+ if (!m)
9
+ return { decision: "escalate", reason: "reviewer gave no verdict" };
10
+ try {
11
+ const j = JSON.parse(m[0]);
12
+ return { decision: j.decision === "approve" ? "approve" : "escalate", reason: String(j.reason ?? "").slice(0, 200) || (j.decision === "approve" ? "judged safe" : "unsure") };
13
+ }
14
+ catch {
15
+ return { decision: "escalate", reason: "reviewer verdict was not valid JSON" };
16
+ }
17
+ }
18
+ /** Wrap `inner` (what asks the user) so a reviewer model gets the first look. `task` is the user's request, for context. */
19
+ export function reviewedApprove(inner, o) {
20
+ return async (req) => {
21
+ if (req.floor)
22
+ return inner(req); // the safety floor and untrusted-text policy always reach a human
23
+ let d;
24
+ try {
25
+ const msgs = [{ role: "system", content: SYSTEM }, { role: "user", content: `Developer's request: ${clip(o.task?.() ?? "(unknown)", 600)}\n\nTool: ${req.name}\nWhy it needs approval: ${req.reason ?? "(not stated)"}\nArguments: ${clip(JSON.stringify(req.args), 1500)}${req.preview ? `\nPreview:\n${clip(req.preview, 1500)}` : ""}` }];
26
+ const ac = new AbortController(), t = setTimeout(() => ac.abort(), o.timeoutMs ?? 20_000);
27
+ try {
28
+ d = parseDecision((await o.model(msgs, [], { signal: ac.signal })).content ?? "");
29
+ }
30
+ finally {
31
+ clearTimeout(t);
32
+ }
33
+ }
34
+ catch {
35
+ d = { decision: "escalate", reason: "reviewer unavailable" };
36
+ }
37
+ o.onDecision?.({ ...d, tool: req.name });
38
+ return d.decision === "approve" ? true : inner(req);
39
+ };
40
+ }