@pavel.sommer/agiter 0.8.0-alpha.0 → 0.27.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.
Files changed (84) hide show
  1. package/README.md +72 -9
  2. package/bin/agiter.mjs +122 -21
  3. package/defs/{agents.json → .agents.json} +12 -2
  4. package/defs/artifacts/checks.json +6 -0
  5. package/defs/artifacts/create-epics.json +32 -0
  6. package/defs/artifacts/create-tests.json +22 -0
  7. package/defs/artifacts/{work.json → create-work.json} +9 -2
  8. package/defs/artifacts/decisions.json +12 -0
  9. package/defs/artifacts/report.json +1 -0
  10. package/defs/blocks/ado-net.json +130 -0
  11. package/defs/blocks/agent-browser.json +59 -0
  12. package/defs/blocks/angular.json +270 -0
  13. package/defs/blocks/aspnet-minimal-api.json +68 -0
  14. package/defs/blocks/aspnet-mvc.json +113 -0
  15. package/defs/blocks/aspnet-webapi.json +79 -0
  16. package/defs/blocks/aspnet-webforms.json +150 -0
  17. package/defs/blocks/browser.json +102 -0
  18. package/defs/blocks/cbor.json +95 -0
  19. package/defs/blocks/clean-architecture.json +127 -0
  20. package/defs/blocks/client-server.json +134 -0
  21. package/defs/blocks/ddd.json +82 -0
  22. package/defs/blocks/dotnet-framework.json +153 -0
  23. package/defs/blocks/dotnet.json +119 -0
  24. package/defs/blocks/dto-templates.json +39 -0
  25. package/defs/blocks/ef6.json +138 -0
  26. package/defs/blocks/entity-drafts.json +84 -0
  27. package/defs/blocks/express.json +135 -0
  28. package/defs/blocks/far-luamacro.json +202 -0
  29. package/defs/blocks/fastify.json +125 -0
  30. package/defs/blocks/javascript.json +40 -0
  31. package/defs/blocks/luajit.json +117 -0
  32. package/defs/blocks/native-aot.json +105 -0
  33. package/defs/blocks/next.json +257 -0
  34. package/defs/blocks/node.json +54 -0
  35. package/defs/blocks/npm.json +72 -0
  36. package/defs/blocks/nuxt.json +265 -0
  37. package/defs/blocks/odbc-ffi.json +154 -0
  38. package/defs/blocks/playwright-core.json +73 -0
  39. package/defs/blocks/react.json +295 -0
  40. package/defs/blocks/sql-server.json +96 -0
  41. package/defs/blocks/tdd.json +47 -0
  42. package/defs/blocks/tsql-migrations.json +260 -0
  43. package/defs/blocks/typescript.json +115 -0
  44. package/defs/blocks/vanilla-web.json +88 -0
  45. package/defs/blocks/vite.json +91 -0
  46. package/defs/blocks/vitest.json +78 -0
  47. package/defs/blocks/vue.json +292 -0
  48. package/defs/blocks/web-components.json +86 -0
  49. package/defs/blocks/windows-auth.json +88 -0
  50. package/defs/presets/angular.json +15 -0
  51. package/defs/presets/aspnet-classic.json +13 -0
  52. package/defs/presets/aspnet-webforms.json +12 -0
  53. package/defs/presets/dotnet-sqlserver.json +14 -0
  54. package/defs/presets/far-luajit.json +12 -0
  55. package/defs/presets/mssql-database.json +16 -0
  56. package/defs/presets/react-express.json +18 -0
  57. package/defs/presets/react-fastify.json +18 -0
  58. package/defs/presets/react-next.json +16 -0
  59. package/defs/presets/react-vite.json +16 -0
  60. package/defs/presets/vanilla-web.json +16 -0
  61. package/defs/presets/vue-nuxt.json +17 -0
  62. package/defs/presets/vue-vite.json +16 -0
  63. package/defs/tasks/create-epics.json +2 -2
  64. package/defs/tasks/create-tests.json +1 -2
  65. package/defs/tasks/create-work.json +1 -1
  66. package/index.mjs +5 -3
  67. package/package.json +3 -3
  68. package/src/agents.mjs +62 -28
  69. package/src/artifacts.mjs +84 -12
  70. package/src/defs.mjs +23 -2
  71. package/src/engine.mjs +790 -104
  72. package/src/fake.mjs +11 -2
  73. package/src/frontmatter.mjs +113 -62
  74. package/src/git.mjs +126 -8
  75. package/src/init.mjs +30 -7
  76. package/src/lang.mjs +21 -0
  77. package/src/links.mjs +55 -0
  78. package/src/log.mjs +53 -53
  79. package/src/models.mjs +21 -10
  80. package/src/pipeline.mjs +5 -1
  81. package/src/prompt.mjs +110 -20
  82. package/src/stack.mjs +338 -0
  83. package/src/tools.mjs +2 -2
  84. package/src/usage.mjs +102 -0
