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
@@ -0,0 +1,650 @@
1
+ // Slash commands. Each gets the running App and the raw argument string.
2
+ import { relatives } from "./session.js";
3
+ import { SHELL_CHOICES, STRATEGIES, chooseShell, currentShell, loadPrompts, setShell, shellFallbackNote } from "../../src/index.js";
4
+ import { join, resolve } from "node:path";
5
+ import { collectFiles, effortsFor, loadMcp, readBundle, writeBundle } from "../../src/index.js";
6
+ import { CLAUDE_MODELS, OPENAI_MODELS, PROVIDERS } from "../config.js";
7
+ import { where } from "../keystore.js";
8
+ import { bold, cyan, dim, green, red, yellow } from "../ui.js";
9
+ import { estimateCost, fmtCost, fmtDuration, fmtTokens } from "./pricing.js";
10
+ import { codeBlocks } from "./clipboard.js";
11
+ import { execFile } from "node:child_process";
12
+ import { caseFromWorkspace, commitLog, createPr, currentBranch, defaultBase, diffForReview, draftPr, hasUpstream, pushBranch, reviewWork, formatMining, Hooks, Permissions, SANDBOX_MODES, cacheWaste, projectConfigFingerprint, sandboxHelp, sandboxKind } from "../../src/index.js";
13
+ import { loadState, saveState } from "../state.js";
14
+ import { tokenReport } from "./tokens.js";
15
+ import { THEMES, getTheme, themeNames } from "./themes.js";
16
+ import { helpLines, renderDiff, toolCall, todoList } from "./view.js";
17
+ const INIT_PROMPT = "Explore this repository and write an AGENTS.md at the repo root for future coding sessions: what the project is, how to build/test/run it (exact commands), the code layout, conventions you can verify from the code, and gotchas. Keep it under 80 lines and do not invent anything you cannot confirm from the files.";
18
+ const EFFORTS = ["low", "medium", "high", "xhigh", "max"];
19
+ export function buildCommands() {
20
+ return [
21
+ { name: "help", alias: "?", help: "keys and commands", live: true, run: (a) => a.openPager(helpLines(a.cmds, (a.io.output.columns || 80) - 1), "help") },
22
+ { name: "model", help: "switch model", args: "[id]", run: async (a, arg) => {
23
+ let id = arg;
24
+ if (!id) {
25
+ const list = a.cfg.provider === "anthropic" ? CLAUDE_MODELS : a.cfg.provider === "openai" ? OPENAI_MODELS : [{ id: a.cfg.model, hint: "current" }];
26
+ const known = list.map((m) => ({ label: m.id, value: m.id, hint: m.hint }));
27
+ id = await a.pick("Model", [...known, { label: "Other…", value: "" }], Math.max(0, known.findIndex((m) => m.value === a.cfg.model)));
28
+ if (id === undefined)
29
+ return;
30
+ if (id === "") {
31
+ id = await a.askText("Model id", { initial: a.cfg.model });
32
+ if (!id)
33
+ return;
34
+ }
35
+ }
36
+ a.cfg.model = id;
37
+ if (a.cfg.effort && a.cfg.provider !== "anthropic" && !effortsFor(id).includes(a.cfg.effort)) {
38
+ a.say(`effort "${a.cfg.effort}" is not valid for ${id}; using the model default`, yellow);
39
+ a.cfg.effort = undefined;
40
+ }
41
+ a.saveConfig();
42
+ a.buildAgent();
43
+ a.say(`model → ${cyan(id)}`, dim);
44
+ } },
45
+ { name: "provider", help: "switch provider (OpenAI, Claude, Ollama…)", run: async (a) => {
46
+ const id = await a.pick("Provider", Object.keys(PROVIDERS).map((k) => ({ label: PROVIDERS[k].label, value: k, hint: PROVIDERS[k].baseUrl })));
47
+ if (!id)
48
+ return;
49
+ const key = id === "anthropic" ? process.env.ANTHROPIC_API_KEY : process.env.OPENAI_API_KEY;
50
+ let apiKey = key;
51
+ if (PROVIDERS[id].needsKey && !apiKey) {
52
+ apiKey = await a.askText(`${PROVIDERS[id].label} API key (hidden; ${where()})`, { secret: true });
53
+ if (!apiKey)
54
+ return;
55
+ }
56
+ Object.assign(a.cfg, { provider: id, baseUrl: PROVIDERS[id].baseUrl, model: PROVIDERS[id].model, apiKey });
57
+ if (id === "custom" || id === "ollama") {
58
+ const m = await a.askText("Model id", { initial: a.cfg.model });
59
+ if (m)
60
+ a.cfg.model = m;
61
+ const u = await a.askText("Base URL", { initial: a.cfg.baseUrl });
62
+ if (u)
63
+ a.cfg.baseUrl = u;
64
+ }
65
+ a.saveConfig();
66
+ a.buildAgent();
67
+ a.say(`provider → ${cyan(PROVIDERS[id].label)}, model ${cyan(a.cfg.model)}`, dim);
68
+ } },
69
+ { name: "effort", help: "reasoning effort for the current model", args: "[level]", run: async (a, arg) => {
70
+ const levels = a.cfg.provider === "anthropic" ? [...EFFORTS] : effortsFor(a.cfg.model);
71
+ if (!levels.length)
72
+ return void a.say(`${a.cfg.model} is not a reasoning model, so there is no effort setting`, yellow);
73
+ let e = arg;
74
+ if (!e)
75
+ e = await a.pick("Reasoning effort", [{ label: "default", value: "default", hint: "let the model decide" }, ...levels.map((x) => ({ label: x, value: x, hint: x === "none" ? "no reasoning: fastest" : x === "max" ? "deepest" : undefined }))], a.cfg.effort ? Math.max(0, levels.indexOf(a.cfg.effort)) + 1 : 0); // preselect what is set now, or "default"
76
+ if (!e)
77
+ return;
78
+ if (e !== "default" && !levels.includes(e))
79
+ return void a.say(`"${e}" is not valid for ${a.cfg.model}. Choose: ${levels.join(", ")}`, red);
80
+ a.cfg.effort = e === "default" ? undefined : e;
81
+ a.saveConfig();
82
+ a.buildAgent();
83
+ a.say(`effort → ${cyan(e)}`, dim);
84
+ } },
85
+ { name: "api", help: "OpenAI wire API: auto · responses · chat", args: "[mode]", run: async (a, arg) => {
86
+ if (a.cfg.provider === "anthropic")
87
+ return void a.say("this applies to OpenAI-compatible providers only", yellow);
88
+ const m = arg || (await a.pick("OpenAI wire API", [{ label: "auto", value: "auto", hint: "Responses for reasoning models, Chat elsewhere; self-correcting (default)" }, { label: "responses", value: "responses", hint: "/v1/responses: required for tools + reasoning on current models" }, { label: "chat", value: "chat", hint: "/v1/chat/completions: Ollama, vLLM, most gateways" }]));
89
+ if (!m)
90
+ return;
91
+ if (!["auto", "responses", "chat"].includes(m))
92
+ return void a.say(`unknown mode "${m}"`, red);
93
+ a.cfg.api = m;
94
+ a.saveConfig();
95
+ a.buildAgent();
96
+ a.say(`api → ${cyan(m)}`, dim);
97
+ } },
98
+ { name: "mode", help: "permissions: default · acceptEdits · plan · yolo", args: "[mode]", run: async (a, arg) => {
99
+ const m = arg || (await a.pick("Permission mode", [
100
+ { label: "default", value: "default", hint: "ask before edits and non-trivial commands" }, { label: "acceptEdits", value: "acceptEdits", hint: "edits are automatic, commands still ask" },
101
+ { label: "plan", value: "plan", hint: "read-only: investigate and propose a plan" }, { label: "yolo", value: "yolo", hint: "never ask (dangerous)" }
102
+ ]));
103
+ if (!m)
104
+ return;
105
+ if (!["default", "acceptEdits", "plan", "yolo"].includes(m))
106
+ return void a.say(`unknown mode "${m}"`, red);
107
+ a.setMode(m);
108
+ } },
109
+ { name: "plan", help: "enter plan mode (read-only)", run: (a) => a.setMode("plan") },
110
+ { name: "clear", help: "start a fresh conversation", alias: "new", run: (a) => {
111
+ a.newConversation();
112
+ a.screen.height = 0;
113
+ a.io.output.write("\x1b[2J\x1b[3J\x1b[H");
114
+ a.commit([dim(" conversation cleared; the old one is saved (use /resume)"), ""]);
115
+ } },
116
+ { name: "compact", help: "summarise the conversation to free up context", run: async (a) => {
117
+ if (!a.session.messages.length)
118
+ return void a.say("nothing to compact yet", dim);
119
+ a.note("compacting…", 60_000);
120
+ const r = await a.agent.compact(a.session.messages);
121
+ a.session.messages = r.messages;
122
+ a.lastPrompt = 0;
123
+ a.saveSession();
124
+ a.noticeUntil = 0;
125
+ a.rebuildBlocks();
126
+ a.commit(["", ` ${green("✓")} ${dim("conversation compacted into a handoff note")}`, ...r.note.split("\n").slice(0, 8).map((l) => " " + dim(l))]);
127
+ } },
128
+ { name: "memory", help: "list notes · forget <name> · add <text> · stale · prune · consolidate · graph · embeddings", args: "[forget|add|stale|prune|consolidate|graph|embeddings]", run: async (a, arg) => {
129
+ const m = a.memory;
130
+ if (!m)
131
+ return void a.say("memory is disabled", yellow);
132
+ const [sub, ...rest] = arg.split(/\s+/), tail = rest.join(" ");
133
+ if (sub === "forget" && tail)
134
+ return void a.say(m.forget(tail) ? `forgot ${tail}` : `no note named ${tail}`, dim);
135
+ if (sub === "add" && tail)
136
+ return a.remember(tail);
137
+ if (sub === "embeddings") {
138
+ if (tail !== "on" && tail !== "off")
139
+ return void a.say(`semantic memory search is ${a.cfg.embeddings ? "on" : "off"}. /memory embeddings on|off`, dim);
140
+ if (tail === "on" && a.cfg.provider === "anthropic")
141
+ return void a.say("Claude has no embeddings endpoint. Switch the provider to an OpenAI-compatible one (/provider), or keep keyword search.", yellow);
142
+ a.cfg.embeddings = tail === "on";
143
+ a.saveConfig();
144
+ a.buildAgent();
145
+ return void a.say(tail === "on" ? "semantic memory search on: note text and your messages are sent to your provider's /embeddings endpoint (stored vectors live in memory/embeddings.json)" : "semantic memory search off", dim);
146
+ }
147
+ if (sub === "graph") {
148
+ const all = m.list(), rows = all.map((e) => [e, m.neighbors(e, all)]).filter(([, n]) => n.length);
149
+ return void a.commit(rows.length ? ["", bold(" Related notes"), ...rows.map(([e, n]) => ` ${cyan(e.name.padEnd(24))} ${dim("↔")} ${n.map((x) => x.name).join(", ")}`), dim(" links come from [[name]] mentions and from notes that keep being retrieved together")] : ["", dim(" no links yet. Mention [[other-note]] in a note, or keep using memory: notes retrieved together get linked.")]);
150
+ }
151
+ if (sub === "consolidate") {
152
+ a.say("asking the model to tidy memory (merge duplicates, archive obsolete notes)…", dim);
153
+ const plan = await m.consolidate(a.roles.compact ?? a.model);
154
+ if (!plan.merges.length && !plan.archive.length)
155
+ return void a.say("memory looks tidy: nothing to merge or archive", dim);
156
+ const lines = [...plan.merges.map((x) => `merge ${x.from.join(", ")} → ${x.keep}`), ...plan.archive.map((n) => `archive ${n}`)];
157
+ a.commit(["", bold(" Proposed memory clean-up"), ...lines.map((l) => " " + l), dim(" nothing is deleted: replaced notes move to memory/archive/")]);
158
+ const ok = await a.pick("Apply this clean-up?", [{ label: "Yes, apply", value: "y" }, { label: "No", value: "n" }]);
159
+ if (ok === "y") {
160
+ const r = m.applyPlan(plan);
161
+ a.say(`merged ${r.merged}, archived ${r.archived}`, dim);
162
+ }
163
+ return;
164
+ }
165
+ if (sub === "stale") {
166
+ const st = m.stale();
167
+ return void a.commit(st.length ? ["", bold(" Never-used notes (older than 90 days, surfaced at most once)"), ...st.map((e) => ` ${cyan(e.name.padEnd(24))} ${dim(`[${e.scope}/${e.type}] last used ${e.used ?? e.updated}`)}`), dim(" /memory prune moves them to an archive folder (reversible)")] : ["", dim(" no stale notes")]);
168
+ }
169
+ if (sub === "prune") {
170
+ const gone = m.prune();
171
+ return void a.say(gone.length ? `archived ${gone.length} stale note${gone.length === 1 ? "" : "s"}: ${gone.join(", ")} (moved to memory/archive/)` : "nothing stale to archive", dim);
172
+ }
173
+ const all = m.list();
174
+ if (!all.length)
175
+ return void a.say("no memory notes yet. Use `# note` or ask me to remember something.", dim);
176
+ a.commit(["", bold(" Memory"), ...all.map((e) => ` ${cyan(e.name.padEnd(24))} ${dim(`[${e.scope}/${e.type}]${e.hits ? ` · used ${e.hits}×` : ""}`)} ${e.description}`)]);
177
+ } },
178
+ { name: "resume", help: "continue an earlier conversation", alias: "sessions", run: async (a) => {
179
+ const list = a.store.list().filter((s) => s.id !== a.session.id);
180
+ if (!list.length)
181
+ return void a.say("no earlier conversations in this project", dim);
182
+ const id = await a.pick("Resume", list.map((s) => ({ label: s.title || "(untitled)", value: s.id, hint: `${s.turns} turns · ${s.updated.slice(0, 16).replace("T", " ")} · ${s.model}` })));
183
+ const s = id && a.store.load(id);
184
+ if (!s)
185
+ return;
186
+ a.loadSession(s);
187
+ a.rebuildBlocks();
188
+ a.commit(["", ` ${green("↻")} ${dim(`resumed "${s.title}" (${s.turns.length} turns)`)}`]);
189
+ } },
190
+ { name: "branch", help: "fork the conversation from an earlier turn (the current one stays available in /branches)", args: "[turn]", run: async (a, arg) => {
191
+ if (a.running)
192
+ return void a.say("wait for the current turn to finish", dim);
193
+ if (!a.session.turns.length)
194
+ return void a.say("nothing to branch from yet", dim);
195
+ const n = arg.trim() ? Number(arg) : await a.pick("Branch after turn…", a.session.turns.map((t, i) => ({ label: `${i + 1}. ${t.text.replace(/\s+/g, " ").slice(0, 60)}`, value: i + 1 })).reverse());
196
+ if (!n || !Number.isInteger(n) || n < 1 || n > a.session.turns.length)
197
+ return void a.say(`give a turn number from 1 to ${a.session.turns.length}`, dim);
198
+ await a.branchAfter(n - 1);
199
+ } },
200
+ { name: "branches", help: "switch between branches of this conversation", run: async (a) => {
201
+ const all = a.store.list().map((i) => a.store.load(i.id)).filter(Boolean);
202
+ if (a.session.turns.length)
203
+ all.push(a.session);
204
+ const ids = new Set(relatives(Array.from(new Map(all.map((s) => [s.id, s])).values()), a.session.id));
205
+ const list = a.store.list().filter((s) => ids.has(s.id) && s.id !== a.session.id);
206
+ if (!list.length)
207
+ return void a.say("this conversation has no other branches (/branch forks one)", dim);
208
+ const id = await a.pick("Branches", list.map((s) => ({ label: s.title || "(untitled)", value: s.id, hint: `${s.turns} turns · ${s.updated.slice(0, 16).replace("T", " ")}` })));
209
+ const s = id && a.store.load(id);
210
+ if (!s)
211
+ return;
212
+ a.loadSession(s);
213
+ a.rebuildBlocks();
214
+ a.commit(["", ` ${green("↻")} ${dim(`switched to "${s.title}" (${s.turns.length} turns)`)}`, dim(" files are not swapped: they stay as they are now. Use /rewind or git if this branch expects different files.")]);
215
+ } },
216
+ { name: "export", help: "save a portable session bundle", args: "[file]", run: async (a, arg) => {
217
+ if (!a.session.turns.length)
218
+ return void a.say("nothing to export yet", dim);
219
+ a.note("writing handoff…", 60_000);
220
+ const file = resolve(a.cwd, arg || join(".amness", "sessions", `${a.session.id}.json`));
221
+ const touched = new Set(a.edits.map((e) => e.path));
222
+ const { bundle } = await a.agent.exportBundle({ messages: a.session.messages, finished: true, text: a.session.title }, undefined);
223
+ bundle.files = collectFiles(a.cwd, touched);
224
+ writeBundle(file, bundle);
225
+ a.noticeUntil = 0;
226
+ a.say(`${green("✓")} ${file}\n${dim(`${Object.keys(bundle.files).length} file(s), ${bundle.memory.length} note(s). Continue anywhere: amness run --bundle <file> "continue" or /import <file>`)}`);
227
+ } },
228
+ { name: "import", help: "continue from a session bundle", args: "<file>", run: async (a, arg) => {
229
+ if (!arg)
230
+ return void a.say("usage: /import <bundle.json>", yellow);
231
+ const b = readBundle(resolve(a.cwd, arg));
232
+ a.newConversation();
233
+ await a.turn(`Continue the work from a previous session.\n\nOriginal task: ${b.task}\nStatus then: ${b.status.finished ? "finished" : "unfinished"}\n\n${b.handoff}\n\nRe-check the workspace before continuing.`);
234
+ } },
235
+ { name: "diff", help: "review every change from this session", live: true, run: async (a) => {
236
+ if (!a.edits.length)
237
+ return void a.say("no edits in this session yet", dim);
238
+ a.openPager(a.edits.flatMap((e) => ["", bold(`${e.path}${e.created ? " (new)" : ""} ${green("+" + e.added)} ${red("-" + e.removed)}`), ...renderDiff(e.diff, a.screen.cols - 1, 400)]), "changes");
239
+ } },
240
+ { name: "undo", help: "revert the last turn (files and conversation)", run: (a) => {
241
+ if (!a.session.turns.length)
242
+ return void a.say("nothing to undo", dim);
243
+ a.rewindTo(a.session.turns.length - 1);
244
+ } },
245
+ { name: "rewind", help: "go back to an earlier turn", run: (a) => a.openRewind() },
246
+ { name: "loop", alias: "trace", help: "step-by-step trace of the agent loop: model turns, tools, guards", live: true, run: (a) => {
247
+ const T = a.trace;
248
+ if (!T.length)
249
+ return a.say(dim("no loop activity yet: send a task first"));
250
+ const tot = T.reduce((n, s) => n + s.completion + s.prompt, 0), ms = T.reduce((n, s) => n + s.ms, 0);
251
+ const waste = cacheWaste(a.tracer?.turns ?? []), wasteCost = waste ? estimateCost(a.cfg.model, waste, 0) : null, wasteNote = waste > 500 ? `${fmtTokens(waste)} tokens re-read at full price after cache breaks${wasteCost !== null ? ` (≈ ${fmtCost(wasteCost - estimateCost(a.cfg.model, waste, 0, waste))} wasted)` : ""} · ` : "";
252
+ const out = ["", bold(`Agent loop · ${T.length} turn${T.length === 1 ? "" : "s"} · ${fmtDuration(ms)} · ${fmtTokens(tot)} tokens`), dim(`${wasteNote}${a.trimmed ? `trimmed ~${fmtTokens(Math.round(a.trimmed / 4))} tokens of stale or noisy output · ` : ""}${a.cacheBreaks ? `${a.cacheBreaks} cache break${a.cacheBreaks === 1 ? "" : "s"} · ` : ""}each turn: the model answers, its tool calls run, results go back; guards are shown where they fired`), ""];
253
+ for (const s of T) {
254
+ const pad = String(s.n).padStart(2);
255
+ out.push(`${cyan(`${pad} ●`)} ${bold("model")} ${dim(`${fmtDuration(s.ms)} · ${fmtTokens(s.prompt)} in · ${fmtTokens(s.completion)} out${s.cached ? ` · ${fmtTokens(s.cached)} cached` : ""}`)}`);
256
+ s.tools.forEach((x, i) => out.push(` ${dim(i === s.tools.length - 1 && !s.notes.length ? "└─" : "├─")} ${x.ok ? green("✓") : red("✗")} ${toolCall(x.name, x.args)} ${dim(fmtDuration(x.ms))}${s.parallel && s.parallel > 1 && i === 0 ? dim(` (${s.parallel} calls this turn)`) : ""}`));
257
+ s.notes.forEach((n, i) => out.push(` ${dim(i === s.notes.length - 1 ? "└─" : "├─")} ${yellow("⚑")} ${dim(n)}`));
258
+ if (!s.tools.length && !s.notes.length)
259
+ out.push(` ${dim("└─ replied without tools")}`);
260
+ }
261
+ return a.openPager(out, "loop trace");
262
+ } },
263
+ { name: "tokens", alias: "cost", help: "context window, what fills it, tokens and cost", live: true, run: (a) => {
264
+ a.commit(tokenReport({ model: a.cfg.model, messages: a.session.messages, usage: a.session.usage, series: a.session.usage.series ?? [], lastPrompt: a.lastPrompt, turnTokens: a.runTokens, turnBudget: a.running ? a.cfg.maxTokens : 0, width: a.width,
265
+ toolDefChars: JSON.stringify(a.agent.registry.visible({ step: 0, messages: [] }).map((t) => ({ name: t.name, description: t.description, parameters: t.schema }))).length }));
266
+ } },
267
+ { name: "todos", help: "show the agent's checklist", live: true, run: (a) => a.commit(["", ...(a.todos.length ? todoList(a.todos, a.width) : [dim(" no todo list right now")])]) },
268
+ { name: "copy", help: "copy the last reply to the clipboard (or: code, all)", args: "[code|all]", live: true, run: (a, arg) => {
269
+ const bs = a.blocks.blocks, last = [...bs].reverse().find((b) => b.kind === "assistant" || b.kind === "summary");
270
+ let text = "";
271
+ if (arg === "all")
272
+ text = bs.flatMap((b) => b.kind === "user" ? ["> " + b.text, ""] : b.kind === "assistant" || b.kind === "summary" ? [b.text, ""] : b.kind === "tool" ? [`[${b.name} ${b.args.slice(0, 120)}]`] : []).join("\n").trim();
273
+ else if (arg === "code") {
274
+ const cb = last && "text" in last ? codeBlocks(last.text) : [];
275
+ text = cb[cb.length - 1] ?? "";
276
+ if (!text)
277
+ return void a.say("no code block in the last reply", dim);
278
+ }
279
+ else
280
+ text = last && "text" in last ? last.text : "";
281
+ if (!text)
282
+ return void a.say("nothing to copy yet", dim);
283
+ a.copyText(text);
284
+ a.say(`copied ${text.length} characters to the clipboard`, dim);
285
+ } },
286
+ { name: "keepwarm", help: "keep the Claude prompt cache alive while you are away (costs a few cache reads)", args: "[on|off]", run: (a, arg) => {
287
+ if (!arg)
288
+ return void a.say(`keep-warm is ${a.cfg.keepWarm ? "on" : "off"}. Claude drops its prompt cache after 5 idle minutes; with this on, a tiny re-send every ~4.5 minutes (at most 3 per break) keeps it. Only affects Claude.`, dim);
289
+ a.cfg.keepWarm = arg === "on";
290
+ a.saveConfig();
291
+ if (!a.cfg.keepWarm)
292
+ a.stopWarm();
293
+ a.say(`keep-warm ${a.cfg.keepWarm ? "on" : "off"}${a.cfg.keepWarm && a.cfg.provider !== "anthropic" ? dim(" (only Claude needs it; OpenAI requests already ask for 24h cache retention)") : ""}`, dim);
294
+ } },
295
+ { name: "procs", help: "background processes the agent started (stop one with /procs stop <id>)", args: "[stop <id>]", live: true, run: async (a, arg) => {
296
+ const [sub, id] = arg.split(/\s+/);
297
+ if (sub === "stop" && id) {
298
+ try {
299
+ const p = await a.agent.procs.stop(id);
300
+ return void a.say(`stopped ${p.id} (${p.name})`, dim);
301
+ }
302
+ catch (e) {
303
+ return void a.say(e.message, red);
304
+ }
305
+ }
306
+ const l = a.agent.procs?.list() ?? [];
307
+ a.commit(["", bold(" Background processes"), ...(l.length ? l.map((p) => ` ${(p.status === "running" ? green : dim)(p.id.padEnd(4))} ${p.name.padEnd(28)} ${dim(`${p.status}${p.status === "exited" ? ` (${p.code})` : ""} · pid ${p.pid ?? "?"} · ${Math.round((Date.now() - p.startedAt) / 1000)}s`)}`) : [dim(" none")])]);
308
+ } },
309
+ { name: "hooks", help: "lifecycle hooks in effect (.amness/hooks.json, ~/.amness/hooks.json)", live: true, run: (a) => {
310
+ const l = a.hooks.list(), fp = Hooks.fingerprint(a.cwd);
311
+ a.commit(["", bold(" Hooks"), ...(l.length ? l.map((h) => ` ${cyan(h.event.padEnd(16))} ${h.def.match ? dim(`[${h.def.match}] `) : ""}${h.def.command} ${dim(`${h.def.timeout ?? 10}s${h.def.failClosed ? " · fail closed" : ""}`)}`) : [dim(" none active")]), ...(fp && !l.some(() => true) ? [dim(" this project has .amness/hooks.json but it is not approved; restart and allow it")] : []), dim(" exit 0 allows · exit 2 blocks (stderr is shown to the model) · stdout JSON can add context or rewrite arguments")]);
312
+ } },
313
+ { name: "permissions", help: "permission rules: list · allow|ask|deny <rule> · e.g. /permissions allow sh(npm test*)", args: "[allow|ask|deny <rule>]", live: true, run: (a, arg) => {
314
+ const [list, ...rest] = arg.split(/\s+/), rule = rest.join(" ");
315
+ if (list === "allow" || list === "ask" || list === "deny") {
316
+ if (!rule)
317
+ return void a.say(`usage: /permissions ${list} sh(npm test*) · edit(src/**) · read`, dim);
318
+ try {
319
+ Permissions.addProjectRule(a.cwd, list, rule);
320
+ }
321
+ catch (e) {
322
+ return void a.say(e.message, red);
323
+ }
324
+ const fp = projectConfigFingerprint(a.cwd), st = loadState();
325
+ if (fp && st.hookTrust?.[a.cwd]) {
326
+ st.hookTrust[a.cwd] = fp;
327
+ saveState(st);
328
+ } // you wrote it, so it stays trusted
329
+ a.loadHooks();
330
+ a.buildAgent();
331
+ return void a.say(`${list} ${rule} saved to .amness/permissions.json`, dim);
332
+ }
333
+ const r = a.rules.rules();
334
+ a.commit(["", bold(" Permission rules"), ...["deny", "ask", "allow"].flatMap((k) => r[k].map((x) => ` ${(k === "deny" ? red : k === "ask" ? yellow : green)(k.padEnd(6))} ${x}`)), ...(!r.deny.length && !r.ask.length && !r.allow.length ? [dim(" no rules yet")] : []),
335
+ "", dim(" always on, in every mode including yolo: no recursive delete of / or ~, no force-push to main, no editing .git or these rule files; curl|sh and force-pushes always ask"), dim(" precedence: that floor > deny > ask > allow > permission mode")]);
336
+ } },
337
+ { name: "budget", help: "show or set the token cap per turn (cached input counts a tenth)", args: "[tokens, e.g. 500k]", run: (a, arg) => {
338
+ const b = a.budget, used = Math.round(b.effective);
339
+ if (!arg)
340
+ return void a.say(`cap per turn: ${a.cfg.maxTokens.toLocaleString()} tokens (cached input counts a tenth). Last/current turn used ${used.toLocaleString()}. Set it with /budget 500k. At 50%, 75% and 90% the model is told to wrap up, and just before the cap it writes a final report.`, dim);
341
+ const m = /^(\d+(?:\.\d+)?)\s*([km]?)$/i.exec(arg.trim());
342
+ if (!m)
343
+ return void a.say("give a number such as 400000, 400k or 1m", dim);
344
+ const n = Math.round(Number(m[1]) * (m[2].toLowerCase() === "m" ? 1e6 : m[2].toLowerCase() === "k" ? 1e3 : 1));
345
+ if (n < 10_000)
346
+ return void a.say("the cap must be at least 10000 tokens", dim);
347
+ a.cfg.maxTokens = n;
348
+ b.maxTokens = n;
349
+ a.saveConfig();
350
+ a.say(`cap per turn is now ${n.toLocaleString()} tokens`, dim);
351
+ } },
352
+ { name: "reload-plugins", help: "load the plugin files again (after you edited one)", run: async (a) => {
353
+ await a.askPluginTrust();
354
+ a.buildAgent();
355
+ a.say(`${a.plugins.length} plugin${a.plugins.length === 1 ? "" : "s"} loaded${a.plugins.length ? ": " + a.plugins.map((p) => p.name).join(", ") : ""}. Project plugins you did not approve are skipped (if you edited one, Amness asks you to approve the new version first).`, dim);
356
+ } },
357
+ { name: "shell", help: "which shell runs the agent's commands (bash, zsh, sh, pwsh, powershell, cmd, or auto)", args: "[name|auto]", run: (a, arg) => {
358
+ const cur = currentShell(), found = SHELL_CHOICES.filter((n) => n !== "auto").filter((n) => { const s = chooseShell(n).shell; return !chooseShell(n).fallback && s.name === n; });
359
+ if (!arg)
360
+ return void a.say(`shell: ${cur.name} (${cur.file}). Setting: ${a.cfg.shell ?? "auto"}. Installed here: ${found.join(", ") || "none found"}. The model is told which shell it has. Change with /shell <name> or /shell auto.`, dim);
361
+ if (!SHELL_CHOICES.includes(arg))
362
+ return void a.say(`unknown shell "${arg}". Choose one of ${SHELL_CHOICES.join(", ")}`, dim);
363
+ a.cfg.shell = arg === "auto" ? undefined : arg;
364
+ setShell(a.cfg.shell);
365
+ a.saveConfig();
366
+ a.buildAgent();
367
+ const now = currentShell(), fb = shellFallbackNote();
368
+ a.say(`shell: ${now.name} (${now.file})${fb ? ". " + fb : ""}. New conversations tell the model about it.`, dim);
369
+ } },
370
+ { name: "prompt", help: "choose the system prompt from .amness/prompts (or off for the built-in one)", args: "[id|list|off]", run: (a, arg) => {
371
+ const list = loadPrompts(a.cwd);
372
+ if (!arg || arg === "list")
373
+ return void a.say(`prompt: ${a.cfg.prompt ?? "built-in"}. ${list.length ? "Available: " + list.map((p) => p.id).join(", ") + ". Use /prompt <id>, or /prompt off." : "None saved yet: put a Markdown file at .amness/prompts/<id>.md (its text replaces the system prompt)."} Compare them with: amness eval --variants full,prompt:<id>`, dim);
374
+ if (arg === "off") {
375
+ a.cfg.prompt = undefined;
376
+ a.saveConfig();
377
+ a.buildAgent();
378
+ return void a.say("prompt: built-in", dim);
379
+ }
380
+ if (!list.some((p) => p.id === arg))
381
+ return void a.say(`no prompt "${arg}". Available: ${list.map((p) => p.id).join(", ") || "none"}`, dim);
382
+ a.cfg.prompt = arg;
383
+ a.saveConfig();
384
+ a.buildAgent();
385
+ a.say(`prompt ${arg} on: it replaces the whole system prompt for new conversations`, dim);
386
+ } },
387
+ { name: "strategy", help: "loop strategy: plan-first, explore-then-act, or off", args: "[name|off]", run: (a, arg) => {
388
+ const names = Object.keys(STRATEGIES);
389
+ if (!arg)
390
+ return void a.say(`strategy: ${a.cfg.strategy ?? "none (default loop)"}. Choose one of ${names.join(", ")}, or off. plan-first writes a plan before any tool call; explore-then-act allows only reading for the first steps. Measure them with: amness eval --variants full,strategy:plan-first`, dim);
391
+ if (arg === "off") {
392
+ a.cfg.strategy = undefined;
393
+ a.saveConfig();
394
+ a.buildAgent();
395
+ return void a.say("strategy off", dim);
396
+ }
397
+ if (!names.includes(arg))
398
+ return void a.say(`unknown strategy "${arg}". Choose one of ${names.join(", ")}, or off`, dim);
399
+ a.cfg.strategy = arg;
400
+ a.saveConfig();
401
+ a.buildAgent();
402
+ a.say(`strategy ${arg} on`, dim);
403
+ } },
404
+ { name: "plugins", help: "plugins loaded (code in .amness/plugins that extends the loop, tools, guards and evals)", run: (a) => {
405
+ if (!a.plugins.length)
406
+ return void a.say("no plugins loaded. Put a file in .amness/plugins/ (or ~/.amness/plugins/) that exports definePlugin({ name, ... }); see docs/plugins.md. Project plugins ask for your approval first.", dim);
407
+ a.commit(["", bold(" Plugins"), ...a.plugins.map((p) => ` ${cyan(p.name.padEnd(22))} ${dim([p.tools?.length ? `${p.tools.length} tool(s)` : "", p.hooks ? `hooks: ${Object.keys(p.hooks).join(", ")}` : "", p.system ? "system prompt" : "", p.cases?.length ? `${p.cases.length} eval case(s)` : "", p.variants ? `variants: ${Object.keys(p.variants).join(", ")}` : ""].filter(Boolean).join(" · "))} ${p.description ?? ""}`)]);
408
+ } },
409
+ { name: "lsp", help: "append language-server errors to edit results (needs typescript-language-server, pyright, gopls or rust-analyzer)", args: "[on|off]", run: (a, arg) => {
410
+ if (arg !== "on" && arg !== "off")
411
+ return void a.say(`language-server diagnostics are ${a.cfg.lsp ? "on" : "off"}. /lsp on: each edit result also lists the errors an installed language server reports for that file.`, dim);
412
+ a.cfg.lsp = arg === "on";
413
+ a.saveConfig();
414
+ a.buildAgent();
415
+ a.say(`language-server diagnostics ${arg}`, dim);
416
+ } },
417
+ { name: "approver", help: "a second model pre-approves clearly safe risky calls (never the safety floor)", args: "[on|off]", run: (a, arg) => {
418
+ if (arg !== "on" && arg !== "off")
419
+ return void a.say(`approval reviewer is ${a.cfg.approvalReviewer ? "on" : "off"}. /approver on: a model reads each risky call before you are asked and may approve it with a stated reason; floor and untrusted-text calls always ask you. One short model call per risky action.`, dim);
420
+ a.cfg.approvalReviewer = arg === "on";
421
+ a.saveConfig();
422
+ a.buildAgent();
423
+ a.say(`approval reviewer ${arg}`, dim);
424
+ } },
425
+ { name: "reviewer", help: "independent skeptical reviewer: criteria before the work, a check before it is accepted (costs extra model calls)", args: "[on|off]", run: (a, arg) => {
426
+ if (!arg)
427
+ return void a.say(`reviewer is ${a.cfg.review ? "on" : "off"}. /reviewer on: a fresh, read-only reviewer writes acceptance criteria before a task and checks the work against them before it can finish (at most two rounds). Roughly +2 to +4 model calls per task.`, dim);
428
+ a.cfg.review = arg === "on";
429
+ a.saveConfig();
430
+ a.buildAgent();
431
+ a.say(`reviewer ${arg === "on" ? "on" : "off"}`, dim);
432
+ } },
433
+ { name: "review", help: "a read-only review of your changes (vs the base branch, or /review <base>)", args: "[base]", run: async (a, arg) => {
434
+ let r;
435
+ try {
436
+ r = await diffForReview(a.cwd, arg || undefined);
437
+ }
438
+ catch (e) {
439
+ return void a.say(`not a git repository, or git failed: ${e.message}`, red);
440
+ }
441
+ if (!r.diff.trim())
442
+ return void a.say(`no changes against ${r.against} to review`, dim);
443
+ a.say(`reviewing ${r.diff.split("\n").filter((l) => l.startsWith("diff --git")).length} changed file(s) against ${r.against}…`, dim);
444
+ const v = await reviewWork({ model: a.roles.review ?? a.model, cwd: a.cwd, task: "Review these changes as a careful colleague would before they are merged.", contract: ["the change does what its code and commit messages suggest, without introducing bugs", "edge cases and error paths are handled", "nothing unrelated or leftover (debug code, secrets, commented-out code) is included", "behaviour that changed is covered by tests, or the missing tests are named"], summary: "(a human asked for a review; there is no author summary)", diffs: r.diff.slice(0, 60_000), commands: "", verify: a.verify, budget: a.budget });
445
+ a.commit(["", v.inconclusive ? ` ${yellow("?")} ${dim("the reviewer gave no usable verdict; try again, or use /reviewer on for tasks")}` : v.pass ? ` ${green("✔")} ${bold("review: no problems found")}` : ` ${yellow("✖")} ${bold(`review: ${v.findings.length} finding${v.findings.length === 1 ? "" : "s"}`)}`, ...v.findings.map((f) => ` ${yellow("•")} ${f}`)]);
446
+ } },
447
+ { name: "pr", help: "draft a pull request from your branch and create it with gh after you confirm (/pr draft for a draft PR)", args: "[draft]", run: async (a, arg) => {
448
+ const have = await new Promise((res) => execFile("gh", ["--version"], (e) => res(!e)));
449
+ if (!have)
450
+ return void a.say("the GitHub CLI (gh) is needed to create pull requests: https://cli.github.com", yellow);
451
+ const branch = await currentBranch(a.cwd), base = await defaultBase(a.cwd);
452
+ if (!base)
453
+ return void a.say("could not find a base branch (no origin/HEAD, main or master)", red);
454
+ if (branch === base.replace(/^origin\//, "") || branch === "HEAD")
455
+ return void a.say(`you are on ${branch}; make a branch for your changes first`, yellow);
456
+ const { diff } = await diffForReview(a.cwd, base), log = await commitLog(a.cwd, base);
457
+ if (!diff.trim() && !log)
458
+ return void a.say(`nothing to open a pull request for against ${base}`, dim);
459
+ a.say("drafting the pull request…", dim);
460
+ let d;
461
+ try {
462
+ d = await draftPr(a.roles.compact ?? a.model, { diff, log, branch });
463
+ }
464
+ catch (e) {
465
+ return void a.say(e.message, red);
466
+ }
467
+ a.commit(["", bold(` ${d.title}`), dim(` ${branch} → ${base.replace(/^origin\//, "")}`), "", ...d.body.split("\n").map((l) => " " + l)]);
468
+ const choice = await a.pick("Create this pull request on GitHub?", [{ label: "Create it", value: "pr" }, { label: "Create it as a draft", value: "draft" }, { label: "Cancel", value: "no" }]);
469
+ if (!choice || choice === "no")
470
+ return void a.say("not created", dim);
471
+ if (!(await hasUpstream(a.cwd))) {
472
+ const push = await a.pick("The branch is not on the remote yet. Push it now (git push -u origin HEAD)?", [{ label: "Yes, push it", value: "y" }, { label: "No", value: "n" }]);
473
+ if (push !== "y")
474
+ return void a.say("not created: push the branch first", dim);
475
+ try {
476
+ await pushBranch(a.cwd);
477
+ }
478
+ catch (e) {
479
+ return void a.say(`push failed: ${e.message}`, red);
480
+ }
481
+ }
482
+ try {
483
+ const url = await createPr(a.cwd, d, { base, draft: choice === "draft" || arg === "draft" });
484
+ a.say(`created: ${url}`, green);
485
+ }
486
+ catch (e) {
487
+ a.say(`gh failed: ${e.message}`, red);
488
+ }
489
+ } },
490
+ { name: "report", help: "recurring problems in your session traces · /report case <name> turns the last request into an eval task", args: "[case <name>]", run: async (a, arg) => {
491
+ const [sub, name] = arg.split(/\s+/);
492
+ if (sub === "case") {
493
+ if (!name)
494
+ return void a.say("usage: /report case <name> (then it asks for the command that proves the task is solved)", dim);
495
+ const prompts = a.blocks.prompts(), last = prompts.at(-1)?.block;
496
+ if (!last || last.kind !== "user")
497
+ return void a.say("send a task first: the case is built from your last request", dim);
498
+ const check = await a.askText(`A command that exits 0 only when "${last.text.slice(0, 50).replace(/\s+/g, " ")}" is really done (the agent never sees it)`);
499
+ if (!check)
500
+ return void a.say("cancelled", dim);
501
+ const r = caseFromWorkspace(a.cwd, { name, task: last.text, check, verify: a.verify });
502
+ return void a.say(`wrote ${r.path.replace(a.cwd + "/", "")} (${r.files} files from git HEAD). Run: amness eval --cases .amness/evals/cases --variants ab`, dim);
503
+ }
504
+ a.commit(["", ...formatMining(join(a.cwd, ".amness", "traces")).split("\n").map((l) => " " + l)]);
505
+ } },
506
+ { name: "models", help: "use other models for some jobs: /models explore|compact|review <model|off> · /models fallback <model|off>", args: "[role model]", run: (a, arg) => {
507
+ const [role, id] = arg.split(/\s+/), roles = ["explore", "compact", "review", "cheap"];
508
+ if (!role) {
509
+ const m = a.cfg.models ?? {};
510
+ return void a.commit(["", bold(" Models"), ` ${dim("main".padEnd(10))} ${a.cfg.model}`, ...roles.map((r) => ` ${dim(r.padEnd(10))} ${m[r] ?? dim("(main model)")}`), ` ${dim("fallback".padEnd(10))} ${a.cfg.fallback ?? dim("(none)")}`, dim(" e.g. /models explore gpt-6.1-luna (a cheaper model for read-only exploration)")]);
511
+ }
512
+ if (role !== "fallback" && !roles.includes(role))
513
+ return void a.say("usage: /models explore|compact|review|cheap|fallback <model id | off>", red);
514
+ if (!id)
515
+ return void a.say(`usage: /models ${role} <model id | off>`, red);
516
+ if (role === "fallback")
517
+ a.cfg.fallback = id === "off" ? undefined : id;
518
+ else
519
+ a.cfg.models = { ...(a.cfg.models ?? {}), [role]: id === "off" ? undefined : id };
520
+ a.saveConfig();
521
+ a.buildAgent();
522
+ a.say(`${role} → ${id === "off" ? "main model" : id}`, dim);
523
+ } },
524
+ { name: "policy", help: "why recent actions were allowed, asked about or refused (rules, the safety floor, untrusted-text checks)", live: true, run: (a) => {
525
+ a.commit(["", bold(" Recent policy decisions"), ...(a.policyLog.length ? a.policyLog.map((l) => " " + (l.startsWith("deny") ? red(l.slice(0, 5)) : l.startsWith("ask") ? yellow(l.slice(0, 5)) : green(l.slice(0, 5))) + l.slice(5)) : [dim(" none yet: every call so far went through the permission mode alone")]),
526
+ "", dim(" layers: safety floor > deny rules > untrusted-text check (commands copied from web pages or tool servers) > ask rules > allow rules > permission mode"), dim(" /permissions lists your rules, /sandbox shows OS enforcement")]);
527
+ } },
528
+ { name: "profile", help: "harness settings for this model (.amness/profile.json): guards switched off, limits", live: true, run: (a) => {
529
+ const p = a.profile.profile;
530
+ a.commit(["", bold(" Harness profile"), ` ${dim("model")} ${a.cfg.model}`, ` ${dim("from")} ${a.profile.sources.length ? a.profile.sources.map((s) => s.replace(a.cwd + "/", "")).join(", ") : dim("(none: built-in defaults)")}`, ` ${dim("off")} ${p.off?.length ? p.off.join(", ") : dim("nothing: every guard is on")}`, ` ${dim("limits")} ${[p.maxSteps !== undefined && `max steps ${p.maxSteps}`, p.stagnationTurns !== undefined && `stagnation after ${p.stagnationTurns} turns`].filter(Boolean).join(" · ") || dim("defaults")}`, ...(p.note ? [` ${dim("note")} ${p.note}`] : []), dim(" write one from measurements: amness eval --variants ablate --profile-out profile.json")]);
531
+ } },
532
+ { name: "router", help: "route exploration steps to a cheaper model when that pays off, cache cost included (needs /models cheap <id>)", args: "[on|off]", run: (a, arg) => {
533
+ if (!arg)
534
+ return void a.say(`router is ${a.cfg.router ? "on" : "off"}${a.cfg.models?.cheap ? ` (cheap model: ${a.cfg.models.cheap})` : dim(" (set one with /models cheap <model id>)")}. Planning, edits, failures and answers always use ${a.cfg.model}; a run of read-only steps moves to the cheap model once the saving repays the cold read of the conversation on that model's own cache.`, dim);
535
+ if (arg === "on" && !a.cfg.models?.cheap)
536
+ return void a.say("choose the cheap model first: /models cheap <model id>", red);
537
+ a.cfg.router = arg === "on";
538
+ a.saveConfig();
539
+ a.buildAgent();
540
+ a.say(`router ${arg === "on" ? "on" : "off"}`, dim);
541
+ } },
542
+ { name: "sandbox", help: "OS sandbox for shell commands: off · workspace · strict (no network)", args: "[off|workspace|strict]", run: (a, arg) => {
543
+ const kind = sandboxKind();
544
+ if (!arg)
545
+ return void a.say(`sandbox: ${a.cfg.sandbox ?? "off"}${kind ? dim(` (${kind === "seatbelt" ? "macOS Seatbelt" : "Bubblewrap"} available)`) : dim(` (not available here: ${sandboxHelp()})`)}. workspace: commands can write only in the project and temp/cache folders, .git stays read-only. strict: also no network.`, dim);
546
+ if (!SANDBOX_MODES.includes(arg))
547
+ return void a.say("usage: /sandbox off | workspace | strict", red);
548
+ if (arg !== "off" && !kind)
549
+ return void a.say(`cannot turn the sandbox on: ${sandboxHelp()}`, red);
550
+ a.cfg.sandbox = arg;
551
+ a.saveConfig();
552
+ a.say(`sandbox ${arg}${arg === "strict" ? dim(" (commands that need the network, like npm install, will fail)") : ""}`, dim);
553
+ } },
554
+ { name: "agents", help: "custom subagents (.amness/agents/*.md, .claude/agents/*.md) available to the task tool", live: true, run: (a) => {
555
+ const l = a.agent.agents ?? [], w = a.agent.agentWarnings ?? [];
556
+ a.commit(["", bold(" Subagents"), ` ${cyan("explore".padEnd(16))} ${dim("[built in · read-only]")} broad searches and questions that need reading many files`, ...l.map((s) => ` ${cyan(s.name.padEnd(16))} ${dim(`[${s.scope} · ${s.tools.join(", ")}${s.isolation ? " · isolated worktree" : ""}${s.model ? " · model " + s.model : ""}]`)} ${s.description}`), ...(!l.length ? [dim(" add .amness/agents/<name>.md (front matter: name, description, tools, model, isolation) to define your own")] : []), ...w.map((x) => " " + yellow("⚠ ") + dim(x))]);
557
+ } },
558
+ { name: "skills", help: "skills found (SKILL.md folders in .amness/.agents/.claude skills, project and home)", live: true, run: (a) => {
559
+ const l = a.agent.skills ?? [], w = a.agent.skillWarnings ?? [];
560
+ a.commit(["", bold(" Skills"), ...(l.length ? l.map((s) => ` ${cyan(s.name.padEnd(22))} ${dim(`[${s.scope} · ${s.source}]`)} ${s.description}`) : [dim(" none found. Add .amness/skills/<name>/SKILL.md with a name and description in its front matter.")]), ...w.map((x) => " " + yellow("⚠ ") + dim(x))]);
561
+ } },
562
+ { name: "check", help: "fast command run after every edit (typecheck, lint); failures go straight back to the model", args: "[cmd|off]", run: (a, arg) => {
563
+ if (!arg)
564
+ return void a.say(a.check ? `post-edit check: ${cyan(a.check)}` : "no post-edit check set. /check npx tsc --noEmit", dim);
565
+ a.setCheck(arg === "off" ? undefined : arg);
566
+ a.say(arg === "off" ? "post-edit check off" : `post-edit check → ${cyan(arg)}`, dim);
567
+ } },
568
+ { name: "verify", help: "command that must pass before it may finish", args: "[cmd|off]", run: (a, arg) => {
569
+ if (!arg)
570
+ return void a.say(a.verify ? `verify: ${cyan(a.verify)}` : "no verify command set. /verify npm test", dim);
571
+ a.setVerify(arg === "off" ? undefined : arg);
572
+ a.say(arg === "off" ? "verify gate off" : `verify gate → ${cyan(arg)}`, dim);
573
+ } },
574
+ { name: "live", help: "open the browser live view (diffs stream there)", live: true, run: async (a) => {
575
+ const l = await a.ensureLive();
576
+ a.say(`${cyan("●")} live view: ${bold(l.url)}`);
577
+ const { execFile } = await import("node:child_process");
578
+ execFile(process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open", [l.url], () => { });
579
+ } },
580
+ { name: "mcp", help: "attach an MCP server: /mcp <command…> · /mcp list · /mcp off", args: "<command>", run: async (a, arg) => {
581
+ if (!arg || arg === "list")
582
+ return void a.commit(["", ...(a.mcp.length ? a.mcp.map((m) => ` ${cyan("●")} ${m.cmd} ${dim(m.tools.length + " tools")}`) : [dim(" no MCP servers attached. /mcp npx -y @modelcontextprotocol/server-filesystem .")])]);
583
+ if (arg === "off") {
584
+ a.mcp.splice(0).forEach((m) => m.close());
585
+ a.buildAgent();
586
+ return void a.say("MCP servers detached", dim);
587
+ }
588
+ const [command, ...rest] = arg.split(/\s+/);
589
+ const prefix = (rest.find((x) => !x.startsWith("-")) ?? command).split("/").pop().replace(/\.\w+$/, "").replace(/[^\w]+/g, "_");
590
+ a.note("connecting…", 60_000);
591
+ const h = await loadMcp(command, rest, { prefix });
592
+ a.noticeUntil = 0;
593
+ a.mcp.push({ cmd: arg, tools: h.tools, close: h.close });
594
+ a.agent.registry.add(...h.tools);
595
+ a.say(`${green("✓")} ${arg}\n${dim(h.tools.length + " tools: " + h.tools.slice(0, 6).map((t) => t.name).join(", ") + (h.tools.length > 6 ? "…" : "") + ". Tools that change things will ask first.")}`);
596
+ } },
597
+ { name: "init", help: "have the agent write AGENTS.md for this repo", run: (a) => a.turn(INIT_PROMPT) },
598
+ { name: "tools", help: "list the tools the agent can use right now", live: true, run: (a) => {
599
+ const vis = a.agent.registry.visible({ step: 0, messages: [] });
600
+ a.commit(["", bold(" Tools"), ...vis.map((t) => ` ${cyan(t.name.padEnd(16))} ${dim(t.description.split(".")[0].slice(0, a.width - 22))}`)]);
601
+ } },
602
+ { name: "status", help: "model, mode, project, session", live: true, run: (a) => {
603
+ a.commit(["", bold(" Status"), ` ${dim("model")} ${a.cfg.model} ${dim("via " + a.providerInfo().label + (a.model.api ? ` · ${a.model.api()} API` : ""))}`, ...(a.cfg.effort ? [` ${dim("effort")} ${a.cfg.effort}`] : []), ` ${dim("mode")} ${a.mode}`, ` ${dim("project")} ${a.cwd}${a.branch ? dim(" ⎇ " + a.branch) : ""}`,
604
+ ` ${dim("verify")} ${a.verify ?? "none"}`, ` ${dim("session")} ${a.session.id} ${dim(`(${a.session.turns.length} turns)`)}`, ` ${dim("memory")} ${a.memory?.list().length ?? 0} notes`, ` ${dim("sandbox")} ${a.cfg.sandbox ?? "off"}`, ` ${dim("trace")} ${a.tracer?.file ? a.tracer.file.replace(a.cwd + "/", "") : "(after the first message)"}`, ` ${dim("instructions")} ${a.agent.guidance?.files.join(", ") || "none loaded (add an AGENTS.md)"}`, ` ${dim("running")} ${a.running ? "yes, " + fmtDuration(Date.now() - a.runStart) : "no"}`]);
605
+ } },
606
+ { name: "theme", help: "colour theme (live preview)", args: "[name]", run: async (a, arg) => {
607
+ const prev = a.cfg.theme;
608
+ const choose = (n) => { a.cfg.theme = n ?? prev; a.full?.invalidate(); a.render(); };
609
+ let name = arg;
610
+ if (!name) {
611
+ name = await a.pick("Theme", themeNames().map((n) => ({ label: THEMES[n].label, value: n, hint: THEMES[n].dark ? "dark" : "light" })), Math.max(0, themeNames().indexOf(getTheme(prev).name)), (v) => choose(v));
612
+ if (!name) {
613
+ choose(prev);
614
+ return;
615
+ }
616
+ }
617
+ if (!THEMES[name]) {
618
+ choose(prev);
619
+ return void a.say(`unknown theme "${name}". Try: ${themeNames().join(", ")}`, red);
620
+ }
621
+ choose(name);
622
+ a.saveConfig();
623
+ a.say(`theme → ${cyan(THEMES[name].label)}${a.full ? "" : dim(" (themes apply to the full-screen UI; try /ui full)")}`, dim);
624
+ } },
625
+ { name: "sidebar", help: "show or hide the side panel", alias: "side", live: true, run: (a) => { if (!a.full)
626
+ return void a.say("the side panel is part of the full-screen UI (/ui full)", dim); a.full.showSidebar = !a.full.showSidebar; a.full.invalidate(); } },
627
+ { name: "details", help: "expand or collapse all tool output", live: true, run: (a) => { if (!a.full)
628
+ return void a.say("full-screen UI only (/ui full)", dim); a.full.expandAll = !a.full.expandAll; a.full.toast(a.full.expandAll ? "all tool output expanded" : "tool output collapsed"); } },
629
+ { name: "diffmode", help: "diff layout: auto · stacked · split", args: "[mode]", live: true, run: (a, arg) => {
630
+ if (!a.full)
631
+ return void a.say("full-screen UI only (/ui full)", dim);
632
+ if (!["auto", "stacked", "split"].includes(arg))
633
+ return void a.say(`diff layout is ${cyan(a.full.diffMode)}. Choose: auto, stacked, split`, dim);
634
+ a.full.diffMode = arg;
635
+ a.full.invalidate();
636
+ a.say(`diff layout → ${cyan(arg)}`, dim);
637
+ } },
638
+ { name: "mouse", help: "mouse on/off (off restores native text selection)", args: "[on|off]", live: true, run: (a, arg) => { if (!a.full)
639
+ return void a.say("full-screen UI only (/ui full)", dim); a.full.setMouse(arg ? arg !== "off" : !a.full.mouseOn); } },
640
+ { name: "ui", help: "switch between the full-screen and inline UI", args: "[full|inline]", run: (a, arg) => {
641
+ const to = arg === "full" || arg === "inline" ? arg : a.ui === "full" ? "inline" : "full";
642
+ a.cfg.ui = to;
643
+ a.saveConfig();
644
+ if (to === a.ui)
645
+ return void a.say(`already using the ${to} UI`, dim);
646
+ a.switchUi(to);
647
+ } },
648
+ { name: "exit", alias: "quit", help: "leave", live: true, run: (a) => a.exit(0) },
649
+ ];
650
+ }