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,590 @@
1
+ import { execFile } from "node:child_process";
2
+ import { wrapCommand } from "./sandbox.js";
3
+ import { childEnv } from "./secrets.js";
4
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
5
+ import { basename, dirname, relative, resolve, sep } from "node:path";
6
+ import { applyBundle, collectFiles, readBundle, writeBundle } from "./bundle.js";
7
+ import { unifiedDiff } from "./diff.js";
8
+ import { createToolTool, loadSavedTools } from "./dynamic.js";
9
+ import { createHarness, defineTool, clip, makeHandoff, Registry } from "./harness.js";
10
+ import { MemoryStore, isMemoryBackend, memoryTools } from "./memory.js";
11
+ import { Guidance } from "./guidance.js";
12
+ import { currentShell, shellInvocation, shellNote, slash } from "./shell.js";
13
+ import { ProcessManager } from "./procs.js";
14
+ import { reviewedApprove } from "./approver.js";
15
+ import { Diagnostics } from "./lsp.js";
16
+ import { Ledger } from "./ledger.js";
17
+ import { CodeIndex, indexTools } from "./codeindex.js";
18
+ import { WRITE_TOOLS, isWriter, loadAgents } from "./agents.js";
19
+ import { createWorktree, isRepo, removeWorktree, worktreeDiff } from "./worktree.js";
20
+ import { loadSkills, skillsBlock, skillTool } from "./skills.js";
21
+ import { applyOps, parsePatch, replaceMany } from "./patch.js";
22
+ import { proposeContract, reviewWork } from "./review.js";
23
+ import { routedByFunction } from "./router.js";
24
+ import { applySystem, checkPolicies, composeHooks } from "./plugins.js";
25
+ import { OBFUSCATED_EXEC, Permissions, commandViews } from "./permissions.js";
26
+ const RISKY = [
27
+ [/\brm\s+(-\w*[rf]|--recursive|--force)/, "recursive/forced delete"], [/\bsudo\b/, "sudo"],
28
+ [/\bgit\s+(push|reset\s+--hard|clean|checkout\s+--|branch\s+-D|rebase)/, "destructive or outward git operation"],
29
+ [/(curl|wget)[^|;]*\|\s*(sudo\s+)?(ba|z)?sh/, "pipes a download into a shell"],
30
+ [/\b(npm|pnpm|yarn)\s+(publish|login)|\b(pip|pip3|npm|pnpm|yarn|brew|apt(-get)?|cargo)\s+(install|add|i)\b/, "installs or publishes packages"],
31
+ [/\b(chmod|chown)\s+-R|\bdd\s+if=|\bmkfs|>\s*\/dev\/|:\(\)\s*\{/, "dangerous system command"],
32
+ [/\b(ssh|scp|rsync)\b/, "remote access"],
33
+ ];
34
+ export const riskOf = (cmd) => { const views = commandViews(cmd); return RISKY.find(([re]) => views.some((v) => re.test(v)))?.[1] ?? (OBFUSCATED_EXEC.test(cmd) || OBFUSCATED_EXEC.test(views[1] ?? "") ? "decodes or downloads a payload and runs it" : false); };
35
+ /** Inspection-only shell commands (no chaining, redirects or substitutions): always allowed, even in plan mode. */
36
+ export const READONLY_CMD = /^\s*(ls|cat|head|tail|wc|pwd|echo|grep|rg|tree|stat|file|which|date|whoami|git\s+(status|log|diff|show|rev-parse|ls-files|blame)|find(?!.*\s-(exec|delete|ok|fprint))|node\s+--version|npm\s+(ls|list|view))(\s[^;&|<>`$()]*)?$/;
37
+ const IGNORE = new Set(["node_modules", ".git", "dist", ".amness", "__pycache__", ".venv"]);
38
+ /** Explain "command not found" (exit 127): the name is not a program here, and say so when it is really one of the agent's own tools. */
39
+ export function withNotFoundHint(out, isTool = () => false, forUser = false) {
40
+ const miss = /^exit=127\b/.test(out) ? /(?:^|\n|: )([\w.@+-]+): (?:command )?not found/.exec(out)?.[1] : undefined;
41
+ if (!miss)
42
+ return out;
43
+ return `${out.trimEnd()}\n[exit 127 means "${miss}" is not a program on this machine.${isTool(miss) ? `\n"${miss}" is one of your tools: call it as a tool, not as a command.` : ""}\n${forUser ? `After ! type a command, for example !ls or !git status.` : `Check the spelling, check that it is installed (for example: command -v ${miss}), or use a different command. The \`cmd\` argument must be a shell command line, such as "ls src".`}]`;
44
+ }
45
+ export function runShell(cmd, cwd, ms = 60_000, signal, sandbox = "off") {
46
+ let wrapped;
47
+ try {
48
+ wrapped = wrapCommand(cmd, cwd, sandbox);
49
+ }
50
+ catch (e) {
51
+ return Promise.resolve(`exit=126\nERROR: ${e.message}`);
52
+ } // asked for a sandbox we cannot provide: refuse, never run unprotected
53
+ const done = (err, out, errOut) => `exit=${err ? (err.name === "AbortError" ? "interrupted" : err.killed ? "timeout" : err.code ?? 1) : 0}\n${out}${errOut}`;
54
+ const inv = wrapped ?? shellInvocation(cmd);
55
+ return new Promise((res) => execFile(inv.file, inv.args, { cwd, env: childEnv(process.env, !!wrapped), timeout: ms, maxBuffer: 10 << 20, signal, windowsHide: true }, (err, out, errOut) => res(done(err, out, errOut))));
56
+ }
57
+ /** File + shell tools confined to `cwd`. Enforces read-before-edit; flags dangerous shell commands as risky. */
58
+ export function codeTools(cwd, o = {}) {
59
+ const mode = o.mode;
60
+ const mutating = mode ? { risky: () => (mode() === "default" ? "modify a file" : false), when: () => mode() !== "plan" } : {};
61
+ const root = resolve(cwd);
62
+ const shRisk = ({ cmd }) => (!mode ? riskOf(cmd) : mode() === "yolo" ? false : riskOf(cmd) || (READONLY_CMD.test(cmd) ? false : "run a shell command"));
63
+ const readSet = new Set();
64
+ const patchText = (a) => { if (a.patch)
65
+ return a.patch; if (!a.file)
66
+ throw new Error("give `patch` or `file`"); return readFileSync(abs(a.file), "utf8"); };
67
+ const abs = (p) => {
68
+ const a = resolve(root, p);
69
+ if (a !== root && !a.startsWith(root + sep))
70
+ throw new Error(`path escapes the workspace: ${p}`);
71
+ return a;
72
+ };
73
+ const mustRead = (p) => {
74
+ if (existsSync(abs(p)) && !readSet.has(abs(p)))
75
+ throw new Error(`read ${p} (read_file or grep) before changing it, so your edit is based on its real contents`);
76
+ };
77
+ const walk = (d, out) => {
78
+ for (const e of readdirSync(d).sort()) {
79
+ if (IGNORE.has(e) || out.length >= 300)
80
+ continue;
81
+ const p = resolve(d, e);
82
+ statSync(p).isDirectory() ? walk(p, out) : out.push(slash(relative(root, p)));
83
+ }
84
+ return out;
85
+ };
86
+ /** A missing file is usually a wrong guess at a name: offer the closest real files (same name elsewhere, or a similar name) so the next call is not another guess. */
87
+ const suggest = (p) => {
88
+ const all = [];
89
+ const scan = (d) => { for (const e of readdirSync(d)) {
90
+ if (IGNORE.has(e) || all.length >= 6000)
91
+ continue;
92
+ const f = resolve(d, e);
93
+ try {
94
+ statSync(f).isDirectory() ? scan(f) : all.push(slash(relative(root, f)));
95
+ }
96
+ catch { /* unreadable */ }
97
+ } };
98
+ try {
99
+ scan(root);
100
+ }
101
+ catch { /* none */ }
102
+ const base = p.split(/[\\/]/).pop().toLowerCase(), stem = base.replace(/\.[^.]*$/, "");
103
+ const scored = all.map((f) => { const b = f.split("/").pop().toLowerCase(), s = b.replace(/\.[^.]*$/, ""); return { f, score: b === base ? 3 : s === stem ? 2 : stem.length >= 3 && (s.includes(stem) || stem.includes(s)) ? 1 : 0 }; }).filter((x) => x.score > 0).sort((a, b) => b.score - a.score || a.f.length - b.f.length).slice(0, 4);
104
+ return scored.length ? ` Did you mean: ${scored.map((x) => x.f).join(", ")}?` : " Use list_files or grep to find the right path.";
105
+ };
106
+ return [
107
+ defineTool({ name: "list_files", description: "List files in the workspace (skips node_modules, .git, dist). Max 300.",
108
+ schema: { type: "object", properties: { path: { type: "string" } } }, readOnly: true,
109
+ run: ({ path }) => walk(abs(path ?? "."), []).join("\n") || "(empty)" }),
110
+ defineTool({ name: "read_file", description: "Read a file with line numbers (optionally lines start..end).",
111
+ schema: { type: "object", required: ["path"], properties: { path: { type: "string" }, start: { type: "integer" }, end: { type: "integer" } } }, readOnly: true,
112
+ run: ({ path, start, end }) => {
113
+ const a = abs(path);
114
+ if (!existsSync(a))
115
+ throw new Error(`no such file: ${path}.${suggest(path)}`);
116
+ readSet.add(a);
117
+ const all = readFileSync(a, "utf8").split("\n");
118
+ if (all.at(-1) === "")
119
+ all.pop(); // a trailing newline is not an extra empty line
120
+ return all.slice((start ?? 1) - 1, end).map((l, i) => `${(start ?? 1) + i}\t${l}`).join("\n") + (o.guidance?.nested(path) ?? "");
121
+ } }),
122
+ defineTool({ name: "grep", description: "Recursive regex search (file:line:text). Counts as having read matching files.",
123
+ schema: { type: "object", required: ["pattern"], properties: { pattern: { type: "string" }, path: { type: "string" } } }, readOnly: true,
124
+ run: ({ pattern, path }) => path && !existsSync(abs(path)) ? `ERROR: ${path} does not exist, so nothing was searched.${suggest(path)}` : new Promise((res) => execFile("grep", ["-rnIE", "-m", "20", ...[...IGNORE].map((d) => `--exclude-dir=${d}`), "-e", pattern, path ?? "."], { cwd: root, maxBuffer: 10 << 20 }, (_e, out) => {
125
+ for (const l of out.split("\n")) {
126
+ const f = l.split(":")[0];
127
+ if (f) {
128
+ try {
129
+ readSet.add(abs(f));
130
+ }
131
+ catch { /* outside */ }
132
+ }
133
+ }
134
+ res(out.split("\n").slice(0, 100).join("\n") || "no matches");
135
+ })) }),
136
+ defineTool({ name: "write_file", description: "Create a file, or fully overwrite one you have already read.",
137
+ schema: { type: "object", required: ["path", "content"], properties: { path: { type: "string" }, content: { type: "string" } } }, ...mutating,
138
+ preview: ({ path, content }) => unifiedDiff(path, existsSync(abs(path)) ? readFileSync(abs(path), "utf8") : null, content).diff,
139
+ run: async ({ path, content }, ctx) => {
140
+ mustRead(path);
141
+ const a = abs(path), before = existsSync(a) ? readFileSync(a, "utf8") : null;
142
+ mkdirSync(dirname(a), { recursive: true });
143
+ writeFileSync(a, content);
144
+ readSet.add(a);
145
+ o.onChange?.(path);
146
+ ctx.emit?.({ kind: "edit", ...unifiedDiff(path, before, content), before: before === null || before.length < 500_000 ? before : undefined });
147
+ return `wrote ${content.length} chars to ${path}` + (await o.check?.([path]) ?? "") + (o.guidance?.nested(path) ?? "");
148
+ } }),
149
+ defineTool({ name: "edit_file", description: "Replace exact, unique text in a file you have read. Give `old` and `new`, or an `edits` list of several replacements applied in order (all or nothing). If the text differs only in indentation it is still matched when unique. Shows the result.",
150
+ schema: { type: "object", required: ["path"], properties: { path: { type: "string" }, old: { type: "string" }, new: { type: "string" }, edits: { type: "array", items: { type: "object", required: ["old", "new"], properties: { old: { type: "string" }, new: { type: "string" } } } } } }, ...mutating,
151
+ preview: ({ path, old, new: nw, edits }) => { const s = readFileSync(abs(path), "utf8"), r = replaceMany(s, edits?.length ? edits : old !== undefined && nw !== undefined ? [{ old, new: nw }] : []); return "error" in r ? undefined : unifiedDiff(path, s, r.out).diff; },
152
+ run: async ({ path, old, new: nw, edits }, ctx) => {
153
+ mustRead(path);
154
+ const list = edits?.length ? edits : old !== undefined && nw !== undefined ? [{ old, new: nw }] : [];
155
+ if (!list.length)
156
+ return "ERROR: give `old` and `new`, or an `edits` list.";
157
+ const a = abs(path), s = readFileSync(a, "utf8"), r = replaceMany(s, list);
158
+ if ("error" in r)
159
+ return `ERROR: ${r.error}`;
160
+ writeFileSync(a, r.out);
161
+ o.onChange?.(path);
162
+ ctx.emit?.({ kind: "edit", ...unifiedDiff(path, s, r.out), before: s.length < 500_000 ? s : undefined });
163
+ const first = list[0], at = r.out.indexOf(first.new.split("\n")[0] ?? ""), line = at >= 0 ? r.out.slice(0, at).split("\n").length : 1;
164
+ return `edited ${path}${list.length > 1 ? ` (${list.length} edits)` : ""}${r.fuzzy ? `; ${r.fuzzy} matched ignoring indentation` : ""}:\n` + r.out.split("\n").slice(Math.max(0, line - 3), line + first.new.split("\n").length + 2).map((l, i) => `${Math.max(1, line - 2) + i}\t${l}`).join("\n") + (await o.check?.([path]) ?? "") + (o.guidance?.nested(path) ?? "");
165
+ } }),
166
+ defineTool({ name: "apply_patch", description: "Change several files at once with a patch, all or nothing. Format: `*** Begin Patch`, then per file `*** Update File: path` (hunks start with `@@`, lines begin with space, `-` or `+`), `*** Add File: path` (all lines start with `+`) or `*** Delete File: path`, then `*** End Patch`. A unified diff also works. Existing files must have been read first. Instead of `patch` you can pass `file`: a patch file inside the workspace (a subagent's saved result).",
167
+ schema: { type: "object", properties: { patch: { type: "string" }, file: { type: "string" } } }, ...mutating,
168
+ preview: (a) => { try {
169
+ const patch = patchText(a);
170
+ const r = applyOps(parsePatch(patch), (p) => (existsSync(abs(p)) ? readFileSync(abs(p), "utf8") : undefined));
171
+ return r.errors.length ? undefined : r.applied.map((x) => unifiedDiff(x.path, x.before, x.after ?? "").diff).join("\n");
172
+ }
173
+ catch {
174
+ return undefined;
175
+ } },
176
+ run: async (a, ctx) => {
177
+ let patch;
178
+ try {
179
+ patch = patchText(a);
180
+ }
181
+ catch (e) {
182
+ return `ERROR: ${e.message}`;
183
+ }
184
+ const ops = parsePatch(patch);
185
+ if (!ops.length)
186
+ return "ERROR: no files found in the patch. Use `*** Begin Patch` / `*** Update File: path` ... `*** End Patch`, or a unified diff.";
187
+ for (const op of ops) {
188
+ if (op.op !== "add")
189
+ mustRead(op.path);
190
+ else
191
+ abs(op.path);
192
+ }
193
+ const r = applyOps(ops, (p) => (existsSync(abs(p)) ? readFileSync(abs(p), "utf8") : undefined));
194
+ if (r.errors.length)
195
+ return `ERROR: nothing was changed.\n${r.errors.map((e) => "- " + e).join("\n")}\nRe-read the files if they may differ from what you assumed.`;
196
+ const lines = [];
197
+ for (const x of r.applied) {
198
+ const a = abs(x.path);
199
+ if (x.after === null) {
200
+ rmSync(a, { force: true });
201
+ readSet.delete(a);
202
+ }
203
+ else {
204
+ mkdirSync(dirname(a), { recursive: true });
205
+ writeFileSync(a, x.after);
206
+ readSet.add(a);
207
+ }
208
+ o.onChange?.(x.path);
209
+ const ch = unifiedDiff(x.path, x.before, x.after ?? "");
210
+ ctx.emit?.({ kind: "edit", ...ch, before: x.before === null || x.before.length < 500_000 ? x.before : undefined });
211
+ lines.push(`${x.after === null ? "deleted" : x.before === null ? "added" : "updated"} ${x.path} (+${ch.added} -${ch.removed})${x.fuzzy ? `, ${x.fuzzy} hunk(s) matched ignoring indentation` : ""}`);
212
+ }
213
+ return `patched ${r.applied.length} file(s):\n${lines.join("\n")}` + (await o.check?.(r.applied.filter((x) => x.after !== null).map((x) => x.path)) ?? "") + (o.guidance?.nested(r.applied[0].path) ?? "");
214
+ } }),
215
+ ...(o.procs ? bgTools(o.procs, shRisk, mode) : []),
216
+ defineTool({ name: "sh", description: `Run a command in the workspace shell (${currentShell().name}); returns exit code and output. Dangerous commands need human approval.`,
217
+ schema: { type: "object", required: ["cmd"], properties: { cmd: { type: "string" }, timeout: { type: "integer", description: "seconds, default 60" } } },
218
+ risky: shRisk,
219
+ preview: ({ cmd }) => cmd,
220
+ run: async ({ cmd, timeout }, ctx) => {
221
+ if (mode?.() === "plan" && !READONLY_CMD.test(cmd))
222
+ return "ERROR: plan mode is read-only; only inspection commands are allowed (ls, cat, head, tail, grep, find, git status/log/diff/show, wc, pwd). Finish your plan and call exit_plan_mode.";
223
+ if (o.beforeShell && !READONLY_CMD.test(cmd)) {
224
+ try {
225
+ await o.beforeShell(cmd);
226
+ }
227
+ catch { /* a failed snapshot must not stop the command */ }
228
+ }
229
+ const out = await runShell(cmd, root, (timeout ?? 60) * 1000, ctx.signal, o.sandbox?.() ?? "off");
230
+ return withNotFoundHint(out, (n) => !!ctx.registry.get(n));
231
+ } }),
232
+ ];
233
+ }
234
+ /** Tools for long-running commands: start a dev server or watcher, look at its output later, stop it. */
235
+ function bgTools(procs, risky, mode) {
236
+ const fmt = (p) => `${p.id} (${p.name}) ${p.status}${p.status === "exited" ? ` with code ${p.code}` : ""}`;
237
+ return [
238
+ defineTool({ name: "sh_start", description: "Start a long-running command in the background (dev server, test watcher, build --watch). Returns an id; use sh_read to see its output and sh_stop to end it. For commands that finish by themselves, use sh.",
239
+ schema: { type: "object", required: ["cmd"], properties: { cmd: { type: "string" }, name: { type: "string", description: "short label" } } }, risky, when: () => mode?.() !== "plan", preview: ({ cmd }) => cmd,
240
+ run: ({ cmd, name }) => `started ${fmt(procs.start(cmd, name))}. Read its output with sh_read.` }),
241
+ defineTool({ name: "sh_read", description: "New output of a background process since the last read, and whether it is still running. `wait_ms` (max 10000) waits for output or exit.", readOnly: true,
242
+ schema: { type: "object", required: ["id"], properties: { id: { type: "string" }, wait_ms: { type: "integer" }, max_chars: { type: "integer" } } },
243
+ run: async ({ id, wait_ms, max_chars }) => { const r = await procs.read(id, { waitMs: wait_ms, maxChars: max_chars }); return `[${id} ${r.status}${r.status === "exited" ? ` code ${r.code}` : ""}${r.skipped ? `; ${r.skipped} earlier chars were dropped` : ""}]\n${r.output || "(no new output)"}`; } }),
244
+ defineTool({ name: "sh_stop", description: "Stop a background process (SIGTERM, then SIGKILL after 2 seconds).", schema: { type: "object", required: ["id"], properties: { id: { type: "string" } } },
245
+ run: async ({ id }) => `stopped ${fmt(await procs.stop(id))}` }),
246
+ defineTool({ name: "sh_list", description: "List background processes and their status.", readOnly: true, schema: { type: "object", properties: {} }, run: () => procs.list().map(fmt).join("\n") || "no background processes" }),
247
+ ];
248
+ }
249
+ export const CODE_SYSTEM = `You are an expert software engineer working autonomously in a repository. Complete the task, then call \`finish\`.
250
+ Workflow:
251
+ 1. EXPLORE (proportionate: a small fix needs a handful of reads, not a survey; stop when you know where to change): list_files / grep / read_file to understand the code before changing it. Reproduce the problem or run the existing tests first when possible.
252
+ 2. EDIT: smallest correct change, matching surrounding style. Prefer edit_file over rewriting files (several replacements in one file: its \`edits\` list); use apply_patch when one change spans several files. Do not touch unrelated code. Never edit tests to make them pass unless the task says so.
253
+ 3. VERIFY: run the tests/build/command that proves it works. Read failures carefully and fix the root cause. If an approach fails twice, rethink.
254
+ 4. FINISH: \`finish(summary)\` with what changed, how you verified, and assumptions. It is rejected if verification fails.
255
+ MEMORY: notes from earlier sessions may appear below the task; treat them as hints to verify. When you learn something durable that is NOT obvious from the code (the real test command, a convention the user insists on, a correction), save it with memory_save. Never save secrets or task-progress chatter.
256
+ Never invent file contents or command output. Don't ask the user questions; choose sensible defaults. Some commands need human approval; if denied, use another route.`;
257
+ /** Conversational variant of the prompt: answers questions directly, plans, delegates exploration, works within permission modes. */
258
+ export const CHAT_SYSTEM = `You are an expert software engineer pair-programming with the user in their terminal, inside their repository. Be concise and direct; format with Markdown (short paragraphs, \`inline code\`, fenced code blocks).
259
+ How to work:
260
+ - Questions: answer from the code. Explore first (list_files, grep, read_file; in a large repository start with repo_map, find_refs and impact when you have them) instead of guessing; cite files as path:line. Use the \`task\` tool for broad searches so your own context stays clean.
261
+ - Effort: match effort to the question. A fact or one-file question: 3-10 tool calls, no subagent. Comparing or tracing a few things: 10-15 calls. Only a genuinely broad survey justifies \`task\`, and then give each subagent a distinct slice. When your budget notes say it is running low, stop exploring and give the best answer you have, saying what you did not cover.
262
+ - Changes: read a file before editing it, make the smallest correct change, then verify it (run the tests/build). For anything with 3+ steps, keep a checklist with \`todo_write\` and update it as you go (exactly one item in_progress).
263
+ - When you finish a coding task, call \`finish(summary)\` (what changed, how you verified). For plain questions just answer; no \`finish\` needed.
264
+ - Permissions: the user may be asked to approve edits and shell commands. If a call is denied, do not retry it; use another approach or explain what you need. In plan mode you are read-only: investigate, then present a plan with \`exit_plan_mode\` and wait for approval.
265
+ - If the request is ambiguous in a way that matters, ask ONE short clarifying question; otherwise choose sensible defaults and say what you assumed.
266
+ - Notes from earlier sessions may appear below the user's message; treat them as hints to verify. Save durable, non-obvious lessons with memory_save (never secrets).
267
+ - Servers and watchers: start them with \`sh_start\` (plain \`sh\` waits for the command to end), read their output with \`sh_read\`, and \`sh_stop\` them when you are done.
268
+ - Never invent file contents or command output. The user can type while you work; their new messages arrive between your steps, so adjust course when they do.`;
269
+ const EXPLORE_SYSTEM = `You are a read-only code explorer working for another engineer. Investigate the question using list_files, read_file, grep and read-only shell commands. You have a small budget (about 12 tool calls): ask for the most informative things first (grep, then read only the relevant ranges), do not read whole files you do not need, and stop as soon as you can answer. If the question is too broad, answer what you can and say what you did not cover. Report concisely (under 300 words): the answer, the relevant files with line numbers, and anything surprising. You cannot change anything. Finish with \`finish(summary)\`; the requester sees only that summary.`;
270
+ export function createCodeAgent(o) {
271
+ const plugins = o.plugins ?? [];
272
+ const routers = plugins.filter((p) => p.route);
273
+ if (plugins.length)
274
+ o = { ...o, model: routers.length && o.modelById ? routedByFunction(o.model, o.modelById, (i) => { for (const p of routers) {
275
+ const r = p.route(i);
276
+ if (r)
277
+ return r;
278
+ } }) : o.model, hooks: composeHooks(o.hooks, plugins, o.cwd), tools: [...(o.tools ?? []), ...plugins.flatMap((p) => p.tools ?? [])] };
279
+ const policyFn = plugins.some((p) => p.policy) ? (c) => checkPolicies(plugins, { ...c, cwd: o.cwd }) : undefined;
280
+ const SYS = (chat) => applySystem(plugins, (chat ? CHAT_SYSTEM : CODE_SYSTEM) + "\n" + shellNote(), o.cwd);
281
+ const toolsFile = resolve(o.cwd, ".amness/tools.json");
282
+ const touched = new Set();
283
+ const guidance = o.guidance === false ? undefined : new Guidance(o.cwd);
284
+ const sbx = o.sandbox, sandboxMode = typeof sbx === "function" ? sbx : () => sbx ?? "off";
285
+ let agentList = [];
286
+ const agentWarnings = [];
287
+ let patchCount = 0;
288
+ const agentOf = (name) => (name && name !== "explore" ? agentList.find((a) => a.name === name) : undefined);
289
+ const rescanAgents = () => { if (o.agents === false)
290
+ return; const r = loadAgents(o.cwd); agentList = r.agents; agentWarnings.splice(0, agentWarnings.length, ...r.warnings); };
291
+ rescanAgents();
292
+ let tasksThisRequest = 0;
293
+ const MAX_SUBAGENTS_PER_REQUEST = 6;
294
+ let taskSlots = 3;
295
+ const taskWaiters = [];
296
+ const acquireTask = async () => { if (taskSlots > 0) {
297
+ taskSlots--;
298
+ return;
299
+ } await new Promise((r) => taskWaiters.push(r)); };
300
+ const releaseTask = () => { const w = taskWaiters.shift(); if (w)
301
+ w();
302
+ else
303
+ taskSlots++; };
304
+ const codeIndex = o.index === false ? undefined : new CodeIndex(o.cwd), indexMin = typeof o.index === "object" ? o.index.minFiles ?? 25 : 25;
305
+ let skillList = [];
306
+ const skillWarnings = [];
307
+ const rescan = () => { if (o.skills === false)
308
+ return; const r = loadSkills(o.cwd); skillList = r.skills; skillWarnings.splice(0, skillWarnings.length, ...r.warnings); if (skillList.length && !registry.get("skill"))
309
+ registry.add(skillTool(() => skillList)); };
310
+ const procs = o.background === false ? undefined : new ProcessManager(resolve(o.cwd), { sandbox: sandboxMode });
311
+ const fromPlugin = plugins.map((p) => (typeof p.memory === "function" ? p.memory(o.cwd) : p.memory)).find(Boolean);
312
+ const backend = o.memory === false ? undefined : o.memory instanceof MemoryStore ? o.memory : isMemoryBackend(o.memory) ? o.memory : fromPlugin ?? MemoryStore.forWorkspace(resolve(o.cwd));
313
+ const memory = backend instanceof MemoryStore ? backend : undefined; // the built-in store has extras (embeddings, consolidation, graph) that a custom backend does not need to offer
314
+ if (memory && o.embedder)
315
+ memory.embedder = o.embedder;
316
+ const mode = o.permissions?.mode;
317
+ const approve = o.approve && o.approvalReviewer ? reviewedApprove(o.approve, { model: (typeof o.approvalReviewer === "object" && o.approvalReviewer.model) || o.model, task: () => task, onDecision: (d) => o.onEvent?.({ kind: "policy", tool: d.tool, action: d.decision === "approve" ? "allow" : "ask", reason: `reviewer: ${d.reason}` }) }) : o.approve;
318
+ const lsp = o.lsp ? new Diagnostics(o.cwd, typeof o.lsp === "object" ? o.lsp : {}) : undefined;
319
+ const shellCheck = o.check ? async () => { const out = await runShell(o.check, o.cwd, 60_000); return out.startsWith("exit=0") ? "" : `\n\n[post-edit check \`${o.check}\` failed; fix this before moving on]\n${clip(out, 1500)}`; } : undefined;
320
+ const postEdit = shellCheck || lsp ? async (paths) => (await shellCheck?.() ?? "") + (await lsp?.report(paths) ?? "") : undefined;
321
+ const wrap = o.wrapTools ?? ((t) => t);
322
+ const registry = new Registry(wrap([...codeTools(o.cwd, { guidance, procs, beforeShell: o.beforeShell, sandbox: sandboxMode, onChange: (p) => touched.add(p), mode, check: postEdit }), ...(backend ? memoryTools(backend) : []), ...(o.tools ?? [])]));
323
+ const shown = new Set(); // memory notes already in this conversation
324
+ const rescanIndex = () => { if (codeIndex && !registry.get("repo_map") && codeIndex.list().length >= indexMin)
325
+ registry.add(...indexTools(codeIndex)); };
326
+ rescan();
327
+ rescanIndex();
328
+ let todos = [];
329
+ if (o.chat) {
330
+ registry.add(defineTool({
331
+ name: "todo_write", readOnly: true,
332
+ description: "Maintain a checklist for multi-step work. Send the FULL list each time. Exactly one item in_progress; mark items done as you finish them.",
333
+ schema: { type: "object", required: ["todos"], properties: { todos: { type: "array", items: { type: "object", required: ["content", "status"],
334
+ properties: { content: { type: "string" }, status: { type: "string", enum: ["pending", "in_progress", "done"] } } } } } },
335
+ run: ({ todos: t }, ctx) => { todos = t; ctx.emit?.({ kind: "todos", todos: t }); return "todo list updated"; },
336
+ }), defineTool({
337
+ name: "task", readOnly: true, parallel: true, concurrent: ({ agent }) => { const d = agentOf(agent); return !d || !isWriter(d) || d.isolation === "worktree"; },
338
+ get description() { return `Delegate work to a subagent with its own fresh context: it sees only your brief and returns only its summary, so the brief must state the objective, where to look, what NOT to cover (so parallel subagents do not repeat each other) and what to report. Default: a read-only explorer for broad searches or questions that need reading many files; at most 3 run at once. Do not delegate what one grep or read would answer: a subagent costs a full session of tokens.${agentList.length ? ` Other agents (pass \`agent\`): ${agentList.map((a) => `${a.name}: ${a.description}${isWriter(a) ? (a.isolation === "worktree" ? " [edits in an isolated copy and returns a patch]" : " [can edit files]") : ""}`).join("; ")}.` : ""}`; },
339
+ schema: { type: "object", required: ["description"], properties: { description: { type: "string", description: "complete brief: the question or job, where to look, what to report" }, agent: { type: "string", description: "which agent (default: the read-only explorer)" } } },
340
+ run: async (args, ctx) => { await acquireTask(); try {
341
+ return await runTask(args, ctx);
342
+ }
343
+ finally {
344
+ releaseTask();
345
+ } },
346
+ }), defineTool({
347
+ name: "exit_plan_mode", readOnly: true, risky: true, when: () => mode?.() === "plan", preview: ({ plan }) => plan,
348
+ description: "Plan mode only: present your finished plan to the user for approval. After approval you can edit files.",
349
+ schema: { type: "object", required: ["plan"], properties: { plan: { type: "string", description: "the plan, in Markdown: what you will change and why, in order" } } },
350
+ run: () => "The user approved the plan. Implement it now.",
351
+ }));
352
+ }
353
+ /** The `task` tool's work: a read-only explorer, or one of the custom agents. */
354
+ async function runTask({ description, agent }, ctx) {
355
+ let slice;
356
+ const out = await runTaskInner({ description, agent }, ctx, (b) => (slice = b));
357
+ return out + (slice && o.budget && Number.isFinite(o.budget.maxTokens) ? `\n[subagent used about ${Math.max(1, Math.round(slice.effective / 1000))}k tokens; the budget for this request is ${Math.round(o.budget.fraction * 100)}% used]` : "");
358
+ }
359
+ async function runTaskInner({ description, agent }, ctx, setSlice) {
360
+ const track = (b) => { setSlice(b); return b; };
361
+ if (++tasksThisRequest > MAX_SUBAGENTS_PER_REQUEST)
362
+ return `ERROR: ${MAX_SUBAGENTS_PER_REQUEST} subagents have already run for this request. Work with what they reported, read the few remaining files yourself, or answer and say what you did not cover.`;
363
+ const d = agentOf(agent);
364
+ const unknown = agent && !["explore", "explorer"].includes(agent.toLowerCase()) && !d ? agent : undefined; // a wrong name should not cost a round trip: use the read-only explorer and say so
365
+ if (unknown)
366
+ o.onEvent?.({ kind: "nudge", detail: `no agent named "${unknown}"; used the read-only explorer` });
367
+ const plan = mode?.() === "plan", emitAs = (name) => (e) => o.onEvent?.({ ...e, agent: name, parent: ctx.callId });
368
+ if (!d) {
369
+ const r = await createHarness({ model: o.models?.explore ?? o.model, system: EXPLORE_SYSTEM, tools: codeTools(o.cwd, { mode: () => "plan" }), maxSteps: 14, policy: policyFn, budget: track(o.budget?.slice(0.12, 60_000)), signal: ctx.signal, onEvent: emitAs("explore") }).run(description);
370
+ const note = unknown ? `(no agent named "${unknown}"; the read-only explorer answered. Available: explore${agentList.map((a) => ", " + a.name).join("")})\n` : "";
371
+ return note + clip(r.text || "(the explorer returned nothing)", 8000); // subagents hand back a condensed result (Anthropic: 1-2k tokens), never their working notes
372
+ }
373
+ const names = plan ? d.tools.filter((t) => !WRITE_TOOLS.has(t)) : d.tools, isolated = d.isolation === "worktree" && isWriter(d) && !plan;
374
+ let wt;
375
+ if (isolated) {
376
+ if (!(await isRepo(o.cwd)))
377
+ return "ERROR: isolation: worktree needs a git repository with at least one commit";
378
+ wt = await createWorktree(o.cwd);
379
+ }
380
+ try {
381
+ const base = codeTools(wt?.dir ?? o.cwd, { mode: plan ? () => "plan" : (mode ?? (() => "default")), sandbox: sandboxMode }).filter((t) => names.includes(t.name));
382
+ if (names.includes("skill") && skillList.length)
383
+ base.push(skillTool(() => skillList));
384
+ const r = await createHarness({
385
+ model: (d.model && o.agentModels?.[d.model]) || o.model, system: `${d.prompt}\n\nYou are a subagent working for another engineer. You see only the brief below. Finish with \`finish(summary)\`: what you found or did, concretely, with file paths.`, tools: base, maxSteps: d.maxSteps,
386
+ budget: track(o.budget?.slice(0.25, 120_000)), signal: ctx.signal, approve, permissions: o.rules === false ? undefined : o.rules ?? new Permissions({}, wt?.dir ?? o.cwd), policy: policyFn, hooks: o.hooks, ledger: isolated ? undefined : isWriter(d) ? ledger : undefined, onEvent: emitAs(d.name),
387
+ }).run(description);
388
+ let out = clip(r.finished ? r.text : `(${d.name} did not finish cleanly) ${r.text}`, 8000);
389
+ if (wt) {
390
+ const diff = await worktreeDiff(wt);
391
+ if (diff) {
392
+ const n = ++patchCount, rel = `.amness/agents/out/${d.name}-${Date.now().toString(36)}-${n}.patch`;
393
+ mkdirSync(dirname(resolve(o.cwd, rel)), { recursive: true });
394
+ writeFileSync(resolve(o.cwd, rel), diff + "\n");
395
+ const files = [...diff.matchAll(/^diff --git a\/(\S+)/gm)].map((m) => m[1]);
396
+ out += `\n\n${d.name} worked in an isolated copy; nothing in your working tree has changed yet. Its changes (${files.length} file(s): ${files.slice(0, 8).join(", ")}) are saved as a patch: read ${rel}, then apply it with apply_patch({ file: "${rel}" }).`;
397
+ }
398
+ else
399
+ out += `\n\n(${d.name} made no file changes in its isolated copy.)`;
400
+ }
401
+ return out;
402
+ }
403
+ finally {
404
+ if (wt)
405
+ await removeWorktree(wt);
406
+ }
407
+ }
408
+ if (o.selfExtend) {
409
+ mkdirSync(dirname(toolsFile), { recursive: true });
410
+ registry.add(...loadSavedTools(toolsFile, o.cwd), createToolTool({ cwd: o.cwd, file: toolsFile }));
411
+ }
412
+ /** SessionStart (first message of a conversation) and UserPromptSubmit hooks: may add context to the message or refuse it. */
413
+ const promptHooks = async (first, prompt) => {
414
+ if (!o.hooks?.count())
415
+ return {};
416
+ const parts = [];
417
+ for (const [ev, payload] of [...(first ? [["SessionStart", {}]] : []), ["UserPromptSubmit", { prompt }]]) {
418
+ const r = await o.hooks.run(ev, payload).catch(() => undefined);
419
+ for (const w of r?.warnings ?? [])
420
+ o.onEvent?.({ kind: "hook", event: ev, detail: w, blocked: false });
421
+ if (r?.block) {
422
+ o.onEvent?.({ kind: "hook", event: ev, detail: r.block, blocked: true });
423
+ return { block: `blocked by a ${ev} hook: ${r.block}` };
424
+ }
425
+ if (r?.context) {
426
+ parts.push(r.context);
427
+ o.onEvent?.({ kind: "hook", event: ev, detail: "added context", blocked: false });
428
+ }
429
+ }
430
+ return { context: parts.length ? `[hook context]\n${parts.join("\n")}` : undefined };
431
+ };
432
+ const ledger = new Ledger({ testCommand: o.verify ?? o.check });
433
+ const reviewModel = (typeof o.review === "object" && o.review.model) || o.models?.review || o.model;
434
+ let contract = [];
435
+ /** Acceptance criteria for a task-like message (reviewer on): written first, shown to the builder, checked at the end. */
436
+ const withContract = async (text, force = false) => {
437
+ if (!o.review)
438
+ return text;
439
+ contract = [];
440
+ if (!force && !(text.length >= 25 && /\b(fix|add|implement|refactor|change|update|create|write|build|make|rename|remove|migrate|optimi[sz]e|support|bug|test|handle)/i.test(text)))
441
+ return text;
442
+ contract = await proposeContract(reviewModel, text, o.budget);
443
+ return contract.length ? `${text}\n\n## Acceptance criteria (an independent reviewer will check these before your work is accepted)\n${contract.map((c, i) => `${i + 1}. ${c}`).join("\n")}` : text;
444
+ };
445
+ const sessionFile = resolve(o.cwd, ".amness/handoff.md");
446
+ let task = "";
447
+ const h = createHarness({
448
+ model: o.model, compactModel: o.models?.compact, system: SYS(o.chat), tools: registry, maxSteps: o.maxSteps ?? (o.chat ? 60 : 40), inbox: o.inbox,
449
+ budget: o.budget, approve, policy: policyFn, context: plugins.some((p) => p.context) ? async (m, info) => { let cur = m, changed = false; for (const p of plugins)
450
+ if (p.context) {
451
+ const r = await p.context(cur, info);
452
+ if (Array.isArray(r) && r.length) {
453
+ cur = r;
454
+ changed = true;
455
+ }
456
+ } return changed ? cur : undefined; } : undefined, off: o.off, onEvent: o.onEvent, signal: o.signal, record: o.record, redact: o.redact !== false, taint: o.taint ?? "ask", hooks: o.hooks, stagnationTurns: o.stagnationTurns, ledger, reviewer: o.review ? (ctx) => reviewWork({ model: reviewModel, cwd: o.cwd, task: ctx.task, contract, summary: ctx.summary, diffs: ledger.diffs(), commands: ledger.recentCommands(), verify: o.verify, budget: o.budget, signal: typeof o.signal === "function" ? o.signal() : o.signal }) : undefined, permissions: o.rules === false ? undefined : o.rules ?? new Permissions({}, o.cwd),
457
+ rollover: o.rollover === false ? undefined : o.rollover ?? { afterTokens: 120_000, afterChars: 500_000 },
458
+ onHandoff: (note) => {
459
+ mkdirSync(dirname(sessionFile), { recursive: true });
460
+ writeFileSync(sessionFile, `# Handoff
461
+
462
+ Task: ${task}
463
+
464
+ ${note}
465
+ `);
466
+ },
467
+ verify: o.verify ? async () => {
468
+ const out = await runShell(o.verify, o.cwd, 300_000);
469
+ return out.startsWith("exit=0") ? true : `\`${o.verify}\` failed:\n${clip(out, 3000)}`;
470
+ } : undefined,
471
+ checkpoint: o.checkpointFile ? (m) => { mkdirSync(dirname(resolve(o.checkpointFile)), { recursive: true }); writeFileSync(o.checkpointFile, JSON.stringify(m)); } : undefined,
472
+ });
473
+ return {
474
+ /** The live toolbox: add/remove tools between or during runs. */
475
+ registry, memory, guidance, procs, lsp, codeIndex, get agents() { return agentList; }, get agentWarnings() { return agentWarnings; }, get skills() { return skillList; }, get skillWarnings() { return skillWarnings; },
476
+ /** Keep the provider's prompt cache alive across an idle gap (see createHarness().warm). */
477
+ warm: (msgs, opts) => h.warm(msgs, opts),
478
+ get todos() { return todos; },
479
+ /** One conversational turn: `history` is everything so far (empty to start); returns the updated transcript in `messages`. */
480
+ async chat(history, userText, extra = {}) {
481
+ tasksThisRequest = 0;
482
+ task ||= userText;
483
+ let text = userText;
484
+ const gate = await promptHooks(history.length === 0, userText);
485
+ if (gate.block)
486
+ return { text: gate.block, finished: false, steps: 0, messages: history, stopped: "blocked", tokens: 0 };
487
+ if (gate.context)
488
+ text += "\n\n" + gate.context;
489
+ text = await withContract(text);
490
+ if (memory?.embedder) {
491
+ await memory.index().catch(() => { });
492
+ await memory.warmQuery(userText);
493
+ }
494
+ if (!history.length && guidance) {
495
+ const g = guidance.start();
496
+ if (g)
497
+ text += "\n\n" + g;
498
+ }
499
+ if (!history.length) {
500
+ rescanIndex();
501
+ rescanAgents();
502
+ rescan();
503
+ const sb = skillsBlock(skillList);
504
+ if (sb)
505
+ text += "\n\n" + sb;
506
+ }
507
+ if (!history.length && backend) {
508
+ shown.clear();
509
+ const ctx = backend.context(userText, 6000, shown);
510
+ if (ctx)
511
+ text += "\n\n" + ctx;
512
+ }
513
+ else if (backend) {
514
+ const d = backend.delta(userText, shown);
515
+ if (d)
516
+ text += "\n\n" + d;
517
+ } // only new, relevant notes, at the end: the cached prefix stays intact
518
+ const img = extra.images?.length ? { images: extra.images } : {};
519
+ const msgs = history.length ? [...history, { role: "user", content: text, ...img }]
520
+ : [{ role: "system", content: SYS(o.chat) }, { role: "user", content: text, ...img }];
521
+ const res = await h.run(text, { resume: msgs });
522
+ // Learning from outcomes: a finished (verified) run credits the notes that were in front of the model, a run that gave up or got stuck counts against them.
523
+ backend?.endTurn(res.finished ? true : res.stopped && res.stopped !== "aborted" ? false : undefined);
524
+ return res;
525
+ },
526
+ async run(t, opts = {}) {
527
+ tasksThisRequest = 0;
528
+ task = t;
529
+ let resume = opts.resume && o.checkpointFile && existsSync(o.checkpointFile) ? JSON.parse(readFileSync(o.checkpointFile, "utf8")) : undefined;
530
+ // resuming with a NEW instruction: add it as a follow-up rather than silently ignoring it
531
+ if (resume && t.trim() && !(resume.find((m) => m.role === "user")?.content ?? "").startsWith(t.trim()))
532
+ resume = [...resume, { role: "user", content: t }];
533
+ let prompt = t;
534
+ const gate = await promptHooks(!resume, t);
535
+ if (gate.block)
536
+ return { text: gate.block, finished: false, steps: 0, messages: resume ?? [], stopped: "blocked", tokens: 0 };
537
+ if (gate.context)
538
+ prompt += "\n\n" + gate.context;
539
+ prompt = await withContract(prompt, true);
540
+ if (opts.bundle) {
541
+ const b = typeof opts.bundle === "string" ? readBundle(opts.bundle) : opts.bundle;
542
+ const r = applyBundle(b, { cwd: o.cwd, memory, overwrite: opts.overwrite });
543
+ prompt = `${t || b.task}\n\n## Handoff from a previous session (possibly another machine or model)\nOriginal task: ${b.task}\nStatus then: ${b.status.finished ? "finished" : "unfinished"}${b.status.summary ? ` - ${b.status.summary}` : ""}\n${b.handoff}\n\nRestored ${r.restored.length} file(s) from that session${r.skipped.length ? `; kept your version of: ${r.skipped.join(", ")}` : ""}. Re-check the workspace before continuing; it may have changed.`;
544
+ }
545
+ if (guidance && !resume) {
546
+ const g = guidance.start();
547
+ if (g)
548
+ prompt += "\n\n" + g;
549
+ }
550
+ if (!resume) {
551
+ rescanIndex();
552
+ rescan();
553
+ const sb = skillsBlock(skillList);
554
+ if (sb)
555
+ prompt += "\n\n" + sb;
556
+ }
557
+ if (backend && !resume) {
558
+ const ctx = backend.context(prompt);
559
+ if (ctx)
560
+ prompt += "\n\n" + ctx;
561
+ }
562
+ if (opts.images?.length && !resume)
563
+ resume = [{ role: "system", content: SYS(o.chat) }, { role: "user", content: prompt, images: opts.images }];
564
+ const res = await h.run(prompt, { resume });
565
+ backend?.endTurn(res.finished ? true : res.stopped && res.stopped !== "aborted" ? false : undefined);
566
+ return res;
567
+ },
568
+ /** Keep talking to the agent after it finished (or stopped): same conversation, full context. */
569
+ followUp(prev, text) {
570
+ return h.run(text, { resume: [...prev.messages, { role: "user", content: text }] });
571
+ },
572
+ /** Replace a long conversation by a model-written handoff note (same mechanism as automatic rollover, on demand). */
573
+ async compact(messages) {
574
+ const specs = registry.visible({ step: 0, messages }).map((t) => ({ name: t.name, description: t.description, parameters: t.schema }));
575
+ const note = await makeHandoff(o.models?.compact ?? o.model, specs, messages, o.budget);
576
+ const first = (messages.find((m) => m.role === "user")?.content ?? task).split("\n\n## Handoff from your earlier context")[0];
577
+ return { note, messages: [messages[0], { role: "user", content: `${first}\n\n## Handoff from your earlier context (you are continuing, not starting over)\n${note}` }] };
578
+ },
579
+ /** Package the work so far for another machine / model / day. Asks the model for a handoff note unless one is supplied. */
580
+ async exportBundle(result, path, handoff) {
581
+ const note = handoff ?? (await makeHandoff(o.models?.compact ?? o.model, registry.visible({ step: 0, messages: result.messages }).map((t) => ({ name: t.name, description: t.description, parameters: t.schema })), result.messages, o.budget));
582
+ const bundle = {
583
+ schema: "amness.session/1", createdAt: new Date().toISOString(), task: result.messages.find((m) => m.role === "user")?.content?.split("\n\n## ")[0] ?? task,
584
+ status: { finished: result.finished, summary: result.text.slice(0, 500) }, handoff: note, memory: backend?.list() ?? [],
585
+ files: collectFiles(o.cwd, touched), from: { ...o.meta, cwd: basename(resolve(o.cwd)) },
586
+ };
587
+ return { bundle, path: path ? writeBundle(path, bundle) : undefined };
588
+ },
589
+ };
590
+ }