package/README.md CHANGED
@@ -3,15 +3,44 @@
3
3
  A client-agnostic harness for spec-driven agentic development. The design is in
4
4
  [docs/design-recap.md](docs/design-recap.md).
5
5
 
6
+ ## The key idea: diverse points of view
7
+
8
+ Every step of a run is done by separate agents, each starting with a fresh context: the author
9
+ writes, the reviewer judges what was written against the input and the criteria, without the
10
+ author's reasoning in view, so it has nothing to anchor on and no decision of its own to
11
+ defend. A next round's author gets its version and the review, not its earlier train of
12
+ thought. One long prompting session cannot give this: there a model judges its own work with
13
+ the whole path to it still in context, like a person proofreading what he wrote a minute ago.
14
+
15
+ Context diversity works with the same model too. Model diversity adds a second layer: a
16
+ model's blind spots come from its training, and a fresh context does not remove them, a
17
+ different model does. That is why each role has its own model profile
18
+ (`agiter/config/.models.<role>.json`): a cheap author and a stronger reviewer of another family
19
+ make a good pair.
20
+
21
+ ## How it works
22
+
6
23
  A run's pipeline is always given explicitly: a comma list of tokens, or a pipeline file
7
24
  (`*.json`, `{ "steps": [...] }`). A token is `task` (one round, no review) or `task:N` (at most
8
25
  N rounds; each round is a version and its review report).
9
26
 
27
+ The project's tech stack lives in `agiter/config/.stack.json` (blocks and presets, `agiter
28
+ stack --help`); the harness puts into each model's prompt only what its role needs from it, then the owner's
29
+ `agiter/config/project.md` (structure, naming, scaffolding, rules) for every role. `--includeFile
30
+ <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
31
+ epic (`<run>.create-epics.<round>.E1.md`, …) next to an overview. `--lang <tag>` (an ISO tag: `cs`, `en`,
32
+ `cs-CZ`, `en-US`, …) sets the language of the output; without it the models write in the language
33
+ of the input. The headings the harness reads stay in English, and so do established technical
34
+ terms and common anglicisms.
35
+
10
36
  Status: step 1 (inputs, artifacts, tasks, pipelines, the run engine, simulation) and a first
11
37
  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.
