@pavel.sommer/agiter 0.8.0-alpha.0 → 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 (74) hide show
  1. package/README.md +26 -9
  2. package/bin/agiter.mjs +110 -19
  3. package/defs/artifacts/create-epics.json +25 -0
  4. package/defs/blocks/ado-net.json +130 -0
  5. package/defs/blocks/agent-browser.json +59 -0
  6. package/defs/blocks/angular.json +270 -0
  7. package/defs/blocks/aspnet-minimal-api.json +68 -0
  8. package/defs/blocks/aspnet-mvc.json +113 -0
  9. package/defs/blocks/aspnet-webapi.json +79 -0
  10. package/defs/blocks/aspnet-webforms.json +150 -0
  11. package/defs/blocks/browser.json +102 -0
  12. package/defs/blocks/cbor.json +95 -0
  13. package/defs/blocks/clean-architecture.json +127 -0
  14. package/defs/blocks/client-server.json +134 -0
  15. package/defs/blocks/ddd.json +82 -0
  16. package/defs/blocks/dotnet-framework.json +153 -0
  17. package/defs/blocks/dotnet.json +119 -0
  18. package/defs/blocks/dto-templates.json +39 -0
  19. package/defs/blocks/ef6.json +138 -0
  20. package/defs/blocks/entity-drafts.json +84 -0
  21. package/defs/blocks/express.json +135 -0
  22. package/defs/blocks/far-luamacro.json +202 -0
  23. package/defs/blocks/fastify.json +125 -0
  24. package/defs/blocks/javascript.json +40 -0
  25. package/defs/blocks/luajit.json +117 -0
  26. package/defs/blocks/native-aot.json +105 -0
  27. package/defs/blocks/next.json +257 -0
  28. package/defs/blocks/node.json +54 -0
  29. package/defs/blocks/npm.json +72 -0
  30. package/defs/blocks/nuxt.json +265 -0
  31. package/defs/blocks/odbc-ffi.json +154 -0
  32. package/defs/blocks/playwright-core.json +73 -0
  33. package/defs/blocks/react.json +295 -0
  34. package/defs/blocks/sql-server.json +96 -0
  35. package/defs/blocks/tdd.json +47 -0
  36. package/defs/blocks/tsql-migrations.json +260 -0
  37. package/defs/blocks/typescript.json +115 -0
  38. package/defs/blocks/vanilla-web.json +88 -0
  39. package/defs/blocks/vite.json +91 -0
  40. package/defs/blocks/vitest.json +78 -0
  41. package/defs/blocks/vue.json +292 -0
  42. package/defs/blocks/web-components.json +86 -0
  43. package/defs/blocks/windows-auth.json +88 -0
  44. package/defs/presets/angular.json +15 -0
  45. package/defs/presets/aspnet-classic.json +13 -0
  46. package/defs/presets/aspnet-webforms.json +12 -0
  47. package/defs/presets/dotnet-sqlserver.json +14 -0
  48. package/defs/presets/far-luajit.json +12 -0
  49. package/defs/presets/mssql-database.json +16 -0
  50. package/defs/presets/react-express.json +18 -0
  51. package/defs/presets/react-fastify.json +18 -0
  52. package/defs/presets/react-next.json +16 -0
  53. package/defs/presets/react-vite.json +16 -0
  54. package/defs/presets/vanilla-web.json +16 -0
  55. package/defs/presets/vue-nuxt.json +17 -0
  56. package/defs/presets/vue-vite.json +16 -0
  57. package/defs/tasks/create-epics.json +2 -2
  58. package/index.mjs +4 -2
  59. package/package.json +2 -2
  60. package/src/agents.mjs +31 -15
  61. package/src/artifacts.mjs +42 -4
  62. package/src/defs.mjs +11 -2
  63. package/src/engine.mjs +266 -85
  64. package/src/fake.mjs +10 -1
  65. package/src/git.mjs +126 -8
  66. package/src/init.mjs +30 -7
  67. package/src/lang.mjs +21 -0
  68. package/src/log.mjs +53 -53
  69. package/src/models.mjs +21 -10
  70. package/src/pipeline.mjs +5 -1
  71. package/src/prompt.mjs +76 -15
  72. package/src/stack.mjs +338 -0
  73. package/src/tools.mjs +2 -2
  74. /package/defs/{agents.json → .agents.json} +0 -0
