@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.
- package/README.md +26 -9
- package/bin/agiter.mjs +110 -19
- package/defs/artifacts/create-epics.json +25 -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/index.mjs +4 -2
- package/package.json +2 -2
- package/src/agents.mjs +31 -15
- package/src/artifacts.mjs +42 -4
- package/src/defs.mjs +11 -2
- package/src/engine.mjs +266 -85
- package/src/fake.mjs +10 -1
- package/src/git.mjs +126 -8
- package/src/init.mjs +30 -7
- package/src/lang.mjs +21 -0
- package/src/log.mjs +53 -53
- package/src/models.mjs +21 -10
- package/src/pipeline.mjs +5 -1
- package/src/prompt.mjs +76 -15
- package/src/stack.mjs +338 -0
- package/src/tools.mjs +2 -2
- /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
|
-
(
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
21
|
-
console.log(`
|
|
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, "
|
|
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
|
-
|
|
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:
|
|
161
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
+
}
|