@staix/agent-research 0.0.0-stage → 0.2.1

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 (55) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/LICENSE +21 -0
  3. package/README.md +666 -2
  4. package/bin/ares.js +11 -0
  5. package/package.json +37 -4
  6. package/src/autonomy.ts +50 -0
  7. package/src/cli.ts +308 -0
  8. package/src/commands-table.ts +75 -0
  9. package/src/commands.ts +561 -0
  10. package/src/console/registry.ts +97 -0
  11. package/src/copilot.ts +91 -0
  12. package/src/exp/ablate.ts +140 -0
  13. package/src/exp/artifacts.ts +229 -0
  14. package/src/exp/attempts.ts +483 -0
  15. package/src/exp/executor.ts +207 -0
  16. package/src/exp/final.ts +197 -0
  17. package/src/exp/ideas.ts +117 -0
  18. package/src/exp/investigate.ts +187 -0
  19. package/src/exp/logs.ts +63 -0
  20. package/src/exp/metrics.ts +132 -0
  21. package/src/exp/protocol.ts +148 -0
  22. package/src/exp/selection.ts +53 -0
  23. package/src/exp/snapshot.ts +147 -0
  24. package/src/http.ts +106 -0
  25. package/src/index.ts +4 -0
  26. package/src/lit/bibtex.ts +61 -0
  27. package/src/lit/candidates.ts +148 -0
  28. package/src/lit/cards.ts +21 -0
  29. package/src/lit/cite.ts +26 -0
  30. package/src/lit/sources.ts +122 -0
  31. package/src/lit/verify.ts +110 -0
  32. package/src/lit/watch.ts +122 -0
  33. package/src/manifest.ts +39 -0
  34. package/src/mcp-tools.ts +210 -0
  35. package/src/mcp.ts +113 -0
  36. package/src/memory/amem.ts +97 -0
  37. package/src/memory/archive.ts +66 -0
  38. package/src/memory/lessons.ts +88 -0
  39. package/src/memory/skills.ts +99 -0
  40. package/src/paper/build.ts +73 -0
  41. package/src/paper/check.ts +181 -0
  42. package/src/paper/placeholders.ts +118 -0
  43. package/src/paper/review.ts +127 -0
  44. package/src/plan/controller.ts +222 -0
  45. package/src/plan/definition.ts +168 -0
  46. package/src/plan/hub.ts +56 -0
  47. package/src/plan/state.ts +270 -0
  48. package/src/playbook.ts +141 -0
  49. package/src/scope.ts +53 -0
  50. package/src/setup.ts +296 -0
  51. package/src/study-export.ts +254 -0
  52. package/src/study.ts +324 -0
  53. package/src/ui/panel.css +11 -0
  54. package/src/ui/panel.js +161 -0
  55. package/src/ui/plugin.ts +375 -0