package/README.md CHANGED
@@ -7,11 +7,23 @@ A run's pipeline is always given explicitly: a comma list of tokens, or a pipeli
7
7
  (`*.json`, `{ "steps": [...] }`). A token is `task` (one round, no review) or `task:N` (at most
8
8
  N rounds; each round is a version and its review report).
9
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
+
10
19
  Status: step 1 (inputs, artifacts, tasks, pipelines, the run engine, simulation) and a first
11
20
  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.
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.
15
27
 
16
28
  ## Layout
17
29
 
@@ -21,20 +33,25 @@ of a step is checked against what git sees, and `agiter finish [--merge]` ends t
21
33
  | `bin/agiter.mjs` | the CLI on commander (`agiter --help`, `agiter <command> --help`) |
22
34
  | `sidecar.mjs` | JSON-RPC over stdio for long-lived clients (Far Manager) |
23
35
  | `src/` | the modules: definitions, pipelines, artifacts, engine, tools, log |
24
- | `defs/` | the package's artifact types and tasks |
36
+ | `defs/` | the package's artifact types, tasks, stack blocks and presets, default config |
25
37
  | `test/` | `npm test` (node:test, no dependencies) |
26
38
 
27
- A project keeps everything in `.agiter/`: `docs/<run>/` (the chain of each run), `runs/`,
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/`,
28
45
  `logs/`, its own definitions (`artifacts/`, `tasks/`, merged over the package's) and
29
46
  `checks.json` (the required checks).
30
47
 
31
48
  ## A run's files
32
49
 
33
50
  ```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
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
38
55
  ...
39
56
  ```
40
57
 
package/bin/agiter.mjs CHANGED
@@ -8,6 +8,7 @@
8
8
  import fs from "node:fs";
9
9
  import os from "node:os";
10
10
  import path from "node:path";
11
+ import { spawnSync } from "node:child_process";
11
12
  import { Command, CommanderError, Option } from "commander";
12
13
  import * as api from "../index.mjs";
13
14
  import { logCli } from "../src/log.mjs";
@@ -17,14 +18,16 @@ const USAGE = 2;
17
18
  function tokenOf(s) { return s.rounds ? `${s.task}:${s.rounds}` : s.task; }
18
19
 
19
20
  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}`);
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}` : ""}`);
22
24
  for (const s of run.steps) {
23
25
  const what = s.outcome === "failed" ? `failed [${s.guard}] ${s.error}` : `${s.file ? path.posix.basename(s.file) : ""}${s.verdict ? ` ${s.verdict}` : ""}`;
24
26
  console.log(` ${String(s.step).padEnd(3)} ${s.task.padEnd(13)} ${(s.round ? `r${s.round}` : "").padEnd(4)}${s.role.padEnd(9)} ${what}`);
25
27
  }
26
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}`);
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)`);
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");
28
31
  }
29
32
 
30
33
  const pad2 = (n) => String(n).padStart(2, "0");
@@ -62,7 +65,7 @@ async function confirm(question) {
62
65
  async function deleteAllOf(opts, cmd, root) {
63
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" });
64
67
  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;
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;
66
69
  if (!folders) { print(opts, { deleted: [] }, () => console.log("no run to delete")); return; }
67
70
  if (!opts.yes) {
68
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" });
@@ -136,7 +139,14 @@ function live(opts) {
136
139
  /** The --pipeline value: the shell may split an unquoted list ("a:2, b") into several items. */
137
140
  const pipelineOf = (items) => [].concat(items).join(" ");
138
141
 
139
- const rootOf = (opts) => path.resolve(opts.root ?? ".");
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()));
140
150
  const print = (opts, data, text) => { if (opts.json) console.log(JSON.stringify(data, null, "\t")); else text(); };
141
151
  const problems = (opts, r) => { print(opts, r, () => r.problems.forEach((p) => console.log(`problem: ${p}`))); return 1; };
142
152
 
@@ -146,10 +156,13 @@ function scenario(cmd, required = true) {
146
156
  return (required ? cmd.requiredOption(...pipeline) : cmd.option(...pipeline))
147
157
  .addOption(new Option("--input <file>", "the input file: prose, or an Active artifact").conflicts("text"))
148
158
  .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)");
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], []);
150
163
  }
