@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.
- package/README.md +72 -9
- package/bin/agiter.mjs +122 -21
- package/defs/{agents.json → .agents.json} +12 -2
- package/defs/artifacts/checks.json +6 -0
- package/defs/artifacts/create-epics.json +32 -0
- package/defs/artifacts/create-tests.json +22 -0
- package/defs/artifacts/{work.json → create-work.json} +9 -2
- package/defs/artifacts/decisions.json +12 -0
- package/defs/artifacts/report.json +1 -0
- package/defs/blocks/ado-net.json +130 -0
- package/defs/blocks/agent-browser.json +59 -0
- package/defs/blocks/angular.json +270 -0
- package/defs/blocks/aspnet-minimal-api.json +68 -0
- package/defs/blocks/aspnet-mvc.json +113 -0
- package/defs/blocks/aspnet-webapi.json +79 -0
- package/defs/blocks/aspnet-webforms.json +150 -0
- package/defs/blocks/browser.json +102 -0
- package/defs/blocks/cbor.json +95 -0
- package/defs/blocks/clean-architecture.json +127 -0
- package/defs/blocks/client-server.json +134 -0
- package/defs/blocks/ddd.json +82 -0
- package/defs/blocks/dotnet-framework.json +153 -0
- package/defs/blocks/dotnet.json +119 -0
- package/defs/blocks/dto-templates.json +39 -0
- package/defs/blocks/ef6.json +138 -0
- package/defs/blocks/entity-drafts.json +84 -0
- package/defs/blocks/express.json +135 -0
- package/defs/blocks/far-luamacro.json +202 -0
- package/defs/blocks/fastify.json +125 -0
- package/defs/blocks/javascript.json +40 -0
- package/defs/blocks/luajit.json +117 -0
- package/defs/blocks/native-aot.json +105 -0
- package/defs/blocks/next.json +257 -0
- package/defs/blocks/node.json +54 -0
- package/defs/blocks/npm.json +72 -0
- package/defs/blocks/nuxt.json +265 -0
- package/defs/blocks/odbc-ffi.json +154 -0
- package/defs/blocks/playwright-core.json +73 -0
- package/defs/blocks/react.json +295 -0
- package/defs/blocks/sql-server.json +96 -0
- package/defs/blocks/tdd.json +47 -0
- package/defs/blocks/tsql-migrations.json +260 -0
- package/defs/blocks/typescript.json +115 -0
- package/defs/blocks/vanilla-web.json +88 -0
- package/defs/blocks/vite.json +91 -0
- package/defs/blocks/vitest.json +78 -0
- package/defs/blocks/vue.json +292 -0
- package/defs/blocks/web-components.json +86 -0
- package/defs/blocks/windows-auth.json +88 -0
- package/defs/presets/angular.json +15 -0
- package/defs/presets/aspnet-classic.json +13 -0
- package/defs/presets/aspnet-webforms.json +12 -0
- package/defs/presets/dotnet-sqlserver.json +14 -0
- package/defs/presets/far-luajit.json +12 -0
- package/defs/presets/mssql-database.json +16 -0
- package/defs/presets/react-express.json +18 -0
- package/defs/presets/react-fastify.json +18 -0
- package/defs/presets/react-next.json +16 -0
- package/defs/presets/react-vite.json +16 -0
- package/defs/presets/vanilla-web.json +16 -0
- package/defs/presets/vue-nuxt.json +17 -0
- package/defs/presets/vue-vite.json +16 -0
- package/defs/tasks/create-epics.json +2 -2
- package/defs/tasks/create-tests.json +1 -2
- package/defs/tasks/create-work.json +1 -1
- package/index.mjs +5 -3
- package/package.json +3 -3
- package/src/agents.mjs +62 -28
- package/src/artifacts.mjs +84 -12
- package/src/defs.mjs +23 -2
- package/src/engine.mjs +790 -104
- package/src/fake.mjs +11 -2
- package/src/frontmatter.mjs +113 -62
- package/src/git.mjs +126 -8
- package/src/init.mjs +30 -7
- package/src/lang.mjs +21 -0
- package/src/links.mjs +55 -0
- package/src/log.mjs +53 -53
- package/src/models.mjs +21 -10
- package/src/pipeline.mjs +5 -1
- package/src/prompt.mjs +110 -20
- package/src/stack.mjs +338 -0
- package/src/tools.mjs +2 -2
- 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
|
-
(
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
21
|
-
console.log(`
|
|
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, "
|
|
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
|
-
|
|
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:
|
|
161
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
}
|
|
@@ -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
|
-
"
|
|
3
|
-
"
|
|
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
|
+
}
|
|
@@ -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
|
+
}
|