@staix/agent-research 0.0.0-stage → 0.1.0

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 (52) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/LICENSE +21 -0
  3. package/README.md +572 -2
  4. package/bin/ares.js +11 -0
  5. package/package.json +35 -4
  6. package/src/cli.ts +295 -0
  7. package/src/commands-table.ts +72 -0
  8. package/src/commands.ts +520 -0
  9. package/src/console/registry.ts +97 -0
  10. package/src/exp/ablate.ts +140 -0
  11. package/src/exp/artifacts.ts +229 -0
  12. package/src/exp/attempts.ts +475 -0
  13. package/src/exp/executor.ts +207 -0
  14. package/src/exp/final.ts +197 -0
  15. package/src/exp/ideas.ts +117 -0
  16. package/src/exp/investigate.ts +182 -0
  17. package/src/exp/logs.ts +63 -0
  18. package/src/exp/metrics.ts +132 -0
  19. package/src/exp/protocol.ts +145 -0
  20. package/src/exp/selection.ts +53 -0
  21. package/src/exp/snapshot.ts +147 -0
  22. package/src/http.ts +62 -0
  23. package/src/index.ts +4 -0
  24. package/src/lit/bibtex.ts +61 -0
  25. package/src/lit/candidates.ts +148 -0
  26. package/src/lit/cards.ts +21 -0
  27. package/src/lit/cite.ts +26 -0
  28. package/src/lit/sources.ts +122 -0
  29. package/src/lit/verify.ts +110 -0
  30. package/src/lit/watch.ts +109 -0
  31. package/src/manifest.ts +38 -0
  32. package/src/mcp-tools.ts +200 -0
  33. package/src/mcp.ts +111 -0
  34. package/src/memory/amem.ts +97 -0
  35. package/src/memory/archive.ts +63 -0
  36. package/src/memory/lessons.ts +88 -0
  37. package/src/memory/skills.ts +99 -0
  38. package/src/paper/build.ts +70 -0
  39. package/src/paper/check.ts +180 -0
  40. package/src/paper/placeholders.ts +116 -0
  41. package/src/paper/review.ts +115 -0
  42. package/src/plan/controller.ts +192 -0
  43. package/src/plan/definition.ts +156 -0
  44. package/src/plan/hub.ts +56 -0
  45. package/src/plan/state.ts +234 -0
  46. package/src/playbook.ts +129 -0
  47. package/src/scope.ts +53 -0
  48. package/src/setup.ts +296 -0
  49. package/src/study.ts +296 -0
  50. package/src/ui/panel.css +11 -0
  51. package/src/ui/panel.js +156 -0
  52. package/src/ui/plugin.ts +361 -0
