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,225 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ // Plugins: harness behaviour written as code. A plugin is a TypeScript or JavaScript file in `.amness/plugins/` (or `~/.amness/plugins/`) that can add tools, run code at every
10
+ // point of the loop (the eight hook events, in-process, with the full payload), rewrite the system prompt, and contribute evaluation tasks, graders and harness variants.
11
+ // Everything the built-in guards do can be done from here, and `amness eval` can then compare your harness against the others like any built-in variant.
12
+ // A plugin runs with your full permissions, so project plugins load only after you approve that exact set of files (same rule as project hooks).
13
+ import { createHash } from "node:crypto";
14
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
15
+ import { homedir } from "node:os";
16
+ import { extname, join, resolve } from "node:path";
17
+ import { pathToFileURL } from "node:url";
18
+ import { execFile } from "node:child_process";
19
+ import { shellInvocation } from "./shell.js";
20
+ /**
21
+ * Ask every plugin's `policy`. A deny always wins, then an ask, then an allow. The caller decides what each answer may do:
22
+ * the harness never lets a policy relax the safety floor or the untrusted-text check, only tighten them or skip an ordinary prompt.
23
+ */
24
+ export async function checkPolicies(plugins, call) {
25
+ let best;
26
+ const rank = { allow: 1, ask: 2, deny: 3 };
27
+ for (const p of plugins) {
28
+ if (!p.policy)
29
+ continue;
30
+ let r;
31
+ try {
32
+ r = await p.policy(call);
33
+ }
34
+ catch (e) {
35
+ r = { action: p.failClosed ? "deny" : "ask", reason: `policy of plugin ${p.name} failed: ${e.message}` };
36
+ } // a broken policy never silently allows
37
+ if (!r)
38
+ continue;
39
+ const d = { ...(typeof r === "string" ? { action: r } : r), plugin: p.name };
40
+ if (!best || rank[d.action] > rank[best.action])
41
+ best = d;
42
+ }
43
+ return best;
44
+ }
45
+ /** Several reviewers vote: the finish is accepted when at least `quorum` of them pass ("all", "majority", or a number). */
46
+ export function votingGate(name, reviewers, quorum = "majority", maxRejections = 2) {
47
+ return { name, maxRejections, async check(ctx) {
48
+ const votes = await Promise.all(reviewers.map(async (r) => { try {
49
+ return { name: r.name, ...normalizeGate(await r.review(ctx)) };
50
+ }
51
+ catch (e) {
52
+ return { name: r.name, pass: true, findings: [`(${r.name} could not run: ${e.message})`] };
53
+ } }));
54
+ const need = quorum === "all" ? votes.length : quorum === "majority" ? Math.floor(votes.length / 2) + 1 : quorum, passes = votes.filter((v) => v.pass).length;
55
+ return passes >= need ? true : { pass: false, findings: [`only ${passes} of ${votes.length} reviewers accepted this (${need} needed)`, ...votes.filter((v) => !v.pass).flatMap((v) => v.findings.map((f) => `${v.name}: ${f}`))] };
56
+ } };
57
+ }
58
+ const normalizeGate = (r) => r === undefined || r === true ? { pass: true, findings: [] } : r === false ? { pass: false, findings: ["the check failed"] } : typeof r === "string" ? { pass: false, findings: [r] } : Array.isArray(r) ? { pass: r.length === 0, findings: r } : { pass: r.pass, findings: r.findings ?? [] };
59
+ const shellRun = (cwd) => (cmd, ms = 60_000) => new Promise((res) => execFile(shellInvocation(cmd).file, shellInvocation(cmd).args, { cwd, windowsHide: true, timeout: ms, maxBuffer: 10 << 20 }, (err, out, errOut) => res({ code: err ? (typeof err.code === "number" ? err.code : 1) : 0, output: String(out) + String(errOut) })));
60
+ /** Identity function that gives you types and completion while writing a plugin. */
61
+ export const definePlugin = (p) => p;
62
+ const EXT = new Set([".ts", ".mts", ".js", ".mjs"]);
63
+ const filesIn = (dir) => (existsSync(dir) ? readdirSync(dir).filter((f) => EXT.has(extname(f)) && statSync(join(dir, f)).isFile()).sort().map((f) => join(dir, f)) : []);
64
+ export const projectPluginDir = (cwd) => join(cwd, ".amness", "plugins");
65
+ export const projectPluginFiles = (cwd) => filesIn(projectPluginDir(cwd));
66
+ /** A fingerprint of the project's plugin files: approval is remembered for exactly this content. */
67
+ export function pluginFingerprint(cwd) {
68
+ const fs = filesIn(projectPluginDir(cwd));
69
+ if (!fs.length)
70
+ return undefined;
71
+ const h = createHash("sha256");
72
+ for (const f of fs)
73
+ h.update(f).update(readFileSync(f));
74
+ return h.digest("hex").slice(0, 16);
75
+ }
76
+ const valid = (p, file) => {
77
+ if (!p || typeof p !== "object")
78
+ return `${file}: must export a plugin object (export default definePlugin({ name, ... }))`;
79
+ if (typeof p.name !== "string" || !/^[\w.-]{1,40}$/.test(p.name))
80
+ return `${file}: needs a "name" (letters, numbers, . _ -)`;
81
+ for (const k of ["tools", "cases"])
82
+ if (p[k] !== undefined && !Array.isArray(p[k]))
83
+ return `${file}: "${k}" must be a list`;
84
+ if (p.hooks !== undefined && (typeof p.hooks !== "object" || Object.values(p.hooks).some((h) => typeof h !== "function")))
85
+ return `${file}: "hooks" must map event names to functions`;
86
+ return undefined;
87
+ };
88
+ /** Import one plugin file (also used by `amness eval --plugin <file>`, where passing the file is the approval). */
89
+ export async function importPlugins(file) {
90
+ const warnings = [], plugins = [];
91
+ try {
92
+ const mod = await import(__rewriteRelativeImportExtension(pathToFileURL(resolve(file)).href + `?v=${Date.now()}`)), exp = mod.default ?? mod.plugin ?? mod.plugins;
93
+ for (const p of Array.isArray(exp) ? exp : [exp]) {
94
+ const bad = valid(p, file);
95
+ if (bad)
96
+ warnings.push(bad);
97
+ else
98
+ plugins.push(p);
99
+ }
100
+ }
101
+ catch (e) {
102
+ warnings.push(`${file}: could not load: ${e.message.split("\n")[0]}`);
103
+ }
104
+ return { plugins, warnings };
105
+ }
106
+ /** User plugins always load; project plugins only when `trustProject`. Problems are warnings, never exceptions: a broken plugin must not stop the agent starting. */
107
+ export async function loadPlugins(cwd, o = {}) {
108
+ const out = { plugins: [], warnings: [] }, seen = new Set();
109
+ const add = async (files) => { for (const f of files) {
110
+ const r = await importPlugins(f);
111
+ out.warnings.push(...r.warnings);
112
+ for (const p of r.plugins) {
113
+ if (seen.has(p.name))
114
+ out.warnings.push(`${f}: a plugin named "${p.name}" is already loaded; skipped`);
115
+ else {
116
+ seen.add(p.name);
117
+ out.plugins.push(p);
118
+ }
119
+ }
120
+ } };
121
+ await add(filesIn(join(o.userDir ?? process.env.AMNESS_HOME ?? join(homedir(), ".amness"), "plugins")));
122
+ const proj = filesIn(projectPluginDir(cwd));
123
+ if (proj.length) {
124
+ if (o.trustProject)
125
+ await add(proj);
126
+ else
127
+ out.untrusted = { count: proj.length, fingerprint: pluginFingerprint(cwd) };
128
+ }
129
+ return out;
130
+ }
131
+ /** Hooks from plugins, run in-process after the shell hooks. Same rules as shell hooks: the first block stops the rest, context accumulates, the last rewritten args win. */
132
+ export function composeHooks(shell, plugins, cwd) {
133
+ const withHooks = plugins.filter((p) => (p.hooks && Object.keys(p.hooks).length) || p.gates?.length);
134
+ if (!withHooks.length)
135
+ return shell;
136
+ const rejections = new Map();
137
+ let lastTask;
138
+ return {
139
+ count: () => (shell?.count() ?? 0) + withHooks.reduce((n, p) => n + Object.keys(p.hooks ?? {}).length + (p.gates?.length ?? 0), 0),
140
+ async run(event, payload) {
141
+ const res = shell ? await shell.run(event, payload) : { warnings: [] };
142
+ if (res.block)
143
+ return res;
144
+ for (const p of withHooks) {
145
+ const h = p.hooks?.[event];
146
+ if (!h)
147
+ continue;
148
+ try {
149
+ const r = await h({ event, cwd, ...payload });
150
+ if (r === undefined || r === null)
151
+ continue;
152
+ if (typeof r === "string") {
153
+ res.context = (res.context ? res.context + "\n" : "") + r;
154
+ continue;
155
+ }
156
+ if (r.block !== undefined) {
157
+ res.block = r.block;
158
+ return res;
159
+ }
160
+ if (r.context)
161
+ res.context = (res.context ? res.context + "\n" : "") + r.context;
162
+ if (r.args)
163
+ res.args = r.args;
164
+ if (r.tools)
165
+ res.tools = r.tools;
166
+ if (r.then)
167
+ res.then = r.then;
168
+ }
169
+ catch (e) {
170
+ const msg = `plugin ${p.name} (${event}): ${e.message}`;
171
+ if (p.failClosed) {
172
+ res.block = msg;
173
+ return res;
174
+ }
175
+ res.warnings.push(msg);
176
+ }
177
+ }
178
+ if (event === "Stop") { // finish gates run after the hooks, with what really happened
179
+ if (payload.task !== lastTask) {
180
+ lastTask = payload.task;
181
+ rejections.clear();
182
+ }
183
+ for (const p of withHooks)
184
+ for (const g of p.gates ?? []) {
185
+ const key = `${p.name}:${g.name}`, n = rejections.get(key) ?? 0;
186
+ let v;
187
+ try {
188
+ v = normalizeGate(await g.check({ summary: String(payload.summary ?? ""), task: String(payload.task ?? ""), changed: payload.changed ?? [], testsAfterLastEdit: payload.testsAfterLastEdit, notes: payload.notes ?? [], cwd, run: shellRun(cwd), rejections: n }));
189
+ }
190
+ catch (e) {
191
+ res.warnings.push(`plugin ${p.name} gate ${g.name}: ${e.message}`);
192
+ continue;
193
+ }
194
+ if (v.pass)
195
+ continue;
196
+ const text = `gate "${g.name}" (plugin ${p.name}) found problems:\n${v.findings.map((f) => `- ${f}`).join("\n")}`;
197
+ if (n < (g.maxRejections ?? 2)) {
198
+ rejections.set(key, n + 1);
199
+ res.block = text;
200
+ return res;
201
+ }
202
+ res.warnings.push(`finishing despite ${text}`);
203
+ }
204
+ }
205
+ return res;
206
+ },
207
+ };
208
+ }
209
+ export const applySystem = (plugins, base, cwd) => plugins.reduce((s, p) => (p.system ? p.system(s, { cwd }) : s), base);
210
+ /** A starting point for `amness plugin new <name>`. */
211
+ export const pluginTemplate = (name) => `// .amness/plugins/${name}.ts: a plugin is just an exported object. See docs/plugins.md.
212
+ export default {
213
+ name: ${JSON.stringify(name)},
214
+ description: "what this plugin is for",
215
+
216
+ // Runs inside the loop. Return nothing to continue, a string to add context, or { block: "why" } to refuse.
217
+ hooks: {
218
+ // PreToolUse: ({ tool, args }) => (tool === "sh" && /rm -rf/.test(String(args.cmd)) ? { block: "no recursive deletes" } : undefined),
219
+ // Stop: ({ summary, changed }) => (changed.length && !/test/i.test(String(summary)) ? { block: "say which tests you ran" } : undefined),
220
+ },
221
+
222
+ // A slash command: /${name}
223
+ commands: [{ name: ${JSON.stringify(name)}, help: "say hello from the plugin", run: (ctx) => ctx.say("hello from the ${name} plugin") }],
224
+ };
225
+ `;
@@ -0,0 +1,8 @@
1
+ export type Capability = {
2
+ id: string;
3
+ label: string;
4
+ };
5
+ export declare function scanSource(source: string): Capability[];
6
+ export declare const scanFile: (file: string) => Capability[];
7
+ /** What each of this project's plugin files can touch, one line per file, for the approval prompt. */
8
+ export declare function describeProjectPlugins(cwd: string): string[];
@@ -0,0 +1,20 @@
1
+ // A plain-text scan of a plugin's source for what it can touch. This is a heads-up for the approval prompt, NOT a sandbox: a plugin still runs with your full permissions,
2
+ // and a determined plugin can hide what it does. Its job is to make the ordinary case visible ("this plugin runs shell commands and uses the network").
3
+ import { readFileSync } from "node:fs";
4
+ import { basename } from "node:path";
5
+ import { projectPluginFiles } from "./plugins.js";
6
+ const CHECKS = [
7
+ { id: "shell", label: "runs shell commands", re: /\bchild_process\b|\bexecSync\b|\bspawnSync\b|\bexecFile\b|\bBun\.spawn\b|\bDeno\.Command\b/ },
8
+ { id: "network", label: "uses the network", re: /\bfetch\s*\(|node:(https?|net|dgram|tls)\b|\bWebSocket\b|\bXMLHttpRequest\b/ },
9
+ { id: "files-write", label: "writes files", re: /\b(writeFile|writeFileSync|appendFile|appendFileSync|rmSync|unlinkSync|rename(Sync)?|mkdirSync|createWriteStream)\b/ },
10
+ { id: "files-read", label: "reads files", re: /\b(readFile|readFileSync|readdirSync|createReadStream)\b/ },
11
+ { id: "env", label: "reads environment variables (may include keys)", re: /\bprocess\.env\b/ },
12
+ { id: "dynamic-code", label: "runs generated code (eval, new Function, dynamic import)", re: /\beval\s*\(|new Function\s*\(|\bimport\s*\(\s*(?!["'`][^"'`]*["'`]\s*\))/ },
13
+ { id: "home", label: "looks outside the project (home folder, absolute paths)", re: /\bhomedir\s*\(|["'`]\/(etc|Users|home|root)\b|~\// },
14
+ ];
15
+ export function scanSource(source) { const code = source.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, ""); return CHECKS.filter((c) => c.re.test(code)).map(({ id, label }) => ({ id, label })); }
16
+ export const scanFile = (file) => scanSource(readFileSync(file, "utf8"));
17
+ /** What each of this project's plugin files can touch, one line per file, for the approval prompt. */
18
+ export function describeProjectPlugins(cwd) {
19
+ return projectPluginFiles(cwd).map((f) => { const caps = scanFile(f); return `${basename(f)}: ${caps.length ? caps.map((c) => c.label).join(", ") : "no shell, network, file or environment access found"}`; });
20
+ }
@@ -0,0 +1,41 @@
1
+ import { type SandboxMode } from "./sandbox.ts";
2
+ export type ProcInfo = {
3
+ id: string;
4
+ name: string;
5
+ cmd: string;
6
+ pid?: number;
7
+ status: "running" | "exited" | "killed";
8
+ code?: number | null;
9
+ startedAt: number;
10
+ bytes: number;
11
+ };
12
+ export declare class ProcessManager {
13
+ private procs;
14
+ private n;
15
+ private cwd;
16
+ private max;
17
+ private sandbox;
18
+ constructor(cwd: string, o?: {
19
+ max?: number;
20
+ sandbox?: () => SandboxMode;
21
+ });
22
+ start(cmd: string, name?: string): ProcInfo;
23
+ private get;
24
+ private info;
25
+ list(): ProcInfo[];
26
+ /** Output produced since the last read. With `waitMs`, waits (up to that long) for new output or for the process to end. */
27
+ read(id: string, o?: {
28
+ waitMs?: number;
29
+ maxChars?: number;
30
+ }): Promise<{
31
+ output: string;
32
+ status: ProcInfo["status"];
33
+ code?: number | null;
34
+ skipped: number;
35
+ }>;
36
+ stop(id: string): Promise<ProcInfo>;
37
+ /** Kill everything still running (call on exit). */
38
+ dispose(): Promise<void>;
39
+ /** Synchronous last-resort kill for process 'exit' handlers. */
40
+ killAllNow(): void;
41
+ }
@@ -0,0 +1,94 @@
1
+ // Background processes: start a dev server or a watcher, read its output later, stop it. Output is kept in a bounded buffer; everything is killed on dispose().
2
+ import { spawn } from "node:child_process";
3
+ import { wrapCommand } from "./sandbox.js";
4
+ import { childEnv } from "./secrets.js";
5
+ import { shellInvocation } from "./shell.js";
6
+ const CAP = 200_000;
7
+ export class ProcessManager {
8
+ procs = new Map();
9
+ n = 0;
10
+ cwd;
11
+ max;
12
+ sandbox;
13
+ constructor(cwd, o = {}) { this.cwd = cwd; this.max = o.max ?? 5; this.sandbox = o.sandbox ?? (() => "off"); }
14
+ start(cmd, name) {
15
+ if ([...this.procs.values()].filter((p) => p.status === "running").length >= this.max)
16
+ throw new Error(`already running ${this.max} background processes; stop one with sh_stop first`);
17
+ const id = `p${++this.n}`;
18
+ const w = wrapCommand(cmd, this.cwd, this.sandbox()); // throws when a sandbox is on but unavailable
19
+ const child = spawn(w?.file ?? shellInvocation(cmd).file, w?.args ?? shellInvocation(cmd).args, { cwd: this.cwd, env: childEnv(process.env, this.sandbox() !== "off"), detached: true, stdio: ["ignore", "pipe", "pipe"] });
20
+ const p = { id, name: name?.slice(0, 40) || cmd.split(/\s+/).slice(0, 3).join(" ").slice(0, 40), cmd, pid: child.pid, status: "running", startedAt: Date.now(), bytes: 0, child, buf: "", dropped: 0, readPos: 0, waiters: [] };
21
+ const take = (d) => {
22
+ const s = d.toString("utf8");
23
+ p.bytes += s.length;
24
+ p.buf += s;
25
+ if (p.buf.length > CAP) {
26
+ const cut = p.buf.length - CAP;
27
+ p.buf = p.buf.slice(cut);
28
+ p.dropped += cut;
29
+ p.readPos = Math.max(0, p.readPos - cut);
30
+ }
31
+ for (const w of p.waiters.splice(0))
32
+ w();
33
+ };
34
+ child.stdout.on("data", take);
35
+ child.stderr.on("data", take);
36
+ child.on("error", (e) => { p.status = "exited"; p.code = -1; take(Buffer.from(`[could not start: ${e.message}]\n`)); });
37
+ child.on("close", (code) => { if (p.status === "running") {
38
+ p.status = "exited";
39
+ p.code = code;
40
+ } for (const w of p.waiters.splice(0))
41
+ w(); });
42
+ this.procs.set(id, p);
43
+ return this.info(p);
44
+ }
45
+ get(id) { const p = this.procs.get(id); if (!p)
46
+ throw new Error(`no background process "${id}". Running: ${this.list().map((x) => x.id).join(", ") || "none"}`); return p; }
47
+ info(p) { const { child: _c, buf: _b, dropped: _d, readPos: _r, waiters: _w, ...i } = p; return i; }
48
+ list() { return [...this.procs.values()].map((p) => this.info(p)); }
49
+ /** Output produced since the last read. With `waitMs`, waits (up to that long) for new output or for the process to end. */
50
+ async read(id, o = {}) {
51
+ const p = this.get(id);
52
+ if (p.readPos >= p.buf.length && p.status === "running" && o.waitMs)
53
+ await new Promise((res) => { const t = setTimeout(res, Math.min(o.waitMs, 10_000)); p.waiters.push(() => { clearTimeout(t); res(); }); });
54
+ const max = o.maxChars ?? 8000;
55
+ let skipped = p.dropped;
56
+ p.dropped = 0;
57
+ let out = p.buf.slice(p.readPos);
58
+ p.readPos = p.buf.length;
59
+ if (out.length > max) {
60
+ skipped += out.length - max;
61
+ out = out.slice(-max);
62
+ }
63
+ return { output: out, status: p.status, code: p.code, skipped };
64
+ }
65
+ async stop(id) {
66
+ const p = this.get(id);
67
+ if (p.status === "running") {
68
+ p.status = "killed";
69
+ const kill = (sig) => { try {
70
+ if (p.pid)
71
+ process.kill(-p.pid, sig);
72
+ }
73
+ catch {
74
+ try {
75
+ p.child.kill(sig);
76
+ }
77
+ catch { /* already gone */ }
78
+ } };
79
+ kill("SIGTERM");
80
+ await new Promise((res) => { const t = setTimeout(() => { kill("SIGKILL"); res(); }, 2000); p.child.once("close", () => { clearTimeout(t); res(); }); });
81
+ }
82
+ return this.info(p);
83
+ }
84
+ /** Kill everything still running (call on exit). */
85
+ async dispose() { await Promise.all([...this.procs.values()].filter((p) => p.status === "running").map((p) => this.stop(p.id))); }
86
+ /** Synchronous last-resort kill for process 'exit' handlers. */
87
+ killAllNow() { for (const p of this.procs.values())
88
+ if (p.status === "running" && p.pid) {
89
+ try {
90
+ process.kill(-p.pid, "SIGKILL");
91
+ }
92
+ catch { /* gone */ }
93
+ } }
94
+ }
@@ -0,0 +1,36 @@
1
+ import type { Analysis } from "./evals.ts";
2
+ import type { Guard } from "./harness.ts";
3
+ export type Profile = {
4
+ off?: Guard[];
5
+ stagnationTurns?: number;
6
+ maxSteps?: number;
7
+ contextBudget?: number;
8
+ review?: boolean;
9
+ note?: string;
10
+ };
11
+ export type ProfileFile = {
12
+ default?: Profile;
13
+ models?: Record<string, Profile>;
14
+ };
15
+ /** The profile for `model`: the user's file first, then the project's (the project's values win), then the matching `models` entries (most specific pattern last). */
16
+ export declare function loadProfile(cwd: string, model: string, o?: {
17
+ userDir?: string;
18
+ }): {
19
+ profile: Profile;
20
+ sources: string[];
21
+ };
22
+ export type GuardVerdict = {
23
+ guard: string;
24
+ action: "remove" | "keep" | "undecided";
25
+ why: string;
26
+ };
27
+ /**
28
+ * Read an ablation (`full` against `full-<guard>` for each guard) and say, per guard, whether removing it is supported by the data.
29
+ * Conservative on purpose: a guard is only recommended for removal when taking it out is significantly better, or when it is not worse and clearly cheaper;
30
+ * a guard whose removal significantly hurts is load-bearing; anything else is "undecided" and stays on.
31
+ */
32
+ export declare function recommend(a: Analysis, fullName?: string): {
33
+ verdicts: GuardVerdict[];
34
+ profile: Profile;
35
+ strength: string;
36
+ };
@@ -0,0 +1,61 @@
1
+ // Harness profiles: settings that depend on the model. Scaffolding that rescued one model is dead weight (or a hazard) for the next, so each model family can switch guards off or
2
+ // retune limits, and the eval runner can recommend what to switch off from measurements instead of guesswork.
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { join } from "node:path";
6
+ const GUARDS = ["nudge", "loop", "compact", "stuck", "wrapup", "condense", "schedule", "ledger", "stagnation", "review", "redact"];
7
+ const glob = (pat) => new RegExp("^" + pat.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".") + "$", "i");
8
+ function check(file, j) {
9
+ const bad = (m) => new Error(`${file}: ${m}`), one = (name, p) => {
10
+ if (p.off !== undefined && (!Array.isArray(p.off) || p.off.some((g) => !GUARDS.includes(g))))
11
+ throw bad(`${name}: "off" must be a list of guards (${GUARDS.join(", ")})`);
12
+ for (const k of ["stagnationTurns", "maxSteps", "contextBudget"])
13
+ if (p[k] !== undefined && (typeof p[k] !== "number" || p[k] < 0))
14
+ throw bad(`${name}: "${k}" must be a non-negative number`);
15
+ };
16
+ if (j.default)
17
+ one("default", j.default);
18
+ for (const [m, p] of Object.entries(j.models ?? {}))
19
+ one(m, p);
20
+ return j;
21
+ }
22
+ /** The profile for `model`: the user's file first, then the project's (the project's values win), then the matching `models` entries (most specific pattern last). */
23
+ export function loadProfile(cwd, model, o = {}) {
24
+ const files = [join(o.userDir ?? process.env.AMNESS_HOME ?? join(homedir(), ".amness"), "profile.json"), join(cwd, ".amness", "profile.json")], merged = {}, sources = [];
25
+ for (const f of files) {
26
+ if (!existsSync(f))
27
+ continue;
28
+ const j = check(f, JSON.parse(readFileSync(f, "utf8")));
29
+ const picks = [j.default, ...Object.entries(j.models ?? {}).filter(([pat]) => glob(pat).test(model)).sort(([a], [b]) => a.length - b.length).map(([, p]) => p)].filter(Boolean);
30
+ for (const p of picks) {
31
+ Object.assign(merged, p, { off: [...new Set([...(merged.off ?? []), ...(p.off ?? [])])] });
32
+ sources.push(f);
33
+ }
34
+ }
35
+ return { profile: merged, sources };
36
+ }
37
+ /**
38
+ * Read an ablation (`full` against `full-<guard>` for each guard) and say, per guard, whether removing it is supported by the data.
39
+ * Conservative on purpose: a guard is only recommended for removal when taking it out is significantly better, or when it is not worse and clearly cheaper;
40
+ * a guard whose removal significantly hurts is load-bearing; anything else is "undecided" and stays on.
41
+ */
42
+ export function recommend(a, fullName = "full") {
43
+ const verdicts = [];
44
+ for (const c of a.comparisons) {
45
+ const m = /^full-(.+)$/.exec(c.variant);
46
+ if (!m || c.vs !== fullName)
47
+ continue;
48
+ const pts = (x) => `${(x * 100).toFixed(0)} pts`, cheaper = c.dTokensPerSolved < 0 && Number.isFinite(c.dTokensPerSolved);
49
+ if (c.verdict === "worse")
50
+ verdicts.push({ guard: m[1], action: "keep", why: `removing it loses ${pts(-c.dPass)} (95% CI ${pts(c.ci.lo)} to ${pts(c.ci.hi)})` });
51
+ else if (c.verdict === "better")
52
+ verdicts.push({ guard: m[1], action: "remove", why: `removing it gains ${pts(c.dPass)} (95% CI ${pts(c.ci.lo)} to ${pts(c.ci.hi)})` });
53
+ else if (Math.abs(c.dPass) <= 0.02 && cheaper && c.ci.lo > -0.1)
54
+ verdicts.push({ guard: m[1], action: "remove", why: `removing it makes no real difference to the pass rate (${pts(c.dPass)}, CI ${pts(c.ci.lo)} to ${pts(c.ci.hi)}) and lowers tokens per solved task by ${Math.round(-c.dTokensPerSolved).toLocaleString()}` });
55
+ else
56
+ verdicts.push({ guard: m[1], action: "undecided", why: `no clear effect (${pts(c.dPass)}, CI ${pts(c.ci.lo)} to ${pts(c.ci.hi)}); it stays on` });
57
+ }
58
+ const strength = a.minDetectable > 0.4 ? `weak evidence: ${a.tasks} task(s) can only detect differences above ~${(a.minDetectable * 100).toFixed(0)} points; add tasks before acting on "remove"` : `${a.tasks} tasks × ${a.trials} trial(s): differences above ~${(a.minDetectable * 100).toFixed(0)} points are detectable`;
59
+ const off = verdicts.filter((v) => v.action === "remove" && GUARDS.includes(v.guard)).map((v) => v.guard);
60
+ return { verdicts, profile: { ...(off.length ? { off } : {}), note: `from an ablation of ${a.tasks} task(s) × ${a.trials} trial(s)` }, strength };
61
+ }
@@ -0,0 +1,29 @@
1
+ /** Number of failures in a test/build/lint output, or undefined when it does not look like one. A failing exit code with no readable count counts as 1. */
2
+ export declare function countFailures(cmd: string, result: string): number | undefined;
3
+ export type TurnFacts = {
4
+ calls: {
5
+ name: string;
6
+ args: any;
7
+ result: string;
8
+ }[];
9
+ edits: {
10
+ path: string;
11
+ diff: string;
12
+ }[];
13
+ };
14
+ export declare class ProgressTracker {
15
+ private bestFail;
16
+ private everFailed;
17
+ private editedPaths;
18
+ private seenDiffs;
19
+ private reads;
20
+ /** Turns in a row without an objective improvement. */
21
+ stale: number;
22
+ lastFailures: number | undefined;
23
+ reasons: string[];
24
+ private readCap;
25
+ constructor(readCap?: number);
26
+ /** Feed one turn; returns why it counted as progress (empty = it did not). */
27
+ observe(t: TurnFacts): string[];
28
+ summary(): string;
29
+ }
@@ -0,0 +1,75 @@
1
+ // Is the run actually getting anywhere? Judged from objective signals (failing-test counts, new files edited, new code written), never from what the model says.
2
+ // A loop that keeps acting without any of them improving is stagnating, however confident its messages sound.
3
+ import { BUILD_CMD, TEST_CMD } from "./ledger.js";
4
+ /** Number of failures in a test/build/lint output, or undefined when it does not look like one. A failing exit code with no readable count counts as 1. */
5
+ export function countFailures(cmd, result) {
6
+ if (!TEST_CMD.test(cmd) && !BUILD_CMD.test(cmd))
7
+ return undefined;
8
+ const exit = /^exit=(\d+)/.exec(result)?.[1], out = result;
9
+ const num = (re) => { const m = re.exec(out); return m ? Number(m[1]) : undefined; };
10
+ const counts = [num(/(\d+)\s+(?:tests?\s+)?(?:failed|failing|failures?)\b/i), num(/\bfail(?:ed|ures?|ing)?:\s*(\d+)/i), num(/Tests:\s+(\d+)\s+failed/i), num(/#\s*fail\s+(\d+)/i), num(/ℹ\s*fail\s+(\d+)/i), num(/failures=(\d+)/i)].filter((x) => x !== undefined);
11
+ const ts = (out.match(/\berror TS\d+/g) ?? []).length, lint = num(/(\d+)\s+problems?\b/i), errs = num(/\b(\d+)\s+errors?\b/i);
12
+ const total = (counts.length ? Math.max(...counts) : 0) + ts + (lint ?? 0) + (counts.length || ts || lint ? 0 : errs ?? 0);
13
+ if (total > 0)
14
+ return total;
15
+ return exit && exit !== "0" ? 1 : 0;
16
+ }
17
+ /** The inverse of a unified diff hunk set (swap + and -), as a comparable string. */
18
+ const invert = (diff) => diff.split("\n").filter((l) => /^[+-]/.test(l) && !/^(\+\+\+|---)/.test(l)).map((l) => (l[0] === "+" ? "-" : "+") + l.slice(1)).sort().join("\n");
19
+ const body = (diff) => diff.split("\n").filter((l) => /^[+-]/.test(l) && !/^(\+\+\+|---)/.test(l)).sort().join("\n");
20
+ export class ProgressTracker {
21
+ bestFail = Infinity;
22
+ everFailed = new Set();
23
+ editedPaths = new Set();
24
+ seenDiffs = new Map();
25
+ reads = new Set();
26
+ /** Turns in a row without an objective improvement. */
27
+ stale = 0;
28
+ lastFailures;
29
+ reasons = [];
30
+ readCap;
31
+ constructor(readCap = 15) { this.readCap = readCap; }
32
+ /** Feed one turn; returns why it counted as progress (empty = it did not). */
33
+ observe(t) {
34
+ const why = [];
35
+ for (const e of t.edits) {
36
+ const seen = this.seenDiffs.get(e.path) ?? new Set(), inv = invert(e.diff), b = body(e.diff);
37
+ if (!this.editedPaths.has(e.path)) {
38
+ this.editedPaths.add(e.path);
39
+ why.push(`first edit to ${e.path}`);
40
+ }
41
+ else if (b && !seen.has(b) && !seen.has(inv))
42
+ why.push(`new change to ${e.path}`); // a change that undoes an earlier one is not new
43
+ seen.add(b);
44
+ seen.add(inv);
45
+ this.seenDiffs.set(e.path, seen);
46
+ }
47
+ for (const c of t.calls) {
48
+ if (c.name === "sh" && typeof c.args?.cmd === "string") {
49
+ const n = countFailures(c.args.cmd, c.result);
50
+ if (n !== undefined) {
51
+ this.lastFailures = n;
52
+ if (n < this.bestFail) {
53
+ why.push(n === 0 ? "the checks pass" : this.bestFail === Infinity ? `baseline measured: ${n} failing` : `failures down to ${n}`);
54
+ this.bestFail = n;
55
+ }
56
+ else if (n === 0 && this.everFailed.has(c.args.cmd))
57
+ why.push("a failing command now passes");
58
+ if (n > 0)
59
+ this.everFailed.add(c.args.cmd);
60
+ }
61
+ }
62
+ else if ((c.name === "read_file" || c.name === "grep" || c.name === "list_files") && !/^ERROR/.test(c.result)) {
63
+ const k = c.name + ":" + (c.args?.path ?? c.args?.pattern ?? "");
64
+ if (!this.reads.has(k) && this.reads.size < this.readCap) {
65
+ this.reads.add(k);
66
+ why.push("read something new");
67
+ }
68
+ }
69
+ }
70
+ this.stale = why.length ? 0 : this.stale + 1;
71
+ this.reasons = why;
72
+ return why;
73
+ }
74
+ summary() { return `${this.editedPaths.size} file(s) edited so far; failing count ${this.lastFailures === undefined ? "unknown" : this.lastFailures} (best ${this.bestFail === Infinity ? "unknown" : this.bestFail})`; }
75
+ }
@@ -0,0 +1,10 @@
1
+ import type { Plugin } from "./plugins.ts";
2
+ export type PromptDef = {
3
+ id: string;
4
+ text: string;
5
+ file: string;
6
+ scope: "project" | "user";
7
+ };
8
+ export declare function loadPrompts(cwd: string): PromptDef[];
9
+ /** A plugin that swaps the system prompt for a registry prompt (the id `base` keeps the built-in one). */
10
+ export declare function promptPlugin(cwd: string, id: string): Plugin;
@@ -0,0 +1,28 @@
1
+ // A small prompt registry: system prompts as versioned files, so a prompt change is something you can name, diff and test.
2
+ // `.amness/prompts/<id>.md` (and `~/.amness/prompts/`) replace the whole system prompt; the id `base` is the built-in one. `amness eval --variants full,prompt:terse` compares them.
3
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { join } from "node:path";
6
+ const dirs = (cwd) => [{ dir: join(cwd, ".amness", "prompts"), scope: "project" }, { dir: join(process.env.AMNESS_HOME ?? join(homedir(), ".amness"), "prompts"), scope: "user" }];
7
+ export function loadPrompts(cwd) {
8
+ const seen = new Set(), out = [];
9
+ for (const { dir, scope } of dirs(cwd))
10
+ if (existsSync(dir))
11
+ for (const f of readdirSync(dir).filter((x) => x.endsWith(".md")).sort()) {
12
+ const id = f.replace(/\.md$/, "");
13
+ if (!/^[\w.-]{1,40}$/.test(id) || seen.has(id))
14
+ continue;
15
+ seen.add(id);
16
+ out.push({ id, text: readFileSync(join(dir, f), "utf8").replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "").trim(), file: join(dir, f), scope });
17
+ }
18
+ return out;
19
+ }
20
+ /** A plugin that swaps the system prompt for a registry prompt (the id `base` keeps the built-in one). */
21
+ export function promptPlugin(cwd, id) {
22
+ if (id === "base")
23
+ return { name: "prompt-base", description: "the built-in system prompt" };
24
+ const p = loadPrompts(cwd).find((x) => x.id === id);
25
+ if (!p)
26
+ throw new Error(`no prompt "${id}" in .amness/prompts or ~/.amness/prompts. Found: ${loadPrompts(cwd).map((x) => x.id).join(", ") || "none"}`);
27
+ return { name: `prompt-${id}`, description: `system prompt from ${p.file}`, system: () => p.text };
28
+ }