package/bin/ares.js ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env -S bun --smol
2
+ const version = /^(\d+)\.(\d+)\.(\d+)/.exec(process.versions.bun ?? "");
3
+ if (!version || Number(version[1]) < 1 || (Number(version[1]) === 1 && (Number(version[2]) < 4 || (Number(version[2]) === 4 && Number(version[3]) < 2)))) {
4
+ console.error("ares: Bun >=1.4.2 is required. Install Bun: https://bun.sh");
5
+ process.exit(1);
6
+ }
7
+ const { main, runMcp } = await import("../src/cli.ts");
8
+ if (process.argv.length === 3 && process.argv[2] === "mcp") {
9
+ try { await runMcp(); }
10
+ catch (error) { console.error(`ares mcp: ${error instanceof Error ? error.message : "failed"}`); process.exitCode = 1; }
11
+ } else process.exitCode = await main(process.argv.slice(2));
package/package.json CHANGED
@@ -1,6 +1,39 @@
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.2.1",
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 scripts/readme-facts.ts && bun scripts/check-docs.mjs && bun test && bun pm pack --dry-run",
34
+ "release:prepare": "bun scripts/release-tag.ts prepare",
35
+ "release:tag": "bun scripts/check-docs.mjs --require-fresh && bun scripts/release-tag.ts tag",
36
+ "release:stage": "bun scripts/release.ts stage",
37
+ "release:smoke": "bun scripts/release-smoke.ts"
38
+ }
39
+ }
@@ -0,0 +1,50 @@
1
+ export const AUTONOMY_MODES = ["auto", "gates", "copilot", "step"] as const;
2
+ export const PHASES = ["scope", "literature", "hypotheses", "design", "investigate", "ablate", "final", "posthoc", "write", "review", "archive"] as const;
3
+ export type AutonomyMode = typeof AUTONOMY_MODES[number];
4
+ export type Phase = typeof PHASES[number];
5
+ export const ACCEPT_ARTIFACTS = ["hypotheses", "paper", "reviews"] as const;
6
+ export type AcceptArtifact = typeof ACCEPT_ARTIFACTS[number];
7
+ export type Autonomy = AutonomyMode | { default: AutonomyMode; phases: Partial<Record<Phase, AutonomyMode>> };
8
+
9
+ /** Direct gate mappings are explicit; plan gates receive their declared guarded phase. */
10
+ export const GATE_PHASES = { scope: "scope", screen: "literature", design: "design", final: "final", review: "review", "accept:hypotheses": "hypotheses", "accept:paper": "write", "accept:reviews": "review" } as const satisfies Record<string, Phase>;
11
+
12
+ function refuse(field: string, reason: string): never {
13
+ throw new Error(`${field}: ${reason}; correct the policy and retry ares autonomy set with a supported mode and phase`);
14
+ }
15
+ function object(raw: unknown): raw is Record<string, unknown> {
16
+ return typeof raw === "object" && raw !== null && !Array.isArray(raw) && (Object.getPrototypeOf(raw) === Object.prototype || Object.getPrototypeOf(raw) === null);
17
+ }
18
+ export function validateAutonomyMode(raw: unknown): AutonomyMode {
19
+ if (typeof raw !== "string" || !AUTONOMY_MODES.includes(raw as AutonomyMode)) refuse("autonomy.mode", "expected auto, gates, copilot or step");
20
+ return raw as AutonomyMode;
21
+ }
22
+ export function validatePhase(raw: unknown): Phase {
23
+ if (typeof raw !== "string" || !PHASES.includes(raw as Phase)) refuse("autonomy.phase", `expected one of ${PHASES.join(", ")}`);
24
+ return raw as Phase;
25
+ }
26
+ /** Validate strictly and return a detached policy, leaving the input unchanged. */
27
+ export function validateAutonomy(raw: unknown): Autonomy {
28
+ if (typeof raw === "string") return validateAutonomyMode(raw);
29
+ if (!object(raw)) refuse("autonomy", "mode string or policy object required");
30
+ for (const key of Reflect.ownKeys(raw)) if (key !== "default" && key !== "phases") refuse("autonomy", "unknown policy field");
31
+ const mode = validateAutonomyMode(raw.default);
32
+ if (!object(raw.phases)) refuse("autonomy.phases", "phase-to-mode object required");
33
+ const phases: Partial<Record<Phase, AutonomyMode>> = {};
34
+ for (const key of Reflect.ownKeys(raw.phases)) {
35
+ const phase = validatePhase(key);
36
+ phases[phase] = validateAutonomyMode(raw.phases[phase]);
37
+ }
38
+ return { default: mode, phases };
39
+ }
40
+
41
+ export function resolvePhaseAutonomy(policy: Autonomy, phase?: Phase): AutonomyMode {
42
+ if (typeof policy === "string") return policy;
43
+ return phase === undefined ? policy.default : policy.phases[phase] ?? policy.default;
44
+ }
45
+
46
+ /** Unknown gate kinds and phase-ambiguous plan gates use the study default. */
47
+ export function resolveAutonomy(policy: Autonomy, gate: string, guardedPhase?: Phase): AutonomyMode {
48
+ const phase = gate.startsWith("plan:") ? guardedPhase : Object.hasOwn(GATE_PHASES, gate) ? GATE_PHASES[gate as keyof typeof GATE_PHASES] : undefined;
49
+ return resolvePhaseAutonomy(policy, phase);
50
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,308 @@
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, releaseStudyLocksForExit } 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
+ const completed = await ops.planRun(positional[0]!, { ...(parallel !== undefined ? { parallel } : {}) }, ctx);
101
+ result = completed; if (completed.state === "failed") code = 1; break;
102
+ }
103
+ case "manifest": count(spec, positional, 0); result = ops.manifest(); break;
104
+ case "playbook": count(spec, positional, 0); return { text: PLAYBOOK.trimEnd(), code: 0 };
105
+ case "mcp": throw new Error("mcp: run `ares mcp` with no other arguments");
106
+ case "autonomy show": count(spec, positional, 1); result = ops.autonomyShow(positional[0]!); break;
107
+ case "autonomy set": count(spec, positional, 2); result = await ops.autonomySet(positional[0]!, positional[1]!, str("--phase"), ctx); break;
108
+ case "init": {
109
+ count(spec, positional, 1);
110
+ const question = str("--question");
111
+ if (question === undefined) throw new Error("init: --question is required");
112
+ result = ops.init(positional[0]!, { question, ...(str("--mode") ? { mode: str("--mode")! } : {}), ...(str("--autonomy") ? { autonomy: str("--autonomy")! } : {}) }, ctx);
113
+ break;
114
+ }
115
+ case "plan set": {
116
+ count(spec, positional, 1);
117
+ const file = str("--file");
118
+ if (file === undefined) throw new Error("plan set: --file is required");
119
+ result = await ops.planSet(positional[0]!, await readInput(file, io), ctx); break;
120
+ }
121
+ case "plan control": {
122
+ count(spec, positional, 2, 3);
123
+ const commandId = str("--command-id"), expectedRevision = integer(flags["--expected-revision"], "--expected-revision");
124
+ 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");
125
+ 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;
126
+ }
127
+ case "plan show": count(spec, positional, 1); result = ops.planShow(positional[0]!); break;
128
+ case "status": count(spec, positional, 1); result = ops.status(positional[0]!); break;
129
+ case "study export": {
130
+ count(spec, positional, 1); const bundle = str("--bundle");
131
+ if (bundle === undefined) throw new Error("study export: --bundle DIR is required; pass a new or empty output directory");
132
+ result = await ops.studyExport(positional[0]!, { bundle, ...(flags["--list"] ? { list: true } : {}) }); break;
133
+ }
134
+ case "scope": {
135
+ count(spec, positional, 1);
136
+ const file = str("--file");
137
+ if (file === undefined) throw new Error("scope: --file is required");
138
+ result = await ops.scope(positional[0]!, await readInput(file, io), ctx);
139
+ break;
140
+ }
141
+ case "idea add": {
142
+ count(spec, positional, 1); const file = str("--file");
143
+ if (file === undefined) throw new Error("idea add: input missing; pass --file PATH or --file - for JSON");
144
+ result = await ops.ideaAdd(positional[0]!, await readInput(file, io), ctx); break;
145
+ }
146
+ case "idea decide": {
147
+ count(spec, positional, 3); const reason = str("--reason"), attempts = str("--attempts");
148
+ 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");
149
+ 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;
150
+ }
151
+ case "investigate show": count(spec, positional, 1); result = ops.investigateShow(positional[0]!); break;
152
+ case "investigate close": count(spec, positional, 1); result = await ops.investigateClose(positional[0]!, ctx); break;
153
+ case "ablate add": {
154
+ count(spec, positional, 1); const file = str("--file");
155
+ if (file === undefined) throw new Error("ablate add: input missing; pass --file PATH or --file - for JSON");
156
+ result = await ops.ablateAdd(positional[0]!, await readInput(file, io), ctx); break;
157
+ }
158
+ case "ablate show": count(spec, positional, 1); result = ops.ablateShow(positional[0]!); break;
159
+ case "ablate close": count(spec, positional, 1); result = await ops.ablateClose(positional[0]!, ctx); break;
160
+ case "posthoc show": count(spec, positional, 1); result = ops.posthocShow(positional[0]!); break;
161
+ case "final close": count(spec, positional, 1); result = await ops.finalClose(positional[0]!, ctx); break;
162
+ case "final seeds": {
163
+ count(spec, positional, 1); const file = str("--file");
164
+ if (file === undefined) throw new Error("final seeds: input missing; pass --file PATH or --file - for JSON");
165
+ result = await ops.finalSeeds(positional[0]!, await readInput(file, io), ctx); break;
166
+ }
167
+ case "selection": {
168
+ count(spec, positional, 1);
169
+ const file = str("--file");
170
+ if (file === undefined) throw new Error("selection: input missing; pass --file PATH or --file - for a JSON rule");
171
+ result = await ops.selection(positional[0]!, await readInput(file, io), ctx); break;
172
+ }
173
+ case "protocol": {
174
+ count(spec, positional, 1);
175
+ const file = str("--file");
176
+ if (file === undefined) throw new Error("protocol: input file missing; use --file PATH or --file - for JSON standard input");
177
+ result = await ops.protocol(positional[0]!, await readInput(file, io), ctx); break;
178
+ }
179
+ case "executor set": {
180
+ count(spec, positional, 1);
181
+ const kind = str("--kind"), workdir = str("--workdir");
182
+ if (!kind || !workdir) throw new Error("executor set: kind/workdir missing; use --kind local|ssh --workdir PATH and --host ALIAS for ssh");
183
+ result = await ops.executorSet(positional[0]!, { v: 1, kind, workdir, ...(str("--host") !== undefined ? { host: str("--host") } : {}) }, ctx); break;
184
+ }
185
+ case "executor check": count(spec, positional, 1); result = await ops.executorCheck(positional[0]!, ctx); break;
186
+ case "attempt run": {
187
+ count(spec, positional, 1);
188
+ const repo = str("--repo"), commit = str("--commit"), chosenSeed = seed(str("--seed"));
189
+ if (!repo || !commit) throw new Error("attempt run: repo/commit missing; use --repo PATH --commit FULL_40_HEX_SHA");
190
+ 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);
191
+ if (attempt.status !== "running" && attempt.status !== "succeeded") code = 1;
192
+ result = attempt; break;
193
+ }
194
+ case "attempt list": {
195
+ count(spec, positional, 1); const limit = integer(flags["--limit"], "--limit"), offset = integer(flags["--offset"], "--offset");
196
+ result = ops.attemptList(positional[0]!, { ...(str("--run") !== undefined ? { run: str("--run") } : {}), ...(str("--status") !== undefined ? { status: str("--status") } : {}), ...(limit !== undefined ? { limit } : {}), ...(offset !== undefined ? { offset } : {}) }); break;
197
+ }
198
+ case "attempt show": count(spec, positional, 2); result = ops.attemptShow(positional[0]!, positional[1]!); break;
199
+ case "attempt cancel": count(spec, positional, 2); result = await ops.attemptCancel(positional[0]!, positional[1]!, str("--reason"), ctx); break;
200
+ case "attempt reconcile": count(spec, positional, 1); result = await ops.attemptReconcile(positional[0]!, ctx); break;
201
+ case "attempt logs": {
202
+ count(spec, positional, 2); const tailBytes = integer(flags["--tail"], "--tail");
203
+ 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);
204
+ if (!flags["--json"]) return { text: logs.logs.map(log => `[${log.stage} ${log.stream}${log.truncated ? ", truncated copy" : ""}]\n${log.text}`).join("\n"), code: 0 };
205
+ result = logs; break;
206
+ }
207
+ case "gate list": count(spec, positional, 1); result = ops.gateList(positional[0]!); break;
208
+ case "gate approve": case "gate reject":
209
+ count(spec, positional, 2);
210
+ result = await ops.gateDecide(positional[0]!, positional[1]!, spec.name === "gate approve" ? "approve" : "reject", str("--reason"), ctx);
211
+ break;
212
+ case "lit search": {
213
+ count(spec, positional, 2);
214
+ const limit = integer(flags["--limit"], "--limit"), yearFrom = integer(flags["--year-from"], "--year-from");
215
+ const out = await ops.search(positional[0]!, positional[1]!, { ...(str("--source") ? { source: str("--source")! } : {}), ...(limit !== undefined ? { limit } : {}), ...(yearFrom !== undefined ? { yearFrom } : {}) }, ctx);
216
+ if (!out.sources.some(s => s.ok)) code = 1;
217
+ result = out;
218
+ break;
219
+ }
220
+ case "lit list": {
221
+ count(spec, positional, 1);
222
+ const limit = integer(flags["--limit"], "--limit");
223
+ result = ops.list(positional[0]!, { ...(str("--status") ? { status: str("--status")! } : {}), ...(limit !== undefined ? { limit } : {}) });
224
+ break;
225
+ }
226
+ case "lit screen": {
227
+ if (positional.length < 2) throw new Error(`usage: ares ${spec.usage}`);
228
+ if (!!flags["--include"] === !!flags["--exclude"]) throw new Error("lit screen: exactly one of --include or --exclude");
229
+ result = await ops.screen(positional[0]!, positional.slice(1), flags["--include"] ? "include" : "exclude", str("--reason"), ctx);
230
+ break;
231
+ }
232
+ case "lit card": {
233
+ count(spec, positional, 2);
234
+ const file = str("--file");
235
+ if (file === undefined) throw new Error("lit card: --file is required");
236
+ result = await ops.card(positional[0]!, positional[1]!, await readInput(file, io), ctx);
237
+ break;
238
+ }
239
+ case "lit verify": if (!positional.length) throw new Error(`usage: ares ${spec.usage}`); result = await ops.verify(positional[0]!, positional.slice(1), ctx); break;
240
+ case "lit bib": count(spec, positional, 1); result = await ops.bib(positional[0]!, ctx); break;
241
+ case "review add": {
242
+ count(spec, positional, 1); const file = str("--file");
243
+ if (file === undefined) throw new Error("review add: --file is required; pass --file PATH|- with a current-build report");
244
+ result = await ops.reviewAdd(positional[0]!, await readInput(file, io), ctx); break;
245
+ }
246
+ case "review resolve": {
247
+ count(spec, positional, 2); const reason = str("--reason");
248
+ if (reason === undefined) throw new Error("review resolve: --reason is required; explain how the finding was resolved");
249
+ result = await ops.reviewResolve(positional[0]!, positional[1]!, reason, ctx); break;
250
+ }
251
+ case "review show": count(spec, positional, 1); result = ops.reviewShow(positional[0]!); break;
252
+ case "paper build": count(spec, positional, 1); result = await ops.paperBuild(positional[0]!, ctx); break;
253
+ case "paper check": {
254
+ count(spec, positional, 1); const report = ops.paperCheck(positional[0]!);
255
+ if (!report.ok) code = 1;
256
+ result = report; break;
257
+ }
258
+ case "cite check": {
259
+ count(spec, positional, 2);
260
+ const report = ops.cite(positional[0]!, positional[1]!);
261
+ if (!report.ok) code = 1;
262
+ result = report;
263
+ break;
264
+ }
265
+ default: throw new Error(`unknown command: ${spec.name}`);
266
+ }
267
+ return { text: JSON.stringify(result, null, flags["--json"] ? undefined : 2), code };
268
+ }
269
+
270
+ /** `ares mcp`: stdout carries protocol frames only. */
271
+ export async function runMcp(env: NodeJS.ProcessEnv = process.env, http?: Http): Promise<void> {
272
+ const [{ serveMcp }, { createAresMcp }] = await Promise.all([import("./mcp.ts"), import("./mcp-tools.ts")]);
273
+ const server = createAresMcp(env, http);
274
+ const terminate = () => {
275
+ try { server.terminate(); }
276
+ finally { releaseStudyLocksForExit(); process.exit(143); }
277
+ };
278
+ process.once("SIGTERM", terminate);
279
+ try { await serveMcp({ tools: server.tools, handlers: server.handlers, name: "ares", version }); }
280
+ finally { try { await server.waitForIdle(); } finally { process.off("SIGTERM", terminate); } }
281
+ }
282
+
283
+ /** Standard input stops at the input bound instead of buffering everything first. */
284
+ async function readStdinBounded(): Promise<string> {
285
+ const chunks: Uint8Array[] = [];
286
+ let size = 0;
287
+ for await (const chunk of Bun.stdin.stream()) {
288
+ size += chunk.byteLength;
289
+ if (size > INPUT_BYTES) throw new Error(`standard input larger than ${INPUT_BYTES} bytes`);
290
+ chunks.push(chunk);
291
+ }
292
+ return new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks));
293
+ }
294
+
295
+ export async function main(args: string[]): Promise<number> {
296
+ const stop = new AbortController(), interrupt = () => stop.abort();
297
+ if (args[0] === "run") { process.on("SIGINT", interrupt); process.on("SIGTERM", interrupt); }
298
+ try {
299
+ const output = await run(args, process.env, { readStdin: readStdinBounded, signal: stop.signal });
300
+ console.log(output.text);
301
+ return output.code;
302
+ } catch (error) {
303
+ console.error(`ares: ${error instanceof Error ? error.message : "operation failed"}`);
304
+ return 1;
305
+ } finally {
306
+ if (args[0] === "run") { process.off("SIGINT", interrupt); process.off("SIGTERM", interrupt); }
307
+ }
308
+ }
@@ -0,0 +1,75 @@
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: "autonomy show", usage: "autonomy show <dir> [--json]", mutates: false, options: json },
6
+ { name: "autonomy set", usage: "autonomy set <dir> <auto|gates|copilot|step> [--phase PHASE] [--json]", mutates: true, options: { "--phase": "value", ...json } },
7
+ { name: "skill propose", usage: "skill propose <dir> --file PATH|- [--json]", mutates: true, options: { "--file": "value", ...json } },
8
+ { name: "skill decide", usage: "skill decide <dir> <name> <accept|reject|withdraw> --reason TEXT [--pr URL] [--json]", mutates: true, options: { "--reason": "value", "--pr": "value", ...json } },
9
+ { name: "skill list", usage: "skill list <dir> [--json]", mutates: false, options: json },
10
+ { name: "lessons", usage: "lessons <dir> [--for TEXT] [--half-life DAYS] [--limit N] [--json]", mutates: false, options: { "--for": "value", "--half-life": "value", "--limit": "value", ...json } },
11
+ { name: "archive", usage: "archive <dir> [--confirm FINGERPRINT] [--json]", mutates: "network", options: { "--confirm": "value", ...json } },
12
+ { 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 } },
13
+ { name: "watch set", usage: "watch set <dir> --file PATH|- [--json]", mutates: true, options: { "--file": "value", ...json } },
14
+ { name: "watch run", usage: "watch run <dir> [--json]", mutates: "network", options: json },
15
+ { name: "watch show", usage: "watch show <dir> [--json]", mutates: false, options: json },
16
+ { name: "console add", usage: "console add <dir> [--json]", mutates: true, options: json },
17
+ { name: "console remove", usage: "console remove <dir> [--json]", mutates: true, options: json },
18
+ { name: "console list", usage: "console list [--json]", mutates: false, options: json },
19
+ { name: "manifest", usage: "manifest [--json]", mutates: false, options: json },
20
+ { name: "init", usage: "init <dir> --question TEXT [--mode survey|experiment] [--autonomy auto|gates|copilot|step] [--json]", mutates: true, options: { "--question": "value", "--mode": "value", "--autonomy": "value", ...json } },
21
+ { name: "run", usage: "run <dir> [--parallel 1..8] [--json]", mutates: "network", options: { "--parallel": "value", ...json } },
22
+ { name: "plan set", usage: "plan set <dir> --file PATH|- [--json]", mutates: true, options: { "--file": "value", ...json } },
23
+ { 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 } },
24
+ { name: "plan show", usage: "plan show <dir> [--json]", mutates: false, options: json },
25
+ { name: "status", usage: "status <dir> [--json]", mutates: false, options: json },
26
+ { name: "study export", usage: "study export <dir> --bundle DIR [--list] [--json]", mutates: true, options: { "--bundle": "value", "--list": "flag", ...json } },
27
+ { name: "scope", usage: "scope <dir> --file PATH|- [--json]", mutates: true, options: { "--file": "value", ...json } },
28
+ { name: "idea add", usage: "idea add <dir> --file PATH|- [--json]", mutates: true, options: { "--file": "value", ...json } },
29
+ { 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 } },
30
+ { name: "investigate show", usage: "investigate show <dir> [--json]", mutates: false, options: json },
31
+ { name: "investigate close", usage: "investigate close <dir> [--json]", mutates: true, options: json },
32
+ { name: "ablate add", usage: "ablate add <dir> --file PATH|- [--json]", mutates: true, options: { "--file": "value", ...json } },
33
+ { name: "ablate show", usage: "ablate show <dir> [--json]", mutates: false, options: json },
34
+ { name: "ablate close", usage: "ablate close <dir> [--json]", mutates: true, options: json },
35
+ { name: "posthoc show", usage: "posthoc show <dir> [--json]", mutates: false, options: json },
36
+ { name: "final close", usage: "final close <dir> [--json]", mutates: true, options: json },
37
+ { name: "final seeds", usage: "final seeds <dir> --file PATH|- [--json]", mutates: true, options: { "--file": "value", ...json } },
38
+ { name: "selection", usage: "selection <dir> --file PATH|- [--json]", mutates: true, options: { "--file": "value", ...json } },
39
+ { name: "protocol", usage: "protocol <dir> --file PATH|- [--json]", mutates: true, options: { "--file": "value", ...json } },
40
+ { 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 } },
41
+ { name: "executor check", usage: "executor check <dir> [--json]", mutates: "network", options: json },
42
+ { name: "gate list", usage: "gate list <dir> [--json]", mutates: false, options: json },
43
+ { name: "gate approve", usage: "gate approve <dir> <scope|screen|design|final|review|plan:node-id|accept:artifact> [--reason TEXT] [--json]", mutates: true, options: { "--reason": "value", ...json } },
44
+ { name: "gate reject", usage: "gate reject <dir> <scope|screen|design|final|review|plan:node-id|accept:artifact> --reason TEXT [--json]", mutates: true, options: { "--reason": "value", ...json } },
45
+ { 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 } },
46
+ { 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 } },
47
+ { name: "attempt show", usage: "attempt show <dir> <id> [--json]", mutates: false, options: json },
48
+ { name: "attempt cancel", usage: "attempt cancel <dir> <id> [--reason TEXT] [--json]", mutates: "network", options: { "--reason": "value", ...json } },
49
+ { name: "attempt reconcile", usage: "attempt reconcile <dir> [--json]", mutates: "network", options: json },
50
+ { 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 } },
51
+ { 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 } },
52
+ { name: "lit list", usage: "lit list <dir> [--status new|included|excluded] [--limit 1..500] [--json]", mutates: false, options: { "--status": "value", "--limit": "value", ...json } },
53
+ { name: "lit screen", usage: "lit screen <dir> <id>... --include|--exclude [--reason TEXT] [--json]", mutates: true, options: { "--include": "flag", "--exclude": "flag", "--reason": "value", ...json } },
54
+ { name: "lit card", usage: "lit card <dir> <id> --file PATH|- [--json]", mutates: true, options: { "--file": "value", ...json } },
55
+ { name: "lit verify", usage: "lit verify <dir> [<id>...] [--json]", mutates: "network", options: json },
56
+ { name: "lit bib", usage: "lit bib <dir> [--json]", mutates: true, options: json },
57
+ { name: "review add", usage: "review add <dir> --file PATH|- [--json]", mutates: true, options: { "--file": "value", ...json } },
58
+ { name: "review resolve", usage: "review resolve <dir> <review-id>/<finding-id> --reason TEXT [--json]", mutates: true, options: { "--reason": "value", ...json } },
59
+ { name: "review show", usage: "review show <dir> [--json]", mutates: false, options: json },
60
+ { name: "paper build", usage: "paper build <dir> [--json]", mutates: true, options: json },
61
+ { name: "paper check", usage: "paper check <dir> [--json]", mutates: false, options: json },
62
+ { name: "cite check", usage: "cite check <dir> <draft> [--json]", mutates: false, options: json },
63
+ { name: "playbook", usage: "playbook", mutates: false, options: {} },
64
+ { name: "mcp", usage: "mcp", mutates: false, options: {} },
65
+ ];
66
+ export function findCommand(args: string[]): { spec: CommandSpec; rest: string[] } | undefined {
67
+ for (const words of [2, 1]) {
68
+ const name = args.slice(0, words).join(" "), spec = COMMANDS.find(c => c.name === name);
69
+ if (spec) return { spec, rest: args.slice(words) };
70
+ }
71
+ return undefined;
72
+ }
73
+ export function help(): string {
74
+ 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";
75
+ }