151
164
 
152
- const common = (cmd) => cmd.option("--root <dir>", "the project (default: the current folder)").option("--json", "the result as JSON");
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");
153
166
 
154
167
  export function program() {
155
168
  const prog = new Command("agiter")
@@ -157,8 +170,12 @@ export function program() {
157
170
  .showHelpAfterError("(agiter --help, or agiter <command> --help)")
158
171
  .exitOverride();
159
172
 
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) });
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}`);
162
179
  print(opts, r, () => {
163
180
  for (const f of r.created) console.log(`created ${f}`);
164
181
  for (const f of r.existed) console.log(`kept ${f} (exists)`);
@@ -166,9 +183,11 @@ export function program() {
166
183
  });
167
184
 
168
185
  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 }, () => {
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 }, () => {
171
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(", ")}`);
172
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" : ""}`);
173
192
  for (const p of r.problems) console.log(`problem: ${p}`);
174
193
  });
@@ -182,12 +201,13 @@ export function program() {
182
201
  let script = {};
183
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}`); } }
184
203
  simulation = { script };
185
- if (!opts.root) { root = fs.mkdtempSync(path.join(os.tmpdir(), "agiter-sim-")); fs.mkdirSync(path.join(root, ".agiter")); }
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")); }
186
206
  }
187
207
  logCli(root, process.argv.slice(2));
188
208
  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 });
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 });
191
211
  view.done?.();
192
212
  if (!r.ok) { process.exitCode = problems(opts, r); return; }
193
213
  print(opts, { root, resumed: r.resumed, run: r.run }, () => {
@@ -199,14 +219,14 @@ export function program() {
199
219
  };
200
220
  const runOptions = (cmd) => cmd
201
221
  .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")
222
+ .option("--audit", "record every executor call in agiter/runs/<run>/audit.jsonl")
203
223
  .option("--quiet", "show only the harness's events, not the agents' output");
204
224
 
205
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))
206
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)")
207
- .conflicts(["pipeline", "input", "text", "name", "new", "audit", "quiet"]))
227
+ .conflicts(["pipeline", "input", "text", "name", "new", "audit", "quiet", "includeFile", "isolation", "lang"]))
208
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)")
209
- .conflicts(["pipeline", "input", "text", "name", "new", "audit", "quiet", "list"]))
229
+ .conflicts(["pipeline", "input", "text", "name", "new", "audit", "quiet", "includeFile", "isolation", "lang", "list"]))
210
230
  .option("--failed", "with --delete: only failed runs (with --done: failed or done ones)")
211
231
  .option("--done", "with --delete: only done runs (with --failed: failed or done ones)")
212
232
  .option("--all", "with --delete: delete every run and clear last.json (asks twice)")
@@ -221,7 +241,7 @@ export function program() {
221
241
  if (!opts.pipeline) cmd.error("error: required option '--pipeline <steps...>' not specified (or --list)", { exitCode: USAGE, code: "agiter.pipeline" });
222
242
  return runner(false)(opts);
223
243
  });
224
- common(runOptions(scenario(prog.command("simulate").description("run a scenario with a scripted fake model (a new temporary project without --root)")))
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)")))
225
245
  .option("--script <file>", "what the fake model returns, per task, role and attempt")).action(runner(true));
226
246
 
227
247
  common(prog.command("continue").argument("[run]", "the run id", "last").description("walk a run's steps again from its files")
@@ -230,6 +250,7 @@ export function program() {
230
250
  const root = rootOf(opts);
231
251
  logCli(root, process.argv.slice(2));
232
252
  const view = live(opts);
253
+ if (root !== process.cwd() && !opts.json) console.log(`project: ${root}`);
233
254
  const r = await api.continueRun({ root, run: id, audit: Boolean(opts.audit), onEvent: view.onEvent, onOutput: view.onOutput });
234
255
  view.done?.();
235
256
  if (!r.ok) { process.exitCode = problems(opts, r); return; }
@@ -238,7 +259,7 @@ export function program() {
238
259
  });
239
260
 
240
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")
241
- .option("--merge", "merge the run's branch into the branch it started from (no fast-forward)")).action((id, opts) => {
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) => {
242
263
  const root = rootOf(opts);
243
264
  logCli(root, process.argv.slice(2));
244
265
  const r = api.finishRun({ root, run: id, merge: Boolean(opts.merge) });
@@ -299,6 +320,76 @@ export function program() {
299
320
  process.exitCode = defs.problems.length ? 1 : 0;
300
321
  });
301
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
+
302
393
  prog.command("sidecar").description("serve the API as JSON-RPC over stdio (for Far Manager)").action(async () => {
303
394
  const { serve } = await import("../sidecar.mjs");
304
395
  await serve();
@@ -0,0 +1,25 @@
1
+ {
2
+ "title": "Breakdown into epics",
3
+ "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).",
4
+ "sections": [
5
+ "Functional areas",
6
+ "Epics"
7
+ ],
8
+ "criteria": [
9
+ "Every functional area of the input is listed, and every one is covered by exactly one epic.",
10
+ "The epics do not overlap: nothing is in the scope of two epics.",
11
+ "Each epic can be delivered and checked on its own; where one needs another, its scope says which.",
12
+ "There are as many epics as the input needs: no epic for a trivial part, no epic that bundles unrelated areas.",
13
+ "Nothing outside the input is added."
14
+ ],
15
+ "items": {
16
+ "type": "epic",
17
+ "prefix": "E",
18
+ "section": "Epics"
19
+ },
20
+ "statuses": [
21
+ "Draft",
22
+ "Active",
23
+ "Done"
24
+ ]
25
+ }
@@ -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
+ }
@@ -0,0 +1,59 @@
1
+ {
2
+ "id": "agent-browser",
3
+ "title": "agent-browser",
4
+ "kind": "tool",
5
+ "summary": "interactive acceptance in a real browser",
6
+ "requires": [
7
+ "browser"
8
+ ],
9
+ "fragments": {
10
+ "stack": [
11
+ {
12
+ "id": "ab.stack",
13
+ "text": "Browser automation: the existing `agent-browser` CLI with the installed Google Chrome."
14
+ }
15
+ ],
16
+ "rules": [
17
+ {
18
+ "id": "ab.help",
19
+ "group": "Dependencies and tools",
20
+ "text": "`agent-browser`: run `agent-browser --help` before first use. Never report that visual verification is impossible; report what was run and what could not be driven, as `not run` with the reason."
21
+ }
22
+ ],
23
+ "commands": [
24
+ {
25
+ "id": "ab.acceptance",
26
+ "purpose": "interactive acceptance tool",
27
+ "command": "`agent-browser`"
28
+ },
29
+ {
30
+ "id": "ab.clipboard",
31
+ "purpose": "clipboard access",
32
+ "command": "`node tools/grant-clipboard.mjs --origin <app url> --session <name>` after opening the session",
33
+ "after": "interactive acceptance tool"
34
+ },
35
+ {
36
+ "id": "ab.readiness",
37
+ "purpose": "browser readiness",
38
+ "command": "`agent-browser --version`",
39
+ "after": "interactive acceptance tool"
40
+ }
41
+ ],
42
+ "environment": [
43
+ {
44
+ "id": "ab.version",
45
+ "setting": "agent-browser",
46
+ "value": "`<existing executable path and version>`"
47
+ }
48
+ ],
49
+ "notes": {
50
+ "environment": "Use a separate browser session or ephemeral context per run; do not reuse the user's personal profile."
51
+ }
52
+ },
53
+ "files": [
54
+ {
55
+ "from": "templates/stacks/html-javascript/tools/grant-clipboard.mjs",
56
+ "to": "tools/grant-clipboard.mjs"
57
+ }
58
+ ]
59
+ }