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.
- package/LICENSE +21 -0
- package/README.md +237 -2
- package/dist/cli/acpcli.d.ts +7 -0
- package/dist/cli/acpcli.js +14 -0
- package/dist/cli/amness.d.ts +2 -0
- package/dist/cli/amness.js +694 -0
- package/dist/cli/benchcli.d.ts +5 -0
- package/dist/cli/benchcli.js +160 -0
- package/dist/cli/config.d.ts +79 -0
- package/dist/cli/config.js +111 -0
- package/dist/cli/demo.d.ts +13 -0
- package/dist/cli/demo.js +44 -0
- package/dist/cli/detect.d.ts +2 -0
- package/dist/cli/detect.js +20 -0
- package/dist/cli/evalcli.d.ts +15 -0
- package/dist/cli/evalcli.js +198 -0
- package/dist/cli/format.d.ts +5 -0
- package/dist/cli/format.js +55 -0
- package/dist/cli/headless.d.ts +2 -0
- package/dist/cli/headless.js +169 -0
- package/dist/cli/keystore.d.ts +9 -0
- package/dist/cli/keystore.js +108 -0
- package/dist/cli/model.d.ts +11 -0
- package/dist/cli/model.js +19 -0
- package/dist/cli/plugincli.d.ts +2 -0
- package/dist/cli/plugincli.js +176 -0
- package/dist/cli/schedulecli.d.ts +2 -0
- package/dist/cli/schedulecli.js +66 -0
- package/dist/cli/state.d.ts +11 -0
- package/dist/cli/state.js +19 -0
- package/dist/cli/tui/ansi.d.ts +38 -0
- package/dist/cli/tui/ansi.js +142 -0
- package/dist/cli/tui/app.d.ts +328 -0
- package/dist/cli/tui/app.js +1358 -0
- package/dist/cli/tui/blocks.d.ts +151 -0
- package/dist/cli/tui/blocks.js +128 -0
- package/dist/cli/tui/brand.d.ts +9 -0
- package/dist/cli/tui/brand.js +16 -0
- package/dist/cli/tui/canvas.d.ts +92 -0
- package/dist/cli/tui/canvas.js +199 -0
- package/dist/cli/tui/cards.d.ts +44 -0
- package/dist/cli/tui/cards.js +215 -0
- package/dist/cli/tui/clipboard.d.ts +5 -0
- package/dist/cli/tui/clipboard.js +29 -0
- package/dist/cli/tui/commands.d.ts +10 -0
- package/dist/cli/tui/commands.js +650 -0
- package/dist/cli/tui/complete.d.ts +39 -0
- package/dist/cli/tui/complete.js +113 -0
- package/dist/cli/tui/custom.d.ts +9 -0
- package/dist/cli/tui/custom.js +34 -0
- package/dist/cli/tui/diffview.d.ts +25 -0
- package/dist/cli/tui/diffview.js +130 -0
- package/dist/cli/tui/editor.d.ts +60 -0
- package/dist/cli/tui/editor.js +230 -0
- package/dist/cli/tui/full.d.ts +86 -0
- package/dist/cli/tui/full.js +630 -0
- package/dist/cli/tui/keys.d.ts +31 -0
- package/dist/cli/tui/keys.js +134 -0
- package/dist/cli/tui/markdown.d.ts +37 -0
- package/dist/cli/tui/markdown.js +155 -0
- package/dist/cli/tui/mentions.d.ts +15 -0
- package/dist/cli/tui/mentions.js +48 -0
- package/dist/cli/tui/modal.d.ts +26 -0
- package/dist/cli/tui/modal.js +107 -0
- package/dist/cli/tui/overlays.d.ts +98 -0
- package/dist/cli/tui/overlays.js +237 -0
- package/dist/cli/tui/pager.d.ts +15 -0
- package/dist/cli/tui/pager.js +66 -0
- package/dist/cli/tui/pricing.d.ts +9 -0
- package/dist/cli/tui/pricing.js +23 -0
- package/dist/cli/tui/rich.d.ts +16 -0
- package/dist/cli/tui/rich.js +72 -0
- package/dist/cli/tui/screen.d.ts +30 -0
- package/dist/cli/tui/screen.js +62 -0
- package/dist/cli/tui/session.d.ts +77 -0
- package/dist/cli/tui/session.js +103 -0
- package/dist/cli/tui/sidebar.d.ts +84 -0
- package/dist/cli/tui/sidebar.js +114 -0
- package/dist/cli/tui/text.d.ts +15 -0
- package/dist/cli/tui/text.js +94 -0
- package/dist/cli/tui/theme.d.ts +59 -0
- package/dist/cli/tui/theme.js +148 -0
- package/dist/cli/tui/themes.d.ts +56 -0
- package/dist/cli/tui/themes.js +80 -0
- package/dist/cli/tui/tokens.d.ts +39 -0
- package/dist/cli/tui/tokens.js +71 -0
- package/dist/cli/tui/view.d.ts +60 -0
- package/dist/cli/tui/view.js +178 -0
- package/dist/cli/ui.d.ts +34 -0
- package/dist/cli/ui.js +224 -0
- package/dist/src/acp.d.ts +28 -0
- package/dist/src/acp.js +111 -0
- package/dist/src/agents.d.ts +23 -0
- package/dist/src/agents.js +71 -0
- package/dist/src/anthropic.d.ts +30 -0
- package/dist/src/anthropic.js +91 -0
- package/dist/src/approver.d.ts +15 -0
- package/dist/src/approver.js +40 -0
- package/dist/src/bench.d.ts +67 -0
- package/dist/src/bench.js +201 -0
- package/dist/src/bundle.d.ts +36 -0
- package/dist/src/bundle.js +55 -0
- package/dist/src/chaos.d.ts +15 -0
- package/dist/src/chaos.js +26 -0
- package/dist/src/code.d.ts +187 -0
- package/dist/src/code.js +590 -0
- package/dist/src/codeindex.d.ts +65 -0
- package/dist/src/codeindex.js +300 -0
- package/dist/src/deep.d.ts +62 -0
- package/dist/src/deep.js +177 -0
- package/dist/src/diff.d.ts +8 -0
- package/dist/src/diff.js +74 -0
- package/dist/src/dynamic.d.ts +42 -0
- package/dist/src/dynamic.js +133 -0
- package/dist/src/embed.d.ts +8 -0
- package/dist/src/embed.js +25 -0
- package/dist/src/evalcases.d.ts +11 -0
- package/dist/src/evalcases.js +196 -0
- package/dist/src/evals.d.ts +197 -0
- package/dist/src/evals.js +340 -0
- package/dist/src/gitflow.d.ts +27 -0
- package/dist/src/gitflow.js +56 -0
- package/dist/src/grade.d.ts +42 -0
- package/dist/src/grade.js +96 -0
- package/dist/src/guidance.d.ts +15 -0
- package/dist/src/guidance.js +68 -0
- package/dist/src/hardcases.d.ts +2 -0
- package/dist/src/hardcases.js +94 -0
- package/dist/src/harness.d.ts +411 -0
- package/dist/src/harness.js +725 -0
- package/dist/src/hooks.d.ts +44 -0
- package/dist/src/hooks.js +116 -0
- package/dist/src/images.d.ts +13 -0
- package/dist/src/images.js +27 -0
- package/dist/src/index.d.ts +57 -0
- package/dist/src/index.js +57 -0
- package/dist/src/ledger.d.ts +58 -0
- package/dist/src/ledger.js +78 -0
- package/dist/src/live.d.ts +16 -0
- package/dist/src/live.js +80 -0
- package/dist/src/lsp.d.ts +31 -0
- package/dist/src/lsp.js +147 -0
- package/dist/src/memory.d.ts +128 -0
- package/dist/src/memory.js +390 -0
- package/dist/src/metrics.d.ts +3 -0
- package/dist/src/metrics.js +42 -0
- package/dist/src/mining.d.ts +49 -0
- package/dist/src/mining.js +126 -0
- package/dist/src/openai.d.ts +50 -0
- package/dist/src/openai.js +307 -0
- package/dist/src/parallel.d.ts +66 -0
- package/dist/src/parallel.js +135 -0
- package/dist/src/patch.d.ts +45 -0
- package/dist/src/patch.js +219 -0
- package/dist/src/permissions.d.ts +46 -0
- package/dist/src/permissions.js +245 -0
- package/dist/src/plugins.d.ts +148 -0
- package/dist/src/plugins.js +225 -0
- package/dist/src/pluginscan.d.ts +8 -0
- package/dist/src/pluginscan.js +20 -0
- package/dist/src/procs.d.ts +41 -0
- package/dist/src/procs.js +94 -0
- package/dist/src/profiles.d.ts +36 -0
- package/dist/src/profiles.js +61 -0
- package/dist/src/progress.d.ts +29 -0
- package/dist/src/progress.js +75 -0
- package/dist/src/prompts.d.ts +10 -0
- package/dist/src/prompts.js +28 -0
- package/dist/src/ratelimit.d.ts +34 -0
- package/dist/src/ratelimit.js +116 -0
- package/dist/src/replay.d.ts +25 -0
- package/dist/src/replay.js +99 -0
- package/dist/src/review.d.ts +20 -0
- package/dist/src/review.js +49 -0
- package/dist/src/route.d.ts +9 -0
- package/dist/src/route.js +30 -0
- package/dist/src/router.d.ts +61 -0
- package/dist/src/router.js +126 -0
- package/dist/src/sandbox.d.ts +13 -0
- package/dist/src/sandbox.js +68 -0
- package/dist/src/schedule.d.ts +20 -0
- package/dist/src/schedule.js +59 -0
- package/dist/src/schema.d.ts +13 -0
- package/dist/src/schema.js +36 -0
- package/dist/src/secrets.d.ts +10 -0
- package/dist/src/secrets.js +41 -0
- package/dist/src/shell.d.ts +34 -0
- package/dist/src/shell.js +95 -0
- package/dist/src/skills.d.ts +20 -0
- package/dist/src/skills.js +112 -0
- package/dist/src/snapshot.d.ts +20 -0
- package/dist/src/snapshot.js +93 -0
- package/dist/src/stats.d.ts +24 -0
- package/dist/src/stats.js +62 -0
- package/dist/src/strategies.d.ts +16 -0
- package/dist/src/strategies.js +35 -0
- package/dist/src/suites.d.ts +30 -0
- package/dist/src/suites.js +112 -0
- package/dist/src/trace.d.ts +63 -0
- package/dist/src/trace.js +153 -0
- package/dist/src/tune.d.ts +38 -0
- package/dist/src/tune.js +49 -0
- package/dist/src/web.d.ts +19 -0
- package/dist/src/web.js +56 -0
- package/dist/src/worktree.d.ts +24 -0
- package/dist/src/worktree.js +85 -0
- package/docs/agent-loop.md +222 -0
- package/docs/architecture.md +228 -0
- package/docs/bench.md +292 -0
- package/docs/budgets.md +185 -0
- package/docs/build-your-own-harness.md +424 -0
- package/docs/cookbook.md +375 -0
- package/docs/development.md +150 -0
- package/docs/evaluation.md +450 -0
- package/docs/features.md +1320 -0
- package/docs/getting-started.md +205 -0
- package/docs/headless.md +188 -0
- package/docs/images/approval.png +0 -0
- package/docs/images/chat.png +0 -0
- package/docs/images/daylight.png +0 -0
- package/docs/images/loop.png +0 -0
- package/docs/images/palette.png +0 -0
- package/docs/images/running.png +0 -0
- package/docs/images/subagent-done.png +0 -0
- package/docs/images/subagent-running.png +0 -0
- package/docs/images/welcome.png +0 -0
- package/docs/index.md +25 -0
- package/docs/library.md +267 -0
- package/docs/plugins.md +691 -0
- package/docs/providers.md +175 -0
- package/docs/safety.md +250 -0
- package/docs/usage.md +348 -0
- package/docs/why.md +155 -0
- package/docs/writing-guide.md +39 -0
- package/package.json +66 -4
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFile, execFileSync, spawnSync } from "node:child_process";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
7
|
+
import { createSessionWorktree, isRepo, removeWorktree, worktreeIsUntouched, caseFromWorkspace, formatMining, formatReplay, replayFile, Budget, MemoryStore, effortsFor, codeVariants, createCodeAgent, createDeepAgent, evaluate, loadMcp, readBundle, startLive } from "../src/index.js";
|
|
8
|
+
import { createModel } from "./model.js";
|
|
9
|
+
import { detectVerify } from "./detect.js";
|
|
10
|
+
import { runHeadless } from "./headless.js";
|
|
11
|
+
import { App } from "./tui/app.js";
|
|
12
|
+
import { builtinCases as cases } from "../src/evalcases.js";
|
|
13
|
+
import { runBench } from "./benchcli.js";
|
|
14
|
+
import { runPlugin } from "./plugincli.js";
|
|
15
|
+
import { runSchedule } from "./schedulecli.js";
|
|
16
|
+
import { ACP_HELP, runAcp } from "./acpcli.js";
|
|
17
|
+
import { runEval, runTune } from "./evalcli.js";
|
|
18
|
+
import { CLAUDE_MODELS, OPENAI_MODELS, PROVIDERS, home, loadConfig, mask, ready, saveConfig } from "./config.js";
|
|
19
|
+
import { where } from "./keystore.js";
|
|
20
|
+
import { DEMO_TASK, explain, runDemo } from "./demo.js";
|
|
21
|
+
import { formatEvent } from "./format.js";
|
|
22
|
+
import { Cancel, ask, bold, box, clearScreen, confirm, cyan, dim, gray, green, log, pause, red, select, spin, yellow } from "./ui.js";
|
|
23
|
+
import { loadState, rememberTask, saveState } from "./state.js";
|
|
24
|
+
const cfg = loadConfig();
|
|
25
|
+
const fail = async (m) => { console.log(red("✗ " + m)); await pause(); };
|
|
26
|
+
const secs = (ms) => (ms / 1000).toFixed(1) + "s";
|
|
27
|
+
function banner() {
|
|
28
|
+
const p = PROVIDERS[cfg.provider];
|
|
29
|
+
const key = !p.needsKey ? gray("no key needed") : cfg.apiKey ? green("key set") : red("no API key");
|
|
30
|
+
console.log("\n" + box([
|
|
31
|
+
bold("Amness") + dim(" · reliable agents for GPT-style models"),
|
|
32
|
+
`${dim("model")} ${cyan(cfg.model)} ${dim("via")} ${p.label} ${dim("·")} ${key} ${dim("·")} ${dim("budget")} ${cfg.maxTokens.toLocaleString()} tok`,
|
|
33
|
+
]));
|
|
34
|
+
}
|
|
35
|
+
/** Live status shown next to the spinner while the model works. */
|
|
36
|
+
const status = { t0: Date.now(), step: 0, tokens: 0 };
|
|
37
|
+
const fmtTok = (n) => (n >= 1000 ? (n / 1000).toFixed(1) + "k" : String(n));
|
|
38
|
+
const statusLine = () => `working… ${Math.round((Date.now() - status.t0) / 1000)}s · turn ${status.step + 1} · ${fmtTok(status.tokens)} tokens · ctrl-c stops safely`;
|
|
39
|
+
/** Wrap a model so a live spinner shows while it thinks (and the abort signal still reaches the adapter). */
|
|
40
|
+
const withSpinner = (m) => async (msgs, tools, o) => { const stop = spin(statusLine); try {
|
|
41
|
+
return await m(msgs, tools, o);
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
stop();
|
|
45
|
+
} };
|
|
46
|
+
const rawModel = (retries) => createModel(cfg, retries);
|
|
47
|
+
// keep appendOnly (Claude) visible through the spinner wrapper
|
|
48
|
+
const makeModel = () => { const m = rawModel(), w = withSpinner(m); w.appendOnly = m.appendOnly; return w; };
|
|
49
|
+
/** Human-in-the-loop prompt for risky tool calls. */
|
|
50
|
+
const approve = async ({ name, args, reason }) => {
|
|
51
|
+
log();
|
|
52
|
+
log(box([yellow(bold("Approval needed")), `${dim("tool ")} ${name}`, `${dim("why ")} ${reason ?? "marked risky"}`,
|
|
53
|
+
`${dim("args ")} ${JSON.stringify(args).slice(0, 300)}`], yellow));
|
|
54
|
+
const a = await select("Allow this?", [
|
|
55
|
+
{ label: "Allow once", value: "y" }, { label: "Deny", value: "n" }, { label: "Deny and tell the agent what to do instead", value: "note" },
|
|
56
|
+
], 1);
|
|
57
|
+
if (a === "y")
|
|
58
|
+
return true;
|
|
59
|
+
if (a === "note") {
|
|
60
|
+
try {
|
|
61
|
+
return (await ask("Instruction for the agent")) || false;
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
if (e instanceof Cancel)
|
|
65
|
+
return false;
|
|
66
|
+
throw e;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
};
|
|
71
|
+
const onEvent = (e) => { if (e.kind === "turn") {
|
|
72
|
+
status.step = e.step;
|
|
73
|
+
status.tokens = e.tokens;
|
|
74
|
+
} const l = formatEvent(e); if (l)
|
|
75
|
+
log(l.split("\n").map((x, i) => (i ? x : " " + x)).join("\n")); };
|
|
76
|
+
async function ensureReady() {
|
|
77
|
+
if (ready(cfg))
|
|
78
|
+
return true;
|
|
79
|
+
console.log("\n" + box([bold("Let's get you set up"), dim("Pick a provider, paste a key, and we'll test it. Esc cancels; the guided demo works without a key.")], cyan));
|
|
80
|
+
const id = await select("Which provider?", [
|
|
81
|
+
{ label: "Anthropic (Claude)", value: "anthropic", hint: "recommended · key from console.anthropic.com" },
|
|
82
|
+
{ label: "OpenAI", value: "openai", hint: "key from platform.openai.com" },
|
|
83
|
+
{ label: "Ollama (local)", value: "ollama", hint: "no key · tool-calling quality varies by model" },
|
|
84
|
+
{ label: "Other OpenAI-compatible", value: "custom", hint: "vLLM, LM Studio, OpenRouter…" },
|
|
85
|
+
]);
|
|
86
|
+
if (!id)
|
|
87
|
+
return false;
|
|
88
|
+
Object.assign(cfg, { provider: id, baseUrl: PROVIDERS[id].baseUrl, model: PROVIDERS[id].model });
|
|
89
|
+
if (id === "custom" || id === "ollama") {
|
|
90
|
+
cfg.baseUrl = (await ask("Base URL", cfg.baseUrl)) || cfg.baseUrl;
|
|
91
|
+
cfg.model = (await ask("Model", cfg.model)) || cfg.model;
|
|
92
|
+
}
|
|
93
|
+
if (PROVIDERS[id].needsKey)
|
|
94
|
+
cfg.apiKey = (await ask(`API key (hidden; ${where()})`, "", { secret: true })) || undefined;
|
|
95
|
+
saveConfig(cfg);
|
|
96
|
+
if (!ready(cfg))
|
|
97
|
+
return false;
|
|
98
|
+
return (await testConnection(true)) || (await confirm("The test failed. Keep going anyway?", false));
|
|
99
|
+
}
|
|
100
|
+
function summaryBox(title, ok, lines) {
|
|
101
|
+
console.log("\n" + box(lines, ok ? green : red, ok ? green(title) : red(title)));
|
|
102
|
+
}
|
|
103
|
+
const gitStat = (dir) => new Promise((res) => execFile("git", ["-C", dir, "diff", "--stat"], (e, out) => res(e ? "" : out.trim())));
|
|
104
|
+
// ------------------------------------------------------------------ screens
|
|
105
|
+
const tryOpen = (url) => execFile(process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open", [url], () => { });
|
|
106
|
+
const checkpointOf = (dir) => join(dir, ".amness/checkpoint.json");
|
|
107
|
+
const untilde = (p) => resolve(p.replace(/^~/, homedir()));
|
|
108
|
+
const oneLine = (s, n = 70) => { const l = s.split("\n")[0]; return l.length > n || s.includes("\n") ? l.slice(0, n - 1) + "…" : l; };
|
|
109
|
+
/** Defaults for a directory: remembered verify command → auto-detected one; and whether an interrupted run is waiting. */
|
|
110
|
+
function applyDir(job, dir) {
|
|
111
|
+
job.dir = dir;
|
|
112
|
+
const st = loadState();
|
|
113
|
+
job.verify = (st.verifyByDir[dir] ?? detectVerify(dir)) || undefined;
|
|
114
|
+
job.canResume = existsSync(checkpointOf(dir));
|
|
115
|
+
job.resume = job.canResume;
|
|
116
|
+
}
|
|
117
|
+
/** One summary card instead of a chain of questions: everything has a sensible default, change only what you care about. */
|
|
118
|
+
async function plan(job) {
|
|
119
|
+
for (;;) {
|
|
120
|
+
console.log("\n" + box([
|
|
121
|
+
`${dim("task ")} ${oneLine(job.task || "(continue the bundle's task)")}`,
|
|
122
|
+
`${dim("directory ")} ${job.dir}`,
|
|
123
|
+
`${dim("verify ")} ${job.verify ?? dim("none: the agent may finish without proof")}`,
|
|
124
|
+
`${dim("live view ")} ${job.live ? green("on") : "off"} ${dim("own tools")} ${job.selfExtend ? "on" : "off"} ${dim("mcp")} ${job.mcpCmd ?? "none"}`,
|
|
125
|
+
...(job.bundle ? [`${dim("bundle ")} ${basename(job.bundle)}`] : []),
|
|
126
|
+
...(job.canResume ? [`${dim("interrupted")} ${job.resume ? green("will continue where it stopped") + dim(" (your task is added as a follow-up)") : "ignored: starting fresh"}`] : []),
|
|
127
|
+
], cyan, "Ready to run"));
|
|
128
|
+
const act = await select("Go?", [
|
|
129
|
+
{ label: "▶ Start", value: "go" },
|
|
130
|
+
{ label: "Change directory", value: "dir", hint: job.dir },
|
|
131
|
+
{ label: "Change verify command", value: "verify", hint: job.verify ?? "none" },
|
|
132
|
+
{ label: `Live view in browser: ${job.live ? "on → off" : "off → on"}`, value: "live" },
|
|
133
|
+
{ label: `Let the agent define its own tools: ${job.selfExtend ? "on → off" : "off → on"}`, value: "self", hint: "each needs approval" },
|
|
134
|
+
{ label: "Attach an MCP server…", value: "mcp" },
|
|
135
|
+
...(job.canResume ? [{ label: job.resume ? "Start fresh instead of resuming" : "Resume the interrupted run", value: "resume" }] : []),
|
|
136
|
+
{ label: "Cancel", value: "cancel" },
|
|
137
|
+
]);
|
|
138
|
+
if (!act || act === "cancel")
|
|
139
|
+
return false;
|
|
140
|
+
if (act === "go")
|
|
141
|
+
return true;
|
|
142
|
+
if (act === "live")
|
|
143
|
+
job.live = !job.live;
|
|
144
|
+
else if (act === "self")
|
|
145
|
+
job.selfExtend = !job.selfExtend;
|
|
146
|
+
else if (act === "resume")
|
|
147
|
+
job.resume = !job.resume;
|
|
148
|
+
else if (act === "verify")
|
|
149
|
+
job.verify = (await ask("Verify command (blank = none)", job.verify ?? "")) || undefined;
|
|
150
|
+
else if (act === "mcp")
|
|
151
|
+
job.mcpCmd = (await ask("MCP server command (blank = none)", job.mcpCmd ?? "")) || undefined;
|
|
152
|
+
else if (act === "dir") {
|
|
153
|
+
const d = untilde(await ask("Directory", job.dir));
|
|
154
|
+
if (existsSync(d))
|
|
155
|
+
applyDir(job, d);
|
|
156
|
+
else
|
|
157
|
+
console.log(red(`✗ ${d} does not exist`));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const pager = (text) => {
|
|
162
|
+
if (process.stdout.isTTY && spawnSync("less", ["-V"], { stdio: "ignore" }).status === 0)
|
|
163
|
+
spawnSync("less", ["-R"], { input: text, stdio: ["pipe", "inherit", "inherit"] });
|
|
164
|
+
else
|
|
165
|
+
console.log(text);
|
|
166
|
+
};
|
|
167
|
+
const colorDiff = (e) => [bold(`${e.path}${e.created ? " (new)" : ""} ${green("+" + e.added)} ${red("-" + e.removed)}`),
|
|
168
|
+
...e.diff.split("\n").filter((l) => !/^(---|\+\+\+)/.test(l)).map((l) => (l[0] === "+" ? green(l) : l[0] === "-" ? red(l) : l[0] === "@" ? cyan(l) : gray(l))), ""].join("\n");
|
|
169
|
+
async function runCode(job) {
|
|
170
|
+
if (!(await plan(job)))
|
|
171
|
+
return;
|
|
172
|
+
let st = rememberTask(loadState(), job.task);
|
|
173
|
+
st = { ...st, lastDir: job.dir, verifyByDir: { ...st.verifyByDir, ...(job.verify ? { [job.dir]: job.verify } : {}) } };
|
|
174
|
+
saveState(st);
|
|
175
|
+
const live = job.live ? await startLive({ title: basename(job.dir) }) : undefined;
|
|
176
|
+
if (live) {
|
|
177
|
+
console.log(`${cyan("●")} live view: ${bold(live.url)}`);
|
|
178
|
+
tryOpen(live.url);
|
|
179
|
+
}
|
|
180
|
+
const mcp = job.mcpCmd ? await loadMcp(job.mcpCmd.split(" ")[0], job.mcpCmd.split(" ").slice(1)).catch((e) => (console.log(red("✗ MCP: " + e.message)), undefined)) : undefined;
|
|
181
|
+
const budget = new Budget({ maxTokens: cfg.maxTokens, maxMs: 30 * 60_000 });
|
|
182
|
+
const edits = [];
|
|
183
|
+
let ac = new AbortController();
|
|
184
|
+
const cp = checkpointOf(job.dir);
|
|
185
|
+
const agent = createCodeAgent({
|
|
186
|
+
model: makeModel(), cwd: job.dir, verify: job.verify, budget, approve, selfExtend: job.selfExtend, tools: mcp?.tools, signal: () => ac.signal,
|
|
187
|
+
checkpointFile: cp, meta: { provider: cfg.provider, model: cfg.model },
|
|
188
|
+
onEvent: (e) => { onEvent(e); live?.onEvent(e); if (e.kind === "edit")
|
|
189
|
+
edits.push(e); },
|
|
190
|
+
});
|
|
191
|
+
/** Run one agent call with Ctrl-C = stop at the next safe point (progress is saved), twice = quit now. */
|
|
192
|
+
const execute = async (go) => {
|
|
193
|
+
ac = new AbortController();
|
|
194
|
+
status.t0 = Date.now();
|
|
195
|
+
status.step = 0;
|
|
196
|
+
status.tokens = 0;
|
|
197
|
+
let hits = 0;
|
|
198
|
+
const onSigint = () => { if (++hits === 1) {
|
|
199
|
+
log(yellow("\n⏹ stopping after the current step… (Ctrl-C again quits immediately)"));
|
|
200
|
+
ac.abort();
|
|
201
|
+
}
|
|
202
|
+
else
|
|
203
|
+
process.exit(130); };
|
|
204
|
+
process.on("SIGINT", onSigint);
|
|
205
|
+
try {
|
|
206
|
+
const r = await go();
|
|
207
|
+
const stat = await gitStat(job.dir);
|
|
208
|
+
const head = r.finished ? "Done" : r.stopped === "aborted" ? "Interrupted · progress saved" : "Stopped";
|
|
209
|
+
summaryBox(head, r.finished, [r.text, "", dim(`${r.steps} steps · ${budget.tokens.toLocaleString()} tokens · ${secs(Date.now() - status.t0)} this round`), ...(stat ? ["", stat] : [])]);
|
|
210
|
+
if (r.finished)
|
|
211
|
+
rmSync(cp, { force: true }); // a finished run is not "interrupted"
|
|
212
|
+
return r;
|
|
213
|
+
}
|
|
214
|
+
catch (e) {
|
|
215
|
+
summaryBox("Error", false, [e.message, "", dim("Progress was checkpointed. Run again and choose to resume.")]);
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
process.off("SIGINT", onSigint);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
console.log(`\n${dim("Working in")} ${job.dir}${job.verify ? dim(" · gate: ") + job.verify : ""}\n`);
|
|
223
|
+
let r = await execute(() => agent.run(job.task, { resume: job.resume, bundle: job.bundle }));
|
|
224
|
+
try {
|
|
225
|
+
while (r) {
|
|
226
|
+
const act = await select("What next?", [
|
|
227
|
+
{ label: "Follow up", value: "follow", hint: "keep going in the same conversation" },
|
|
228
|
+
...(edits.length ? [{ label: `Review changes (${edits.length} edit${edits.length > 1 ? "s" : ""})`, value: "diff", hint: [...new Set(edits.map((e) => e.path))].slice(0, 3).join(", ") }] : []),
|
|
229
|
+
{ label: "Save portable session bundle", value: "bundle", hint: "continue elsewhere, on any model" },
|
|
230
|
+
{ label: "Done", value: "done" },
|
|
231
|
+
], 0);
|
|
232
|
+
if (!act || act === "done")
|
|
233
|
+
break;
|
|
234
|
+
if (act === "diff")
|
|
235
|
+
pager(edits.map(colorDiff).join("\n"));
|
|
236
|
+
else if (act === "follow") {
|
|
237
|
+
const text = await ask("Follow-up (paste freely; enter to send)", "", { multiline: true, history: loadState().tasks });
|
|
238
|
+
if (!text)
|
|
239
|
+
continue;
|
|
240
|
+
saveState(rememberTask(loadState(), text));
|
|
241
|
+
const prev = r;
|
|
242
|
+
r = (await execute(() => agent.followUp(prev, text))) ?? r;
|
|
243
|
+
}
|
|
244
|
+
else if (act === "bundle") {
|
|
245
|
+
const stop = spin("writing handoff…");
|
|
246
|
+
try {
|
|
247
|
+
const file = join(job.dir, ".amness/sessions", `${new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19)}.json`);
|
|
248
|
+
const { bundle } = await agent.exportBundle(r, file);
|
|
249
|
+
stop();
|
|
250
|
+
console.log(`${green("✓")} ${file}\n ${dim(`${Object.keys(bundle.files).length} file(s), ${bundle.memory.length} memory note(s). Resume anywhere: amness run --bundle <file> "continue"`)}`);
|
|
251
|
+
}
|
|
252
|
+
catch (e) {
|
|
253
|
+
stop();
|
|
254
|
+
console.log(red("✗ " + e.message));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
finally {
|
|
260
|
+
mcp?.close();
|
|
261
|
+
await live?.close();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function newJob(dir, task, bundle) {
|
|
265
|
+
const job = { dir, task, bundle, selfExtend: false, live: false, resume: false, canResume: false };
|
|
266
|
+
applyDir(job, dir);
|
|
267
|
+
return job;
|
|
268
|
+
}
|
|
269
|
+
async function codeScreen() {
|
|
270
|
+
if (!(await ensureReady()))
|
|
271
|
+
return;
|
|
272
|
+
const st = loadState();
|
|
273
|
+
const dir = existsSync(st.lastDir ?? "") ? st.lastDir : process.cwd();
|
|
274
|
+
console.log(dim(`\nProject: ${dir} (change it on the next screen)`));
|
|
275
|
+
const task = await ask("What should the agent do? (paste freely; enter to continue; esc cancels)", "", { multiline: true, history: st.tasks });
|
|
276
|
+
if (!task)
|
|
277
|
+
return;
|
|
278
|
+
await runCode(newJob(dir, task));
|
|
279
|
+
}
|
|
280
|
+
async function importScreen() {
|
|
281
|
+
if (!(await ensureReady()))
|
|
282
|
+
return;
|
|
283
|
+
const file = untilde(await ask("Session bundle file (.json)"));
|
|
284
|
+
let b;
|
|
285
|
+
try {
|
|
286
|
+
b = readBundle(file);
|
|
287
|
+
}
|
|
288
|
+
catch (e) {
|
|
289
|
+
return fail(e.message);
|
|
290
|
+
}
|
|
291
|
+
console.log("\n" + box([`${bold("Task")} ${oneLine(b.task)}`, `${dim("saved")} ${b.createdAt} ${dim("from")} ${b.from.provider ?? "?"}/${b.from.model ?? "?"}`,
|
|
292
|
+
`${dim("state")} ${b.status.finished ? "finished" : "unfinished"} · ${Object.keys(b.files).length} file(s) · ${b.memory.length} memory note(s)`], cyan, "Session bundle"));
|
|
293
|
+
const dir = untilde(await ask("Restore into directory", loadState().lastDir ?? process.cwd()));
|
|
294
|
+
if (!existsSync(dir))
|
|
295
|
+
return fail(`${dir} does not exist`);
|
|
296
|
+
const task = await ask("What should happen next? (blank = continue the original task)", "", { multiline: true });
|
|
297
|
+
await runCode(newJob(dir, task, file));
|
|
298
|
+
}
|
|
299
|
+
async function memoryScreen() {
|
|
300
|
+
const dir = untilde(await ask("Project directory (its .amness/memory is shown alongside your global memory)", loadState().lastDir ?? process.cwd()));
|
|
301
|
+
const store = MemoryStore.forWorkspace(dir, join(home(), "memory"));
|
|
302
|
+
for (;;) {
|
|
303
|
+
const entries = store.list();
|
|
304
|
+
const pick = await select(`Memory · ${entries.length} note(s)`, [
|
|
305
|
+
...entries.map((e) => ({ label: e.name, value: e.scope + "/" + e.name, hint: `${e.scope} · ${e.type} · ${e.description}` })),
|
|
306
|
+
{ label: "+ Add a note", value: "+add" }, { label: "Back", value: "+back" },
|
|
307
|
+
], entries.length);
|
|
308
|
+
if (!pick || pick === "+back")
|
|
309
|
+
return;
|
|
310
|
+
try {
|
|
311
|
+
if (pick === "+add") {
|
|
312
|
+
const name = await ask("Name (short slug)");
|
|
313
|
+
if (!name)
|
|
314
|
+
continue;
|
|
315
|
+
const type = await select("Type", [{ label: "project", value: "project", hint: "conventions, commands, decisions" }, { label: "user", value: "user", hint: "your preferences" },
|
|
316
|
+
{ label: "feedback", value: "feedback", hint: "corrections to the agent" }, { label: "reference", value: "reference", hint: "pointers to docs/dashboards" }]);
|
|
317
|
+
const scope = await select("Applies to", [{ label: "this project", value: "project" }, { label: "everywhere (global)", value: "global" }]);
|
|
318
|
+
const description = await ask("One line: when is this useful?"), content = await ask("Note", "", { multiline: true });
|
|
319
|
+
try {
|
|
320
|
+
store.save({ name, type: type ?? "project", scope: scope ?? "project", description, content });
|
|
321
|
+
console.log(green("✓ saved"));
|
|
322
|
+
}
|
|
323
|
+
catch (e) {
|
|
324
|
+
console.log(red("✗ " + e.message));
|
|
325
|
+
}
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
const e = entries.find((x) => x.scope + "/" + x.name === pick);
|
|
329
|
+
console.log("\n" + box([...e.content.split("\n")], cyan, `${e.name} · ${e.scope}/${e.type}`));
|
|
330
|
+
if (await confirm("Delete this note?", false)) {
|
|
331
|
+
store.forget(e.name, e.scope);
|
|
332
|
+
console.log(green("✓ forgotten"));
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
catch (err) {
|
|
336
|
+
if (!(err instanceof Cancel))
|
|
337
|
+
throw err;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
async function researchScreen() {
|
|
342
|
+
if (!(await ensureReady()))
|
|
343
|
+
return;
|
|
344
|
+
const goal = await ask("Research question / goal");
|
|
345
|
+
if (!goal)
|
|
346
|
+
return;
|
|
347
|
+
const slug = goal.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 40).replace(/^-|-$/g, "") || "research";
|
|
348
|
+
const dir = join(home(), "research", slug);
|
|
349
|
+
mkdirSync(dir, { recursive: true });
|
|
350
|
+
const resume = existsSync(join(dir, ".checkpoint.json")) && (await confirm("Found an earlier run on this topic. Resume it?", true));
|
|
351
|
+
if (cfg.tavilyKey)
|
|
352
|
+
process.env.TAVILY_API_KEY = cfg.tavilyKey;
|
|
353
|
+
console.log(`\n${dim("Search:")} ${cfg.tavilyKey ? "Tavily" : "DuckDuckGo (keyless, fragile)"} ${dim("· files:")} ${dir}\n`);
|
|
354
|
+
const budget = new Budget({ maxTokens: cfg.maxTokens, maxMs: 30 * 60_000 });
|
|
355
|
+
const t0 = Date.now();
|
|
356
|
+
try {
|
|
357
|
+
const r = await createDeepAgent({ model: makeModel(), dir, budget, approve, onEvent }).run(goal, { resume });
|
|
358
|
+
summaryBox(r.finished ? "Report ready" : "Stopped", r.finished, [dim(`${r.steps} lead steps · ${budget.tokens.toLocaleString()} tokens · ${secs(Date.now() - t0)}`), dim(`saved: ${join(dir, "report.md")}`)]);
|
|
359
|
+
if (r.report)
|
|
360
|
+
console.log("\n" + r.report.split("\n").slice(0, 60).join("\n") + (r.report.split("\n").length > 60 ? dim("\n… (see file for the rest)") : ""));
|
|
361
|
+
}
|
|
362
|
+
catch (e) {
|
|
363
|
+
summaryBox("Error", false, [e.message]);
|
|
364
|
+
}
|
|
365
|
+
await pause();
|
|
366
|
+
}
|
|
367
|
+
async function evalScreen() {
|
|
368
|
+
if (!(await ensureReady()))
|
|
369
|
+
return;
|
|
370
|
+
const trials = await select("Trials per case × variant", [
|
|
371
|
+
{ label: "1", value: 1, hint: "quick smoke test" }, { label: "3", value: 3, hint: "recommended" }, { label: "5", value: 5, hint: "steadier numbers" },
|
|
372
|
+
], 1);
|
|
373
|
+
if (!trials)
|
|
374
|
+
return;
|
|
375
|
+
const n = cases.length * 2 * trials;
|
|
376
|
+
if (!(await confirm(`This runs ${n} agent sessions (up to ${cfg.maxTokens.toLocaleString()} tokens each). Continue?`, false)))
|
|
377
|
+
return;
|
|
378
|
+
const { table } = await evaluate(cases, codeVariants(makeModel(), { maxTokens: cfg.maxTokens }), {
|
|
379
|
+
trials, onRow: (r) => log(` ${r.pass ? green("PASS") : red("FAIL")} ${r.case.padEnd(22)} ${r.variant.padEnd(14)} ${dim(`${r.steps} steps · ${r.tokens} tok · ${secs(r.ms)}${r.note ? " · " + r.note : ""}`)}`),
|
|
380
|
+
});
|
|
381
|
+
console.log("\n" + box(table.split("\n"), cyan, bold("baseline vs full harness")));
|
|
382
|
+
await pause();
|
|
383
|
+
}
|
|
384
|
+
async function demoScreen() {
|
|
385
|
+
console.log(`\n${bold("Guided demo")} ${dim("— a scripted model makes the mistakes real models make. Watch the harness catch each one.")}`);
|
|
386
|
+
console.log(`${dim("Task:")} ${DEMO_TASK}\n`);
|
|
387
|
+
if (!(await confirm("Start?", true)))
|
|
388
|
+
return;
|
|
389
|
+
const { result, events, dir } = await runDemo({ approve, onEvent, model: withSpinner });
|
|
390
|
+
const seen = new Map();
|
|
391
|
+
events.forEach((e) => seen.set(e.kind, (seen.get(e.kind) ?? 0) + 1));
|
|
392
|
+
const lines = ["nudge", "repair", "denied", "rejected"].filter((k) => seen.has(k)).map((k) => `${green("✓")} ${k.padEnd(9)} ${dim(explain[k])}`);
|
|
393
|
+
summaryBox(result.finished ? "Demo complete" : "Demo stopped", result.finished, [result.text, "", bold("Guards that fired:"), ...lines, "", dim(`workspace: ${dir}`)]);
|
|
394
|
+
await pause();
|
|
395
|
+
}
|
|
396
|
+
async function testConnection(quiet = false) {
|
|
397
|
+
const t0 = Date.now(), stop = spin("contacting model…");
|
|
398
|
+
let ok = false;
|
|
399
|
+
try {
|
|
400
|
+
const m = await rawModel(1)([{ role: "user", content: "Reply with exactly: OK" }], []);
|
|
401
|
+
stop();
|
|
402
|
+
ok = true;
|
|
403
|
+
console.log(`${green("✓")} ${cfg.model} replied ${cyan(JSON.stringify((m.content ?? "").slice(0, 40)))} in ${secs(Date.now() - t0)}${m.usage?.total_tokens ? dim(` · ${m.usage.total_tokens} tokens`) : ""}`);
|
|
404
|
+
if (!quiet)
|
|
405
|
+
console.log(dim(" (tool-calling support is not verified by this ping; local models vary — try the guided demo or a small task)"));
|
|
406
|
+
}
|
|
407
|
+
catch (e) {
|
|
408
|
+
stop();
|
|
409
|
+
console.log(`${red("✗")} ${e.message.slice(0, 300)}`);
|
|
410
|
+
}
|
|
411
|
+
if (!quiet)
|
|
412
|
+
await pause();
|
|
413
|
+
return ok;
|
|
414
|
+
}
|
|
415
|
+
async function settings() {
|
|
416
|
+
for (;;) {
|
|
417
|
+
const p = PROVIDERS[cfg.provider];
|
|
418
|
+
const what = await select("Settings", [
|
|
419
|
+
{ label: "Provider", value: "provider", hint: p.label },
|
|
420
|
+
{ label: "Model", value: "model", hint: cfg.model },
|
|
421
|
+
{ label: "Base URL", value: "url", hint: cfg.baseUrl },
|
|
422
|
+
{ label: "API key", value: "key", hint: mask(cfg.apiKey) },
|
|
423
|
+
{ label: "Tavily key (better web search)", value: "tavily", hint: mask(cfg.tavilyKey) },
|
|
424
|
+
...(cfg.provider === "anthropic" || effortsFor(cfg.model).length ? [{ label: "Reasoning effort", value: "effort", hint: cfg.effort ?? "model default" }] : []),
|
|
425
|
+
{ label: "Token budget per run", value: "budget", hint: cfg.maxTokens.toLocaleString() },
|
|
426
|
+
{ label: "Test connection", value: "test" },
|
|
427
|
+
{ label: "Back", value: "back" },
|
|
428
|
+
]);
|
|
429
|
+
if (!what || what === "back")
|
|
430
|
+
return;
|
|
431
|
+
try {
|
|
432
|
+
if (what === "provider") {
|
|
433
|
+
const id = await select("Provider", Object.keys(PROVIDERS).map((k) => ({ label: PROVIDERS[k].label, value: k, hint: PROVIDERS[k].baseUrl })));
|
|
434
|
+
if (id)
|
|
435
|
+
Object.assign(cfg, { provider: id, baseUrl: PROVIDERS[id].baseUrl, model: PROVIDERS[id].model });
|
|
436
|
+
}
|
|
437
|
+
else if (what === "model") {
|
|
438
|
+
if (cfg.provider === "anthropic" || cfg.provider === "openai") {
|
|
439
|
+
const pick = await select(cfg.provider === "anthropic" ? "Claude model" : "OpenAI model", [...(cfg.provider === "anthropic" ? CLAUDE_MODELS : OPENAI_MODELS).map((m) => ({ label: m.id, value: m.id, hint: m.hint })), { label: "Other…", value: "" }]);
|
|
440
|
+
cfg.model = pick === undefined ? cfg.model : pick || (await ask("Model id", cfg.model)) || cfg.model;
|
|
441
|
+
if (cfg.provider === "openai" && cfg.effort && !effortsFor(cfg.model).includes(cfg.effort))
|
|
442
|
+
cfg.effort = undefined;
|
|
443
|
+
}
|
|
444
|
+
else
|
|
445
|
+
cfg.model = (await ask("Model", cfg.model)) || cfg.model;
|
|
446
|
+
}
|
|
447
|
+
else if (what === "effort") {
|
|
448
|
+
const levels = cfg.provider === "anthropic" ? ["low", "medium", "high", "xhigh", "max"] : effortsFor(cfg.model);
|
|
449
|
+
const pick = await select("Reasoning effort (higher = better on hard tasks, more tokens)", [{ label: "model default", value: "" }, ...levels.map((e) => ({ label: e, value: e }))]);
|
|
450
|
+
if (pick !== undefined)
|
|
451
|
+
cfg.effort = (pick || undefined);
|
|
452
|
+
}
|
|
453
|
+
else if (what === "url")
|
|
454
|
+
cfg.baseUrl = (await ask("Base URL", cfg.baseUrl)) || cfg.baseUrl;
|
|
455
|
+
else if (what === "key")
|
|
456
|
+
cfg.apiKey = (await ask(`API key (hidden; ${where()})`, cfg.apiKey ?? "", { secret: true })) || undefined;
|
|
457
|
+
else if (what === "tavily")
|
|
458
|
+
cfg.tavilyKey = (await ask("Tavily API key (blank to clear)", "", { secret: true })) || undefined;
|
|
459
|
+
else if (what === "budget")
|
|
460
|
+
cfg.maxTokens = Math.max(1000, Number(await ask("Max tokens per run", String(cfg.maxTokens))) || cfg.maxTokens);
|
|
461
|
+
else if (what === "test") {
|
|
462
|
+
saveConfig(cfg);
|
|
463
|
+
await testConnection();
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
catch (e) {
|
|
468
|
+
if (!(e instanceof Cancel))
|
|
469
|
+
throw e;
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
saveConfig(cfg);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
// ------------------------------------------------------------------ chat TUI
|
|
476
|
+
const VERSION = (() => { for (const rel of ["../package.json", "../../package.json"]) {
|
|
477
|
+
try {
|
|
478
|
+
return JSON.parse(readFileSync(new URL(rel, import.meta.url), "utf8")).version;
|
|
479
|
+
}
|
|
480
|
+
catch { /* the built copy sits one folder deeper */ }
|
|
481
|
+
} return "0.0.0"; })();
|
|
482
|
+
const TOP_HELP = `amness ${VERSION}: coding agent for your terminal
|
|
483
|
+
|
|
484
|
+
amness start a conversation (chat TUI)
|
|
485
|
+
amness "fix the failing test" start with a prompt
|
|
486
|
+
amness -c continue the most recent conversation here
|
|
487
|
+
amness --resume <id> continue a specific conversation
|
|
488
|
+
amness -p "task" [flags] one-shot, non-interactive (same as: amness run)
|
|
489
|
+
amness run [flags] "task" headless mode for CI/scripts (amness run --help)
|
|
490
|
+
amness menu the classic menu (demo, evals, research, settings…)
|
|
491
|
+
amness --worktree [name] work in a fresh git worktree on its own branch (your checkout stays untouched)
|
|
492
|
+
amness --inline | --full choose the interface (default: full-screen; AMNESS_UI or /ui also work)
|
|
493
|
+
amness eval [flags] compare harness variants statistically (amness eval --help)
|
|
494
|
+
amness bench [flags] compare Amness, Claude Code and Codex on the same tasks (amness bench --help)
|
|
495
|
+
amness plugin new <name> scaffold a plugin (guardrails, loop rules, tools, commands) (amness plugin --help)
|
|
496
|
+
amness schedule <cmd> recurring headless runs through cron (amness schedule --help)
|
|
497
|
+
amness acp serve the Agent Client Protocol on stdio for editors (Zed, JetBrains)
|
|
498
|
+
amness tune [flags] find guard settings that hold up on unseen tasks (amness tune --help)
|
|
499
|
+
amness demo guided demo, no API key needed
|
|
500
|
+
|
|
501
|
+
inside the chat: /help for commands · @file to mention files · shift+tab to change permissions`;
|
|
502
|
+
/** amness traces [--dir d] [--case name --task "..." --check "cmd" [--verify "cmd"]]: failure report from the session traces, or snapshot the repo into an eval task. */
|
|
503
|
+
function traces(args) {
|
|
504
|
+
const val = (f) => { const i = args.indexOf(f); return i >= 0 ? args[i + 1] : undefined; }, dir = resolve(val("--dir") ?? join(".amness", "traces"));
|
|
505
|
+
const name = val("--case");
|
|
506
|
+
if (name) {
|
|
507
|
+
if (!val("--task") || !val("--check")) {
|
|
508
|
+
console.error('usage: amness traces --case <name> --task "what to do" --check "<command that exits 0 only when the task is really done>" [--verify "<command the agent may run>"]');
|
|
509
|
+
return 2;
|
|
510
|
+
}
|
|
511
|
+
const r = caseFromWorkspace(process.cwd(), { name, task: val("--task"), check: val("--check"), verify: val("--verify") });
|
|
512
|
+
console.log(`${green("✓")} wrote ${r.path} (${r.files} files, ${(r.bytes / 1024).toFixed(0)} KB, from git HEAD)\n${dim("run it: amness eval --cases " + dirname(r.path) + " --variants ab --trials 3 · add a \"solution\" to the JSON to include it in --selftest")}`);
|
|
513
|
+
return 0;
|
|
514
|
+
}
|
|
515
|
+
console.log(formatMining(dir));
|
|
516
|
+
return 0;
|
|
517
|
+
}
|
|
518
|
+
/** amness replay <trace.jsonl>: run a recorded session through the current harness (no model, no tools) and show what behaves differently. */
|
|
519
|
+
async function runReplay(args) {
|
|
520
|
+
const val = (f) => { const i = args.indexOf(f); return i >= 0 ? args[i + 1] : undefined; }, file = args.find((a) => !a.startsWith("--") && args[args.indexOf(a) - 1]?.startsWith("--") !== true);
|
|
521
|
+
if (!file) {
|
|
522
|
+
console.error("usage: amness replay <trace.jsonl> [--off guard,guard] [--stagnation-turns n] [--max-steps n] [--context-budget chars]\n Traces are written to .amness/traces/ (full messages are recorded in chat sessions and headless runs with --trace).");
|
|
523
|
+
return 2;
|
|
524
|
+
}
|
|
525
|
+
try {
|
|
526
|
+
const o = {};
|
|
527
|
+
if (val("--off"))
|
|
528
|
+
o.off = val("--off").split(",");
|
|
529
|
+
if (val("--stagnation-turns"))
|
|
530
|
+
o.stagnationTurns = Number(val("--stagnation-turns"));
|
|
531
|
+
if (val("--max-steps"))
|
|
532
|
+
o.maxSteps = Number(val("--max-steps"));
|
|
533
|
+
if (val("--context-budget"))
|
|
534
|
+
o.contextBudget = Number(val("--context-budget"));
|
|
535
|
+
const reports = await replayFile(resolve(file), o);
|
|
536
|
+
console.log(formatReplay(reports));
|
|
537
|
+
return reports.every((r) => r.same) ? 0 : 1;
|
|
538
|
+
}
|
|
539
|
+
catch (e) {
|
|
540
|
+
console.error(red("✗ " + e.message));
|
|
541
|
+
return 2;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
async function startChat(argv) {
|
|
545
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
546
|
+
console.error("amness chat needs an interactive terminal. For scripts use: amness run [flags] \"task\"");
|
|
547
|
+
process.exitCode = 2;
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
let resume;
|
|
551
|
+
const rest = [];
|
|
552
|
+
const envUi = process.env.AMNESS_UI === "inline" || process.env.AMNESS_UI === "full" ? process.env.AMNESS_UI : undefined;
|
|
553
|
+
let ui = envUi ?? cfg.ui, wtName;
|
|
554
|
+
for (let i = 0; i < argv.length; i++) {
|
|
555
|
+
if (argv[i] === "-c" || argv[i] === "--continue")
|
|
556
|
+
resume = "last";
|
|
557
|
+
else if (argv[i] === "--resume")
|
|
558
|
+
resume = argv[++i] ?? "last";
|
|
559
|
+
else if (argv[i] === "--worktree")
|
|
560
|
+
wtName = argv[i + 1] && !argv[i + 1].startsWith("-") ? argv[++i] : `session-${new Date().toISOString().slice(5, 16).replace(/\D/g, "")}`;
|
|
561
|
+
else if (argv[i] === "--inline")
|
|
562
|
+
ui = "inline";
|
|
563
|
+
else if (argv[i] === "--full")
|
|
564
|
+
ui = "full";
|
|
565
|
+
else
|
|
566
|
+
rest.push(argv[i]);
|
|
567
|
+
}
|
|
568
|
+
if (!(await ensureReady()))
|
|
569
|
+
return;
|
|
570
|
+
let wt;
|
|
571
|
+
if (wtName) {
|
|
572
|
+
if (!(await isRepo(process.cwd()))) {
|
|
573
|
+
console.error(red("✗ --worktree needs a git repository with at least one commit"));
|
|
574
|
+
process.exitCode = 2;
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
try {
|
|
578
|
+
wt = await createSessionWorktree(process.cwd(), wtName);
|
|
579
|
+
process.chdir(wt.dir);
|
|
580
|
+
console.log(dim(`worktree ${wt.dir} on branch ${wt.branch} (your main checkout is untouched)`));
|
|
581
|
+
}
|
|
582
|
+
catch (e) {
|
|
583
|
+
console.error(red("✗ " + e.message));
|
|
584
|
+
process.exitCode = 2;
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
const tiny = (process.stdout.columns ?? 80) < 60 || (process.stdout.rows ?? 24) < 14;
|
|
589
|
+
if (process.env.TERM === "dumb" || tiny)
|
|
590
|
+
ui = "inline";
|
|
591
|
+
const app = new App({ cfg, cwd: process.cwd(), version: VERSION, resume, ui: ui ?? "full", prompt: rest.join(" ").trim() || undefined });
|
|
592
|
+
process.exitCode = await app.start();
|
|
593
|
+
if (wt) {
|
|
594
|
+
process.chdir(wt.repo);
|
|
595
|
+
if (await worktreeIsUntouched(wt.dir, wt.base)) {
|
|
596
|
+
await removeWorktree({ dir: wt.dir, repo: wt.repo });
|
|
597
|
+
execFileSync("git", ["branch", "-D", wt.branch], { cwd: wt.repo, stdio: "ignore" });
|
|
598
|
+
console.log(dim(`worktree ${wt.branch} had no changes: removed`));
|
|
599
|
+
}
|
|
600
|
+
else
|
|
601
|
+
console.log(`${green("✓")} work is in ${wt.dir} on branch ${bold(wt.branch)}\n${dim(`merge it with: git merge ${wt.branch} · remove it with: git worktree remove ${wt.dir}`)}`);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
// ------------------------------------------------------------------ main
|
|
605
|
+
async function main() {
|
|
606
|
+
const argv = process.argv.slice(2), quick = argv[0];
|
|
607
|
+
const screens = { code: codeScreen, memory: memoryScreen, import: importScreen, research: researchScreen, eval: evalScreen, demo: demoScreen, settings };
|
|
608
|
+
if (quick === "--version" || quick === "-v")
|
|
609
|
+
return void console.log(VERSION);
|
|
610
|
+
if (quick === "--help" || quick === "-h" || quick === "help")
|
|
611
|
+
return void console.log(TOP_HELP);
|
|
612
|
+
if (quick === "eval" && argv.length > 1) {
|
|
613
|
+
process.exitCode = await runEval(argv.slice(1), { model: makeModel, modelFor: (id) => createModel({ ...cfg, model: id }), maxTokens: cfg.maxTokens, confirm });
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
if (quick === "bench") {
|
|
617
|
+
process.exitCode = await runBench(argv.slice(1), { confirm, amnessCli: fileURLToPath(import.meta.url) });
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
if (quick === "plugin") {
|
|
621
|
+
process.exitCode = await runPlugin(argv.slice(1));
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
if (quick === "schedule") {
|
|
625
|
+
process.exitCode = runSchedule(argv.slice(1));
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
if (quick === "acp") {
|
|
629
|
+
if (argv.includes("--help"))
|
|
630
|
+
return void console.log(ACP_HELP);
|
|
631
|
+
process.exitCode = await runAcp({ model: rawModel, version: VERSION, maxTokens: cfg.maxTokens });
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
if (quick === "tune") {
|
|
635
|
+
process.exitCode = await runTune(argv.slice(1), { model: makeModel, modelName: cfg.model, maxTokens: cfg.maxTokens, confirm });
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
if (quick === "traces") {
|
|
639
|
+
process.exitCode = traces(argv.slice(1));
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
if (quick === "replay") {
|
|
643
|
+
process.exitCode = await runReplay(argv.slice(1));
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
if (quick === "run") {
|
|
647
|
+
process.exitCode = await runHeadless(argv.slice(1));
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
if (quick === "-p" || quick === "--print") {
|
|
651
|
+
process.exitCode = await runHeadless(argv.slice(1));
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
if (quick && quick in screens)
|
|
655
|
+
return void (await screens[quick]().catch((e) => { if (!(e instanceof Cancel))
|
|
656
|
+
throw e; }));
|
|
657
|
+
if (quick !== "menu")
|
|
658
|
+
return startChat(argv);
|
|
659
|
+
let keep = false;
|
|
660
|
+
for (;;) {
|
|
661
|
+
if (!keep)
|
|
662
|
+
clearScreen();
|
|
663
|
+
keep = false;
|
|
664
|
+
banner();
|
|
665
|
+
const noKey = !ready(cfg);
|
|
666
|
+
const hint = noKey ? yellow("needs API key") : "";
|
|
667
|
+
const choice = await select("What do you want to do?", [
|
|
668
|
+
{ label: "Code agent", value: "code", hint: hint || "fix or build in a repo · approvals · test gate" },
|
|
669
|
+
{ label: "Research agent", value: "research", hint: hint || "plans, delegates, searches the web, cites sources" },
|
|
670
|
+
{ label: "Guided demo", value: "demo", hint: "no API key · watch every guard catch a mistake" },
|
|
671
|
+
{ label: "Evals: baseline vs harness", value: "eval", hint: hint || "measure what the guards are worth" },
|
|
672
|
+
{ label: "Memory", value: "memory", hint: "see, add, or delete what the agent remembers" },
|
|
673
|
+
{ label: "Import session", value: "import", hint: hint || "continue a portable bundle · any machine or model" },
|
|
674
|
+
{ label: "Settings", value: "settings", hint: noKey ? "set your API key here" : "provider · model · keys · budget" },
|
|
675
|
+
{ label: "Quit", value: "quit" },
|
|
676
|
+
]);
|
|
677
|
+
if (!choice || choice === "quit")
|
|
678
|
+
return void console.log(dim("bye\n"));
|
|
679
|
+
try {
|
|
680
|
+
await screens[choice]();
|
|
681
|
+
}
|
|
682
|
+
catch (e) {
|
|
683
|
+
if (e instanceof Cancel) {
|
|
684
|
+
console.log(dim("cancelled"));
|
|
685
|
+
keep = true;
|
|
686
|
+
}
|
|
687
|
+
else
|
|
688
|
+
throw e;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
// Exit as soon as output is flushed: idle keep-alive HTTP sockets would otherwise hold the process open for seconds after "Quit".
|
|
693
|
+
main().catch((e) => { console.error(red("fatal: " + (e?.stack ?? e))); process.exitCode = 1; })
|
|
694
|
+
.finally(() => process.stdout.write("", () => process.exit(process.exitCode ?? 0)));
|