@pavel.sommer/agiter 0.0.0-stage → 0.8.0-alpha.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.
- package/README.md +64 -2
- package/bin/agiter.mjs +326 -0
- package/defs/.models.architect.json +9 -0
- package/defs/.models.author.json +11 -0
- package/defs/.models.drafter.json +11 -0
- package/defs/.models.json +11 -0
- package/defs/.models.reviewer.json +9 -0
- package/defs/.models.worker.json +11 -0
- package/defs/agents.json +48 -0
- package/defs/artifacts/checks.json +12 -0
- package/defs/artifacts/draft.json +18 -0
- package/defs/artifacts/epic.json +17 -0
- package/defs/artifacts/idea.json +10 -0
- package/defs/artifacts/initiative.json +18 -0
- package/defs/artifacts/report.json +12 -0
- package/defs/artifacts/spec.json +18 -0
- package/defs/artifacts/work.json +17 -0
- package/defs/tasks/checks.json +7 -0
- package/defs/tasks/create-draft.json +10 -0
- package/defs/tasks/create-epics.json +10 -0
- package/defs/tasks/create-initiative.json +9 -0
- package/defs/tasks/create-spec.json +10 -0
- package/defs/tasks/create-tests.json +14 -0
- package/defs/tasks/create-work.json +13 -0
- package/defs/tasks/review.json +9 -0
- package/index.mjs +32 -0
- package/package.json +43 -5
- package/sidecar.mjs +44 -0
- package/src/agents.mjs +144 -0
- package/src/artifacts.mjs +196 -0
- package/src/defs.mjs +75 -0
- package/src/engine.mjs +650 -0
- package/src/fake.mjs +34 -0
- package/src/frontmatter.mjs +62 -0
- package/src/git.mjs +121 -0
- package/src/glob.mjs +28 -0
- package/src/init.mjs +22 -0
- package/src/log.mjs +53 -0
- package/src/models.mjs +121 -0
- package/src/pipeline.mjs +90 -0
- package/src/prompt.mjs +72 -0
- package/src/roles.mjs +22 -0
- package/src/tools.mjs +34 -0
package/README.md
CHANGED
|
@@ -1,3 +1,65 @@
|
|
|
1
|
-
#
|
|
1
|
+
# agiter
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A client-agnostic harness for spec-driven agentic development. The design is in
|
|
4
|
+
[docs/design-recap.md](docs/design-recap.md).
|
|
5
|
+
|
|
6
|
+
A run's pipeline is always given explicitly: a comma list of tokens, or a pipeline file
|
|
7
|
+
(`*.json`, `{ "steps": [...] }`). A token is `task` (one round, no review) or `task:N` (at most
|
|
8
|
+
N rounds; each round is a version and its review report).
|
|
9
|
+
|
|
10
|
+
Status: step 1 (inputs, artifacts, tasks, pipelines, the run engine, simulation) and a first
|
|
11
|
+
real executor: `agiter run` calls the agent CLI of each role's model profile
|
|
12
|
+
(`.agiter/config/.models.<role>.json`, `.agiter/config/.models.json`, copied from the package's defaults on the first run; agents in `defs/agents.json`). In a git repository
|
|
13
|
+
every run works on its own branch `agiter/<run>`, the harness commits every call, the scope
|
|
14
|
+
of a step is checked against what git sees, and `agiter finish [--merge]` ends the run.
|
|
15
|
+
|
|
16
|
+
## Layout
|
|
17
|
+
|
|
18
|
+
| Path | Content |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| `index.mjs` | the public API |
|
|
21
|
+
| `bin/agiter.mjs` | the CLI on commander (`agiter --help`, `agiter <command> --help`) |
|
|
22
|
+
| `sidecar.mjs` | JSON-RPC over stdio for long-lived clients (Far Manager) |
|
|
23
|
+
| `src/` | the modules: definitions, pipelines, artifacts, engine, tools, log |
|
|
24
|
+
| `defs/` | the package's artifact types and tasks |
|
|
25
|
+
| `test/` | `npm test` (node:test, no dependencies) |
|
|
26
|
+
|
|
27
|
+
A project keeps everything in `.agiter/`: `docs/<run>/` (the chain of each run), `runs/`,
|
|
28
|
+
`logs/`, its own definitions (`artifacts/`, `tasks/`, merged over the package's) and
|
|
29
|
+
`checks.json` (the required checks).
|
|
30
|
+
|
|
31
|
+
## A run's files
|
|
32
|
+
|
|
33
|
+
```text
|
|
34
|
+
.agiter/docs/<run>/<run>.md the input, copied as it is (never changed)
|
|
35
|
+
.agiter/docs/<run>/<run>.draft.1.md round 1 of create-draft:3
|
|
36
|
+
.agiter/docs/<run>/<run>.draft.1.report.md its review: Block or Done, cumulative
|
|
37
|
+
.agiter/docs/<run>/<run>.draft.2.md round 2: input + draft.1 + its report
|
|
38
|
+
...
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
A version is Active when another step follows and it has no review or its report is Done;
|
|
42
|
+
otherwise Draft. A step takes only prose or an Active artifact whose latest report is not
|
|
43
|
+
Block. The state of a run is its files: `agiter continue` walks the steps again and repeats
|
|
44
|
+
nothing done.
|
|
45
|
+
|
|
46
|
+
## Try it
|
|
47
|
+
|
|
48
|
+
```text
|
|
49
|
+
agiter list tasks
|
|
50
|
+
agiter plan --pipeline "create-draft:3,create-spec:2,create-work,checks" --text "Add a login form"
|
|
51
|
+
agiter simulate --pipeline "create-draft:3,create-spec:2,create-work" --text "Add a login form" --script script.json
|
|
52
|
+
agiter continue --root <project printed by simulate>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
A simulation script says what the fake model returns, per task, role and attempt:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"steps": {
|
|
60
|
+
"create-draft#1/review": { "findings": [{ "text": "Problem unclear" }] },
|
|
61
|
+
"create-draft#2": { "responses": { "F-1": "fixed: Problem rewritten" } },
|
|
62
|
+
"create-work": { "changes": ["src/login.js"] }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
package/bin/agiter.mjs
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* agiter: the command line, a thin layer over the API (index.mjs), parsed with commander.
|
|
4
|
+
* `agiter --help` and `agiter <command> --help` list the commands and their options.
|
|
5
|
+
* Exit codes: 0 done (or planned), 1 blocked, failed or invalid, 2 usage.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import os from "node:os";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { Command, CommanderError, Option } from "commander";
|
|
12
|
+
import * as api from "../index.mjs";
|
|
13
|
+
import { logCli } from "../src/log.mjs";
|
|
14
|
+
|
|
15
|
+
const USAGE = 2;
|
|
16
|
+
|
|
17
|
+
function tokenOf(s) { return s.rounds ? `${s.task}:${s.rounds}` : s.task; }
|
|
18
|
+
|
|
19
|
+
function printRun(run) {
|
|
20
|
+
console.log(`run ${run.id}: ${run.status}${run.git ? ` (branch ${run.git.branch}${run.finished ? `, finished${run.finished.merged ? ", merged" : ""}` : ""})` : ""}`);
|
|
21
|
+
console.log(` pipeline ${run.pipeline.steps.map(tokenOf).join(",")}, input ${run.input.type}`);
|
|
22
|
+
for (const s of run.steps) {
|
|
23
|
+
const what = s.outcome === "failed" ? `failed [${s.guard}] ${s.error}` : `${s.file ? path.posix.basename(s.file) : ""}${s.verdict ? ` ${s.verdict}` : ""}`;
|
|
24
|
+
console.log(` ${String(s.step).padEnd(3)} ${s.task.padEnd(13)} ${(s.round ? `r${s.round}` : "").padEnd(4)}${s.role.padEnd(9)} ${what}`);
|
|
25
|
+
}
|
|
26
|
+
if (run.error) console.log(` ${run.status} at step ${run.error.step} ${run.error.task}: ${run.error.message}\n next: ${run.status === "blocked" ? "a new input with the answers, or " : ""}agiter continue ${run.id}`);
|
|
27
|
+
else if (run.status === "done" && run.git && !run.finished) console.log(` next: agiter finish ${run.id} --merge (or without --merge to keep the branch)`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const pad2 = (n) => String(n).padStart(2, "0");
|
|
31
|
+
/** A time in the machine's zone: yyyy-MM-dd HH:mm. */
|
|
32
|
+
const local = (iso) => { const d = new Date(iso); return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}`; };
|
|
33
|
+
const duration = (s) => (s < 60 ? `${Math.round(s)} s` : s < 3600 ? `${Math.floor(s / 60)} min ${pad2(Math.round(s % 60))} s` : `${Math.floor(s / 3600)} h ${pad2(Math.floor((s % 3600) / 60))} min`);
|
|
34
|
+
|
|
35
|
+
/** The RegExp of a --list or --delete value (true: no pattern), or a usage error. */
|
|
36
|
+
function patternOf(value, option, cmd) {
|
|
37
|
+
if (typeof value !== "string") return null;
|
|
38
|
+
try { return new RegExp(value, "i"); } catch (e) { cmd.error(`error: --${option}: ${e.message}`, { exitCode: USAGE, code: "agiter.pattern" }); }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const runLine = (r) => `${local(r.created)} ${r.status.padEnd(8)} ${duration(r.seconds).padStart(12)} ${String(r.calls).padStart(3)} calls ${r.id} ${r.pipeline}${r.name ? ` "${r.name}"` : ""}${r.finished ? (r.finished.merged ? " merged" : " finished") : ""}${r.error ? ` [${r.error}]` : ""}`;
|
|
42
|
+
|
|
43
|
+
/** run --list [pattern]: the runs, one per line. */
|
|
44
|
+
function listRunsOf(opts, cmd) {
|
|
45
|
+
if (opts.failed || opts.done || opts.all || opts.yes) cmd.error(`error: options '--failed', '--done', '--all' and '--yes' belong to --delete`, { exitCode: USAGE, code: "agiter.delete" });
|
|
46
|
+
const pattern = patternOf(opts.list, "list", cmd);
|
|
47
|
+
const runs = api.listRuns(rootOf(opts), pattern, { order: opts.asc ? "asc" : "desc" });
|
|
48
|
+
print(opts, runs, () => {
|
|
49
|
+
for (const r of runs) console.log(runLine(r));
|
|
50
|
+
if (!runs.length) console.log(pattern ? `no run matches /${opts.list}/` : "no runs");
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Ask a yes/no question on the terminal. */
|
|
55
|
+
async function confirm(question) {
|
|
56
|
+
const { createInterface } = await import("node:readline/promises");
|
|
57
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
58
|
+
try { return /^y(es)?$/i.test((await rl.question(question)).trim()); } finally { rl.close(); }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** run --delete --all [--yes]: every run and last.json, after two warnings. */
|
|
62
|
+
async function deleteAllOf(opts, cmd, root) {
|
|
63
|
+
if (typeof opts.delete === "string" || opts.failed || opts.done) cmd.error("error: --all stands alone: no pattern, --failed or --done with it", { exitCode: USAGE, code: "agiter.all" });
|
|
64
|
+
const runs = api.listRuns(root);
|
|
65
|
+
const folders = fs.existsSync(path.join(root, ".agiter", "runs")) ? fs.readdirSync(path.join(root, ".agiter", "runs"), { withFileTypes: true }).filter((d) => d.isDirectory()).length : 0;
|
|
66
|
+
if (!folders) { print(opts, { deleted: [] }, () => console.log("no run to delete")); return; }
|
|
67
|
+
if (!opts.yes) {
|
|
68
|
+
if (!process.stdin.isTTY) cmd.error(`error: --delete --all would delete all ${folders} run(s); confirm with --yes when the input is not a terminal`, { exitCode: USAGE, code: "agiter.confirm" });
|
|
69
|
+
for (const r of runs) console.log(runLine(r));
|
|
70
|
+
if (folders > runs.length) console.log(`and ${folders - runs.length} broken run folder(s)`);
|
|
71
|
+
console.log(`\nWARNING: this deletes ALL ${folders} run(s) of ${root}: their records, audits, logs of calls and every artifact they made. It cannot be undone.`);
|
|
72
|
+
if (!(await confirm("Delete all runs? [y/N] "))) { console.log("nothing deleted"); process.exitCode = 1; return; }
|
|
73
|
+
const { createInterface } = await import("node:readline/promises");
|
|
74
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
75
|
+
let typed;
|
|
76
|
+
try { typed = (await rl.question(`SECOND WARNING: type "delete all" to delete all ${folders} run(s): `)).trim(); } finally { rl.close(); }
|
|
77
|
+
if (typed !== "delete all") { console.log("nothing deleted"); process.exitCode = 1; return; }
|
|
78
|
+
}
|
|
79
|
+
logCli(root, process.argv.slice(2));
|
|
80
|
+
const { deleted } = api.deleteRuns(root, { all: true });
|
|
81
|
+
print(opts, { deleted }, () => console.log(`deleted all ${deleted.length} run(s); last.json cleared`));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** run --delete [pattern] [--failed] [--done] [--yes]: delete runs, after showing them. */
|
|
85
|
+
async function deleteRunsOf(opts, cmd) {
|
|
86
|
+
if (opts.asc || opts.desc) cmd.error(`error: options '--asc' and '--desc' belong to --list`, { exitCode: USAGE, code: "agiter.order" });
|
|
87
|
+
const root = rootOf(opts);
|
|
88
|
+
if (opts.all) return deleteAllOf(opts, cmd, root);
|
|
89
|
+
const pattern = patternOf(opts.delete, "delete", cmd);
|
|
90
|
+
const statuses = [...(opts.failed ? ["failed"] : []), ...(opts.done ? ["done"] : [])];
|
|
91
|
+
if (!pattern && !statuses.length) {
|
|
92
|
+
const all = api.listRuns(root);
|
|
93
|
+
const message = "--delete needs a restriction: a pattern, --failed or --done (a pattern \".\" selects every run)";
|
|
94
|
+
print(opts, { deleted: [], problems: [message], runs: all }, () => {
|
|
95
|
+
console.log(`${message}. The runs:`);
|
|
96
|
+
for (const r of all) console.log(` ${runLine(r)}`);
|
|
97
|
+
if (!all.length) console.log(" no runs");
|
|
98
|
+
});
|
|
99
|
+
process.exitCode = USAGE;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const runs = api.listRuns(root, pattern).filter((r) => !statuses.length || statuses.includes(r.status));
|
|
103
|
+
if (!runs.length) { print(opts, { deleted: [] }, () => console.log("no run to delete")); return; }
|
|
104
|
+
if (!opts.yes) {
|
|
105
|
+
if (!process.stdin.isTTY) cmd.error(`error: --delete would delete ${runs.length} run(s); confirm with --yes when the input is not a terminal`, { exitCode: USAGE, code: "agiter.confirm" });
|
|
106
|
+
for (const r of runs) console.log(runLine(r));
|
|
107
|
+
if (!(await confirm(`Delete these ${runs.length} run(s) with their artifacts? This cannot be undone. [y/N] `))) { console.log("nothing deleted"); process.exitCode = 1; return; }
|
|
108
|
+
}
|
|
109
|
+
logCli(root, process.argv.slice(2));
|
|
110
|
+
const { deleted } = api.deleteRuns(root, { pattern, statuses });
|
|
111
|
+
print(opts, { deleted }, () => console.log(`deleted ${deleted.length} run(s)${opts.yes ? `: ${deleted.map((r) => r.id).join(", ")}` : ""}`));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const clockOf = (iso) => { const d = new Date(iso); return `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`; };
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The live view of a run for the terminal: every event as it happens, and (unless quiet) the
|
|
118
|
+
* agents' output as it comes, indented under the call. None with --json (the result is JSON).
|
|
119
|
+
*/
|
|
120
|
+
function live(opts) {
|
|
121
|
+
if (opts.json) return {};
|
|
122
|
+
let pending = "";
|
|
123
|
+
const flush = () => { if (pending) { process.stdout.write(` | ${pending}\n`); pending = ""; } };
|
|
124
|
+
const onEvent = (e) => {
|
|
125
|
+
flush();
|
|
126
|
+
console.log(`${clockOf(e.at)} ${e.event.padEnd(13)} ${e.detail}`);
|
|
127
|
+
};
|
|
128
|
+
const onOutput = opts.quiet ? undefined : ({ text }) => {
|
|
129
|
+
const lines = (pending + text.replace(/\r\n?/g, "\n")).split("\n");
|
|
130
|
+
pending = lines.pop();
|
|
131
|
+
for (const l of lines) process.stdout.write(` | ${l}\n`);
|
|
132
|
+
};
|
|
133
|
+
return { onEvent, onOutput, done: flush };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** The --pipeline value: the shell may split an unquoted list ("a:2, b") into several items. */
|
|
137
|
+
const pipelineOf = (items) => [].concat(items).join(" ");
|
|
138
|
+
|
|
139
|
+
const rootOf = (opts) => path.resolve(opts.root ?? ".");
|
|
140
|
+
const print = (opts, data, text) => { if (opts.json) console.log(JSON.stringify(data, null, "\t")); else text(); };
|
|
141
|
+
const problems = (opts, r) => { print(opts, r, () => r.problems.forEach((p) => console.log(`problem: ${p}`))); return 1; };
|
|
142
|
+
|
|
143
|
+
/** The options of a command that takes a scenario (`required`: --pipeline must be given). */
|
|
144
|
+
function scenario(cmd, required = true) {
|
|
145
|
+
const pipeline = ["--pipeline <steps...>", "a pipeline file (*.json) or a comma list of task (one round, no review) and task:N (at most N rounds with review)"];
|
|
146
|
+
return (required ? cmd.requiredOption(...pipeline) : cmd.option(...pipeline))
|
|
147
|
+
.addOption(new Option("--input <file>", "the input file: prose, or an Active artifact").conflicts("text"))
|
|
148
|
+
.addOption(new Option("--text <text>", "the input as text").conflicts("input"))
|
|
149
|
+
.option("--name <name>", "the request's name (default: the input file's name; for text, each step's own heading)");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const common = (cmd) => cmd.option("--root <dir>", "the project (default: the current folder)").option("--json", "the result as JSON");
|
|
153
|
+
|
|
154
|
+
export function program() {
|
|
155
|
+
const prog = new Command("agiter")
|
|
156
|
+
.description("A harness for spec-driven agentic development.")
|
|
157
|
+
.showHelpAfterError("(agiter --help, or agiter <command> --help)")
|
|
158
|
+
.exitOverride();
|
|
159
|
+
|
|
160
|
+
common(prog.command("init").description("make the folder an agiter project: .agiter/ with the default model config in .agiter/config/")).action((opts) => {
|
|
161
|
+
const r = api.initProject({ root: rootOf(opts) });
|
|
162
|
+
print(opts, r, () => {
|
|
163
|
+
for (const f of r.created) console.log(`created ${f}`);
|
|
164
|
+
for (const f of r.existed) console.log(`kept ${f} (exists)`);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
common(scenario(prog.command("plan").description("plan a scenario without running it"))).action((opts) => {
|
|
169
|
+
const r = api.planScenario({ root: rootOf(opts), pipeline: pipelineOf(opts.pipeline), input: opts.input, text: opts.text, name: opts.name });
|
|
170
|
+
print(opts, { ok: r.ok, problems: r.problems, name: r.name, plan: r.plan }, () => {
|
|
171
|
+
if (r.ok) console.log(`name: ${r.name ?? "(each step's own heading)"}`);
|
|
172
|
+
for (const s of r.plan ?? []) console.log(` ${String(s.n).padEnd(3)} ${tokenOf(s).padEnd(16)} ${(s.input ?? "-").padEnd(6)} -> ${s.output.padEnd(7)} <run>.${s.name}.<round>.md${s.rounds ? " + .report.md" : ""}`);
|
|
173
|
+
for (const p of r.problems) console.log(`problem: ${p}`);
|
|
174
|
+
});
|
|
175
|
+
process.exitCode = r.ok ? 0 : 1;
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const runner = (simulate) => async (opts) => {
|
|
179
|
+
let root = rootOf(opts);
|
|
180
|
+
let simulation = null;
|
|
181
|
+
if (simulate) {
|
|
182
|
+
let script = {};
|
|
183
|
+
if (opts.script) { try { script = JSON.parse(fs.readFileSync(path.resolve(opts.script), "utf8")); } catch (e) { throw new CommanderError(USAGE, "agiter.script", `--script: ${e.message}`); } }
|
|
184
|
+
simulation = { script };
|
|
185
|
+
if (!opts.root) { root = fs.mkdtempSync(path.join(os.tmpdir(), "agiter-sim-")); fs.mkdirSync(path.join(root, ".agiter")); }
|
|
186
|
+
}
|
|
187
|
+
logCli(root, process.argv.slice(2));
|
|
188
|
+
const view = live(opts);
|
|
189
|
+
if (simulate && !opts.json) console.log(`project: ${root}`);
|
|
190
|
+
const r = await api.startRun({ root, pipeline: pipelineOf(opts.pipeline), input: opts.input, text: opts.text, name: opts.name, simulation, fresh: Boolean(opts.new), audit: Boolean(opts.audit), onEvent: view.onEvent, onOutput: view.onOutput });
|
|
191
|
+
view.done?.();
|
|
192
|
+
if (!r.ok) { process.exitCode = problems(opts, r); return; }
|
|
193
|
+
print(opts, { root, resumed: r.resumed, run: r.run }, () => {
|
|
194
|
+
if (r.resumed) console.log("resumed the unfinished run of the same scenario (--new starts a new one)");
|
|
195
|
+
console.log("");
|
|
196
|
+
printRun(r.run);
|
|
197
|
+
});
|
|
198
|
+
process.exitCode = r.run.status === "done" ? 0 : 1;
|
|
199
|
+
};
|
|
200
|
+
const runOptions = (cmd) => cmd
|
|
201
|
+
.option("--new", "start a new run even when an unfinished run of the same scenario exists")
|
|
202
|
+
.option("--audit", "record every executor call in .agiter/runs/<run>/audit.jsonl")
|
|
203
|
+
.option("--quiet", "show only the harness's events, not the agents' output");
|
|
204
|
+
|
|
205
|
+
const run = runOptions(scenario(prog.command("run").description("run a scenario (an unfinished run of the same scenario is resumed), or list the runs with --list"), false))
|
|
206
|
+
.addOption(new Option("--list [pattern]", "list the runs (newest first; --asc for oldest first); a regular expression keeps those whose id, name or pipeline it matches (case-insensitive)")
|
|
207
|
+
.conflicts(["pipeline", "input", "text", "name", "new", "audit", "quiet"]))
|
|
208
|
+
.addOption(new Option("--delete [pattern]", "delete runs (their records and artifacts) whose id, name or pipeline the regular expression matches, narrowed by --failed and --done; one restriction at least (\".\" for every run)")
|
|
209
|
+
.conflicts(["pipeline", "input", "text", "name", "new", "audit", "quiet", "list"]))
|
|
210
|
+
.option("--failed", "with --delete: only failed runs (with --done: failed or done ones)")
|
|
211
|
+
.option("--done", "with --delete: only done runs (with --failed: failed or done ones)")
|
|
212
|
+
.option("--all", "with --delete: delete every run and clear last.json (asks twice)")
|
|
213
|
+
.option("--yes", "with --delete: do not ask (needed when the input is not a terminal)")
|
|
214
|
+
.addOption(new Option("--asc", "with --list: oldest first").conflicts(["desc"]))
|
|
215
|
+
.addOption(new Option("--desc", "with --list: newest first (the default)").conflicts(["asc"]));
|
|
216
|
+
common(run).action(async (opts, cmd) => {
|
|
217
|
+
if (opts.list !== undefined) return listRunsOf(opts, cmd);
|
|
218
|
+
if (opts.delete !== undefined) return deleteRunsOf(opts, cmd);
|
|
219
|
+
for (const o of ["failed", "done", "all", "yes"]) if (opts[o]) cmd.error(`error: option '--${o}' belongs to --delete`, { exitCode: USAGE, code: "agiter.delete" });
|
|
220
|
+
if (opts.asc || opts.desc) cmd.error(`error: option '--${opts.asc ? "asc" : "desc"}' belongs to --list`, { exitCode: USAGE, code: "agiter.order" });
|
|
221
|
+
if (!opts.pipeline) cmd.error("error: required option '--pipeline <steps...>' not specified (or --list)", { exitCode: USAGE, code: "agiter.pipeline" });
|
|
222
|
+
return runner(false)(opts);
|
|
223
|
+
});
|
|
224
|
+
common(runOptions(scenario(prog.command("simulate").description("run a scenario with a scripted fake model (a new temporary project without --root)")))
|
|
225
|
+
.option("--script <file>", "what the fake model returns, per task, role and attempt")).action(runner(true));
|
|
226
|
+
|
|
227
|
+
common(prog.command("continue").argument("[run]", "the run id", "last").description("walk a run's steps again from its files")
|
|
228
|
+
.option("--audit", "turn the run's audit on from now")
|
|
229
|
+
.option("--quiet", "show only the harness's events, not the agents' output")).action(async (id, opts) => {
|
|
230
|
+
const root = rootOf(opts);
|
|
231
|
+
logCli(root, process.argv.slice(2));
|
|
232
|
+
const view = live(opts);
|
|
233
|
+
const r = await api.continueRun({ root, run: id, audit: Boolean(opts.audit), onEvent: view.onEvent, onOutput: view.onOutput });
|
|
234
|
+
view.done?.();
|
|
235
|
+
if (!r.ok) { process.exitCode = problems(opts, r); return; }
|
|
236
|
+
print(opts, { run: r.run }, () => { console.log(""); printRun(r.run); });
|
|
237
|
+
process.exitCode = r.run.status === "done" ? 0 : 1;
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
common(prog.command("finish").argument("[run]", "the run id", "last").description("end a git run: back to the branch it started from, with --merge of its branch (a done run) or without")
|
|
241
|
+
.option("--merge", "merge the run's branch into the branch it started from (no fast-forward)")).action((id, opts) => {
|
|
242
|
+
const root = rootOf(opts);
|
|
243
|
+
logCli(root, process.argv.slice(2));
|
|
244
|
+
const r = api.finishRun({ root, run: id, merge: Boolean(opts.merge) });
|
|
245
|
+
if (!r.ok) { process.exitCode = problems(opts, r); return; }
|
|
246
|
+
const back = r.run.git.baseBranch ?? `detached ${r.run.git.base.slice(0, 10)}`;
|
|
247
|
+
print(opts, { run: r.run, merged: r.merged }, () => console.log(r.merged ? `merged ${r.run.git.branch} into ${back}` : `back on ${back}; ${r.run.git.branch} is kept (finish --merge later, or delete it)`));
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
common(prog.command("status").argument("[run]", "the run id", "last").description("show a run")).action((id, opts) => {
|
|
251
|
+
const root = rootOf(opts);
|
|
252
|
+
const run = api.loadRun(root, id);
|
|
253
|
+
if (!run) { console.error(`agiter: no run ${id} in ${root}`); process.exitCode = 1; return; }
|
|
254
|
+
print(opts, run, () => printRun(run));
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
common(prog.command("runs").description("list the runs, newest first")).action((opts) => {
|
|
258
|
+
const runs = api.listRuns(rootOf(opts));
|
|
259
|
+
print(opts, runs, () => runs.forEach((r) => console.log(`${r.id} ${r.status.padEnd(8)} ${r.pipeline}`)));
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
common(prog.command("audit").argument("[run]", "the run id", "last").description("show a run's audit")).action((id, opts) => {
|
|
263
|
+
const root = rootOf(opts);
|
|
264
|
+
const run = api.loadRun(root, id);
|
|
265
|
+
if (!run) { console.error(`agiter: no run ${id} in ${root}`); process.exitCode = 1; return; }
|
|
266
|
+
const lines = api.readAudit(root, run.id);
|
|
267
|
+
if (!run.audit && !lines.length) { console.error(`agiter: run ${run.id} has no audit (it ran without --audit)`); process.exitCode = 1; return; }
|
|
268
|
+
print(opts, lines, () => {
|
|
269
|
+
console.log(`audit of ${run.id} (${run.status}): ${lines.length} call(s), ${lines.reduce((t, a) => t + a.seconds, 0).toFixed(1)} s`);
|
|
270
|
+
for (const a of lines) {
|
|
271
|
+
const who = a.agent ? `${a.agent}${a.model ? ` ${a.model}` : ""}` : a.executor;
|
|
272
|
+
const end = a.outcome === "ok" ? (a.verdict ? `${a.verdict} (${a.blocking} blocking)` : "ok") : `${a.outcome} [${a.guard}]`;
|
|
273
|
+
console.log(` ${String(a.step).padEnd(3)} ${a.task.padEnd(13)} ${(a.round ? `r${a.round}` : "").padEnd(4)}${a.role.padEnd(9)} ${who.padEnd(46)} ${a.seconds.toFixed(1).padStart(7)} s ${end}`);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
common(prog.command("list").addArgument(new Command().createArgument("<kind>", "what to list").choices(["artifacts", "tasks"])).description("list the definitions")).action((kind, opts) => {
|
|
279
|
+
const defs = api.loadDefs(rootOf(opts));
|
|
280
|
+
const items = [...defs[kind].values()];
|
|
281
|
+
print(opts, { problems: defs.problems, items }, () => {
|
|
282
|
+
for (const d of items) {
|
|
283
|
+
const detail = kind === "tasks" ? `${[].concat(d.input).join("|")} -> ${d.output} [${d.executor}]${d.rounds ? ` rounds ${d.rounds}` : ""}` : d.statuses.join(" / ");
|
|
284
|
+
console.log(`${d.id.padEnd(17)} ${detail}${d.layer === "project" ? " (project)" : ""}`);
|
|
285
|
+
}
|
|
286
|
+
for (const p of defs.problems) console.log(`problem: ${p}`);
|
|
287
|
+
});
|
|
288
|
+
process.exitCode = defs.problems.length ? 1 : 0;
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
common(prog.command("roles").description("the roles (for now only --list)").option("--list", "list the roles and their tasks")).action((opts, cmd) => {
|
|
292
|
+
if (!opts.list) cmd.error("error: roles: only --list is supported for now", { exitCode: USAGE, code: "agiter.roles" });
|
|
293
|
+
const defs = api.loadDefs(rootOf(opts));
|
|
294
|
+
const roles = api.listRoles(defs);
|
|
295
|
+
print(opts, { problems: defs.problems, roles }, () => {
|
|
296
|
+
for (const r of roles) console.log(`${r.role.padEnd(10)} ${[...r.tasks, ...(r.reviewsRounds ? ["every round of a step with :N"] : [])].join(", ")}`);
|
|
297
|
+
for (const p of defs.problems) console.log(`problem: ${p}`);
|
|
298
|
+
});
|
|
299
|
+
process.exitCode = defs.problems.length ? 1 : 0;
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
prog.command("sidecar").description("serve the API as JSON-RPC over stdio (for Far Manager)").action(async () => {
|
|
303
|
+
const { serve } = await import("../sidecar.mjs");
|
|
304
|
+
await serve();
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
return prog;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Run the command line `argv` (without node and the script); the exit code. */
|
|
311
|
+
export async function main(argv) {
|
|
312
|
+
const prog = program();
|
|
313
|
+
if (!argv.length) { prog.outputHelp(); return 0; }
|
|
314
|
+
try {
|
|
315
|
+
await prog.parseAsync(argv, { from: "user" });
|
|
316
|
+
return process.exitCode ?? 0;
|
|
317
|
+
} catch (e) {
|
|
318
|
+
if (!(e instanceof CommanderError)) throw e;
|
|
319
|
+
if (["commander.helpDisplayed", "commander.help", "commander.version"].includes(e.code)) return 0;
|
|
320
|
+
if (e.code === "agiter.script") console.error(`agiter: ${e.message}`);
|
|
321
|
+
return USAGE;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const self = process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(new URL(import.meta.url));
|
|
326
|
+
if (self) process.exitCode = await main(process.argv.slice(2));
|
package/defs/agents.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"opencode": {
|
|
3
|
+
"title": "opencode run (as in Methodology's default profile)",
|
|
4
|
+
"command": [
|
|
5
|
+
"opencode",
|
|
6
|
+
"run",
|
|
7
|
+
"--auto",
|
|
8
|
+
"--thinking"
|
|
9
|
+
],
|
|
10
|
+
"model": [
|
|
11
|
+
"-m",
|
|
12
|
+
"{model}"
|
|
13
|
+
],
|
|
14
|
+
"message": [
|
|
15
|
+
"{message}"
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
"claude": {
|
|
19
|
+
"title": "Claude Code, print mode; the prompt comes right after -p (the tool options take several values); the reviewer reads only",
|
|
20
|
+
"command": [
|
|
21
|
+
"claude",
|
|
22
|
+
"-p",
|
|
23
|
+
"{message}"
|
|
24
|
+
],
|
|
25
|
+
"model": [
|
|
26
|
+
"--model",
|
|
27
|
+
"{model}"
|
|
28
|
+
],
|
|
29
|
+
"roles": {
|
|
30
|
+
"reviewer": [
|
|
31
|
+
"--allowedTools",
|
|
32
|
+
"Read,Grep,Glob",
|
|
33
|
+
"--disallowedTools",
|
|
34
|
+
"Write,Edit,NotebookEdit"
|
|
35
|
+
],
|
|
36
|
+
"*": [
|
|
37
|
+
"--allowedTools",
|
|
38
|
+
"Read,Grep,Glob,Write,Edit"
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
"dropEnv": [
|
|
42
|
+
"ANTHROPIC_BASE_URL",
|
|
43
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
44
|
+
"ANTHROPIC_API_KEY",
|
|
45
|
+
"ANTHROPIC_MODEL"
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "Draft",
|
|
3
|
+
"summary": "A worked-out proposal of what to build and why.",
|
|
4
|
+
"sections": [
|
|
5
|
+
"Problem",
|
|
6
|
+
"Proposal"
|
|
7
|
+
],
|
|
8
|
+
"criteria": [
|
|
9
|
+
"The problem is stated from the user's side.",
|
|
10
|
+
"The proposal is concrete enough to be split into specs.",
|
|
11
|
+
"Nothing is guessed: every unclear point is a finding."
|
|
12
|
+
],
|
|
13
|
+
"statuses": [
|
|
14
|
+
"Draft",
|
|
15
|
+
"Active",
|
|
16
|
+
"Done"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "Epic",
|
|
3
|
+
"summary": "A larger goal split into specs.",
|
|
4
|
+
"sections": [
|
|
5
|
+
"Goal",
|
|
6
|
+
"Scope"
|
|
7
|
+
],
|
|
8
|
+
"criteria": [
|
|
9
|
+
"The goal is measurable.",
|
|
10
|
+
"The scope names what is out of it."
|
|
11
|
+
],
|
|
12
|
+
"statuses": [
|
|
13
|
+
"Draft",
|
|
14
|
+
"Active",
|
|
15
|
+
"Done"
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "Initiative",
|
|
3
|
+
"summary": "The whole goal of a larger change and its functional areas.",
|
|
4
|
+
"sections": [
|
|
5
|
+
"Goal",
|
|
6
|
+
"Functional areas"
|
|
7
|
+
],
|
|
8
|
+
"criteria": [
|
|
9
|
+
"The goal says what changes for the users.",
|
|
10
|
+
"Every functional area has an id and a short scope.",
|
|
11
|
+
"Nothing is guessed: every unclear point is a finding."
|
|
12
|
+
],
|
|
13
|
+
"statuses": [
|
|
14
|
+
"Draft",
|
|
15
|
+
"Active",
|
|
16
|
+
"Done"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "Spec",
|
|
3
|
+
"summary": "One implementable change with its acceptance criteria.",
|
|
4
|
+
"sections": [
|
|
5
|
+
"Goal",
|
|
6
|
+
"Acceptance"
|
|
7
|
+
],
|
|
8
|
+
"criteria": [
|
|
9
|
+
"Every acceptance criterion is testable.",
|
|
10
|
+
"The spec names the files or areas it changes.",
|
|
11
|
+
"Nothing is left for the Worker to decide."
|
|
12
|
+
],
|
|
13
|
+
"statuses": [
|
|
14
|
+
"Draft",
|
|
15
|
+
"Active",
|
|
16
|
+
"Done"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "Work",
|
|
3
|
+
"summary": "The record of a change made to the code, tests included.",
|
|
4
|
+
"sections": [
|
|
5
|
+
"Changes"
|
|
6
|
+
],
|
|
7
|
+
"criteria": [
|
|
8
|
+
"The changes meet the spec's acceptance criteria.",
|
|
9
|
+
"Nothing outside the spec's scope changed.",
|
|
10
|
+
"Tests cover the acceptance criteria."
|
|
11
|
+
],
|
|
12
|
+
"statuses": [
|
|
13
|
+
"Draft",
|
|
14
|
+
"Active",
|
|
15
|
+
"Done"
|
|
16
|
+
]
|
|
17
|
+
}
|