agent-quality-kit 0.14.0 → 0.15.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 +29 -5
- package/README.ru.md +31 -6
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/tool/commands/context.mjs +36 -40
- package/tool/commands/doctor.mjs +18 -26
- package/tool/commands/feedback.mjs +157 -0
- package/tool/commands/project.mjs +11 -13
- package/tool/commands/prompt.mjs +2 -1
- package/tool/i18n/en-docs.mjs +1 -0
- package/tool/i18n/en-gates.mjs +27 -0
- package/tool/i18n/en.mjs +5 -0
- package/tool/i18n/ru-docs.mjs +1 -0
- package/tool/i18n/ru-gates.mjs +28 -0
- package/tool/i18n/ru.mjs +7 -0
- package/tool/lib/ask.mjs +118 -0
- package/tool/lib/brief.mjs +17 -38
- package/tool/lib/core.mjs +7 -6
- package/tool/lib/gate-worker.mjs +4 -1
- package/tool/lib/run.mjs +120 -7
- package/tool/program.mjs +6 -0
- package/tool/selfcheck/smoke/fail-closed.test.mjs +96 -1
- package/tool/selfcheck/units-ask.mjs +85 -0
- package/tool/selfcheck/units-brief.mjs +3 -13
- package/tool/selfcheck/units-context.mjs +2 -1
- package/tool/selfcheck/units-feedback.mjs +137 -0
package/README.md
CHANGED
|
@@ -107,6 +107,8 @@ aqk context the repository state in one block, for an agent's contex
|
|
|
107
107
|
aqk prompt one task to paste into an agent: what to fix, in order, and the
|
|
108
108
|
command that proves each item done
|
|
109
109
|
aqk vitals is what the kit runs on wired up: gate tools, hooks, freshness
|
|
110
|
+
aqk feedback feedback to the author: a report from the last run and probe,
|
|
111
|
+
plus a prefilled link. No paths, no code; nothing is sent for you
|
|
110
112
|
aqk doctor --run --brief one line on success, the whole run on failure — for hooks
|
|
111
113
|
aqk context --full the same plus the command map and the rulebook verbatim (~7000
|
|
112
114
|
tokens against ~500: the price of an agent that does not guess)
|
|
@@ -351,7 +353,7 @@ Already using [pre-commit](https://pre-commit.com)? Three lines in the file you
|
|
|
351
353
|
```yaml
|
|
352
354
|
repos:
|
|
353
355
|
- repo: https://github.com/arsen-ask-lx/Agent_Quality_Kit
|
|
354
|
-
rev: v0.
|
|
356
|
+
rev: v0.15.0
|
|
355
357
|
hooks:
|
|
356
358
|
- id: aqk # runs what the repository declares; blocks below AQK-1
|
|
357
359
|
# - id: aqk-doctor # read-only: the level and what is missing, blocks nothing
|
|
@@ -372,7 +374,7 @@ layer AQK adds.
|
|
|
372
374
|
[](https://github.com/marketplace/actions/agent-quality-kit-aqk)
|
|
373
375
|
|
|
374
376
|
```yaml
|
|
375
|
-
- uses: arsen-ask-lx/Agent_Quality_Kit@v0.
|
|
377
|
+
- uses: arsen-ask-lx/Agent_Quality_Kit@v0.15.0
|
|
376
378
|
with:
|
|
377
379
|
min: 1 # the build fails below AQK-1, or if any declared gate failed
|
|
378
380
|
```
|
|
@@ -431,12 +433,34 @@ files the diff touched:
|
|
|
431
433
|
aqk doctor --run --since main # only what this branch introduced
|
|
432
434
|
```
|
|
433
435
|
|
|
434
|
-
|
|
435
|
-
outside it — green, with the number that was hidden, never a silent "all clear".
|
|
436
|
+
Four outcomes, all of them said out loud. Findings inside the diff — red, as usual. Findings only
|
|
437
|
+
outside it — green, with the number that was hidden, never a silent "all clear". A gate whose
|
|
436
438
|
output carries no paths at all (a commit-message check, a CI-config check) **cannot** be narrowed:
|
|
437
|
-
it stays red, and says why.
|
|
439
|
+
it stays red, and says why. And a gate that **could not run at all** — no tool, an unexpected exit
|
|
440
|
+
code, killed by a signal — is never narrowed by the diff: a failure has no place in the code, only
|
|
441
|
+
itself. Calling either of the last two green because there was nothing to narrow would be exactly
|
|
438
442
|
the silence this tool exists to remove.
|
|
439
443
|
|
|
444
|
+
### The only payment: one answer
|
|
445
|
+
|
|
446
|
+
The kit is free and collects nothing about you: it makes exactly one outgoing request — asking the
|
|
447
|
+
npm registry whether a newer version exists. The payment is different: **one answer to the author**.
|
|
448
|
+
So once per project, and only when there is something to tell, `doctor` or the agent block prints a
|
|
449
|
+
line like "AQK could not check `smoke`; that is the most valuable thing to tell the author". Then:
|
|
450
|
+
|
|
451
|
+
```bash
|
|
452
|
+
aqk feedback # builds the message and hands you a prefilled link — you send it, not us
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
The message carries: version, level, stack, what went red, what the kit could not check, which
|
|
456
|
+
defect classes nobody catches here. **No paths, no code, no repository name** — you see every
|
|
457
|
+
character you send. No GitHub? Forward the text as is. Not interested at all? `AQK_FEEDBACK=0`.
|
|
458
|
+
|
|
459
|
+
Somewhere to say it in your own words:
|
|
460
|
+
[where the kit was wrong](https://github.com/arsen-ask-lx/Agent_Quality_Kit/discussions/90) ·
|
|
461
|
+
[what check is missing](https://github.com/arsen-ask-lx/Agent_Quality_Kit/discussions/91) ·
|
|
462
|
+
[show your manifest](https://github.com/arsen-ask-lx/Agent_Quality_Kit/discussions/92).
|
|
463
|
+
|
|
440
464
|
Every `doctor --run` rewrites `.aqk/last-run.md` — a short report of what actually ran and how
|
|
441
465
|
long it took. The list of gates in the manifest says nothing about how many of them are alive
|
|
442
466
|
right now; the report does. The file is ephemeral — keep it in your own `.gitignore`.
|
package/README.ru.md
CHANGED
|
@@ -109,6 +109,8 @@ aqk context состояние репозитория одним б
|
|
|
109
109
|
aqk prompt одно задание для агента: что починить, по порядку, и у каждого
|
|
110
110
|
пункта команда, которая докажет «готово»
|
|
111
111
|
aqk vitals подключено ли то, чем комплект работает: инструменты, хуки, свежесть
|
|
112
|
+
aqk feedback отзыв автору: отчёт из последнего прогона и пробы плюс готовая
|
|
113
|
+
ссылка. Без путей и без кода; ничего не отправляет само
|
|
112
114
|
aqk doctor --run --brief одна строка на успехе, весь прогон при провале — для хуков
|
|
113
115
|
aqk context --full то же плюс карта команд и свод правил дословно (≈7000 токенов
|
|
114
116
|
против ≈500 — плата за то, чтобы агент не догадывался)
|
|
@@ -355,7 +357,7 @@ aqk badge --check # в конвейере: код 1 в тот день, ког
|
|
|
355
357
|
```yaml
|
|
356
358
|
repos:
|
|
357
359
|
- repo: https://github.com/arsen-ask-lx/Agent_Quality_Kit
|
|
358
|
-
rev: v0.
|
|
360
|
+
rev: v0.15.0
|
|
359
361
|
hooks:
|
|
360
362
|
- id: aqk # запускает объявленное; роняет коммит ниже AQK-1
|
|
361
363
|
# - id: aqk-doctor # только осмотр: уровень и чего не хватает, ничего не роняет
|
|
@@ -374,7 +376,7 @@ repos:
|
|
|
374
376
|
[](https://github.com/marketplace/actions/agent-quality-kit-aqk)
|
|
375
377
|
|
|
376
378
|
```yaml
|
|
377
|
-
- uses: arsen-ask-lx/Agent_Quality_Kit@v0.
|
|
379
|
+
- uses: arsen-ask-lx/Agent_Quality_Kit@v0.15.0
|
|
378
380
|
with:
|
|
379
381
|
min: 1 # сборка падает ниже AQK-1 или если упал любой объявленный гейт
|
|
380
382
|
```
|
|
@@ -432,11 +434,34 @@ aqk ratchet no-print-in-prod # старое — долг, новое не пу
|
|
|
432
434
|
aqk doctor --run --since main # только то, что внесла эта ветка
|
|
433
435
|
```
|
|
434
436
|
|
|
435
|
-
|
|
436
|
-
снаружи — зелёный, с числом того, что скрыто, а не молчаливое «всё чисто».
|
|
437
|
+
Четыре исхода, и все четыре названы вслух. Находки внутри дифа — красный, как обычно. Находки
|
|
438
|
+
только снаружи — зелёный, с числом того, что скрыто, а не молчаливое «всё чисто». Гейт, в выводе
|
|
437
439
|
которого путей нет вовсе (проверка сообщения коммита, проверка конфига конвейера), сузиться
|
|
438
|
-
**не может**: он остаётся красным и говорит почему.
|
|
439
|
-
|
|
440
|
+
**не может**: он остаётся красным и говорит почему. А гейт, который вообще **не смог
|
|
441
|
+
отработать** — нет инструмента, неожиданный код возврата, убит сигналом, — дифом не сужается
|
|
442
|
+
никогда: у сбоя нет места в коде, есть только сам сбой. Назвать любой из двух последних зелёным
|
|
443
|
+
потому, что сужать было нечего, — ровно та тишина, ради устранения которой этот инструмент и
|
|
444
|
+
написан.
|
|
445
|
+
|
|
446
|
+
### Единственная плата — один ответ
|
|
447
|
+
|
|
448
|
+
Комплект бесплатный и ничего о вас не собирает: исходящий запрос у него ровно один — спросить
|
|
449
|
+
реестр npm, не вышла ли версия свежее. Плата другая: **один ответ автору**. Поэтому один раз на
|
|
450
|
+
проект — и только когда есть что рассказать — `doctor` или блок для агента печатают строку вроде
|
|
451
|
+
«AQK не смог проверить `smoke`; автору это ценнее всего». Дальше:
|
|
452
|
+
|
|
453
|
+
```bash
|
|
454
|
+
aqk feedback # соберёт письмо и даст готовую ссылку — отправляете вы, не мы
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
В письме: версия, уровень, стек, что покраснело, чего комплект не смог проверить, какие классы
|
|
458
|
+
брака здесь не ловит никто. **Ни путей, ни кода, ни имени репозитория** — вы видите глазами всё,
|
|
459
|
+
что отправляете. Не нужен GitHub — перешлите текст как есть. Не нужно вовсе: `AQK_FEEDBACK=0`.
|
|
460
|
+
|
|
461
|
+
Где рассказать словами:
|
|
462
|
+
[где комплект соврал](https://github.com/arsen-ask-lx/Agent_Quality_Kit/discussions/90) ·
|
|
463
|
+
[какой проверки не хватает](https://github.com/arsen-ask-lx/Agent_Quality_Kit/discussions/91) ·
|
|
464
|
+
[покажите свой манифест](https://github.com/arsen-ask-lx/Agent_Quality_Kit/discussions/92).
|
|
440
465
|
|
|
441
466
|
Каждый `doctor --run` перезаписывает `.aqk/last-run.md` — короткий отчёт, что из объявленного
|
|
442
467
|
реально сработало и за сколько. Список гейтов в манифесте молчит о том, сколько из них живы
|
package/llms.txt
CHANGED
|
@@ -76,7 +76,7 @@ Zero runtime dependencies. Node 18+ and an `sh` shell. MIT.
|
|
|
76
76
|
the files `init` writes are owned by root, so you cannot edit your own manifest. Debian-based
|
|
77
77
|
on purpose: the gates are `sh`, `grep`, `awk`, `find` — under alpine's busybox they behave
|
|
78
78
|
differently, and an image where the gates behave differently is worse than no image
|
|
79
|
-
- As a GitHub Action: `uses: arsen-ask-lx/Agent_Quality_Kit@v0.
|
|
79
|
+
- As a GitHub Action: `uses: arsen-ask-lx/Agent_Quality_Kit@v0.15.0` with `min: 1`
|
|
80
80
|
(https://github.com/marketplace/actions/agent-quality-kit-aqk)
|
|
81
81
|
|
|
82
82
|
## What makes it different
|
package/package.json
CHANGED
|
@@ -21,14 +21,14 @@
|
|
|
21
21
|
// а агент примет его за утверждение. Поэтому каждое незнание называется словом: прогона не было —
|
|
22
22
|
// так и написано, прогон устарел — тоже, инструмента нет — тоже.
|
|
23
23
|
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
24
|
-
import { spawnSync } from "node:child_process";
|
|
25
24
|
import { join } from "node:path";
|
|
26
25
|
import { CWD, TARGET_DIR, SELF, c, exists, commandRows, preCommitHook } from "../lib/core.mjs";
|
|
26
|
+
import { maybeAsk } from "./feedback.mjs";
|
|
27
27
|
import { readManifest, assessLevel, coversOf } from "../lib/manifest.mjs";
|
|
28
28
|
import { detectFacts, readCatalog } from "../lib/repo.mjs";
|
|
29
29
|
import { catalogBuckets, startWith, blindAdvice } from "../lib/advice.mjs";
|
|
30
30
|
import { proposeGates, readAdoptFiles } from "../lib/adopt.mjs";
|
|
31
|
-
import { declaredGates } from "../lib/run.mjs";
|
|
31
|
+
import { declaredGates, readRun } from "../lib/run.mjs";
|
|
32
32
|
import { probeStatus } from "./probe.mjs";
|
|
33
33
|
import { L } from "../i18n/index.mjs";
|
|
34
34
|
|
|
@@ -73,11 +73,17 @@ function contextBlock(state, T = L.context) {
|
|
|
73
73
|
out.push(T.runNone);
|
|
74
74
|
} else {
|
|
75
75
|
const red = state.run.red || [];
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
? `${
|
|
79
|
-
:
|
|
80
|
-
|
|
76
|
+
const cannot = state.run.cannot || [];
|
|
77
|
+
const short = (list) => (list.length > MAX_RED
|
|
78
|
+
? `${list.slice(0, MAX_RED).join(", ")} — ${T.andMore(list.length - MAX_RED)}`
|
|
79
|
+
: list.join(", "));
|
|
80
|
+
if (red.length) out.push(T.runRed(state.run.when, short(red)));
|
|
81
|
+
// «НЕ СМОГЛИ ПРОВЕРИТЬ» — ОТДЕЛЬНОЙ СТРОКОЙ, И ЧИСТО ТОЛЬКО КОГДА ОБА СПИСКА ПУСТЫ.
|
|
82
|
+
// Гейт, который не сумел отработать, не находка о коде: агент, прочитавший его как находку,
|
|
83
|
+
// пойдёт чинить исправный файл. А если бы он не попал НИКУДА, прогон, где всё сломалось,
|
|
84
|
+
// читался бы как «чисто» — та же тишина, только внутри блока, который читает машина.
|
|
85
|
+
if (cannot.length) out.push(T.runCannot(short(cannot)));
|
|
86
|
+
if (!red.length && !cannot.length) out.push(T.runClean(state.run.when));
|
|
81
87
|
if (state.run.stale) out.push(T.runStale(state.run.when));
|
|
82
88
|
if (state.run.skipped) out.push(T.skipped(state.run.skipped));
|
|
83
89
|
}
|
|
@@ -148,20 +154,13 @@ function contextBlock(state, T = L.context) {
|
|
|
148
154
|
// чем промолчать: он пойдёт его читать и получит пустоту вместо правил. Замерено на шести
|
|
149
155
|
// чужих проектах: на flask блок писал «Свод правил: AGENTS.md», которого там нет.
|
|
150
156
|
if (state.entryExists !== false) out.push("", T.where(state.entry || "AGENTS.md"));
|
|
157
|
+
// ПРОСЬБА ОБ ОТЗЫВЕ — последней строкой и только при содержании (feedback.mjs). Последней
|
|
158
|
+
// потому, что это единственная строка блока, которая не про состояние проекта: ставить её
|
|
159
|
+
// выше значило бы отодвинуть работой то, ради чего блок и читают.
|
|
160
|
+
if (state.ask) out.push("", state.ask);
|
|
151
161
|
return out;
|
|
152
162
|
}
|
|
153
163
|
|
|
154
|
-
// Разбор отчёта прошлого прогона. Формат кладёт сам `doctor` в .aqk/last-run.md; читаем его,
|
|
155
|
-
// а не запускаем гейты заново: хук обязан укладываться в секунду-две, а прогон у нас идёт минуту.
|
|
156
|
-
function parseLastRun(text) {
|
|
157
|
-
if (!text) return null;
|
|
158
|
-
const when = (text.match(/^# aqk doctor --run — (.+)$/m) || [])[1] || "";
|
|
159
|
-
const red = [];
|
|
160
|
-
for (const m of text.matchAll(/^✘ ([^\s—]+)/gm)) red.push(m[1]);
|
|
161
|
-
const skipped = (text.match(/^~ /gm) || []).length;
|
|
162
|
-
return { when: when.trim(), red, skipped, stale: false };
|
|
163
|
-
}
|
|
164
|
-
|
|
165
164
|
// Правила и их арбитры: отметка `<!-- aqk: имя -->` рядом с правилом. `человек` — честное
|
|
166
165
|
// признание, что машина этого не держит; так его и считаем, отдельно от машинных.
|
|
167
166
|
function countArbiters(text, humanWords) {
|
|
@@ -173,16 +172,6 @@ function countArbiters(text, humanWords) {
|
|
|
173
172
|
return { total: marks.length, machine: marks.length - human, human };
|
|
174
173
|
}
|
|
175
174
|
|
|
176
|
-
// Прогон старше последнего коммита описывает не тот код, что лежит перед агентом. Молча выдать
|
|
177
|
-
// его за свежий — соврать: именно так «зелёный месяц назад» превращается в «зелёный сейчас».
|
|
178
|
-
function runIsStale(when) {
|
|
179
|
-
if (!when) return false;
|
|
180
|
-
const r = spawnSync("git", ["log", "-1", "--format=%cI"], { cwd: CWD, encoding: "utf8" });
|
|
181
|
-
if (r.status !== 0 || !r.stdout) return false;
|
|
182
|
-
const commit = Date.parse(r.stdout.trim());
|
|
183
|
-
const run = Date.parse(when.replace(" ", "T"));
|
|
184
|
-
return Number.isFinite(commit) && Number.isFinite(run) && run < commit;
|
|
185
|
-
}
|
|
186
175
|
|
|
187
176
|
|
|
188
177
|
// УСТАНОВКА ХУКА — отдельной командой, а не частью `init`, и это решение, а не лень. Комплект
|
|
@@ -260,16 +249,6 @@ async function installHook(full = false) {
|
|
|
260
249
|
console.log(c.dim(` ${T.hookWhat}`));
|
|
261
250
|
}
|
|
262
251
|
|
|
263
|
-
// Прошлый прогон — из отчёта, который кладёт `doctor --run`. Отдельной функцией: его читают и
|
|
264
|
-
// `context`, и `prompt`, и два разбора одного файла разошлись бы.
|
|
265
|
-
async function readRun() {
|
|
266
|
-
const lastRun = join(CWD, TARGET_DIR, "last-run.md");
|
|
267
|
-
if (!(await exists(lastRun))) return null;
|
|
268
|
-
const run = parseLastRun(await readFile(lastRun, "utf8"));
|
|
269
|
-
if (run) run.stale = runIsStale(run.when);
|
|
270
|
-
return run;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
252
|
// Что советовать — теми же функциями, что у `doctor`: корзины каталога, «начните с трёх», совет
|
|
274
253
|
// под язык, чужие проверки проекта. Одно место на `context` и `prompt`: второй расчёт того же
|
|
275
254
|
// самого разошёлся бы с первым. Класс из пробы, чей гейт уже стоит, в совет не идёт — ставить
|
|
@@ -364,10 +343,27 @@ async function cmdContext(args = []) {
|
|
|
364
343
|
next = nextSteps({ init: !man, adopt, blind, start });
|
|
365
344
|
} catch { /* не посчитали — блок скажет остальное; выдумывать шаги нельзя */ }
|
|
366
345
|
|
|
346
|
+
// ЕДИНСТВЕННАЯ ПЛАТА ЗА КОМПЛЕКТ — один ответ автору, и просит о нём агент: он читает этот
|
|
347
|
+
// блок каждую сессию и передаёт человеку то, что в нём написано. Замер 2026-09-14: тысяча
|
|
348
|
+
// скачиваний в неделю и ноль отзывов за всё время — просьба печаталась только при `init`, то
|
|
349
|
+
// есть до того, как комплект сделал хоть что-то.
|
|
350
|
+
//
|
|
351
|
+
// ТРИ УСЛОВИЯ, И ВСЕ ТРИ ОБЯЗАТЕЛЬНЫ: не выключено человеком, не просили на этом проекте
|
|
352
|
+
// раньше, и ЕСТЬ О ЧЁМ рассказать. Без третьего это «оставьте отзыв» — шум, а шум выключают
|
|
353
|
+
// вместе с хуком, в котором он приехал.
|
|
354
|
+
//
|
|
355
|
+
// ЕДИНСТВЕННАЯ ЗАПИСЬ НА ДИСК В ЭТОЙ КОМАНДЕ, кроме `--install`. Без отметки просьба
|
|
356
|
+
// повторялась бы каждую сессию: красный гейт живёт в проекте днями, а блок читается заново
|
|
357
|
+
// при каждом запуске агента и после каждого сжатия контекста.
|
|
358
|
+
const ask = await maybeAsk({
|
|
359
|
+
cannot: run?.cannot || [], red: run?.red || [],
|
|
360
|
+
blind: (probe?.classes || []).map((b) => b.slug),
|
|
361
|
+
}, portableSelf(SELF), { agent: true });
|
|
362
|
+
|
|
367
363
|
console.log(contextBlock({
|
|
368
364
|
entry, entryExists: rules !== null, level, rules, run, ratchets, probe, full: fullPart,
|
|
369
|
-
next, when: { hook: await preCommitHook(CWD) },
|
|
365
|
+
next, when: { hook: await preCommitHook(CWD) }, ask,
|
|
370
366
|
}).join("\n"));
|
|
371
367
|
}
|
|
372
368
|
|
|
373
|
-
export { cmdContext, contextBlock, nextSteps,
|
|
369
|
+
export { cmdContext, contextBlock, nextSteps, countArbiters, withHook, hasOurHook, portableSelf, readAdvice };
|
package/tool/commands/doctor.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { readFile, mkdir, writeFile } from "node:fs/promises";
|
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
6
|
import { CWD, PKG_ROOT, TARGET_DIR, MANIFEST, SELF, c, exists, die, RUNTIME_FILES } from "../lib/core.mjs";
|
|
7
|
+
import { maybeAsk } from "./feedback.mjs";
|
|
7
8
|
import { cmdProbe, probeStatus } from "./probe.mjs";
|
|
8
9
|
import { readManifest, assessLevel, unknownKeys, KNOWN_KEYS, layoutChecks, unparsedLines } from "../lib/manifest.mjs";
|
|
9
10
|
import { proveGates } from "../lib/prove.mjs";
|
|
@@ -12,34 +13,9 @@ import { reportBaseline, reportCatalog } from "./doctor-catalog.mjs";
|
|
|
12
13
|
import { L } from "../i18n/index.mjs";
|
|
13
14
|
import { countArbiters } from "./context.mjs";
|
|
14
15
|
import { beginBrief, finishBrief } from "../lib/brief.mjs";
|
|
15
|
-
import { declaredGates, sinceRef, runGates, progress, listArg } from "../lib/run.mjs";
|
|
16
|
+
import { declaredGates, sinceRef, runGates, progress, listArg, writeRunReport } from "../lib/run.mjs";
|
|
16
17
|
import { autoProbeAllowed, levelLimits } from "../lib/cadence.mjs";
|
|
17
18
|
|
|
18
|
-
// Короткий отчёт «что из этого реально брали» — не для человека, а для агента в следующей
|
|
19
|
-
// сессии и для самого владельца: список объявленных гейтов молчит о том, сколько из них
|
|
20
|
-
// действительно стоят и работают именно СЕЙЧАС. Перезаписывается каждым прогоном, не копится:
|
|
21
|
-
// история — дело git-лога коммитов с этим отчётом, если владелец решит его коммитить.
|
|
22
|
-
async function writeRunReport({ version, reached, results, skipped = [] }) {
|
|
23
|
-
const stamp = new Date().toISOString().replace("T", " ").slice(0, 16);
|
|
24
|
-
const ok = results.filter((r) => r.ok).length;
|
|
25
|
-
const lines = [
|
|
26
|
-
`# ${L.report.title} — ${stamp}`,
|
|
27
|
-
version ? `${L.report.version}: ${version}` : null,
|
|
28
|
-
`${L.report.level}: AQK-${reached < 0 ? L.doctor.levelNone : reached}`,
|
|
29
|
-
"",
|
|
30
|
-
...results.map((r) => `${r.ok ? "✔" : "✘"} ${r.name} — ${r.secs}s${r.ok ? "" : ` (${r.note || L.doctor.exitCode(r.code)})`}`),
|
|
31
|
-
// Пропущенные по --skip/--only — строкой «~»: блок для агента читает их как «не запускались»,
|
|
32
|
-
// а не как зелёные. Молчание о них прочиталось бы как «проверено».
|
|
33
|
-
...skipped.map((n) => `~ ${n} — ${L.report.skippedBySelect}`),
|
|
34
|
-
"",
|
|
35
|
-
L.report.summary(ok, results.length),
|
|
36
|
-
].filter((l) => l !== null);
|
|
37
|
-
|
|
38
|
-
const dst = join(CWD, TARGET_DIR, "last-run.md");
|
|
39
|
-
await mkdir(join(CWD, TARGET_DIR), { recursive: true });
|
|
40
|
-
await writeFile(dst, lines.join("\n") + "\n", "utf8");
|
|
41
|
-
}
|
|
42
|
-
|
|
43
19
|
// ПРОБА ЗАПУСКАЕТСЯ САМА, раз в сто коммитов, — кроме конвейера (там это минуты сюрпризом в
|
|
44
20
|
// быстрой проверке, отзыв с живого проекта 2026-09-11). Не влияет на код возврата никогда: это
|
|
45
21
|
// осмотр, а не порог. Отдельной функцией: внутри прогона эта лесенка дала вложенность 6, и наш же
|
|
@@ -229,6 +205,7 @@ async function cmdDoctor() {
|
|
|
229
205
|
const gates = declaredGates(man);
|
|
230
206
|
let gateFailed = 0;
|
|
231
207
|
let failedNames = [];
|
|
208
|
+
let cannotNames = [];
|
|
232
209
|
let skippedNames = [];
|
|
233
210
|
if (wantRun) {
|
|
234
211
|
// --jobs N: сколько гейтов одновременно. Без флага — по одному, как было: чужие гейты бывают
|
|
@@ -254,6 +231,7 @@ async function cmdDoctor() {
|
|
|
254
231
|
// стоят дороже. Не влияет на код возврата НИКОГДА — это осмотр, а не порог.
|
|
255
232
|
// Выключается AQK_PROBE=0 — у всего, что случается само, обязан быть выключатель.
|
|
256
233
|
if (!brief && process.env.AQK_PROBE !== "0") await autoProbe(brief);
|
|
234
|
+
cannotNames = run.results.filter((r) => r.cannot).map((r) => r.name);
|
|
257
235
|
} else if (gates.length) {
|
|
258
236
|
console.log(
|
|
259
237
|
c.yellow(` ${L.doctor.declaredNotRun(gates.length)}`) +
|
|
@@ -261,6 +239,20 @@ async function cmdDoctor() {
|
|
|
261
239
|
);
|
|
262
240
|
}
|
|
263
241
|
|
|
242
|
+
// ЕДИНСТВЕННАЯ ПЛАТА ЗА КОМПЛЕКТ — один ответ автору. Человеку говорим здесь, агенту — в
|
|
243
|
+
// блоке `context`; текст и решение «есть ли о чём просить» одни на оба места (feedback.mjs),
|
|
244
|
+
// отметка одна на проект (ask.mjs): кто первым дошёл, тот и спросил, второй раз не спрашивает
|
|
245
|
+
// никто. В кратком режиме молчим — там ворота коммита, и лишняя строка там дороже всего.
|
|
246
|
+
const askText = brief ? null : await maybeAsk({
|
|
247
|
+
cannot: cannotNames,
|
|
248
|
+
red: failedNames.filter((n) => !cannotNames.includes(n)),
|
|
249
|
+
blind: (probe?.classes || []).map((b) => b.slug),
|
|
250
|
+
}, SELF);
|
|
251
|
+
if (askText) {
|
|
252
|
+
for (const l of askText.split("\n")) console.log(c.dim(` ${l}`));
|
|
253
|
+
console.log("");
|
|
254
|
+
}
|
|
255
|
+
|
|
264
256
|
// Код возврата — для конвейера. Порог задаётся так: aqk doctor --min 1
|
|
265
257
|
const minIdx = process.argv.indexOf("--min");
|
|
266
258
|
const min = minIdx > -1 ? Number(process.argv[minIdx + 1]) : null;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// tool/commands/feedback.mjs — `aqk feedback`: единственная плата за комплект — один ответ.
|
|
2
|
+
//
|
|
3
|
+
// ЗАЧЕМ. Замер 2026-09-14: 1342 скачивания в неделю в npm и ни одного пользователя — версии
|
|
4
|
+
// качаются равномерно, включая прожившую двадцать пять минут, то есть это зеркала и сканеры. На
|
|
5
|
+
// GitHub за две недели семь уникальных посетителей, две звезды, ноль чужих комментариев за всё
|
|
6
|
+
// время. Обратной связи нет не потому, что люди молчат: просить мы не умеем. Единственная
|
|
7
|
+
// просьба печаталась при `init` — ДО того, как комплект сделал хоть что-то полезное, — и звала
|
|
8
|
+
// поставить звезду, то есть просила у человека, которому ещё ничего не дали.
|
|
9
|
+
//
|
|
10
|
+
// ЧТО ЗДЕСЬ ДРУГОЕ. Просим, только когда есть что рассказать, и рассказ уже собран: версия,
|
|
11
|
+
// уровень, стек, что покраснело, чего комплект НЕ СМОГ проверить, какие классы брака не ловит
|
|
12
|
+
// никто. Человеку остаётся одна строка своими словами.
|
|
13
|
+
//
|
|
14
|
+
// ЧЕГО ЗДЕСЬ НЕТ И НЕ БУДЕТ. Ничего не отправляется само. Исходящий запрос у комплекта ровно
|
|
15
|
+
// один — про свежесть версии, он описан в README и SECURITY.md. Отчёт печатается и отдаётся
|
|
16
|
+
// человеку: он видит глазами всё, что отправляет. Ни путей, ни содержимого файлов, ни имени
|
|
17
|
+
// репозитория в отчёте нет — иначе первый же внимательный читатель назовёт это телеметрией,
|
|
18
|
+
// и будет прав.
|
|
19
|
+
import { readFile } from "node:fs/promises";
|
|
20
|
+
import { PKG_ROOT, SELF, REPO_URL, c, stateDirs } from "../lib/core.mjs";
|
|
21
|
+
import { askAllowed, markAsked } from "../lib/ask.mjs";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
import { readManifest, assessLevel } from "../lib/manifest.mjs";
|
|
24
|
+
import { detectFacts } from "../lib/repo.mjs";
|
|
25
|
+
import { declaredGates, readRun } from "../lib/run.mjs";
|
|
26
|
+
import { probeStatus } from "./probe.mjs";
|
|
27
|
+
import { L } from "../i18n/index.mjs";
|
|
28
|
+
|
|
29
|
+
// О ЧЁМ ПРОСИТЬ — чистая функция от состояния. Порядок не по нашему удобству, а по ценности
|
|
30
|
+
// ответа для того, кто чинит комплект:
|
|
31
|
+
// 1. НЕ СМОГЛИ ПРОВЕРИТЬ — отказ самого прибора. Это жалоба, а жалоба даётся людям легче
|
|
32
|
+
// похвалы, и она же показывает, где инструмент врёт. Дороже всего остального.
|
|
33
|
+
// 2. СЛЕПОЙ КЛАСС — проба подсадила брак, и его не поймал никто. Рассказ об этом проверяет
|
|
34
|
+
// главное наше утверждение: что проба находит настоящие дыры, а не выдуманные.
|
|
35
|
+
// 3. КРАСНЫЙ ГЕЙТ — комплект поймал то, ради чего его ставят. Момент пользы, но самый частый,
|
|
36
|
+
// поэтому последний.
|
|
37
|
+
// Ничего из перечисленного нет — просьбы нет вовсе. «Оставьте отзыв» без содержания это шум,
|
|
38
|
+
// а шум выключают вместе с хуком, в котором он приехал.
|
|
39
|
+
function feedbackAsk(state = {}) {
|
|
40
|
+
const pick = (kind, list) => (list && list.length ? { reason: kind, names: [...list] } : null);
|
|
41
|
+
return pick("cannot", state.cannot) || pick("blind", state.blind) || pick("red", state.red) || null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ОТЧЁТ. Каждая строка — либо факт, либо слово «неизвестно»: пустое место в письме читается как
|
|
45
|
+
// «всё хорошо» ровно так же, как пустой вывод проверки, и это тот же порок, только у нас самих.
|
|
46
|
+
//
|
|
47
|
+
// Слепые классы называются именем класса, без файла. Файл знает проба («blind-class: slug path»), и
|
|
48
|
+
// соблазн положить его сюда велик — он объясняет находку. Нельзя: путь внутри чужого
|
|
49
|
+
// репозитория рассказывает о чужом проекте больше, чем его владелец собирался рассказать.
|
|
50
|
+
function reportText(state = {}, T = L.feedback.report) {
|
|
51
|
+
const u = T.unknown;
|
|
52
|
+
const list = (xs) => (xs && xs.length ? xs.join(", ") : T.none);
|
|
53
|
+
return [
|
|
54
|
+
T.title,
|
|
55
|
+
"",
|
|
56
|
+
T.env(state.version || u, state.node || u, state.platform || u),
|
|
57
|
+
T.level(state.level === null || state.level === undefined ? u : `AQK-${state.level}`),
|
|
58
|
+
T.stack(state.langs && state.langs.length ? state.langs.join(", ") : u),
|
|
59
|
+
T.gates(state.gates === undefined ? u : state.gates, list(state.red), list(state.cannot)),
|
|
60
|
+
// `null` — проба не делалась: это НЕИЗВЕСТНО, а не «нет». Пустой список означал бы, что
|
|
61
|
+
// проба прошла и ничего не нашла, — и автор письма прочитал бы чужое незнание как чистоту.
|
|
62
|
+
T.blind(state.blind === null || state.blind === undefined
|
|
63
|
+
? u
|
|
64
|
+
: list(state.blind.map((b) => (typeof b === "string" ? b : b.slug)))),
|
|
65
|
+
"",
|
|
66
|
+
T.say,
|
|
67
|
+
"",
|
|
68
|
+
T.mark(state.version || u),
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// КАК ЭТО ЗВУЧИТ — один текст на два места: `context` кладёт его агенту, `doctor` печатает
|
|
73
|
+
// человеку. Собери строку в каждом месте отдельно — и через месяц они разойдутся, как разошлись
|
|
74
|
+
// бы `context` и `prompt` без общего `readAdvice`.
|
|
75
|
+
//
|
|
76
|
+
// АГЕНТУ — ОТДЕЛЬНАЯ ОГОВОРКА, и она не вежливость. Строка ложится в контекст ЧУЖОГО агента, а
|
|
77
|
+
// он исполняет написанное: без «скажи один раз и не настаивай» это реклама в чужом окне, и хук,
|
|
78
|
+
// которым она приехала, снесут в первый же день вместе со всей затеей. Человеку вместо неё —
|
|
79
|
+
// как выключить: у всего, что случается само, обязан быть выключатель.
|
|
80
|
+
function askLine(ask, self = "aqk", { agent = false } = {}, T = L.feedback.ask) {
|
|
81
|
+
if (!ask || !ask.reason) return null;
|
|
82
|
+
const say = T[ask.reason];
|
|
83
|
+
if (!say) return null;
|
|
84
|
+
// Перенос, а не пробел: команда обязана оставаться ПОСЛЕДНЕЙ в своей строке — иначе
|
|
85
|
+
// следующее слово приклеится к ней при копировании, и человек выполнит не то.
|
|
86
|
+
return `${say(ask.names.join(", "), `${self} feedback`)}\n${agent ? T.agent : T.once}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Выключатель — тот же, что у совета (AQK_ADVICE=0), пробы (AQK_PROBE=0) и проверки версии
|
|
90
|
+
// (AQK_UPDATE=0). Молчаливой просьбы, которую нельзя отменить, у нас не будет.
|
|
91
|
+
function feedbackWanted(env = process.env) {
|
|
92
|
+
return String(env.AQK_FEEDBACK || "") !== "0";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ОДНА ПРОСЬБА НА ПРОЕКТ — и решение, и ограничитель, и отметка здесь. `doctor` и `context`
|
|
96
|
+
// только печатают: разведи это по двум командам, и они разойдутся в условиях, а человек получит
|
|
97
|
+
// просьбу дважды. Кто первым дошёл, тот и спросил.
|
|
98
|
+
//
|
|
99
|
+
// Ничего не роняет: просьба об одолжении не имеет права стоить человеку прогона.
|
|
100
|
+
async function maybeAsk(state, self, { agent = false } = {}) {
|
|
101
|
+
if (!feedbackWanted()) return null;
|
|
102
|
+
try {
|
|
103
|
+
const dirs = stateDirs();
|
|
104
|
+
if (!(await askAllowed("value", dirs))) return null;
|
|
105
|
+
const line = askLine(feedbackAsk(state), self, { agent });
|
|
106
|
+
if (line) await markAsked("value", dirs);
|
|
107
|
+
return line;
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Предзаполненная ссылка. Параметры `title` и `body` — документация GitHub («Creating an issue
|
|
114
|
+
// from a URL query», сверено 2026-09-14). Кодируется ВСЁ: в теле переносы строк, решётки и
|
|
115
|
+
// пробелы, и незакодированная ссылка обрывается на первом же из них — а всё после решётки
|
|
116
|
+
// браузер считает якорем и не передаёт вовсе.
|
|
117
|
+
function issueUrl(repo, title, body) {
|
|
118
|
+
const q = `title=${encodeURIComponent(title)}&body=${encodeURIComponent(body)}`;
|
|
119
|
+
return `${String(repo).replace(/\/+$/, "")}/issues/new?${q}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function cmdFeedback() {
|
|
123
|
+
const T = L.feedback;
|
|
124
|
+
const man = await readManifest();
|
|
125
|
+
let version = "";
|
|
126
|
+
try { version = JSON.parse(await readFile(join(PKG_ROOT, "package.json"), "utf8")).version || ""; } catch { /* версия просто не покажется */ }
|
|
127
|
+
|
|
128
|
+
const run = await readRun();
|
|
129
|
+
let facts = null;
|
|
130
|
+
try { facts = await detectFacts(man); } catch { /* стек не определили — скажем «неизвестно» */ }
|
|
131
|
+
let level = null;
|
|
132
|
+
if (man?.aqk) { try { level = (await assessLevel(man, null)).reached; } catch { /* уровень не посчитали */ } }
|
|
133
|
+
// Классы известны только когда проба ДЕЙСТВИТЕЛЬНО проходила. «Никогда», «выключена» и «не
|
|
134
|
+
// знаем» — это null, то есть «неизвестно»: см. договор в reportText.
|
|
135
|
+
let blind = null;
|
|
136
|
+
try {
|
|
137
|
+
const st = await probeStatus();
|
|
138
|
+
if (st.state === "fresh" || st.state === "stale") blind = st.classes || [];
|
|
139
|
+
} catch { /* пробы не было — так и скажем */ }
|
|
140
|
+
|
|
141
|
+
const lines = reportText({
|
|
142
|
+
version, node: process.version, platform: process.platform, level,
|
|
143
|
+
langs: facts?.langs ? [...facts.langs] : [],
|
|
144
|
+
gates: declaredGates(man).length,
|
|
145
|
+
red: run?.red || [], cannot: run?.cannot || [], blind,
|
|
146
|
+
});
|
|
147
|
+
const body = lines.join("\n");
|
|
148
|
+
console.log(`\n${body}\n`);
|
|
149
|
+
console.log(c.bold(` ${T.how}`));
|
|
150
|
+
console.log(` ${issueUrl(REPO_URL, T.issueTitle, body)}\n`);
|
|
151
|
+
console.log(c.dim(` ${T.nothingSent}`));
|
|
152
|
+
console.log(c.dim(` ${T.orPaste(`${SELF} feedback`)}\n`));
|
|
153
|
+
// Код возврата всегда 0: команда, которая просит об одолжении и роняет при этом конвейер, —
|
|
154
|
+
// последнее, что человек стерпит.
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export { cmdFeedback, feedbackAsk, reportText, issueUrl, askLine, feedbackWanted, maybeAsk };
|
|
@@ -6,7 +6,8 @@ import { spawnSync } from "node:child_process";
|
|
|
6
6
|
import { join, dirname, relative } from "node:path";
|
|
7
7
|
import {
|
|
8
8
|
CWD, PKG_ROOT, DOCS_SRC, RULES_SRC, TARGET_DIR, MANIFEST, SELF, REPO_URL, c, exists, die,
|
|
9
|
-
copyDir, writeIfAbsent,
|
|
9
|
+
copyDir, writeIfAbsent, stateDirs, docPath, ensureIgnored } from "../lib/core.mjs";
|
|
10
|
+
import { askAllowed, markAsked } from "../lib/ask.mjs";
|
|
10
11
|
import { AGENTS_MD, CLAUDE_MD, MANIFEST_YML } from "../lib/templates.mjs";
|
|
11
12
|
import { banner } from "../lib/banner.mjs";
|
|
12
13
|
import { readManifest } from "../lib/manifest.mjs";
|
|
@@ -95,7 +96,10 @@ ${c.dim(L.init.burned(`${SELF} note "…"`))}
|
|
|
95
96
|
// Ничего не постится само: ссылки печатаются, дальше решает человек. Обратная связь важнее
|
|
96
97
|
// звезды, но без звезды меньше шансов, что кто-то вообще дойдёт до фидбека.
|
|
97
98
|
async function maybeAskFeedback() {
|
|
98
|
-
|
|
99
|
+
// Ограничитель — общий на все обращения комплекта (ask.mjs). Вид `install` разовый и живёт
|
|
100
|
+
// в доме пользователя: второй init в другом репозитории на том же компьютере молчит.
|
|
101
|
+
const dirs = stateDirs();
|
|
102
|
+
if (!(await askAllowed("install", dirs))) return;
|
|
99
103
|
const url = REPO_URL;
|
|
100
104
|
console.log(`
|
|
101
105
|
${c.bold(L.feedback.title)}
|
|
@@ -104,20 +108,14 @@ ${c.bold(L.feedback.title)}
|
|
|
104
108
|
${url}/issues/new
|
|
105
109
|
${c.dim(` ${L.feedback.once}`)}
|
|
106
110
|
`);
|
|
107
|
-
// Пометка «уже показывали» — удобство, а не работа
|
|
108
|
-
//
|
|
109
|
-
// /etc/passwd,
|
|
110
|
-
//
|
|
111
|
-
// docker. Локально не воспроизводилось случайно: uid разработчика 1000 совпадает с
|
|
112
|
-
// пользователем `node` в образе, у которого дом есть. Нашёл конвейер, где uid 1001.
|
|
111
|
+
// Пометка «уже показывали» — удобство, а не работа команды: `markAsked` не бросает, а
|
|
112
|
+
// возвращает, записалось ли. Дом бывает недоступен для записи (в контейнере с `--user
|
|
113
|
+
// 1001:127` у этого uid нет записи в /etc/passwd, homedir() даёт «/»), и до 2026-09-09 это
|
|
114
|
+
// роняло ВЕСЬ `init` — то есть любого, кто набрал команду из нашей же документации по docker.
|
|
113
115
|
//
|
|
114
116
|
// Молча глотать нельзя — это то, что красит наш же swallowed-error. Поэтому вслух: не
|
|
115
117
|
// запомнили, покажем снова. Установка при этом доходит до конца.
|
|
116
|
-
|
|
117
|
-
await writeIfAbsent(FEEDBACK_MARK, "shown\n", { force: false });
|
|
118
|
-
} catch {
|
|
119
|
-
console.log(c.dim(` ${L.feedback.notRemembered}`));
|
|
120
|
-
}
|
|
118
|
+
if (!(await markAsked("install", dirs))) console.log(c.dim(` ${L.feedback.notRemembered}`));
|
|
121
119
|
}
|
|
122
120
|
|
|
123
121
|
function findJournal() {
|
package/tool/commands/prompt.mjs
CHANGED
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
import { readManifest } from "../lib/manifest.mjs";
|
|
16
16
|
import { claudeShimFor } from "../lib/repo.mjs";
|
|
17
17
|
import { CWD, SELF } from "../lib/core.mjs";
|
|
18
|
-
import {
|
|
18
|
+
import { readAdvice, portableSelf } from "./context.mjs";
|
|
19
|
+
import { readRun } from "../lib/run.mjs";
|
|
19
20
|
import { probeStatus } from "./probe.mjs";
|
|
20
21
|
import { L } from "../i18n/index.mjs";
|
|
21
22
|
|
package/tool/i18n/en-docs.mjs
CHANGED
|
@@ -63,6 +63,7 @@ const enDocs = {
|
|
|
63
63
|
"No run has been made — which checks are red is UNKNOWN. This is not \"clean\": `aqk doctor --run`.",
|
|
64
64
|
runStale: (when) =>
|
|
65
65
|
`The last run ${when} is OLDER than the last commit — it describes different code.`,
|
|
66
|
+
runCannot: (names) => `COULD NOT CHECK (the checks themselves failed — not findings about the code): ${names}`,
|
|
66
67
|
runClean: (when) => `Last run ${when} — nothing red.`,
|
|
67
68
|
runRed: (when, names) => `Last run ${when} — RED: ${names}.`,
|
|
68
69
|
andMore: (n) => `and ${n} more`,
|
package/tool/i18n/en-gates.mjs
CHANGED
|
@@ -185,6 +185,33 @@ export const enGates = {
|
|
|
185
185
|
issue: "Found a bug or it did not fit — open an issue; both are the most useful feedback there is.",
|
|
186
186
|
notRemembered: "could not remember this (home is not writable) — the message will appear again",
|
|
187
187
|
once: "This message is shown once: it will not appear again on this machine.",
|
|
188
|
+
|
|
189
|
+
ask: {
|
|
190
|
+
cannot: (names, cmd) =>
|
|
191
|
+
`AQK could not check: ${names}. That is the most valuable thing to tell the author — ${cmd}`,
|
|
192
|
+
blind: (names, cmd) =>
|
|
193
|
+
`The probe found defect classes nobody catches here: ${names}. Does that match your experience? — ${cmd}`,
|
|
194
|
+
red: (names, cmd) =>
|
|
195
|
+
`The kit caught: ${names}. One line to the author — did it help: ${cmd}`,
|
|
196
|
+
once: "Asked once per project; switch it off with AQK_FEEDBACK=0",
|
|
197
|
+
agent: "Say this to the human in one sentence, and do not insist if they do not answer.",
|
|
198
|
+
},
|
|
199
|
+
how: "Sending is one click (a prefilled issue opens; the text stays editable):",
|
|
200
|
+
issueTitle: "Feedback on the kit",
|
|
201
|
+
nothingSent: "Nothing was sent: the kit makes no outgoing requests other than the version check.",
|
|
202
|
+
orPaste: (cmd) => `No GitHub? Forward the text above as is — all of it comes from ${cmd}`,
|
|
203
|
+
report: {
|
|
204
|
+
title: "### Feedback on the kit",
|
|
205
|
+
unknown: "unknown",
|
|
206
|
+
none: "none",
|
|
207
|
+
env: (v, node, os) => `version: ${v} · node: ${node} · system: ${os}`,
|
|
208
|
+
level: (x) => `level: ${x}`,
|
|
209
|
+
stack: (x) => `stack: ${x}`,
|
|
210
|
+
gates: (n, red, cannot) => `gates declared: ${n} · red: ${red} · could not check: ${cannot}`,
|
|
211
|
+
blind: (x) => `classes nobody catches here: ${x}`,
|
|
212
|
+
say: "What you would say in your own words (one line — the most useful part of the whole message):",
|
|
213
|
+
mark: (v) => `<!-- collected by "aqk feedback" ${v}: no paths, no code, no repository name -->`,
|
|
214
|
+
},
|
|
188
215
|
},
|
|
189
216
|
|
|
190
217
|
note: {
|