38
+ (`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
39
+ a run works in the current checkout by default (no harness commits; the owner reviews and
40
+ commits the changes), with `--isolation branch` on its own branch `agiter/<run>` with a commit
41
+ per call, or with `--isolation worktree` in `agiter/.worktrees/<run>` on that branch with the
42
+ project's `node_modules` linked in. The scope of a step is checked against what git sees, and
43
+ `agiter finish [--merge]` ends a branch or worktree run.
15
44
 
16
45
  ## Layout
17
46
 
@@ -21,23 +50,57 @@ of a step is checked against what git sees, and `agiter finish [--merge]` ends t
21
50
  | `bin/agiter.mjs` | the CLI on commander (`agiter --help`, `agiter <command> --help`) |
22
51
  | `sidecar.mjs` | JSON-RPC over stdio for long-lived clients (Far Manager) |
23
52
  | `src/` | the modules: definitions, pipelines, artifacts, engine, tools, log |
24
- | `defs/` | the package's artifact types and tasks |
53
+ | `defs/` | the package's artifact types, tasks, stack blocks and presets, default config |
25
54
  | `test/` | `npm test` (node:test, no dependencies) |
26
55
 
27
- A project keeps everything in `.agiter/`: `docs/<run>/` (the chain of each run), `runs/`,
56
+ The owner's documents stay his: any file a run did not write is an input in the owner's own form
57
+ (with a frontmatter `type` it runs only with status `Active`; without one it runs as it is);
58
+ the pipeline decides what it is (`create-epics`: an initiative), and its frontmatter, status and
59
+ structure are not checked. A run is named after the input file without its leading timestamp and
60
+ stamped in local time (`yyMMddHHmmss`); the titles of the outputs are the models' own.
61
+
62
+ A project keeps everything in `agiter/`: `docs/<run>/` (the chain of each run), `runs/`,
28
63
  `logs/`, its own definitions (`artifacts/`, `tasks/`, merged over the package's) and
29
64
  `checks.json` (the required checks).
30
65
 
31
66
  ## A run's files
32
67
 
33
68
  ```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
69
+ agiter/docs/<run>/.<run>.md the input, copied as it is (never changed; a dot file)
70
+ agiter/docs/<run>/.<run>.draft.1.md round 1 of create-draft:3 (a dot file)
71
+ agiter/docs/<run>/.<run>.draft.1.report.md its review: Block or Done, cumulative (a dot file)
72
+ agiter/docs/<run>/.<run>.draft.2.md round 2: input + draft.1 + its report
73
+ agiter/docs/<run>/<run>.create-work.1.md the record of create-work
74
+ docs/<run>.draft.md the result of a step, published
75
+ docs/<run>.draft.report.md the step's last report, published
76
+ docs/<run>.decisions.md the decisions report, for the owner to answer
77
+ docs/<run>.epic.E1.md an epic of create-epics, published
38
78
  ...
39
79
  ```
40
80
 
81
+ Two kinds of output. Methodical artifacts (initiative, epic, draft, spec) are the documents a
82
+ next run builds on: the result of each step (no review, or its report Done) is published to
83
+ `docs/` (`agiter/config/.settings.json` `{ "docs": "<folder>" }` changes it), never over a
84
+ file there, and agents never change them. Task records (`create-epics`, `create-work`,
85
+ `create-tests`, `checks`) and the reports stay in `agiter/docs/<run>/` with every round; the versions of methodical
86
+ artifacts there are dot files, so a client like Tolaria shows each document once (in `docs/`).
87
+ Where the input does not say something, the models decide on their own and record it under
88
+ "Decisions" (`[owner]` or `[author]`); at the end of a run the harness collects them into
89
+ `docs/<run>.decisions.md` (beside the owner's documents), the owner's first, each with an `Answer:` line, so the
90
+ answered file can be the input of a next run.
91
+ A run on a document an earlier run wrote (an epic, a spec) gets that run's answered decisions in
92
+ its prompt by itself: they override the document where they differ.
93
+ Each document's frontmatter has the `model` that wrote it and the `cost` of that call.
94
+ Every model call's cost and tokens come from the agent's JSON output (opencode `--format json`,
95
+ claude `--output-format json`): `status` shows them per call and for the run, `run --list` per run.
96
+ At the end of a run the file names in its documents become wikilinks (`[[name]]`, as Tolaria
97
+ resolves them), to the notes in view: a version of the chain links to its published document.
98
+ Their frontmatter gets Tolaria's relationships too: `belongs_to` and `related_to` as wikilinks.
99
+ The owner's input file gets `has` (wikilinks to what runs made from it and keep), `model` (the
100
+ models they used) and `cost` (their total).
101
+ Frontmatter types are TitleCase and singular (`Spec`, `Epic`, `Report`, `Decision`); every task's
102
+ record is `Pipeline`, its `task` (`create-epics`, `create-work`, …) tells which.
103
+
41
104
  A version is Active when another step follows and it has no review or its report is Done;
42
105
  otherwise Draft. A step takes only prose or an Active artifact whose latest report is not
43
106
  Block. The state of a run is its files: `agiter continue` walks the steps again and repeats
package/bin/agiter.mjs CHANGED
@@ -6,8 +6,10 @@
6
6
  */
7
7
 
8
8
  import fs from "node:fs";
9
+ import { money, tokens } from "../src/usage.mjs";
9
10
  import os from "node:os";
10
11
  import path from "node:path";
12
+ import { spawnSync } from "node:child_process";
11
13
  import { Command, CommanderError, Option } from "commander";
12
14
  import * as api from "../index.mjs";
13
15
  import { logCli } from "../src/log.mjs";
@@ -17,14 +19,25 @@ const USAGE = 2;
17
19
  function tokenOf(s) { return s.rounds ? `${s.task}:${s.rounds}` : s.task; }
18
20
 
19
21
  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
+ 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" : ""}` : ""})`;
23
+ console.log(`run ${run.id}: ${run.status}${where}`);
24
+ console.log(` pipeline ${run.pipeline.steps.map(tokenOf).join(",")}, input ${run.input.type}${run.lang ? `, output in ${run.lang}` : ""}`);
22
25
  for (const s of run.steps) {
23
26
  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}`);
27
+ console.log(` ${String(s.step).padEnd(3)} ${s.task.padEnd(13)} ${(s.round ? `r${s.round}` : "").padEnd(4)}${s.role.padEnd(9)} ${what}${s.usage ? ` ${money(s.usage)}` : ""}`);
28
+ }
29
+ if (run.steps.some((s) => s.executor === "model" || s.executor === "fake")) {
30
+ const calls = run.steps.filter((s) => s.executor === "model");
31
+ const missing = calls.filter((s) => !s.usage).length;
32
+ console.log(` cost: ${run.usage ? `${money(run.usage)} (${tokens(run.usage)})` : "no usage data"}${missing && run.usage ? `; ${missing} call(s) without usage data` : ""}`);
33
+ }
34
+ if (run.decisions) {
35
+ const d = run.decisions;
36
+ console.log(` decisions: ${d.owner ?? 0} for you${d.review ? `, ${d.review} open from the review` : ""}, ${(d.owner ?? 0) + (d.review ?? 0) + (d.unmarked ?? 0) + (d.author ?? 0)} in all -> ${d.file}`);
25
37
  }