package/package.json CHANGED
@@ -1,6 +1,37 @@
1
1
  {
2
2
  "name": "@staix/agent-research",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Research engine for the agent family: literature with verified references, study ledger and gates (ares)",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/STAIxBWLB/agent-research.git"
10
+ },
11
+ "engines": {
12
+ "bun": ">=1.4.2"
13
+ },
14
+ "devDependencies": {
15
+ "@staix/agent-console": "git+https://github.com/STAIxBWLB/agent-console.git#27e4f89babc7a25df8038918d1181d0af35384e0",
16
+ "@types/bun": "^1.3.0",
17
+ "typescript": "^5.8.0"
18
+ },
19
+ "exports": {
20
+ ".": "./src/index.ts"
21
+ },
22
+ "bin": {
23
+ "ares": "bin/ares.js"
24
+ },
25
+ "files": [
26
+ "src/",
27
+ "bin/",
28
+ "README.md",
29
+ "CHANGELOG.md",
30
+ "LICENSE"
31
+ ],
32
+ "scripts": {
33
+ "check": "bun x tsc --noEmit && bun scripts/check-boundary.ts && bun test && bun pm pack --dry-run",
34
+ "release:stage": "bun scripts/release.ts stage",
35
+ "release:smoke": "bun scripts/release-smoke.ts"
36
+ }
37
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,295 @@
1
+ import { version } from "../package.json";
2
+ import { findCommand, help, type CommandSpec } from "./commands-table.ts";
3
+ import * as ops from "./commands.ts";
4
+ import { createHttp, type Http } from "./http.ts";
5
+ import { PLAYBOOK } from "./playbook.ts";
6
+ import { detectActor, readTextBounded } from "./study.ts";
7
+
8
+ const INPUT_BYTES = 65_536;
9
+ export interface Io extends ops.ExperimentRuntime { readStdin?: () => Promise<string>; http?: Http; signal?: AbortSignal }
10
+
11
+ function parse(spec: CommandSpec, args: string[]): { flags: Record<string, string | true>; positional: string[] } {
12
+ const flags: Record<string, string | true> = {}, positional: string[] = [];
13
+ for (let i = 0; i < args.length; i++) {
14
+ const word = args[i]!;
15
+ if (word === "-" || !word.startsWith("-")) { positional.push(word); continue; }
16
+ const kind = spec.options[word];
17
+ if (!kind || word in flags) throw new Error(`${spec.name}: unknown or repeated option ${word}; use ares --help and pass each supported option once`);
18
+ if (kind === "value") {
19
+ const value = args[++i];
20
+ if (value === undefined || (value.startsWith("--") && value !== "-")) throw new Error(`${spec.name}: ${word} needs a value; use ares --help for its accepted values`);
21
+ flags[word] = value;
22
+ } else flags[word] = true;
23
+ }
24
+ return { flags, positional };
25
+ }
26
+ function count(spec: CommandSpec, positional: string[], min: number, max = min): void {
27
+ if (positional.length < min || positional.length > max) throw new Error(`usage: ares ${spec.usage}`);
28
+ }
29
+ function integer(value: string | true | undefined, what: string): number | undefined {
30
+ if (value === undefined) return undefined;
31
+ if (typeof value !== "string" || !/^\d{1,7}$/.test(value)) throw new Error(`${what}: integer required; pass a nonnegative decimal integer`);
32
+ return Number(value);
33
+ }
34
+ function seed(value: string | undefined): number | undefined {
35
+ if (value === undefined) return undefined;
36
+ if (!/^-?\d{1,10}$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) < -2_147_483_648 || Number(value) > 2_147_483_647) throw new Error("--seed: invalid integer; use a declared protocol seed in -2147483648..2147483647");
37
+ return Number(value);
38
+ }
39
+ async function readInput(path: string, io: Io): Promise<unknown> {
40
+ let text: string;
41
+ if (path === "-") {
42
+ if (!io.readStdin) throw new Error("--file -: no standard input");
43
+ text = await io.readStdin();
44
+ } else text = readTextBounded(path, INPUT_BYTES);
45
+ if (Buffer.byteLength(text) > INPUT_BYTES) throw new Error(`input larger than ${INPUT_BYTES} bytes`);
46
+ try { return JSON.parse(text); } catch { throw new Error(`${path === "-" ? "standard input" : path}: not valid JSON`); }
47
+ }
48
+
49
+ export async function run(args: string[], env: NodeJS.ProcessEnv = process.env, io: Io = {}): Promise<{ text: string; code: number }> {
50
+ if (!args.length || (args.length === 1 && ["--help", "-h", "help"].includes(args[0]!))) return { text: help(), code: 0 };
51
+ if (args.length === 1 && args[0] === "--version") return { text: version, code: 0 };
52
+ const found = findCommand(args);
53
+ if (!found) throw new Error(`unknown command: ${args.slice(0, 2).join(" ")}; use --help`);
54
+ const { spec, rest } = found, { flags, positional } = parse(spec, rest);
55
+ const ctx: ops.Ctx = { actor: detectActor(env), http: io.http ?? createHttp(env), ...(io.signal ? { signal: io.signal } : {}), ...(io.sleep ? { sleep: io.sleep } : {}), ...(io.runner ? { runner: io.runner } : {}), ...(io.now ? { now: io.now } : {}), ...(io.collectEvaluation ? { collectEvaluation: io.collectEvaluation } : {}), ...(io.collectStageEvidence ? { collectStageEvidence: io.collectStageEvidence } : {}) };
56
+ const str = (flag: string) => (typeof flags[flag] === "string" ? flags[flag] as string : undefined);
57
+ let result: unknown, code = 0;
58
+ switch (spec.name) {
59
+ case "console add": count(spec, positional, 1); result = ops.consoleAdd(positional[0]!, env); break;
60
+ case "console remove": count(spec, positional, 1); result = ops.consoleRemove(positional[0]!, env); break;
61
+ case "console list": count(spec, positional, 0); result = ops.consoleList(env); break;
62
+ case "watch set": {
63
+ count(spec, positional, 1); const file = str("--file");
64
+ if (file === undefined) throw new Error("watch set: input missing; pass --file PATH or --file - for watch JSON");
65
+ result = await ops.watchSet(positional[0]!, await readInput(file, io), ctx); break;
66
+ }
67
+ case "watch run": {
68
+ count(spec, positional, 1); const output = await ops.watchRun(positional[0]!, ctx);
69
+ result = output; if (!output.ok) code = 1; break;
70
+ }
71
+ case "watch show": count(spec, positional, 1); result = ops.watchShow(positional[0]!); break;
72
+ case "setup": {
73
+ count(spec, positional, 1);
74
+ if (flags["--apply"] && flags["--undo"]) throw new Error("setup: --apply and --undo conflict; choose one or omit both for preview");
75
+ const root = str("--root"); result = ops.setup(positional[0]!, flags["--apply"] ? "apply" : flags["--undo"] ? "undo" : "dry-run", env, { ...(root === undefined ? {} : { root }) });
76
+ if ((result as ReturnType<typeof ops.setup>).actions.some(action => action.change === "refused")) code = 1;
77
+ break;
78
+ }
79
+ case "skill propose": {
80
+ count(spec, positional, 1); const file = str("--file");
81
+ if (file === undefined) throw new Error("skill propose: input missing; pass --file PATH or --file - for proposal JSON");
82
+ result = await ops.skillPropose(positional[0]!, await readInput(file, io), ctx); break;
83
+ }
84
+ case "skill decide": {
85
+ count(spec, positional, 3); const reason = str("--reason");
86
+ if (reason === undefined) throw new Error("skill decide: reason missing; pass --reason TEXT and --pr URL for accept");
87
+ result = await ops.skillDecide(positional[0]!, positional[1]!, positional[2]!, reason, str("--pr"), ctx); break;
88
+ }
89
+ case "skill list": count(spec, positional, 1); result = ops.skillList(positional[0]!); break;
90
+ case "lessons": {
91
+ count(spec, positional, 1); const intent = str("--for"), halfLife = str("--half-life"), limit = integer(flags["--limit"], "--limit");
92
+ result = await ops.lessons(positional[0]!, { ...(intent === undefined ? {} : { for: intent }), ...(halfLife === undefined ? {} : { halfLife: Number(halfLife) }), ...(limit === undefined ? {} : { limit }) }, ctx, env); break;
93
+ }
94
+ case "archive": {
95
+ count(spec, positional, 1); const confirm = str("--confirm");
96
+ result = await ops.archive(positional[0]!, { ...(confirm !== undefined ? { confirm } : {}) }, ctx, env); break;
97
+ }
98
+ case "run": {
99
+ count(spec, positional, 1); const parallel = integer(flags["--parallel"], "--parallel");
100
+ result = await ops.planRun(positional[0]!, { ...(parallel !== undefined ? { parallel } : {}) }, ctx); break;
101
+ }
102
+ case "manifest": count(spec, positional, 0); result = ops.manifest(); break;
103
+ case "playbook": count(spec, positional, 0); return { text: PLAYBOOK.trimEnd(), code: 0 };
104
+ case "mcp": throw new Error("mcp: run `ares mcp` with no other arguments");
105
+ case "init": {
106
+ count(spec, positional, 1);
107
+ const question = str("--question");
108
+ if (question === undefined) throw new Error("init: --question is required");
109
+ result = ops.init(positional[0]!, { question, ...(str("--mode") ? { mode: str("--mode")! } : {}), ...(str("--autonomy") ? { autonomy: str("--autonomy")! } : {}) }, ctx);
110
+ break;
111
+ }
112
+ case "plan set": {
113
+ count(spec, positional, 1);
114
+ const file = str("--file");
115
+ if (file === undefined) throw new Error("plan set: --file is required");
116
+ result = await ops.planSet(positional[0]!, await readInput(file, io), ctx); break;
117
+ }
118
+ case "plan control": {
119
+ count(spec, positional, 2, 3);
120
+ const commandId = str("--command-id"), expectedRevision = integer(flags["--expected-revision"], "--expected-revision");
121
+ if (!commandId || expectedRevision === undefined) throw new Error("plan control: command id and revision required; pass --command-id ID --expected-revision N from ares plan show");
122
+ result = await ops.planControl(positional[0]!, { action: positional[1]! as Parameters<typeof ops.planControl>[1]["action"], ...(positional[2] !== undefined ? { node: positional[2] } : {}), commandId, expectedRevision }, ctx); break;
123
+ }
124
+ case "plan show": count(spec, positional, 1); result = ops.planShow(positional[0]!); break;
125
+ case "status": count(spec, positional, 1); result = ops.status(positional[0]!); break;
126
+ case "scope": {
127
+ count(spec, positional, 1);
128
+ const file = str("--file");
129
+ if (file === undefined) throw new Error("scope: --file is required");
130
+ result = await ops.scope(positional[0]!, await readInput(file, io), ctx);
131
+ break;
132
+ }
133
+ case "idea add": {
134
+ count(spec, positional, 1); const file = str("--file");
135
+ if (file === undefined) throw new Error("idea add: input missing; pass --file PATH or --file - for JSON");
136
+ result = await ops.ideaAdd(positional[0]!, await readInput(file, io), ctx); break;
137
+ }
138
+ case "idea decide": {
139
+ count(spec, positional, 3); const reason = str("--reason"), attempts = str("--attempts");
140
+ if (reason === undefined) throw new Error("idea decide: reason missing; pass --reason TEXT and --commit SHA for refine, plus --attempts for an idea with commits");
141
+ result = await ops.ideaDecide(positional[0]!, positional[1]!, { decision: positional[2], reason, ...(str("--cause") !== undefined ? { cause: str("--cause") } : {}), ...(str("--retry-when") !== undefined ? { retryWhen: str("--retry-when") } : {}), ...(attempts !== undefined ? { attempts: attempts.split(",") } : {}), ...(str("--commit") !== undefined ? { commit: str("--commit") } : {}) }, ctx); break;
142
+ }
143
+ case "investigate show": count(spec, positional, 1); result = ops.investigateShow(positional[0]!); break;
144
+ case "investigate close": count(spec, positional, 1); result = await ops.investigateClose(positional[0]!, ctx); break;
145
+ case "ablate add": {
146
+ count(spec, positional, 1); const file = str("--file");
147
+ if (file === undefined) throw new Error("ablate add: input missing; pass --file PATH or --file - for JSON");
148
+ result = await ops.ablateAdd(positional[0]!, await readInput(file, io), ctx); break;
149
+ }
150
+ case "ablate show": count(spec, positional, 1); result = ops.ablateShow(positional[0]!); break;
151
+ case "ablate close": count(spec, positional, 1); result = await ops.ablateClose(positional[0]!, ctx); break;
152
+ case "posthoc show": count(spec, positional, 1); result = ops.posthocShow(positional[0]!); break;
153
+ case "final close": count(spec, positional, 1); result = await ops.finalClose(positional[0]!, ctx); break;
154
+ case "final seeds": {
155
+ count(spec, positional, 1); const file = str("--file");
156
+ if (file === undefined) throw new Error("final seeds: input missing; pass --file PATH or --file - for JSON");
157
+ result = await ops.finalSeeds(positional[0]!, await readInput(file, io), ctx); break;
158
+ }
159
+ case "selection": {
160
+ count(spec, positional, 1);
161
+ const file = str("--file");
162
+ if (file === undefined) throw new Error("selection: input missing; pass --file PATH or --file - for a JSON rule");
163
+ result = await ops.selection(positional[0]!, await readInput(file, io), ctx); break;
164
+ }
165
+ case "protocol": {
166
+ count(spec, positional, 1);
167
+ const file = str("--file");
168
+ if (file === undefined) throw new Error("protocol: input file missing; use --file PATH or --file - for JSON standard input");
169
+ result = await ops.protocol(positional[0]!, await readInput(file, io), ctx); break;
170
+ }
171
+ case "executor set": {
172
+ count(spec, positional, 1);
173
+ const kind = str("--kind"), workdir = str("--workdir");
174
+ if (!kind || !workdir) throw new Error("executor set: kind/workdir missing; use --kind local|ssh --workdir PATH and --host ALIAS for ssh");
175
+ result = await ops.executorSet(positional[0]!, { v: 1, kind, workdir, ...(str("--host") !== undefined ? { host: str("--host") } : {}) }, ctx); break;
176
+ }
177
+ case "executor check": count(spec, positional, 1); result = await ops.executorCheck(positional[0]!, ctx); break;
178
+ case "attempt run": {
179
+ count(spec, positional, 1);
180
+ const repo = str("--repo"), commit = str("--commit"), chosenSeed = seed(str("--seed"));
181
+ if (!repo || !commit) throw new Error("attempt run: repo/commit missing; use --repo PATH --commit FULL_40_HEX_SHA");
182
+ const attempt = await ops.attemptRun(positional[0]!, { repo, commit, ...(chosenSeed !== undefined ? { seed: chosenSeed } : {}), ...(str("--run") !== undefined ? { run: str("--run") } : {}), ...(str("--label") !== undefined ? { label: str("--label") } : {}), ...(str("--retry") !== undefined ? { retry: str("--retry") } : {}), detach: !!flags["--detach"] }, ctx);
183
+ if (attempt.status !== "running" && attempt.status !== "succeeded") code = 1;
184
+ result = attempt; break;
185
+ }
186
+ case "attempt list": {
187
+ count(spec, positional, 1); const limit = integer(flags["--limit"], "--limit"), offset = integer(flags["--offset"], "--offset");
188
+ result = ops.attemptList(positional[0]!, { ...(str("--run") !== undefined ? { run: str("--run") } : {}), ...(str("--status") !== undefined ? { status: str("--status") } : {}), ...(limit !== undefined ? { limit } : {}), ...(offset !== undefined ? { offset } : {}) }); break;
189
+ }
190
+ case "attempt show": count(spec, positional, 2); result = ops.attemptShow(positional[0]!, positional[1]!); break;
191
+ case "attempt cancel": count(spec, positional, 2); result = await ops.attemptCancel(positional[0]!, positional[1]!, str("--reason"), ctx); break;
192
+ case "attempt reconcile": count(spec, positional, 1); result = await ops.attemptReconcile(positional[0]!, ctx); break;
193
+ case "attempt logs": {
194
+ count(spec, positional, 2); const tailBytes = integer(flags["--tail"], "--tail");
195
+ const logs = await ops.attemptLogs(positional[0]!, positional[1]!, { ...(str("--stage") !== undefined ? { stage: str("--stage") } : {}), ...(str("--stream") !== undefined ? { stream: str("--stream") } : {}), ...(tailBytes !== undefined ? { tailBytes } : {}) }, ctx);
196
+ if (!flags["--json"]) return { text: logs.logs.map(log => `[${log.stage} ${log.stream}${log.truncated ? ", truncated copy" : ""}]\n${log.text}`).join("\n"), code: 0 };
197
+ result = logs; break;
198
+ }
199
+ case "gate list": count(spec, positional, 1); result = ops.gateList(positional[0]!); break;
200
+ case "gate approve": case "gate reject":
201
+ count(spec, positional, 2);
202
+ result = await ops.gateDecide(positional[0]!, positional[1]!, spec.name === "gate approve" ? "approve" : "reject", str("--reason"), ctx);
203
+ break;
204
+ case "lit search": {
205
+ count(spec, positional, 2);
206
+ const limit = integer(flags["--limit"], "--limit"), yearFrom = integer(flags["--year-from"], "--year-from");
207
+ const out = await ops.search(positional[0]!, positional[1]!, { ...(str("--source") ? { source: str("--source")! } : {}), ...(limit !== undefined ? { limit } : {}), ...(yearFrom !== undefined ? { yearFrom } : {}) }, ctx);
208
+ if (!out.sources.some(s => s.ok)) code = 1;
209
+ result = out;
210
+ break;
211
+ }
212
+ case "lit list": {
213
+ count(spec, positional, 1);
214
+ const limit = integer(flags["--limit"], "--limit");
215
+ result = ops.list(positional[0]!, { ...(str("--status") ? { status: str("--status")! } : {}), ...(limit !== undefined ? { limit } : {}) });
216
+ break;
217
+ }
218
+ case "lit screen": {
219
+ if (positional.length < 2) throw new Error(`usage: ares ${spec.usage}`);
220
+ if (!!flags["--include"] === !!flags["--exclude"]) throw new Error("lit screen: exactly one of --include or --exclude");
221
+ result = await ops.screen(positional[0]!, positional.slice(1), flags["--include"] ? "include" : "exclude", str("--reason"), ctx);
222
+ break;
223
+ }
224
+ case "lit card": {
225
+ count(spec, positional, 2);
226
+ const file = str("--file");
227
+ if (file === undefined) throw new Error("lit card: --file is required");
228
+ result = await ops.card(positional[0]!, positional[1]!, await readInput(file, io), ctx);
229
+ break;
230
+ }
231
+ case "lit verify": if (!positional.length) throw new Error(`usage: ares ${spec.usage}`); result = await ops.verify(positional[0]!, positional.slice(1), ctx); break;
232
+ case "lit bib": count(spec, positional, 1); result = await ops.bib(positional[0]!, ctx); break;
233
+ case "review add": {
234
+ count(spec, positional, 1); const file = str("--file");
235
+ if (file === undefined) throw new Error("review add: --file is required; pass --file PATH|- with a current-build report");
236
+ result = await ops.reviewAdd(positional[0]!, await readInput(file, io), ctx); break;
237
+ }
238
+ case "review resolve": {
239
+ count(spec, positional, 2); const reason = str("--reason");
240
+ if (reason === undefined) throw new Error("review resolve: --reason is required; explain how the finding was resolved");
241
+ result = await ops.reviewResolve(positional[0]!, positional[1]!, reason, ctx); break;
242
+ }
243
+ case "review show": count(spec, positional, 1); result = ops.reviewShow(positional[0]!); break;
244
+ case "paper build": count(spec, positional, 1); result = await ops.paperBuild(positional[0]!, ctx); break;
245
+ case "paper check": {
246
+ count(spec, positional, 1); const report = ops.paperCheck(positional[0]!);
247
+ if (!report.ok) code = 1;
248
+ result = report; break;
249
+ }
250
+ case "cite check": {
251
+ count(spec, positional, 2);
252
+ const report = ops.cite(positional[0]!, positional[1]!);
253
+ if (!report.ok) code = 1;
254
+ result = report;
255
+ break;
256
+ }
257
+ default: throw new Error(`unknown command: ${spec.name}`);
258
+ }
259
+ return { text: JSON.stringify(result, null, flags["--json"] ? undefined : 2), code };
260
+ }
261
+
262
+ /** `ares mcp`: stdout carries protocol frames only. */
263
+ export async function runMcp(env: NodeJS.ProcessEnv = process.env): Promise<void> {
264
+ const [{ serveMcp }, { createAresMcp }] = await Promise.all([import("./mcp.ts"), import("./mcp-tools.ts")]);
265
+ const server = createAresMcp(env);
266
+ try { await serveMcp({ tools: server.tools, handlers: server.handlers, name: "ares", version }); }
267
+ finally { await server.waitForIdle(); }
268
+ }
269
+
270
+ /** Standard input stops at the input bound instead of buffering everything first. */
271
+ async function readStdinBounded(): Promise<string> {
272
+ const chunks: Uint8Array[] = [];
273
+ let size = 0;
274
+ for await (const chunk of Bun.stdin.stream()) {
275
+ size += chunk.byteLength;
276
+ if (size > INPUT_BYTES) throw new Error(`standard input larger than ${INPUT_BYTES} bytes`);
277
+ chunks.push(chunk);
278
+ }
279
+ return new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks));
280
+ }
281
+
282
+ export async function main(args: string[]): Promise<number> {
283
+ const stop = new AbortController(), interrupt = () => stop.abort();
284
+ if (args[0] === "run") { process.on("SIGINT", interrupt); process.on("SIGTERM", interrupt); }
285
+ try {
286
+ const output = await run(args, process.env, { readStdin: readStdinBounded, signal: stop.signal });
287
+ console.log(output.text);
288
+ return output.code;
289
+ } catch (error) {
290
+ console.error(`ares: ${error instanceof Error ? error.message : "operation failed"}`);
291
+ return 1;
292
+ } finally {
293
+ if (args[0] === "run") { process.off("SIGINT", interrupt); process.off("SIGTERM", interrupt); }
294
+ }
295
+ }
@@ -0,0 +1,72 @@
1
+ /** The single source of the CLI surface: help text is rendered from it and the parser reads its options. */
2
+ export interface CommandSpec { name: string; usage: string; mutates: boolean | "network"; options: Record<string, "flag" | "value"> }
3
+ const json = { "--json": "flag" } as const;
4
+ export const COMMANDS: readonly CommandSpec[] = [
5
+ { name: "skill propose", usage: "skill propose <dir> --file PATH|- [--json]", mutates: true, options: { "--file": "value", ...json } },
6
+ { name: "skill decide", usage: "skill decide <dir> <name> <accept|reject|withdraw> --reason TEXT [--pr URL] [--json]", mutates: true, options: { "--reason": "value", "--pr": "value", ...json } },
7
+ { name: "skill list", usage: "skill list <dir> [--json]", mutates: false, options: json },
8
+ { name: "lessons", usage: "lessons <dir> [--for TEXT] [--half-life DAYS] [--limit N] [--json]", mutates: false, options: { "--for": "value", "--half-life": "value", "--limit": "value", ...json } },
9
+ { name: "archive", usage: "archive <dir> [--confirm FINGERPRINT] [--json]", mutates: "network", options: { "--confirm": "value", ...json } },
10
+ { name: "setup", usage: "setup <claude|codex|kimi|pi|qwen|jcode|agy|all|console> [--apply|--undo] [--root ABS] [--json]", mutates: true, options: { "--apply": "flag", "--undo": "flag", "--root": "value", ...json } },
11
+ { name: "watch set", usage: "watch set <dir> --file PATH|- [--json]", mutates: true, options: { "--file": "value", ...json } },
12
+ { name: "watch run", usage: "watch run <dir> [--json]", mutates: "network", options: json },
13
+ { name: "watch show", usage: "watch show <dir> [--json]", mutates: false, options: json },
14
+ { name: "console add", usage: "console add <dir> [--json]", mutates: true, options: json },
15
+ { name: "console remove", usage: "console remove <dir> [--json]", mutates: true, options: json },
16
+ { name: "console list", usage: "console list [--json]", mutates: false, options: json },
17
+ { name: "manifest", usage: "manifest [--json]", mutates: false, options: json },
18
+ { name: "init", usage: "init <dir> --question TEXT [--mode survey|experiment] [--autonomy auto|gates] [--json]", mutates: true, options: { "--question": "value", "--mode": "value", "--autonomy": "value", ...json } },
19
+ { name: "run", usage: "run <dir> [--parallel 1..8] [--json]", mutates: "network", options: { "--parallel": "value", ...json } },
20
+ { name: "plan set", usage: "plan set <dir> --file PATH|- [--json]", mutates: true, options: { "--file": "value", ...json } },
21
+ { name: "plan control", usage: "plan control <dir> <pause|resume|retry|cancel|skip> [node] --command-id ID --expected-revision N [--json]", mutates: "network", options: { "--command-id": "value", "--expected-revision": "value", ...json } },
22
+ { name: "plan show", usage: "plan show <dir> [--json]", mutates: false, options: json },
23
+ { name: "status", usage: "status <dir> [--json]", mutates: false, options: json },
24
+ { name: "scope", usage: "scope <dir> --file PATH|- [--json]", mutates: true, options: { "--file": "value", ...json } },
25
+ { name: "idea add", usage: "idea add <dir> --file PATH|- [--json]", mutates: true, options: { "--file": "value", ...json } },
26
+ { name: "idea decide", usage: "idea decide <dir> <id> <refine|pivot> --reason TEXT [--attempts ID[,ID...]] [--commit SHA] [--cause TEXT] [--retry-when TEXT] [--json]", mutates: true, options: { "--cause": "value", "--retry-when": "value", "--reason": "value", "--attempts": "value", "--commit": "value", ...json } },
27
+ { name: "investigate show", usage: "investigate show <dir> [--json]", mutates: false, options: json },
28
+ { name: "investigate close", usage: "investigate close <dir> [--json]", mutates: true, options: json },
29
+ { name: "ablate add", usage: "ablate add <dir> --file PATH|- [--json]", mutates: true, options: { "--file": "value", ...json } },
30
+ { name: "ablate show", usage: "ablate show <dir> [--json]", mutates: false, options: json },
31
+ { name: "ablate close", usage: "ablate close <dir> [--json]", mutates: true, options: json },
32
+ { name: "posthoc show", usage: "posthoc show <dir> [--json]", mutates: false, options: json },
33
+ { name: "final close", usage: "final close <dir> [--json]", mutates: true, options: json },
34
+ { name: "final seeds", usage: "final seeds <dir> --file PATH|- [--json]", mutates: true, options: { "--file": "value", ...json } },
35
+ { name: "selection", usage: "selection <dir> --file PATH|- [--json]", mutates: true, options: { "--file": "value", ...json } },
36
+ { name: "protocol", usage: "protocol <dir> --file PATH|- [--json]", mutates: true, options: { "--file": "value", ...json } },
37
+ { name: "executor set", usage: "executor set <dir> --kind local|ssh [--host ALIAS] --workdir PATH [--json]", mutates: true, options: { "--kind": "value", "--host": "value", "--workdir": "value", ...json } },
38
+ { name: "executor check", usage: "executor check <dir> [--json]", mutates: "network", options: json },
39
+ { name: "gate list", usage: "gate list <dir> [--json]", mutates: false, options: json },
40
+ { name: "gate approve", usage: "gate approve <dir> <scope|screen|design|final|review|plan:node-id> [--reason TEXT] [--json]", mutates: true, options: { "--reason": "value", ...json } },
41
+ { name: "gate reject", usage: "gate reject <dir> <scope|screen|design|final|review|plan:node-id> --reason TEXT [--json]", mutates: true, options: { "--reason": "value", ...json } },
42
+ { name: "attempt run", usage: "attempt run <dir> --repo PATH --commit SHA [--seed N] [--run NAME] [--label TEXT] [--retry ID] [--detach] [--json]", mutates: "network", options: { "--repo": "value", "--commit": "value", "--seed": "value", "--run": "value", "--label": "value", "--retry": "value", "--detach": "flag", ...json } },
43
+ { name: "attempt list", usage: "attempt list <dir> [--run NAME] [--status running|succeeded|failed|cancelled|unknown] [--limit N] [--offset N] [--json]", mutates: false, options: { "--run": "value", "--status": "value", "--limit": "value", "--offset": "value", ...json } },
44
+ { name: "attempt show", usage: "attempt show <dir> <id> [--json]", mutates: false, options: json },
45
+ { name: "attempt cancel", usage: "attempt cancel <dir> <id> [--reason TEXT] [--json]", mutates: "network", options: { "--reason": "value", ...json } },
46
+ { name: "attempt reconcile", usage: "attempt reconcile <dir> [--json]", mutates: "network", options: json },
47
+ { name: "attempt logs", usage: "attempt logs <dir> <id> [--stage candidate|evaluator] [--stream stdout|stderr] [--tail BYTES] [--json]", mutates: false, options: { "--stage": "value", "--stream": "value", "--tail": "value", ...json } },
48
+ { name: "lit search", usage: "lit search <dir> <query> [--source openalex|s2|arxiv|all] [--limit 1..50] [--year-from YYYY] [--json]", mutates: "network", options: { "--source": "value", "--limit": "value", "--year-from": "value", ...json } },
49
+ { name: "lit list", usage: "lit list <dir> [--status new|included|excluded] [--limit 1..500] [--json]", mutates: false, options: { "--status": "value", "--limit": "value", ...json } },
50
+ { name: "lit screen", usage: "lit screen <dir> <id>... --include|--exclude [--reason TEXT] [--json]", mutates: true, options: { "--include": "flag", "--exclude": "flag", "--reason": "value", ...json } },
51
+ { name: "lit card", usage: "lit card <dir> <id> --file PATH|- [--json]", mutates: true, options: { "--file": "value", ...json } },
52
+ { name: "lit verify", usage: "lit verify <dir> [<id>...] [--json]", mutates: "network", options: json },
53
+ { name: "lit bib", usage: "lit bib <dir> [--json]", mutates: true, options: json },
54
+ { name: "review add", usage: "review add <dir> --file PATH|- [--json]", mutates: true, options: { "--file": "value", ...json } },
55
+ { name: "review resolve", usage: "review resolve <dir> <review-id>/<finding-id> --reason TEXT [--json]", mutates: true, options: { "--reason": "value", ...json } },
56
+ { name: "review show", usage: "review show <dir> [--json]", mutates: false, options: json },
57
+ { name: "paper build", usage: "paper build <dir> [--json]", mutates: true, options: json },
58
+ { name: "paper check", usage: "paper check <dir> [--json]", mutates: false, options: json },
59
+ { name: "cite check", usage: "cite check <dir> <draft> [--json]", mutates: false, options: json },
60
+ { name: "playbook", usage: "playbook", mutates: false, options: {} },
61
+ { name: "mcp", usage: "mcp", mutates: false, options: {} },
62
+ ];
63
+ export function findCommand(args: string[]): { spec: CommandSpec; rest: string[] } | undefined {
64
+ for (const words of [2, 1]) {
65
+ const name = args.slice(0, words).join(" "), spec = COMMANDS.find(c => c.name === name);
66
+ if (spec) return { spec, rest: args.slice(words) };
67
+ }
68
+ return undefined;
69
+ }
70
+ export function help(): string {
71
+ return "ares - research engine: study ledger, literature search, verified references\n\n" + COMMANDS.map(c => ` ares ${c.usage}`).join("\n") + "\n\n ares --help | --version\n";
72
+ }