@pavel.sommer/agiter 0.0.0-stage → 0.14.1-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.
Files changed (99) hide show
  1. package/README.md +81 -2
  2. package/bin/agiter.mjs +417 -0
  3. package/defs/.agents.json +48 -0
  4. package/defs/.models.architect.json +9 -0
  5. package/defs/.models.author.json +11 -0
  6. package/defs/.models.drafter.json +11 -0
  7. package/defs/.models.json +11 -0
  8. package/defs/.models.reviewer.json +9 -0
  9. package/defs/.models.worker.json +11 -0
  10. package/defs/artifacts/checks.json +12 -0
  11. package/defs/artifacts/create-epics.json +25 -0
  12. package/defs/artifacts/draft.json +18 -0
  13. package/defs/artifacts/epic.json +17 -0
  14. package/defs/artifacts/idea.json +10 -0
  15. package/defs/artifacts/initiative.json +18 -0
  16. package/defs/artifacts/report.json +12 -0
  17. package/defs/artifacts/spec.json +18 -0
  18. package/defs/artifacts/work.json +17 -0
  19. package/defs/blocks/ado-net.json +130 -0
  20. package/defs/blocks/agent-browser.json +59 -0
  21. package/defs/blocks/angular.json +270 -0
  22. package/defs/blocks/aspnet-minimal-api.json +68 -0
  23. package/defs/blocks/aspnet-mvc.json +113 -0
  24. package/defs/blocks/aspnet-webapi.json +79 -0
  25. package/defs/blocks/aspnet-webforms.json +150 -0
  26. package/defs/blocks/browser.json +102 -0
  27. package/defs/blocks/cbor.json +95 -0
  28. package/defs/blocks/clean-architecture.json +127 -0
  29. package/defs/blocks/client-server.json +134 -0
  30. package/defs/blocks/ddd.json +82 -0
  31. package/defs/blocks/dotnet-framework.json +153 -0
  32. package/defs/blocks/dotnet.json +119 -0
  33. package/defs/blocks/dto-templates.json +39 -0
  34. package/defs/blocks/ef6.json +138 -0
  35. package/defs/blocks/entity-drafts.json +84 -0
  36. package/defs/blocks/express.json +135 -0
  37. package/defs/blocks/far-luamacro.json +202 -0
  38. package/defs/blocks/fastify.json +125 -0
  39. package/defs/blocks/javascript.json +40 -0
  40. package/defs/blocks/luajit.json +117 -0
  41. package/defs/blocks/native-aot.json +105 -0
  42. package/defs/blocks/next.json +257 -0
  43. package/defs/blocks/node.json +54 -0
  44. package/defs/blocks/npm.json +72 -0
  45. package/defs/blocks/nuxt.json +265 -0
  46. package/defs/blocks/odbc-ffi.json +154 -0
  47. package/defs/blocks/playwright-core.json +73 -0
  48. package/defs/blocks/react.json +295 -0
  49. package/defs/blocks/sql-server.json +96 -0
  50. package/defs/blocks/tdd.json +47 -0
  51. package/defs/blocks/tsql-migrations.json +260 -0
  52. package/defs/blocks/typescript.json +115 -0
  53. package/defs/blocks/vanilla-web.json +88 -0
  54. package/defs/blocks/vite.json +91 -0
  55. package/defs/blocks/vitest.json +78 -0
  56. package/defs/blocks/vue.json +292 -0
  57. package/defs/blocks/web-components.json +86 -0
  58. package/defs/blocks/windows-auth.json +88 -0
  59. package/defs/presets/angular.json +15 -0
  60. package/defs/presets/aspnet-classic.json +13 -0
  61. package/defs/presets/aspnet-webforms.json +12 -0
  62. package/defs/presets/dotnet-sqlserver.json +14 -0
  63. package/defs/presets/far-luajit.json +12 -0
  64. package/defs/presets/mssql-database.json +16 -0
  65. package/defs/presets/react-express.json +18 -0
  66. package/defs/presets/react-fastify.json +18 -0
  67. package/defs/presets/react-next.json +16 -0
  68. package/defs/presets/react-vite.json +16 -0
  69. package/defs/presets/vanilla-web.json +16 -0
  70. package/defs/presets/vue-nuxt.json +17 -0
  71. package/defs/presets/vue-vite.json +16 -0
  72. package/defs/tasks/checks.json +7 -0
  73. package/defs/tasks/create-draft.json +10 -0
  74. package/defs/tasks/create-epics.json +10 -0
  75. package/defs/tasks/create-initiative.json +9 -0
  76. package/defs/tasks/create-spec.json +10 -0
  77. package/defs/tasks/create-tests.json +14 -0
  78. package/defs/tasks/create-work.json +13 -0
  79. package/defs/tasks/review.json +9 -0
  80. package/index.mjs +34 -0
  81. package/package.json +43 -5
  82. package/sidecar.mjs +44 -0
  83. package/src/agents.mjs +160 -0
  84. package/src/artifacts.mjs +234 -0
  85. package/src/defs.mjs +84 -0
  86. package/src/engine.mjs +831 -0
  87. package/src/fake.mjs +43 -0
  88. package/src/frontmatter.mjs +62 -0
  89. package/src/git.mjs +239 -0
  90. package/src/glob.mjs +28 -0
  91. package/src/init.mjs +45 -0
  92. package/src/lang.mjs +21 -0
  93. package/src/log.mjs +53 -0
  94. package/src/models.mjs +132 -0
  95. package/src/pipeline.mjs +94 -0
  96. package/src/prompt.mjs +133 -0
  97. package/src/roles.mjs +22 -0
  98. package/src/stack.mjs +338 -0
  99. package/src/tools.mjs +34 -0