26
38
  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)`);
39
+ 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)`);
40
+ else if (run.status === "done" && run.git?.mode === "none") console.log(" next: review the changes (git status, git diff) and commit them");
28
41
  }
29
42
 
30
43
  const pad2 = (n) => String(n).padStart(2, "0");
@@ -38,7 +51,7 @@ function patternOf(value, option, cmd) {
38
51
  try { return new RegExp(value, "i"); } catch (e) { cmd.error(`error: --${option}: ${e.message}`, { exitCode: USAGE, code: "agiter.pattern" }); }
39
52
  }
40
53
 
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}]` : ""}`;
54
+ const runLine = (r) => `${local(r.created)} ${r.status.padEnd(8)} ${duration(r.seconds).padStart(12)} ${String(r.calls).padStart(3)} calls ${money(r.usage).padStart(9)} ${r.id} ${r.pipeline}${r.name ? ` "${r.name}"` : ""}${r.finished ? (r.finished.merged ? " merged" : " finished") : ""}${r.error ? ` [${r.error}]` : ""}`;
42
55
 
43
56
  /** run --list [pattern]: the runs, one per line. */
44
57
  function listRunsOf(opts, cmd) {
@@ -62,7 +75,7 @@ async function confirm(question) {
62
75
  async function deleteAllOf(opts, cmd, root) {
63
76
  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
77
  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;
78
+ 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
79
  if (!folders) { print(opts, { deleted: [] }, () => console.log("no run to delete")); return; }
67
80
  if (!opts.yes) {
68
81
  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" });
@@ -136,7 +149,14 @@ function live(opts) {
136
149
  /** The --pipeline value: the shell may split an unquoted list ("a:2, b") into several items. */
137
150
  const pipelineOf = (items) => [].concat(items).join(" ");
138
151
 
139
- const rootOf = (opts) => path.resolve(opts.root ?? ".");
152
+ /** The git work tree's top of the current folder, or null. */
153
+ function gitTop() {
154
+ const r = spawnSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8", windowsHide: true });
155
+ return r.status === 0 ? path.resolve(r.stdout.trim()) : null;
156
+ }
157
+
158
+ /** The project: --root, else found from the current folder (src/init.mjs findProjectRoot). */
159
+ const rootOf = (opts) => (opts.root ? path.resolve(opts.root) : api.findProjectRoot(process.cwd(), gitTop()));
140
160
  const print = (opts, data, text) => { if (opts.json) console.log(JSON.stringify(data, null, "\t")); else text(); };
141
161
  const problems = (opts, r) => { print(opts, r, () => r.problems.forEach((p) => console.log(`problem: ${p}`))); return 1; };
142
162
 
@@ -146,10 +166,13 @@ function scenario(cmd, required = true) {
146
166
  return (required ? cmd.requiredOption(...pipeline) : cmd.option(...pipeline))
147
167
  .addOption(new Option("--input <file>", "the input file: prose, or an Active artifact").conflicts("text"))
148
168
  .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)");
169
+ .option("--name <name>", "the request's name (default: the input file's name; for text, each step's own heading)")
170
+ .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")
171
+ .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"]))
172
+ .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], []);
150
173
  }
151
174
 
152
- const common = (cmd) => cmd.option("--root <dir>", "the project (default: the current folder)").option("--json", "the result as JSON");
175
+ 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");
153
176
 
154
177
  export function program() {
155
178
  const prog = new Command("agiter")
@@ -157,8 +180,12 @@ export function program() {
157
180
  .showHelpAfterError("(agiter --help, or agiter <command> --help)")
158
181
  .exitOverride();
159
182
 
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) });
183
+ common(prog.command("init").description("make the current folder (or --root) an agiter project: agiter/ with the default config in agiter/config/")).action((opts) => {
184
+ // The current folder, unless it is already inside a project (then that project).
185
+ const found = api.findProjectRoot(process.cwd());
186
+ const root = opts.root ? path.resolve(opts.root) : api.isProject(found) ? found : process.cwd();
187
+ const r = api.initProject({ root });
188
+ if (!opts.json && root !== process.cwd()) console.log(`project: ${root}`);
162
189
  print(opts, r, () => {
163
190
  for (const f of r.created) console.log(`created ${f}`);
164
191
  for (const f of r.existed) console.log(`kept ${f} (exists)`);
@@ -166,9 +193,11 @@ export function program() {
166
193
  });
167
194
 
168
195
  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 }, () => {
196
+ 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" });
197
+ print(opts, { ok: r.ok, problems: r.problems, name: r.name, lang: r.lang ?? null, include: r.include, plan: r.plan }, () => {
171
198
  if (r.ok) console.log(`name: ${r.name ?? "(each step's own heading)"}`);
199
+ if (r.ok) console.log(`output language: ${r.lang ?? "(the input's)"}`);
200
+ if (r.include?.length) console.log(`files for every call: ${r.include.join(", ")}`);
172
201
  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
202
  for (const p of r.problems) console.log(`problem: ${p}`);
174
203
  });
@@ -182,12 +211,13 @@ export function program() {
182
211
  let script = {};
183
212
  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
213
  simulation = { script };
185
- if (!opts.root) { root = fs.mkdtempSync(path.join(os.tmpdir(), "agiter-sim-")); fs.mkdirSync(path.join(root, ".agiter")); }
214
+ // Without --root: the project the current folder is in, else a new temporary project.
215
+ if (!opts.root && !api.isProject(root)) { root = fs.mkdtempSync(path.join(os.tmpdir(), "agiter-sim-")); fs.mkdirSync(path.join(root, "agiter")); }
186
216
  }
187
217
  logCli(root, process.argv.slice(2));
188
218
  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 });
219
+ if ((simulate || root !== process.cwd()) && !opts.json) console.log(`project: ${root}`);
220
+ 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 });
191
221
  view.done?.();
192
222
  if (!r.ok) { process.exitCode = problems(opts, r); return; }
193
223
  print(opts, { root, resumed: r.resumed, run: r.run }, () => {
@@ -199,14 +229,14 @@ export function program() {
199
229
  };
200
230
  const runOptions = (cmd) => cmd
201
231
  .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")
232
+ .option("--audit", "record every executor call in agiter/runs/<run>/audit.jsonl")
203
233
  .option("--quiet", "show only the harness's events, not the agents' output");
204
234
 
205
235
  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
236
  .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"]))
237
+ .conflicts(["pipeline", "input", "text", "name", "new", "audit", "quiet", "includeFile", "isolation", "lang"]))
208
238
  .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"]))
239
+ .conflicts(["pipeline", "input", "text", "name", "new", "audit", "quiet", "includeFile", "isolation", "lang", "list"]))
210
240
  .option("--failed", "with --delete: only failed runs (with --done: failed or done ones)")
211
241
  .option("--done", "with --delete: only done runs (with --failed: failed or done ones)")
212
242
  .option("--all", "with --delete: delete every run and clear last.json (asks twice)")
@@ -221,7 +251,7 @@ export function program() {
221
251
  if (!opts.pipeline) cmd.error("error: required option '--pipeline <steps...>' not specified (or --list)", { exitCode: USAGE, code: "agiter.pipeline" });
222
252
  return runner(false)(opts);
223
253
  });
224
- common(runOptions(scenario(prog.command("simulate").description("run a scenario with a scripted fake model (a new temporary project without --root)")))
254
+ 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)")))
225
255
  .option("--script <file>", "what the fake model returns, per task, role and attempt")).action(runner(true));
226
256
 