package/README.md CHANGED
@@ -1,3 +1,82 @@
1
- # Temporary Holding Version
1
+ # agiter
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
+ The project's tech stack lives in `agiter/config/.stack.json` (blocks and presets, `agiter
11
+ stack --help`); the harness puts into each model's prompt only what its role needs from it, then the owner's
12
+ `agiter/config/project.md` (structure, naming, scaffolding, rules) for every role. `--includeFile
13
+ <file>` (repeatable) gives any file to every model call of a run. `create-epics` breaks an initiative down into its functional areas and writes one epic file per
14
+ epic (`<run>.create-epics.<round>.E1.md`, …) next to an overview. `--lang <tag>` (an ISO tag: `cs`, `en`,
15
+ `cs-CZ`, `en-US`, …) sets the language of the output; without it the models write in the language
16
+ of the input. The headings the harness reads stay in English, and so do established technical
17
+ terms and common anglicisms.
18
+
19
+ Status: step 1 (inputs, artifacts, tasks, pipelines, the run engine, simulation) and a first
20
+ real executor: `agiter run` calls the agent CLI of each role's model profile
21
+ (`agiter/config/.models.<role>.json`, `agiter/config/.models.json`, and the agents in `agiter/config/.agents.json`, all copied from the package's defaults by `agiter init` or the first run). In a git repository
22
+ a run works in the current checkout by default (no harness commits; the owner reviews and
23
+ commits the changes), with `--isolation branch` on its own branch `agiter/<run>` with a commit
24
+ per call, or with `--isolation worktree` in `agiter/.worktrees/<run>` on that branch with the
25
+ project's `node_modules` linked in. The scope of a step is checked against what git sees, and
26
+ `agiter finish [--merge]` ends a branch or worktree run.
27
+
28
+ ## Layout
29
+
30
+ | Path | Content |
31
+ | --- | --- |
32
+ | `index.mjs` | the public API |
33
+ | `bin/agiter.mjs` | the CLI on commander (`agiter --help`, `agiter <command> --help`) |
34
+ | `sidecar.mjs` | JSON-RPC over stdio for long-lived clients (Far Manager) |
35
+ | `src/` | the modules: definitions, pipelines, artifacts, engine, tools, log |
36
+ | `defs/` | the package's artifact types, tasks, stack blocks and presets, default config |
37
+ | `test/` | `npm test` (node:test, no dependencies) |
38
+
39
+ The owner's documents stay his: any file a run did not write is an input in the owner's own form;
40
+ the pipeline decides what it is (`create-epics`: an initiative), and its frontmatter, status and
41
+ structure are not checked. A run is named after the input file without its leading timestamp and
42
+ stamped in local time (`yyMMddHHmmss`); the titles of the outputs are the models' own.
43
+
44
+ A project keeps everything in `agiter/`: `docs/<run>/` (the chain of each run), `runs/`,
45
+ `logs/`, its own definitions (`artifacts/`, `tasks/`, merged over the package's) and
46
+ `checks.json` (the required checks).
47
+
48
+ ## A run's files
49
+
50
+ ```text
51
+ agiter/docs/<run>/<run>.md the input, copied as it is (never changed)
52
+ agiter/docs/<run>/<run>.draft.1.md round 1 of create-draft:3
53
+ agiter/docs/<run>/<run>.draft.1.report.md its review: Block or Done, cumulative
54
+ agiter/docs/<run>/<run>.draft.2.md round 2: input + draft.1 + its report
55
+ ...
56
+ ```
57
+
58
+ A version is Active when another step follows and it has no review or its report is Done;
59
+ otherwise Draft. A step takes only prose or an Active artifact whose latest report is not
60
+ Block. The state of a run is its files: `agiter continue` walks the steps again and repeats
61
+ nothing done.
62
+
63
+ ## Try it
64
+
65
+ ```text
66
+ agiter list tasks
67
+ agiter plan --pipeline "create-draft:3,create-spec:2,create-work,checks" --text "Add a login form"
68
+ agiter simulate --pipeline "create-draft:3,create-spec:2,create-work" --text "Add a login form" --script script.json
69
+ agiter continue --root <project printed by simulate>
70
+ ```
71
+
72
+ A simulation script says what the fake model returns, per task, role and attempt:
73
+
74
+ ```json
75
+ {
76
+ "steps": {
77
+ "create-draft#1/review": { "findings": [{ "text": "Problem unclear" }] },
78
+ "create-draft#2": { "responses": { "F-1": "fixed: Problem rewritten" } },
79
+ "create-work": { "changes": ["src/login.js"] }
80
+ }
81
+ }
82
+ ```
package/bin/agiter.mjs ADDED
@@ -0,0 +1,417 @@
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 { spawnSync } from "node:child_process";
12
+ import { Command, CommanderError, Option } from "commander";
13
+ import * as api from "../index.mjs";
14
+ import { logCli } from "../src/log.mjs";
15
+
16
+ const USAGE = 2;
17
+
18
+ function tokenOf(s) { return s.rounds ? `${s.task}:${s.rounds}` : s.task; }
19
+
20
+ function printRun(run) {
21
+ const where = !run.git ? "" : (run.git.mode ?? "branch") === "none" ? " (no isolation: the checkout itself)" : ` (${run.git.mode === "worktree" ? `worktree ${run.git.worktree}, ` : ""}branch ${run.git.branch}${run.finished ? `, finished${run.finished.merged ? ", merged" : ""}` : ""})`;
22
+ console.log(`run ${run.id}: ${run.status}${where}`);
23
+ console.log(` pipeline ${run.pipeline.steps.map(tokenOf).join(",")}, input ${run.input.type}${run.lang ? `, output in ${run.lang}` : ""}`);
24
+ for (const s of run.steps) {
25
+ const what = s.outcome === "failed" ? `failed [${s.guard}] ${s.error}` : `${s.file ? path.posix.basename(s.file) : ""}${s.verdict ? ` ${s.verdict}` : ""}`;
26
+ console.log(` ${String(s.step).padEnd(3)} ${s.task.padEnd(13)} ${(s.round ? `r${s.round}` : "").padEnd(4)}${s.role.padEnd(9)} ${what}`);
27
+ }
28
+ 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}`);
29
+ else if (run.status === "done" && run.git && (run.git.mode ?? "branch") !== "none" && !run.finished) console.log(` next: agiter finish ${run.id} --merge (or without --merge to keep the branch)`);
30
+ else if (run.status === "done" && run.git?.mode === "none") console.log(" next: review the changes (git status, git diff) and commit them");
31
+ }
32
+
33
+ const pad2 = (n) => String(n).padStart(2, "0");
34
+ /** A time in the machine's zone: yyyy-MM-dd HH:mm. */
35
+ const local = (iso) => { const d = new Date(iso); return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}`; };
36
+ 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`);
37
+
38
+ /** The RegExp of a --list or --delete value (true: no pattern), or a usage error. */
39
+ function patternOf(value, option, cmd) {
40
+ if (typeof value !== "string") return null;
41
+ try { return new RegExp(value, "i"); } catch (e) { cmd.error(`error: --${option}: ${e.message}`, { exitCode: USAGE, code: "agiter.pattern" }); }
42
+ }
43
+
44
+ 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}]` : ""}`;
45
+
46
+ /** run --list [pattern]: the runs, one per line. */
47
+ function listRunsOf(opts, cmd) {
48
+ 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" });
49
+ const pattern = patternOf(opts.list, "list", cmd);
50
+ const runs = api.listRuns(rootOf(opts), pattern, { order: opts.asc ? "asc" : "desc" });
51
+ print(opts, runs, () => {
52
+ for (const r of runs) console.log(runLine(r));
53
+ if (!runs.length) console.log(pattern ? `no run matches /${opts.list}/` : "no runs");
54
+ });
55
+ }
56
+
57
+ /** Ask a yes/no question on the terminal. */
58
+ async function confirm(question) {
59
+ const { createInterface } = await import("node:readline/promises");
60
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
61
+ try { return /^y(es)?$/i.test((await rl.question(question)).trim()); } finally { rl.close(); }
62
+ }
63
+
64
+ /** run --delete --all [--yes]: every run and last.json, after two warnings. */
65
+ async function deleteAllOf(opts, cmd, root) {
66
+ 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" });
67
+ const runs = api.listRuns(root);
68
+ const folders = fs.existsSync(path.join(root, "agiter", "runs")) ? fs.readdirSync(path.join(root, "agiter", "runs"), { withFileTypes: true }).filter((d) => d.isDirectory()).length : 0;
69
+ if (!folders) { print(opts, { deleted: [] }, () => console.log("no run to delete")); return; }
70
+ if (!opts.yes) {
71
+ 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" });
72
+ for (const r of runs) console.log(runLine(r));
73
+ if (folders > runs.length) console.log(`and ${folders - runs.length} broken run folder(s)`);
74
+ 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.`);
75
+ if (!(await confirm("Delete all runs? [y/N] "))) { console.log("nothing deleted"); process.exitCode = 1; return; }
76
+ const { createInterface } = await import("node:readline/promises");
77
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
78
+ let typed;
79
+ try { typed = (await rl.question(`SECOND WARNING: type "delete all" to delete all ${folders} run(s): `)).trim(); } finally { rl.close(); }
80
+ if (typed !== "delete all") { console.log("nothing deleted"); process.exitCode = 1; return; }
81
+ }
82
+ logCli(root, process.argv.slice(2));
83
+ const { deleted } = api.deleteRuns(root, { all: true });
84
+ print(opts, { deleted }, () => console.log(`deleted all ${deleted.length} run(s); last.json cleared`));
85
+ }
86
+
87
+ /** run --delete [pattern] [--failed] [--done] [--yes]: delete runs, after showing them. */
88
+ async function deleteRunsOf(opts, cmd) {
89
+ if (opts.asc || opts.desc) cmd.error(`error: options '--asc' and '--desc' belong to --list`, { exitCode: USAGE, code: "agiter.order" });
90
+ const root = rootOf(opts);
91
+ if (opts.all) return deleteAllOf(opts, cmd, root);
92
+ const pattern = patternOf(opts.delete, "delete", cmd);
93
+ const statuses = [...(opts.failed ? ["failed"] : []), ...(opts.done ? ["done"] : [])];
94
+ if (!pattern && !statuses.length) {
95
+ const all = api.listRuns(root);
96
+ const message = "--delete needs a restriction: a pattern, --failed or --done (a pattern \".\" selects every run)";
97
+ print(opts, { deleted: [], problems: [message], runs: all }, () => {
98
+ console.log(`${message}. The runs:`);
99
+ for (const r of all) console.log(` ${runLine(r)}`);
100
+ if (!all.length) console.log(" no runs");
101
+ });
102
+ process.exitCode = USAGE;
103
+ return;
104
+ }
105
+ const runs = api.listRuns(root, pattern).filter((r) => !statuses.length || statuses.includes(r.status));
106
+ if (!runs.length) { print(opts, { deleted: [] }, () => console.log("no run to delete")); return; }
107
+ if (!opts.yes) {
108
+ 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" });
109
+ for (const r of runs) console.log(runLine(r));
110
+ 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; }
111
+ }
112
+ logCli(root, process.argv.slice(2));
113
+ const { deleted } = api.deleteRuns(root, { pattern, statuses });
114
+ print(opts, { deleted }, () => console.log(`deleted ${deleted.length} run(s)${opts.yes ? `: ${deleted.map((r) => r.id).join(", ")}` : ""}`));
115
+ }
116
+
117
+ const clockOf = (iso) => { const d = new Date(iso); return `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`; };
118
+
119
+ /**
120
+ * The live view of a run for the terminal: every event as it happens, and (unless quiet) the
121
+ * agents' output as it comes, indented under the call. None with --json (the result is JSON).
122
+ */
123
+ function live(opts) {
124
+ if (opts.json) return {};
125
+ let pending = "";
126
+ const flush = () => { if (pending) { process.stdout.write(` | ${pending}\n`); pending = ""; } };
127
+ const onEvent = (e) => {
128
+ flush();
129
+ console.log(`${clockOf(e.at)} ${e.event.padEnd(13)} ${e.detail}`);
130
+ };
131
+ const onOutput = opts.quiet ? undefined : ({ text }) => {
132
+ const lines = (pending + text.replace(/\r\n?/g, "\n")).split("\n");
133
+ pending = lines.pop();
134
+ for (const l of lines) process.stdout.write(` | ${l}\n`);
135
+ };
136
+ return { onEvent, onOutput, done: flush };
137
+ }
138
+
139
+ /** The --pipeline value: the shell may split an unquoted list ("a:2, b") into several items. */
140
+ const pipelineOf = (items) => [].concat(items).join(" ");
141
+
142
+ /** The git work tree's top of the current folder, or null. */
143
+ function gitTop() {
144
+ const r = spawnSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8", windowsHide: true });
145
+ return r.status === 0 ? path.resolve(r.stdout.trim()) : null;
146
+ }
147
+
148
+ /** The project: --root, else found from the current folder (src/init.mjs findProjectRoot). */
149
+ const rootOf = (opts) => (opts.root ? path.resolve(opts.root) : api.findProjectRoot(process.cwd(), gitTop()));
150
+ const print = (opts, data, text) => { if (opts.json) console.log(JSON.stringify(data, null, "\t")); else text(); };
151
+ const problems = (opts, r) => { print(opts, r, () => r.problems.forEach((p) => console.log(`problem: ${p}`))); return 1; };
152
+
153
+ /** The options of a command that takes a scenario (`required`: --pipeline must be given). */
154
+ function scenario(cmd, required = true) {
155
+ 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)"];
156
+ return (required ? cmd.requiredOption(...pipeline) : cmd.option(...pipeline))
157
+ .addOption(new Option("--input <file>", "the input file: prose, or an Active artifact").conflicts("text"))
158
+ .addOption(new Option("--text <text>", "the input as text").conflicts("input"))
159
+ .option("--name <name>", "the request's name (default: the input file's name; for text, each step's own heading)")
160
+ .option("--lang <tag>", "the language of the output, an ISO tag: cs, en, cs-CZ, en-US, … (default: the language of the input); the headings the harness reads stay in English")
161
+ .addOption(new Option("--isolation <mode>", "branch: a new branch agiter/<run> in this checkout; worktree: a separate checkout in agiter/.worktrees/<run> (node_modules linked). Without it: the checkout itself, no harness commits").choices(["branch", "worktree"]))
162
+ .option("--includeFile <file>", "a file every model call of the run gets whole in its prompt (repeat for more files); relative to the current folder, /<path> from the project's root", (v, prev) => [...prev, v], []);
163
+ }
164
+
165
+ const common = (cmd) => cmd.option("--root <dir>", "the project (default: found from the current folder: the project it is in)").option("--json", "the result as JSON");
166
+
167
+ export function program() {
168
+ const prog = new Command("agiter")
169
+ .description("A harness for spec-driven agentic development.")
170
+ .showHelpAfterError("(agiter --help, or agiter <command> --help)")
171
+ .exitOverride();
172
+
173
+ common(prog.command("init").description("make the current folder (or --root) an agiter project: agiter/ with the default config in agiter/config/")).action((opts) => {
174
+ // The current folder, unless it is already inside a project (then that project).
175
+ const found = api.findProjectRoot(process.cwd());
176
+ const root = opts.root ? path.resolve(opts.root) : api.isProject(found) ? found : process.cwd();
177
+ const r = api.initProject({ root });
178
+ if (!opts.json && root !== process.cwd()) console.log(`project: ${root}`);
179
+ print(opts, r, () => {
180
+ for (const f of r.created) console.log(`created ${f}`);
181
+ for (const f of r.existed) console.log(`kept ${f} (exists)`);
182
+ });
183
+ });
184
+
185
+ common(scenario(prog.command("plan").description("plan a scenario without running it"))).action((opts) => {
186
+ const r = api.planScenario({ root: rootOf(opts), pipeline: pipelineOf(opts.pipeline), input: opts.input, text: opts.text, name: opts.name, include: opts.includeFile, lang: opts.lang, isolation: opts.isolation ?? "none" });
187
+ print(opts, { ok: r.ok, problems: r.problems, name: r.name, lang: r.lang ?? null, include: r.include, plan: r.plan }, () => {
188
+ if (r.ok) console.log(`name: ${r.name ?? "(each step's own heading)"}`);
189
+ if (r.ok) console.log(`output language: ${r.lang ?? "(the input's)"}`);
190
+ if (r.include?.length) console.log(`files for every call: ${r.include.join(", ")}`);
191
+ 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" : ""}`);
192
+ for (const p of r.problems) console.log(`problem: ${p}`);
193
+ });
194
+ process.exitCode = r.ok ? 0 : 1;
195
+ });
196
+
197
+ const runner = (simulate) => async (opts) => {
198
+ let root = rootOf(opts);
199
+ let simulation = null;
200
+ if (simulate) {
201
+ let script = {};
202
+ 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}`); } }
203
+ simulation = { script };
204
+ // Without --root: the project the current folder is in, else a new temporary project.
205
+ if (!opts.root && !api.isProject(root)) { root = fs.mkdtempSync(path.join(os.tmpdir(), "agiter-sim-")); fs.mkdirSync(path.join(root, "agiter")); }
206
+ }
207
+ logCli(root, process.argv.slice(2));
208
+ const view = live(opts);
209
+ if ((simulate || root !== process.cwd()) && !opts.json) console.log(`project: ${root}`);
210
+ const r = await api.startRun({ root, pipeline: pipelineOf(opts.pipeline), input: opts.input, text: opts.text, name: opts.name, include: opts.includeFile, lang: opts.lang, isolation: opts.isolation ?? "none", simulation, fresh: Boolean(opts.new), audit: Boolean(opts.audit), onEvent: view.onEvent, onOutput: view.onOutput });
211
+ view.done?.();
212
+ if (!r.ok) { process.exitCode = problems(opts, r); return; }
213
+ print(opts, { root, resumed: r.resumed, run: r.run }, () => {
214
+ if (r.resumed) console.log("resumed the unfinished run of the same scenario (--new starts a new one)");
215
+ console.log("");
216
+ printRun(r.run);
217
+ });
218
+ process.exitCode = r.run.status === "done" ? 0 : 1;
219
+ };
220
+ const runOptions = (cmd) => cmd
221
+ .option("--new", "start a new run even when an unfinished run of the same scenario exists")
222
+ .option("--audit", "record every executor call in agiter/runs/<run>/audit.jsonl")
223
+ .option("--quiet", "show only the harness's events, not the agents' output");
224
+
225
+ 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))
226
+ .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)")
227
+ .conflicts(["pipeline", "input", "text", "name", "new", "audit", "quiet", "includeFile", "isolation", "lang"]))
228
+ .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)")
229
+ .conflicts(["pipeline", "input", "text", "name", "new", "audit", "quiet", "includeFile", "isolation", "lang", "list"]))
230
+ .option("--failed", "with --delete: only failed runs (with --done: failed or done ones)")
231
+ .option("--done", "with --delete: only done runs (with --failed: failed or done ones)")
232
+ .option("--all", "with --delete: delete every run and clear last.json (asks twice)")
233
+ .option("--yes", "with --delete: do not ask (needed when the input is not a terminal)")
234
+ .addOption(new Option("--asc", "with --list: oldest first").conflicts(["desc"]))
235
+ .addOption(new Option("--desc", "with --list: newest first (the default)").conflicts(["asc"]));
236
+ common(run).action(async (opts, cmd) => {
237
+ if (opts.list !== undefined) return listRunsOf(opts, cmd);
238
+ if (opts.delete !== undefined) return deleteRunsOf(opts, cmd);
239
+ for (const o of ["failed", "done", "all", "yes"]) if (opts[o]) cmd.error(`error: option '--${o}' belongs to --delete`, { exitCode: USAGE, code: "agiter.delete" });
240
+ if (opts.asc || opts.desc) cmd.error(`error: option '--${opts.asc ? "asc" : "desc"}' belongs to --list`, { exitCode: USAGE, code: "agiter.order" });
241
+ if (!opts.pipeline) cmd.error("error: required option '--pipeline <steps...>' not specified (or --list)", { exitCode: USAGE, code: "agiter.pipeline" });
242
+ return runner(false)(opts);
243
+ });
244
+ common(runOptions(scenario(prog.command("simulate").description("run a scenario with a scripted fake model (in the project the current folder is in, or --root; outside a project in a new temporary one)")))
245
+ .option("--script <file>", "what the fake model returns, per task, role and attempt")).action(runner(true));
246
+
247
+ common(prog.command("continue").argument("[run]", "the run id", "last").description("walk a run's steps again from its files")
248
+ .option("--audit", "turn the run's audit on from now")
249
+ .option("--quiet", "show only the harness's events, not the agents' output")).action(async (id, opts) => {
250
+ const root = rootOf(opts);
251
+ logCli(root, process.argv.slice(2));
252
+ const view = live(opts);
253
+ if (root !== process.cwd() && !opts.json) console.log(`project: ${root}`);
254
+ const r = await api.continueRun({ root, run: id, audit: Boolean(opts.audit), onEvent: view.onEvent, onOutput: view.onOutput });
255
+ view.done?.();
256
+ if (!r.ok) { process.exitCode = problems(opts, r); return; }
257
+ print(opts, { run: r.run }, () => { console.log(""); printRun(r.run); });
258
+ process.exitCode = r.run.status === "done" ? 0 : 1;
259
+ });
260
+
261
+ 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")
262
+ .option("--merge", "merge the run's branch into the branch it started from (no fast-forward); a worktree is removed after")).action((id, opts) => {
263
+ const root = rootOf(opts);
264
+ logCli(root, process.argv.slice(2));
265
+ const r = api.finishRun({ root, run: id, merge: Boolean(opts.merge) });
266
+ if (!r.ok) { process.exitCode = problems(opts, r); return; }
267
+ const back = r.run.git.baseBranch ?? `detached ${r.run.git.base.slice(0, 10)}`;
268
+ 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)`));
269
+ });
270
+
271
+ common(prog.command("status").argument("[run]", "the run id", "last").description("show a run")).action((id, opts) => {
272
+ const root = rootOf(opts);
273
+ const run = api.loadRun(root, id);
274
+ if (!run) { console.error(`agiter: no run ${id} in ${root}`); process.exitCode = 1; return; }
275
+ print(opts, run, () => printRun(run));
276
+ });
277
+
278
+ common(prog.command("runs").description("list the runs, newest first")).action((opts) => {
279
+ const runs = api.listRuns(rootOf(opts));
280
+ print(opts, runs, () => runs.forEach((r) => console.log(`${r.id} ${r.status.padEnd(8)} ${r.pipeline}`)));
281
+ });
282
+
283
+ common(prog.command("audit").argument("[run]", "the run id", "last").description("show a run's audit")).action((id, opts) => {
284
+ const root = rootOf(opts);
285
+ const run = api.loadRun(root, id);
286
+ if (!run) { console.error(`agiter: no run ${id} in ${root}`); process.exitCode = 1; return; }
287
+ const lines = api.readAudit(root, run.id);
288
+ if (!run.audit && !lines.length) { console.error(`agiter: run ${run.id} has no audit (it ran without --audit)`); process.exitCode = 1; return; }
289
+ print(opts, lines, () => {
290
+ console.log(`audit of ${run.id} (${run.status}): ${lines.length} call(s), ${lines.reduce((t, a) => t + a.seconds, 0).toFixed(1)} s`);
291
+ for (const a of lines) {
292
+ const who = a.agent ? `${a.agent}${a.model ? ` ${a.model}` : ""}` : a.executor;
293
+ const end = a.outcome === "ok" ? (a.verdict ? `${a.verdict} (${a.blocking} blocking)` : "ok") : `${a.outcome} [${a.guard}]`;
294
+ 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}`);
295
+ }
296
+ });
297
+ });
298
+
299
+ common(prog.command("list").addArgument(new Command().createArgument("<kind>", "what to list").choices(["artifacts", "tasks"])).description("list the definitions")).action((kind, opts) => {
300
+ const defs = api.loadDefs(rootOf(opts));
301
+ const items = [...defs[kind].values()];
302
+ print(opts, { problems: defs.problems, items }, () => {
303
+ for (const d of items) {
304
+ const detail = kind === "tasks" ? `${[].concat(d.input).join("|")} -> ${d.output} [${d.executor}]${d.rounds ? ` rounds ${d.rounds}` : ""}` : d.statuses.join(" / ");
305
+ console.log(`${d.id.padEnd(17)} ${detail}${d.layer === "project" ? " (project)" : ""}`);
306
+ }
307
+ for (const p of defs.problems) console.log(`problem: ${p}`);
308
+ });
309
+ process.exitCode = defs.problems.length ? 1 : 0;
310
+ });
311
+
312
+ common(prog.command("roles").description("the roles (for now only --list)").option("--list", "list the roles and their tasks")).action((opts, cmd) => {
313
+ if (!opts.list) cmd.error("error: roles: only --list is supported for now", { exitCode: USAGE, code: "agiter.roles" });
314
+ const defs = api.loadDefs(rootOf(opts));
315
+ const roles = api.listRoles(defs);
316
+ print(opts, { problems: defs.problems, roles }, () => {
317
+ for (const r of roles) console.log(`${r.role.padEnd(10)} ${[...r.tasks, ...(r.reviewsRounds ? ["every round of a step with :N"] : [])].join(", ")}`);
318
+ for (const p of defs.problems) console.log(`problem: ${p}`);
319
+ });
320
+ process.exitCode = defs.problems.length ? 1 : 0;
321
+ });
322
+
323
+ const stackCmd = prog.command("stack").description("the project's tech stack (agiter/config/.stack.json): list blocks, show what a role gets, change the selection")
324
+ .addOption(new Option("--list", "the blocks and presets of the package and the project").conflicts(["show", "add", "remove", "preset", "detect"]))
325
+ .addOption(new Option("--show [role]", "what the models get about the project (stack and project.md): everything, or only what a role gets").conflicts(["add", "remove", "preset", "detect"]))
326
+ .addOption(new Option("--add <blocks...>", "add blocks (id or id@version)").conflicts(["remove", "preset", "detect"]))
327
+ .addOption(new Option("--remove <blocks...>", "remove blocks").conflicts(["preset", "detect"]))
328
+ .addOption(new Option("--preset <id>", "start the selection from a preset (only when the project has none)").conflicts(["detect"]))
329
+ .option("--detect", "start the selection from the blocks the project's files show (only when the project has none)");
330
+ common(stackCmd).action((opts, cmd) => {
331
+ const root = rootOf(opts);
332
+ const { blocks, presets, problems: loadProblems } = api.loadBlocks(root);
333
+ if (opts.list) {
334
+ print(opts, { blocks: [...blocks.values()].map(({ id, title, kind, summary, layer }) => ({ id, title, kind, summary, layer })), presets: [...presets.values()].map(({ id, title, blocks: b }) => ({ id, title, blocks: b })), problems: loadProblems }, () => {
335
+ for (const b of [...blocks.values()].sort((a, c) => a.kind.localeCompare(c.kind) || a.id.localeCompare(c.id))) console.log(`${b.kind.padEnd(12)} ${b.id.padEnd(20)} ${b.summary ?? b.title}${b.layer === "project" ? " (project)" : ""}`);
336
+ console.log("\npresets:");
337
+ for (const p of presets.values()) console.log(` ${p.id.padEnd(18)} ${p.blocks.map((x) => (typeof x === "string" ? x : x.id)).join(", ")}`);
338
+ });
339
+ return;
340
+ }
341
+ const current = api.readStack(root);
342
+ if (current?.error) { process.exitCode = problems(opts, { problems: [current.error] }); return; }
343
+ const save = (stack, what) => {
344
+ const r = api.resolveSelection(blocks, stack.blocks);
345
+ if (r.problems.length) { process.exitCode = problems(opts, { problems: r.problems.map((p) => `stack: ${p}`) }); return; }
346
+ api.writeStack(root, stack);
347
+ print(opts, { stack, added: r.added, suggestions: r.suggestions }, () => {
348
+ console.log(`${what}: agiter/config/.stack.json`);
349
+ console.log(`blocks: ${[...r.chosen.values()].map((e) => (e.block.versions ? `${e.block.id}@${e.version}` : e.block.id)).join(", ")}`);
350
+ for (const a of r.added) console.log(` ${a.id} added as required by ${a.by}`);
351
+ if (r.suggestions.length) console.log(`suggested: ${r.suggestions.map((x) => `${x.id} (by ${x.by})`).join(", ")}`);
352
+ });
353
+ };
354
+ if (opts.preset || opts.detect) {
355
+ if (current) cmd.error("error: the project has a stack already (agiter/config/.stack.json): change it with --add and --remove", { exitCode: USAGE, code: "agiter.stack" });
356
+ if (opts.preset) {
357
+ const p = presets.get(opts.preset);
358
+ if (!p) cmd.error(`error: no preset ${opts.preset} (agiter stack --list)`, { exitCode: USAGE, code: "agiter.stack" });
359
+ return save({ preset: p.id, blocks: [...p.blocks] }, `created from the preset ${p.id}`);
360
+ }
361
+ const found = api.detectBlocks(blocks, root);
362
+ if (!found.length) { print(opts, { detected: [] }, () => console.log("no block detected: start with --preset or --add")); process.exitCode = 1; return; }
363
+ if (!opts.json) for (const f of found) console.log(`detected ${f.id}: ${f.evidence.join("; ")}`);
364
+ return save({ blocks: found.map((f) => f.id) }, "created from the project's files");
365
+ }
366
+ if (opts.add || opts.remove) {
367
+ const stack = current ?? { blocks: [] };
368
+ const id = (e) => (typeof e === "string" ? e : e.id);
369
+ for (const x of opts.add ?? []) {
370
+ const [bid, version] = x.split("@");
371
+ stack.blocks = stack.blocks.filter((e) => id(e) !== bid);
372
+ stack.blocks.push(version ? { id: bid, version } : bid);
373
+ }
374
+ for (const x of opts.remove ?? []) {
375
+ if (!stack.blocks.some((e) => id(e) === x)) cmd.error(`error: ${x} is not in the selection`, { exitCode: USAGE, code: "agiter.stack" });
376
+ stack.blocks = stack.blocks.filter((e) => id(e) !== x);
377
+ }
378
+ return save(stack, current ? "updated" : "created");
379
+ }
380
+ const role = typeof opts.show === "string" ? opts.show : null;
381
+ const composed = api.composeProject(root, role);
382
+ if (!composed) { print(opts, { stack: null }, () => console.log("the project has no stack yet (agiter stack --preset <id>, --detect or --add <blocks...>) and no agiter/config/project.md")); return; }
383
+ print(opts, { role, ...composed }, () => {
384
+ console.log(role ? `# What the ${role} gets` : "# The project's stack (every section)");
385
+ console.log("");
386
+ console.log(composed.text);
387
+ console.log(`\n(${Buffer.byteLength(composed.text)} bytes in every ${role ? `${role} ` : ""}prompt)`);
388
+ for (const p of composed.problems) console.log(`problem: ${p}`);
389
+ });
390
+ process.exitCode = composed.problems.length ? 1 : 0;
391
+ });
392
+
393
+ prog.command("sidecar").description("serve the API as JSON-RPC over stdio (for Far Manager)").action(async () => {
394
+ const { serve } = await import("../sidecar.mjs");
395
+ await serve();
396
+ });
397
+
398
+ return prog;
399
+ }
400
+
401
+ /** Run the command line `argv` (without node and the script); the exit code. */
402
+ export async function main(argv) {
403
+ const prog = program();
404
+ if (!argv.length) { prog.outputHelp(); return 0; }
405
+ try {
406
+ await prog.parseAsync(argv, { from: "user" });
407
+ return process.exitCode ?? 0;
408
+ } catch (e) {
409
+ if (!(e instanceof CommanderError)) throw e;
410
+ if (["commander.helpDisplayed", "commander.help", "commander.version"].includes(e.code)) return 0;
411
+ if (e.code === "agiter.script") console.error(`agiter: ${e.message}`);
412
+ return USAGE;
413
+ }
414
+ }
415
+
416
+ const self = process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(new URL(import.meta.url));
417
+ if (self) process.exitCode = await main(process.argv.slice(2));
@@ -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,9 @@
1
+ {
2
+ "default": {
3
+ "agent": "claude",
4
+ "models": [],
5
+ "select": "availableFirst",
6
+ "effort": "high",
7
+ "timeoutMinutes": 30
8
+ }
9
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "default": {
3
+ "agent": "opencode",
4
+ "models": [
5
+ "openrouter/deepseek/deepseek-v4.1-flash"
6
+ ],
7
+ "select": "availableFirst",
8
+ "effort": "high",
9
+ "timeoutMinutes": 30
10
+ }
11
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "default": {
3
+ "agent": "opencode",
4
+ "models": [
5
+ "openrouter/deepseek/deepseek-v4.1-flash"
6
+ ],
7
+ "select": "availableFirst",
8
+ "effort": "high",
9
+ "timeoutMinutes": 30
10
+ }
11
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "default": {
3
+ "agent": "opencode",
4
+ "models": [
5
+ "openrouter/deepseek/deepseek-v4.1-flash"
6
+ ],
7
+ "select": "availableFirst",
8
+ "effort": "high",
9
+ "timeoutMinutes": 30
10
+ }
11
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "default": {
3
+ "agent": "claude",
4
+ "models": [],
5
+ "select": "availableFirst",
6
+ "effort": "high",
7
+ "timeoutMinutes": 30
8
+ }
9
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "default": {
3
+ "agent": "opencode",
4
+ "models": [
5
+ "openrouter/deepseek/deepseek-v4.1-flash"
6
+ ],
7
+ "select": "availableFirst",
8
+ "effort": "high",
9
+ "timeoutMinutes": 60
10
+ }
11
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "title": "Checks",
3
+ "summary": "The results of the project's required checks.",
4
+ "sections": [
5
+ "Results"
6
+ ],
7
+ "statuses": [
8
+ "Draft",
9
+ "Active",
10
+ "Done"
11
+ ]
12
+ }