227
257
  common(prog.command("continue").argument("[run]", "the run id", "last").description("walk a run's steps again from its files")
@@ -230,6 +260,7 @@ export function program() {
230
260
  const root = rootOf(opts);
231
261
  logCli(root, process.argv.slice(2));
232
262
  const view = live(opts);
263
+ if (root !== process.cwd() && !opts.json) console.log(`project: ${root}`);
233
264
  const r = await api.continueRun({ root, run: id, audit: Boolean(opts.audit), onEvent: view.onEvent, onOutput: view.onOutput });
234
265
  view.done?.();
235
266
  if (!r.ok) { process.exitCode = problems(opts, r); return; }
@@ -238,7 +269,7 @@ export function program() {
238
269
  });
239
270
 
240
271
  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) => {
272
+ .option("--merge", "merge the run's branch into the branch it started from (no fast-forward); a worktree is removed after")).action((id, opts) => {
242
273
  const root = rootOf(opts);
243
274
  logCli(root, process.argv.slice(2));
244
275
  const r = api.finishRun({ root, run: id, merge: Boolean(opts.merge) });
@@ -299,6 +330,76 @@ export function program() {
299
330
  process.exitCode = defs.problems.length ? 1 : 0;
300
331
  });
301
332
 
333
+ 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")
334
+ .addOption(new Option("--list", "the blocks and presets of the package and the project").conflicts(["show", "add", "remove", "preset", "detect"]))
335
+ .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"]))
336
+ .addOption(new Option("--add <blocks...>", "add blocks (id or id@version)").conflicts(["remove", "preset", "detect"]))
337
+ .addOption(new Option("--remove <blocks...>", "remove blocks").conflicts(["preset", "detect"]))
338
+ .addOption(new Option("--preset <id>", "start the selection from a preset (only when the project has none)").conflicts(["detect"]))
339
+ .option("--detect", "start the selection from the blocks the project's files show (only when the project has none)");
340
+ common(stackCmd).action((opts, cmd) => {
341
+ const root = rootOf(opts);
342
+ const { blocks, presets, problems: loadProblems } = api.loadBlocks(root);
343
+ if (opts.list) {
344
+ 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 }, () => {
345
+ 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)" : ""}`);
346
+ console.log("\npresets:");
347
+ for (const p of presets.values()) console.log(` ${p.id.padEnd(18)} ${p.blocks.map((x) => (typeof x === "string" ? x : x.id)).join(", ")}`);
348
+ });
349
+ return;
350
+ }
351
+ const current = api.readStack(root);
352
+ if (current?.error) { process.exitCode = problems(opts, { problems: [current.error] }); return; }
353
+ const save = (stack, what) => {
354
+ const r = api.resolveSelection(blocks, stack.blocks);
355
+ if (r.problems.length) { process.exitCode = problems(opts, { problems: r.problems.map((p) => `stack: ${p}`) }); return; }
356
+ api.writeStack(root, stack);
357
+ print(opts, { stack, added: r.added, suggestions: r.suggestions }, () => {
358
+ console.log(`${what}: agiter/config/.stack.json`);
359
+ console.log(`blocks: ${[...r.chosen.values()].map((e) => (e.block.versions ? `${e.block.id}@${e.version}` : e.block.id)).join(", ")}`);
360
+ for (const a of r.added) console.log(` ${a.id} added as required by ${a.by}`);
361
+ if (r.suggestions.length) console.log(`suggested: ${r.suggestions.map((x) => `${x.id} (by ${x.by})`).join(", ")}`);
362
+ });
363
+ };
364
+ if (opts.preset || opts.detect) {
365
+ 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" });
366
+ if (opts.preset) {
367
+ const p = presets.get(opts.preset);
368
+ if (!p) cmd.error(`error: no preset ${opts.preset} (agiter stack --list)`, { exitCode: USAGE, code: "agiter.stack" });
369
+ return save({ preset: p.id, blocks: [...p.blocks] }, `created from the preset ${p.id}`);
370
+ }
371
+ const found = api.detectBlocks(blocks, root);
372
+ if (!found.length) { print(opts, { detected: [] }, () => console.log("no block detected: start with --preset or --add")); process.exitCode = 1; return; }
373
+ if (!opts.json) for (const f of found) console.log(`detected ${f.id}: ${f.evidence.join("; ")}`);
374
+ return save({ blocks: found.map((f) => f.id) }, "created from the project's files");
375
+ }
376
+ if (opts.add || opts.remove) {
377
+ const stack = current ?? { blocks: [] };
378
+ const id = (e) => (typeof e === "string" ? e : e.id);
379
+ for (const x of opts.add ?? []) {
380
+ const [bid, version] = x.split("@");
381
+ stack.blocks = stack.blocks.filter((e) => id(e) !== bid);
382
+ stack.blocks.push(version ? { id: bid, version } : bid);
383
+ }
384
+ for (const x of opts.remove ?? []) {
385
+ if (!stack.blocks.some((e) => id(e) === x)) cmd.error(`error: ${x} is not in the selection`, { exitCode: USAGE, code: "agiter.stack" });
386
+ stack.blocks = stack.blocks.filter((e) => id(e) !== x);
387
+ }
388
+ return save(stack, current ? "updated" : "created");
389
+ }
390
+ const role = typeof opts.show === "string" ? opts.show : null;
391
+ const composed = api.composeProject(root, role);
392
+ 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; }
393
+ print(opts, { role, ...composed }, () => {
394
+ console.log(role ? `# What the ${role} gets` : "# The project's stack (every section)");
395
+ console.log("");
396
+ console.log(composed.text);
397
+ console.log(`\n(${Buffer.byteLength(composed.text)} bytes in every ${role ? `${role} ` : ""}prompt)`);
398
+ for (const p of composed.problems) console.log(`problem: ${p}`);
399
+ });
400
+ process.exitCode = composed.problems.length ? 1 : 0;
401
+ });
402
+
302
403
  prog.command("sidecar").description("serve the API as JSON-RPC over stdio (for Far Manager)").action(async () => {
303
404
  const { serve } = await import("../sidecar.mjs");
304
405
  await serve();
@@ -13,7 +13,12 @@
13
13
  ],
14
14
  "message": [
15
15
  "{message}"
16
- ]
16
+ ],
17
+ "format": [
18
+ "--format",
19
+ "json"
20
+ ],
21
+ "output": "opencode-json"
17
22
  },
18
23
  "claude": {
19
24
  "title": "Claude Code, print mode; the prompt comes right after -p (the tool options take several values); the reviewer reads only",
@@ -43,6 +48,11 @@
43
48
  "ANTHROPIC_AUTH_TOKEN",
44
49
  "ANTHROPIC_API_KEY",
45
50
  "ANTHROPIC_MODEL"
46
- ]
51
+ ],
52
+ "format": [
53
+ "--output-format",
54
+ "json"
55
+ ],
56
+ "output": "claude-json"
47
57
  }
48
58
  }
@@ -1,4 +1,10 @@
1
1
  {
2
+ "name": "Pipeline",
3
+ "shared": true,
4
+ "record": true,
5
+ "aliases": [
6
+ "Check"
7
+ ],
2
8
  "title": "Checks",
3
9
  "summary": "The results of the project's required checks.",
4
10
  "sections": [
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "Pipeline",
3
+ "shared": true,
4
+ "record": true,
5
+ "aliases": [
6
+ "epics",
7
+ "CreateEpic"
8
+ ],
9
+ "title": "Breakdown into epics",
10
+ "summary": "The result of create-epics, not a methodical artifact: the input's functional areas and the epics that deliver them; each epic is in its own file (type epic).",
11
+ "sections": [
12
+ "Functional areas",
13
+ "Epics"
14
+ ],
15
+ "criteria": [
16
+ "Every functional area of the input is listed, and every one is covered by exactly one epic.",
17
+ "The epics do not overlap: nothing is in the scope of two epics.",
18
+ "Each epic can be delivered and checked on its own; where one needs another, its scope says which.",
19
+ "There are as many epics as the input needs: no epic for a trivial part, no epic that bundles unrelated areas.",
20
+ "Nothing outside the input is added."
21
+ ],
22
+ "items": {
23
+ "type": "epic",
24
+ "prefix": "E",
25
+ "section": "Epics"
26
+ },
27
+ "statuses": [
28
+ "Draft",
29
+ "Active",
30
+ "Done"
31
+ ]
32
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "Pipeline",
3
+ "shared": true,
4
+ "record": true,
5
+ "aliases": [
6
+ "CreateTest"
7
+ ],
8
+ "title": "Tests record",
9
+ "summary": "The record of create-tests, not a methodical artifact: the tests written for a spec.",
10
+ "sections": [
11
+ "Changes"
12
+ ],
13
+ "criteria": [
14
+ "The tests cover every acceptance criterion of the spec.",
15
+ "Nothing outside the spec's scope changed."
16
+ ],
17
+ "statuses": [
18
+ "Draft",
19
+ "Active",
20
+ "Done"
21
+ ]
22
+ }
@@ -1,6 +1,13 @@
1
1
  {
2
- "title": "Work",
3
- "summary": "The record of a change made to the code, tests included.",
2
+ "name": "Pipeline",
3
+ "shared": true,
4
+ "record": true,
5
+ "aliases": [
6
+ "work",
7
+ "CreateWork"
8
+ ],
9
+ "title": "Work record",
10
+ "summary": "The record of create-work, not a methodical artifact: the change made to the code, tests included.",
4
11
  "sections": [
5
12
  "Changes"
6
13
  ],
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "Decision",
3
+ "record": true,
4
+ "title": "Decisions",
5
+ "summary": "The record of a run's autonomous decisions: what the models decided that the input did not say, those that belong to the owner first, for him to answer.",
6
+ "sections": [],
7
+ "statuses": [
8
+ "Draft",
9
+ "Active",
10
+ "Done"
11
+ ]
12
+ }
@@ -1,4 +1,5 @@
1
1
  {
2
+ "record": true,
2
3
  "title": "Report",
3
4
  "summary": "The review of one version: its findings, cumulative over the rounds.",
4
5
  "statuses": [
@@ -0,0 +1,130 @@
1
+ {
2
+ "id": "ado-net",
3
+ "title": "ADO.NET (Microsoft.Data.SqlClient)",
4
+ "kind": "library",
5
+ "slot": "dotnet-data-access",
6
+ "detect": {
7
+ "text": [
8
+ {
9
+ "files": "**/*.csproj",
10
+ "match": "Microsoft\\.Data\\.SqlClient"
11
+ },
12
+ {
13
+ "files": "**/packages.config",
14
+ "match": "id=\"Microsoft\\.Data\\.SqlClient\""
15
+ }
16
+ ]
17
+ },
18
+ "summary": "explicit SQL and row mapping, no ORM",
19
+ "requires": [
20
+ {
21
+ "anyOf": [
22
+ "dotnet",
23
+ "dotnet-framework"
24
+ ]
25
+ },
26
+ "sql-server"
27
+ ],
28
+ "fragments": {
29
+ "stack": [
30
+ {
31
+ "id": "ado.stack",
32
+ "text": "Direct ADO.NET through `Microsoft.Data.SqlClient`. No EF Core, Dapper or reflection-based object mappers.",
33
+ "unless": [
34
+ "dotnet-framework"
35
+ ]
36
+ },
37
+ {
38
+ "id": "ado.stack-framework",
39
+ "text": "Direct ADO.NET through the SqlClient the project references (`System.Data.SqlClient` or `Microsoft.Data.SqlClient`). No Entity Framework, Dapper or reflection-based object mappers.",
40
+ "when": [
41
+ "dotnet-framework"
42
+ ]
43
+ }
44
+ ],
45
+ "rules": [
46
+ {
47
+ "id": "ado.explicit",
48
+ "group": "ADO.NET and SQL Server",
49
+ "text": "Keep SQL and row mapping explicit. Parameterize values; set `SqlDbType` and relevant size, precision and scale deliberately."
50
+ },
51
+ {
52
+ "id": "ado.async",
53
+ "group": "ADO.NET and SQL Server",
54
+ "text": "Use `OpenAsync`, `ExecuteReaderAsync`/`ExecuteNonQueryAsync` and `ReadAsync` where available, propagating the request's `CancellationToken`."
55
+ },
56
+ {
57
+ "id": "ado.dispose",
58
+ "group": "ADO.NET and SQL Server",
59
+ "text": "Dispose connections, commands and readers deterministically; use asynchronous disposal where supported. Use connection pooling rather than a shared mutable `SqlConnection` singleton."
60
+ },
61
+ {
62
+ "id": "ado.typed",
63
+ "group": "ADO.NET and SQL Server",
64
+ "text": "Use typed reader access and handle `DBNull` explicitly. Select required columns; avoid `DataSet`/`DataTable` unless the named spec requires them."
65
+ },
66
+ {
67
+ "id": "ado.transactions",
68
+ "group": "ADO.NET and SQL Server",
69
+ "text": "Define transaction boundaries, isolation, timeout and concurrency behavior in the spec. Pass the transaction to every participating command."
70
+ },
71
+ {
72
+ "id": "ado.retry",
73
+ "group": "ADO.NET and SQL Server",
74
+ "text": "Do not retry non-idempotent writes without an explicit idempotency strategy. Cancellation does not prove a transaction was rolled back; test the chosen behavior."
75
+ },
76
+ {
77
+ "id": "ado.scripts",
78
+ "group": "ADO.NET and SQL Server",
79
+ "text": "Store schema changes as ordered SQL scripts with verification."
80
+ },
81
+ {
82
+ "id": "ado.aot",
83
+ "group": "Native AOT",
84
+ "text": "Validate the exact SqlClient version, native dependencies and database authentication mode in the published artifact. Do not infer full compatibility from target framework support alone.",
85
+ "when": [
86
+ "native-aot"
87
+ ]
88
+ }
89
+ ],
90
+ "paths": [
91
+ {
92
+ "id": "ado.database",
93
+ "path": "`database/`",
94
+ "content": "Ordered SQL scripts and schema documentation"
95
+ }
96
+ ],
97
+ "scopes": [
98
+ {
99
+ "id": "ado.schema",
100
+ "scope": "schema-only change",
101
+ "folder": "`docs/spec/database/`"
102
+ }
103
+ ],
104
+ "commands": [
105
+ {
106
+ "id": "ado.integration",
107
+ "purpose": "SQL integration tests",
108
+ "command": "`{{option.integration}}`",
109
+ "after": "unit tests"
110
+ }
111
+ ],
112
+ "environment": [
113
+ {
114
+ "id": "ado.sqlclient",
115
+ "setting": "SqlClient",
116
+ "value": "`<pinned version and required native/authentication dependencies>`"
117
+ }
118
+ ],
119
+ "notes": {
120
+ "commands": "SQL integration and native acceptance commands are mandatory for changes exercising those paths; do not report them as passed when credentials, SQL Server or the native toolchain are absent."
121
+ }
122
+ },
123
+ "options": {
124
+ "integration": {
125
+ "title": "the isolated-SQL-Server test command",
126
+ "type": "text",
127
+ "default": "<existing isolated-SQL-Server test command>"
128
+ }
129
+ }
130
+ }