agent-quality-kit 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +155 -0
  3. package/kit/docs/ai/agent-harness-playbook.md +596 -0
  4. package/kit/docs/ai/ai-native-development.md +371 -0
  5. package/kit/docs/ai/ai-sdlc.md +221 -0
  6. package/kit/docs/ai/anthropic-ai-native-sdlc-2026-08.md +294 -0
  7. package/kit/docs/ai/app-owner-strategy.md +921 -0
  8. package/kit/docs/ai/deep-research-2026-07.md +161 -0
  9. package/kit/docs/ai/harness-best-practices.md +385 -0
  10. package/kit/docs/ai/index.md +64 -0
  11. package/kit/docs/ai/project-baseline.md +261 -0
  12. package/kit/docs/ai/quality-gates-checklist.md +322 -0
  13. package/kit/docs/ai/sources-building-with-agents.md +111 -0
  14. package/kit/docs/ai/stream-2026-08-ai-coding-panel.md +304 -0
  15. package/kit/docs/ready-made-rules.md +170 -0
  16. package/kit/gates/README.md +231 -0
  17. package/kit/gates/_skip.sh +75 -0
  18. package/kit/gates/commit-explains-itself/README.md +45 -0
  19. package/kit/gates/commit-explains-itself/check.sh +63 -0
  20. package/kit/gates/commit-explains-itself/gate.yml +10 -0
  21. package/kit/gates/commit-explains-itself/green/COMMIT_MSG +6 -0
  22. package/kit/gates/commit-explains-itself/red/COMMIT_MSG +3 -0
  23. package/kit/gates/complexity-limit/README.md +37 -0
  24. package/kit/gates/complexity-limit/check.sh +44 -0
  25. package/kit/gates/complexity-limit/gate.yml +13 -0
  26. package/kit/gates/complexity-limit/green/flat.py +10 -0
  27. package/kit/gates/complexity-limit/red/deep.py +9 -0
  28. package/kit/gates/dead-code/README.md +30 -0
  29. package/kit/gates/dead-code/gate.yml +23 -0
  30. package/kit/gates/dead-code/green/mod.py +9 -0
  31. package/kit/gates/dead-code/red/mod.py +9 -0
  32. package/kit/gates/deps-are-pinned/README.md +29 -0
  33. package/kit/gates/deps-are-pinned/check.sh +49 -0
  34. package/kit/gates/deps-are-pinned/gate.yml +9 -0
  35. package/kit/gates/deps-are-pinned/green/nodep-go/go.mod +3 -0
  36. package/kit/gates/deps-are-pinned/green/package-lock.json +3 -0
  37. package/kit/gates/deps-are-pinned/green/package.json +4 -0
  38. package/kit/gates/deps-are-pinned/green/requirements.txt +2 -0
  39. package/kit/gates/deps-are-pinned/red/package.json +4 -0
  40. package/kit/gates/deps-are-pinned/red/requirements.txt +2 -0
  41. package/kit/gates/deps-are-pinned/red/withdep-go/go.mod +5 -0
  42. package/kit/gates/duplicate-code/README.md +40 -0
  43. package/kit/gates/duplicate-code/check.sh +58 -0
  44. package/kit/gates/duplicate-code/gate.yml +12 -0
  45. package/kit/gates/duplicate-code/green/common.py +9 -0
  46. package/kit/gates/duplicate-code/green/use.py +9 -0
  47. package/kit/gates/duplicate-code/red/a.py +12 -0
  48. package/kit/gates/duplicate-code/red/b.py +12 -0
  49. package/kit/gates/entry-links-exist/README.md +22 -0
  50. package/kit/gates/entry-links-exist/check.sh +24 -0
  51. package/kit/gates/entry-links-exist/gate.yml +16 -0
  52. package/kit/gates/entry-links-exist/green/AGENTS.md +5 -0
  53. package/kit/gates/entry-links-exist/green/rules/general.md +3 -0
  54. package/kit/gates/entry-links-exist/red/AGENTS.md +3 -0
  55. package/kit/gates/file-size-limit/README.md +22 -0
  56. package/kit/gates/file-size-limit/check.sh +34 -0
  57. package/kit/gates/file-size-limit/gate.yml +9 -0
  58. package/kit/gates/file-size-limit/green/a.py +251 -0
  59. package/kit/gates/file-size-limit/green/b.py +251 -0
  60. package/kit/gates/file-size-limit/red/big.py +601 -0
  61. package/kit/gates/gate-has-samples/README.md +29 -0
  62. package/kit/gates/gate-has-samples/check.sh +48 -0
  63. package/kit/gates/gate-has-samples/gate.yml +9 -0
  64. package/kit/gates/gate-has-samples/green/.aqk.yml +10 -0
  65. package/kit/gates/gate-has-samples/green/gates/no-print-in-prod/check.sh +2 -0
  66. package/kit/gates/gate-has-samples/green/gates/no-print-in-prod/green/good.py +2 -0
  67. package/kit/gates/gate-has-samples/green/gates/no-print-in-prod/red/bad.py +1 -0
  68. package/kit/gates/gate-has-samples/red/.aqk.yml +10 -0
  69. package/kit/gates/gate-has-samples/red/gates/no-print-in-prod/check.sh +2 -0
  70. package/kit/gates/gates-are-runnable/README.md +23 -0
  71. package/kit/gates/gates-are-runnable/check.sh +35 -0
  72. package/kit/gates/gates-are-runnable/gate.yml +9 -0
  73. package/kit/gates/gates-are-runnable/green/.aqk.yml +9 -0
  74. package/kit/gates/gates-are-runnable/green/checks/lint.sh +2 -0
  75. package/kit/gates/gates-are-runnable/red/.aqk.yml +6 -0
  76. package/kit/gates/gates-run-in-ci/README.md +29 -0
  77. package/kit/gates/gates-run-in-ci/check.sh +42 -0
  78. package/kit/gates/gates-run-in-ci/gate.yml +12 -0
  79. package/kit/gates/gates-run-in-ci/green/.aqk.yml +6 -0
  80. package/kit/gates/gates-run-in-ci/green/.github/workflows/ci.yml +7 -0
  81. package/kit/gates/gates-run-in-ci/green/checks/lint.sh +2 -0
  82. package/kit/gates/gates-run-in-ci/red/.aqk.yml +6 -0
  83. package/kit/gates/gates-run-in-ci/red/.github/workflows/ci.yml +7 -0
  84. package/kit/gates/gates-run-in-ci/red/checks/lint.sh +2 -0
  85. package/kit/gates/lesson-has-outcome/README.md +37 -0
  86. package/kit/gates/lesson-has-outcome/check.sh +50 -0
  87. package/kit/gates/lesson-has-outcome/gate.yml +11 -0
  88. package/kit/gates/lesson-has-outcome/green/.aqk.yml +2 -0
  89. package/kit/gates/lesson-has-outcome/green/incidents/README.md +32 -0
  90. package/kit/gates/lesson-has-outcome/red/.aqk.yml +2 -0
  91. package/kit/gates/lesson-has-outcome/red/incidents/README.md +13 -0
  92. package/kit/gates/no-print-in-prod/README.md +44 -0
  93. package/kit/gates/no-print-in-prod/check.sh +36 -0
  94. package/kit/gates/no-print-in-prod/gate.yml +15 -0
  95. package/kit/gates/no-print-in-prod/green/docs.ts +15 -0
  96. package/kit/gates/no-print-in-prod/green/legacy.py +9 -0
  97. package/kit/gates/no-print-in-prod/green/main.go +8 -0
  98. package/kit/gates/no-print-in-prod/green/main.rs +4 -0
  99. package/kit/gates/no-print-in-prod/green/service.py +8 -0
  100. package/kit/gates/no-print-in-prod/red/main.go +8 -0
  101. package/kit/gates/no-print-in-prod/red/main.rs +4 -0
  102. package/kit/gates/no-print-in-prod/red/service.py +3 -0
  103. package/kit/gates/secrets-not-in-code/README.md +29 -0
  104. package/kit/gates/secrets-not-in-code/check.sh +18 -0
  105. package/kit/gates/secrets-not-in-code/gate.yml +9 -0
  106. package/kit/gates/secrets-not-in-code/green/settings.py +4 -0
  107. package/kit/gates/secrets-not-in-code/red/settings.py +2 -0
  108. package/kit/gates/swallowed-error/README.md +26 -0
  109. package/kit/gates/swallowed-error/check.sh +54 -0
  110. package/kit/gates/swallowed-error/gate.yml +12 -0
  111. package/kit/gates/swallowed-error/green/loader.py +11 -0
  112. package/kit/gates/swallowed-error/green/run.js +8 -0
  113. package/kit/gates/swallowed-error/red/loader.py +5 -0
  114. package/kit/gates/swallowed-error/red/run.js +3 -0
  115. package/kit/gates/todo-without-task/README.md +26 -0
  116. package/kit/gates/todo-without-task/check.sh +19 -0
  117. package/kit/gates/todo-without-task/gate.yml +12 -0
  118. package/kit/gates/todo-without-task/green/order.py +9 -0
  119. package/kit/gates/todo-without-task/red/order.py +8 -0
  120. package/kit/ratchet/ratchet.sh +62 -0
  121. package/kit/rules/general.md +55 -0
  122. package/kit/rules/security.md +33 -0
  123. package/kit/rules/testing.md +46 -0
  124. package/package.json +41 -0
  125. package/tool/commands/doctor.mjs +230 -0
  126. package/tool/commands/gates.mjs +445 -0
  127. package/tool/commands/project.mjs +316 -0
  128. package/tool/lib/core.mjs +98 -0
  129. package/tool/lib/manifest.mjs +140 -0
  130. package/tool/lib/repo.mjs +270 -0
  131. package/tool/lib/templates.mjs +187 -0
  132. package/tool/program.mjs +81 -0
  133. package/tool/selfcheck/conditional.sh +24 -0
  134. package/tool/selfcheck/gates.sh +127 -0
  135. package/tool/selfcheck/smoke.sh +539 -0
  136. package/tool/selfcheck/syntax.sh +23 -0
  137. package/tool/selfcheck/units.mjs +105 -0
@@ -0,0 +1,316 @@
1
+ // tool/commands/project.mjs — что делают с проектом целиком: разложить комплект, записать
2
+ // урок в общий журнал, собрать методички одним файлом.
3
+
4
+ import { readdir, mkdir, writeFile, readFile } from "node:fs/promises";
5
+ import { spawnSync } from "node:child_process";
6
+ import { join, dirname, relative } from "node:path";
7
+ import {
8
+ CWD, PKG_ROOT, DOCS_SRC, RULES_SRC, TARGET_DIR, MANIFEST, SELF, REPO, c, exists, die,
9
+ copyDir, writeIfAbsent, FEEDBACK_MARK,
10
+ } from "../lib/core.mjs";
11
+ import { AGENTS_MD, CLAUDE_MD, MANIFEST_YML } from "../lib/templates.mjs";
12
+ import { readManifest } from "../lib/manifest.mjs";
13
+ import { detectFacts, readCatalog, triggerVerdict } from "../lib/repo.mjs";
14
+ import { installGate } from "./gates.mjs";
15
+
16
+ async function cmdInit(args) {
17
+ const force = args.includes("--force");
18
+ const created = [];
19
+ const skipped = [];
20
+
21
+ const track = (ok, path) => (ok ? created : skipped).push(relative(CWD, path));
22
+
23
+ if (!(await exists(DOCS_SRC))) {
24
+ die(`Не найден корпус методичек: ${DOCS_SRC}\nПохоже, пакет установлен не полностью.`);
25
+ }
26
+
27
+ const refs = await copyDir(DOCS_SRC, join(CWD, TARGET_DIR, "docs"), { force });
28
+ for (const f of refs) created.push(relative(CWD, f));
29
+
30
+ const rules = await copyDir(RULES_SRC, join(CWD, TARGET_DIR, "rules"), { force });
31
+ for (const f of rules) created.push(relative(CWD, f));
32
+
33
+
34
+ const manifest = join(CWD, MANIFEST);
35
+ track(await writeIfAbsent(manifest, MANIFEST_YML, { force }), manifest);
36
+
37
+ const agents = join(CWD, "AGENTS.md");
38
+ track(await writeIfAbsent(agents, AGENTS_MD, { force }), agents);
39
+
40
+ const claude = join(CWD, "CLAUDE.md");
41
+ track(await writeIfAbsent(claude, CLAUDE_MD, { force }), claude);
42
+
43
+ console.log(c.bold("\naqk init\n"));
44
+ if (created.length) {
45
+ console.log(c.green(` создано (${created.length}):`));
46
+ for (const f of created.slice(0, 8)) console.log(` ${f}`);
47
+ if (created.length > 8) console.log(c.dim(` … и ещё ${created.length - 8}`));
48
+ }
49
+ if (skipped.length) {
50
+ console.log(c.yellow(`\n уже были на месте, не тронуты (${skipped.length}):`));
51
+ for (const f of skipped) console.log(` ${f}`);
52
+ console.log(c.dim(` перезаписать: ${SELF} init --force`));
53
+ }
54
+
55
+ // Путь ищем, а не пишем: раскладка внутри .aqk — дело владельца комплекта, и жёстко
56
+ // вписанный путь однажды отправит человека в несуществующий файл. Уже отправлял.
57
+ let baseline = join(TARGET_DIR, "docs", "project-baseline.md");
58
+ for (const f of created) if (f.endsWith("project-baseline.md")) baseline = f;
59
+
60
+ console.log(`
61
+ ${c.bold("Что дальше — по порядку:")}
62
+
63
+ 1. Открой ${c.bold("AGENTS.md")} и заполни раздел «Команды». Команда, которую нельзя
64
+ скопировать и выполнить, — не команда, а пожелание.
65
+ 2. Прочитай ${c.bold(baseline)} — это обязательный минимум
66
+ проекта без привязки к языку. Пройди сверху вниз и отметь, чего нет.
67
+ 3. Заполни ${c.bold(".aqk.yml")} — гейты, образцы, журнал. Уровень соответствия AQK
68
+ считается по нему: ${c.bold(`${SELF} doctor`)}.
69
+ 4. Поднимайся по ступеням ${c.bold("по одной")}. Гейт стережёт существующий артефакт:
70
+ проверка на код, которого ещё нет, — мёртвое правило.
71
+
72
+ ${c.dim(`Обжёгся на чём-то — запиши: ${SELF} note "что случилось"`)}
73
+ `);
74
+ await maybeAskFeedback();
75
+ }
76
+
77
+ // Печатается один раз на машину, не на проект: второй init в другом репозитории на том же
78
+ // компьютере её не повторяет — отметка живёт в доме пользователя, вне любого git.
79
+ // Ничего не постится само: ссылки печатаются, дальше решает человек. Обратная связь важнее
80
+ // звезды, но без звезды меньше шансов, что кто-то вообще дойдёт до фидбека.
81
+ async function maybeAskFeedback() {
82
+ if (await exists(FEEDBACK_MARK)) return;
83
+ const url = `https://github.com/${REPO.replace(/^github:/, "")}`;
84
+ console.log(`
85
+ ${c.bold("Если пригодилось:")}
86
+ Поставь звезду — ${url}
87
+ Нашёл баг или не подошло — заведи Issue, самая полезная обратная связь: и то и другое.
88
+ ${url}/issues/new
89
+ ${c.dim(" Это разовое сообщение: больше не покажется на этой машине.")}
90
+ `);
91
+ await writeIfAbsent(FEEDBACK_MARK, "shown\n", { force: false });
92
+ }
93
+
94
+ function findJournal() {
95
+ const fromEnv = process.env.AQK_HOME;
96
+ const candidates = [
97
+ fromEnv,
98
+ join(process.env.HOME || "", "projects", "aqk"),
99
+ join(process.env.HOME || "", "aqk"),
100
+ ].filter(Boolean);
101
+ for (const p of candidates) {
102
+ const r = spawnSync("git", ["-C", p, "rev-parse", "--git-dir"], { stdio: "ignore" });
103
+ if (r.status === 0) return p;
104
+ }
105
+ return null;
106
+ }
107
+
108
+ async function cmdNote(args) {
109
+ const title = args.find((a) => !a.startsWith("--"));
110
+ if (!title) die(`Нужен заголовок: ${SELF} note "что произошло"`);
111
+
112
+ const home = findJournal();
113
+ if (!home) {
114
+ die(`Клон журнала не найден.
115
+ Сделай один раз:
116
+ git clone https://github.com/arsen-ask-lx/Agent_Quality_Kit.git ~/projects/aqk
117
+ или укажи путь: export AQK_HOME=/путь/к/aqk`);
118
+ }
119
+
120
+ const journal = join(home, "incidents", "README.md");
121
+ if (!(await exists(journal))) die(`Журнал не найден: ${journal}`);
122
+
123
+ let body = "";
124
+ if (!process.stdin.isTTY) {
125
+ body = await new Promise((res) => {
126
+ let buf = "";
127
+ process.stdin.setEncoding("utf8");
128
+ process.stdin.on("data", (d) => (buf += d));
129
+ process.stdin.on("end", () => res(buf));
130
+ });
131
+ }
132
+
133
+ if (!body.trim()) {
134
+ die(`Тело записи пустое. Передай его на стандартный ввод, например:
135
+
136
+ aqk note "заголовок" <<'EOF'
137
+ **Класс:** гейт молчал
138
+ **Что случилось.** ...
139
+ **Чем это стоило.** ...
140
+ **Вывод.** 🔧 ...
141
+ EOF`);
142
+ }
143
+
144
+ // ГЕЙТ. Урок без вывода — это история, а не урок: в следующий раз обожжёмся так же.
145
+ // Проверяем ровно то же, что потом сверяет kit/gates/lesson-has-outcome: слово «вывод» в
146
+ // тексте не значит вывод, если рядом нет одной из трёх настоящих отметок. Запись проходила бы
147
+ // здесь и тут же краснела на doctor --run — разные требования в двух местах одного правила.
148
+ if (!/[✅🔧📜👤]/.test(body)) {
149
+ die(
150
+ "В записи нет отметки решения. Урок без вывода — это история, а не урок.\n" +
151
+ "Припиши одну из трёх:\n" +
152
+ " ✅ **Стало гейтом:** <имя записи каталога>\n" +
153
+ " 🔧 **Стало правкой оснастки:** <что именно изменено>\n" +
154
+ " 👤 **Гейтом не станет:** <почему>"
155
+ );
156
+ }
157
+
158
+ const date = new Date().toISOString().slice(0, 10);
159
+ const project = CWD.split("/").filter(Boolean).pop() || "неизвестно";
160
+ const entry = `\n## ${date} — ${title}\n\n**Проект:** ${project}\n\n${body.trim()}\n`;
161
+
162
+ const prev = await readFile(journal, "utf8");
163
+ await writeFile(journal, prev + entry, "utf8");
164
+
165
+ const run = (...a) => spawnSync("git", ["-C", home, ...a], { stdio: "inherit" });
166
+ run("add", "incidents/README.md");
167
+ run("commit", "-q", "-m", `lesson(${project}): ${title}`);
168
+ const pushed = run("push", "-q");
169
+ console.log(
170
+ pushed.status === 0
171
+ ? c.green(`Записано и отправлено: ${title}`)
172
+ : c.yellow(`Записано локально, push не прошёл. Отправить: git -C ${home} push`)
173
+ );
174
+ }
175
+
176
+ // Один файл со всем текстом комплекта — чтобы разом отдать его в чат.
177
+ // СОБИРАЕТСЯ, А НЕ ХРАНИТСЯ. Копия, которую правят руками, через неделю расходится
178
+ // с оригиналом, и никто не знает, какая из двух настоящая.
179
+
180
+ async function cmdBlob() {
181
+ const dir = join(PKG_ROOT, "kit", "docs");
182
+ if (!(await exists(dir))) die(`Не найдены методички: ${dir}`);
183
+
184
+ const stamp = new Date().toISOString().slice(0, 10);
185
+ let out =
186
+ `<!-- СОБРАНО КОМАНДОЙ aqk blob ${stamp} из kit/docs. Не править руками:\n` +
187
+ ` правки затрёт следующая сборка. Источник — отдельные файлы. -->\n\n` +
188
+ `# AQK — методички одним файлом\n`;
189
+
190
+ // Методички могут лежать в подпапках — обходим дерево, порядок стабильный.
191
+ const found = [];
192
+ const walk = async (d) => {
193
+ for (const it of (await readdir(d, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
194
+ const full = join(d, it.name);
195
+ if (it.isDirectory()) await walk(full);
196
+ else if (it.name.endsWith(".md")) found.push(full);
197
+ }
198
+ };
199
+ await walk(dir);
200
+
201
+ for (const full of found) {
202
+ out += `\n\n${"=".repeat(78)}\n<!-- источник: ${relative(PKG_ROOT, full)} -->\n${"=".repeat(78)}\n\n`;
203
+ // Ссылки на соседние файлы в склейке ведут в никуда: соседей рядом больше нет,
204
+ // все они внутри этого же текста. Оставляем подпись, снимаем разметку.
205
+ const body = (await readFile(full, "utf8")).replace(
206
+ /\[([^\]]+)\]\((?!https?:)[^)]+\.md(?:#[^)]*)?\)/g,
207
+ "$1"
208
+ );
209
+ out += body;
210
+ }
211
+
212
+ const dst = join(CWD, "GOD_AI.md");
213
+ await writeFile(dst, out, "utf8");
214
+ console.log(
215
+ `\n ${c.green("✔")} GOD_AI.md — ${found.length} файлов, ` +
216
+ `${Math.round(Buffer.byteLength(out) / 1024)} КБ\n`
217
+ );
218
+ console.log(c.dim(" Собирается заново каждой командой. Править надо оригиналы в kit/docs.\n"));
219
+ }
220
+
221
+
222
+ // --- start: порядок «с нуля» ---------------------------------------------------
223
+ // Сценарий «кода ещё нет». Главное здесь машинное, а не словесное: сторожей ставят ДО первой
224
+ // строки кода. Поставленный потом, сторож красит весь старый код разом — и его выключают.
225
+ // На пустом проекте долга нет вовсе, храповик не нужен ни одному гейту.
226
+ //
227
+ // Порядок работы — задача, ограничения, сайзинг, архитектура — программа НЕ проверяет и не
228
+ // делает вид, что проверяет: это текст в методичках, который агент может проигнорировать.
229
+ // Сказать об этом вслух дешевле, чем изобразить проверку.
230
+
231
+ async function cmdStart(args) {
232
+ const force = args.includes("--force");
233
+ let man = await readManifest();
234
+ if (!man) {
235
+ await cmdInit(args.filter((a) => a !== "--force"));
236
+ man = await readManifest();
237
+ if (!man) die(`Не получилось разложить комплект. Начни с ${SELF} init`);
238
+ }
239
+
240
+ const facts = await detectFacts(man);
241
+ console.log(c.bold("\naqk start\n"));
242
+
243
+ if (facts.files > 30 && !force) {
244
+ console.log(c.yellow(` В репозитории уже ${facts.files} файлов кода — это другой сценарий.\n`));
245
+ console.log(` ${c.bold(`${SELF} doctor`)} осмотрит, что есть, и разделит записи на три списка:`);
246
+ console.log(c.dim(" держит машина · применимо и не поставлено · не применимо и почему.\n"));
247
+ console.log(c.dim(` Всё равно поставить сторожей дня 0: ${SELF} start --force`));
248
+ console.log(c.dim(" Готовься к красному: сторож, поставленный на живой код, краснеет на нём весь."));
249
+ console.log(c.dim(` Это лечится храповиком — ${SELF} ratchet <имя>, — а не отключением.\n`));
250
+ return;
251
+ }
252
+
253
+ // --- сторожа дня 0 ---------------------------------------------------------
254
+ const declared = new Set(Object.keys(
255
+ man.gates && typeof man.gates === "object" && !Array.isArray(man.gates) ? man.gates : {}
256
+ ));
257
+ const put = [];
258
+ let skipped = [];
259
+ const catalog = await readCatalog();
260
+
261
+ // Проходим по каталогу, пока он не перестанет расти. Установка меняет признаки репозитория:
262
+ // первый же поставленный гейт делает применимыми записи с условием has_gates. Один проход
263
+ // объявлял их неприменимыми «гейтов не объявлено» — ровно в тот момент, когда они появились.
264
+ let facts0 = facts;
265
+ for (let pass = 0; pass < 3; pass++) {
266
+ skipped = [];
267
+ let added = 0;
268
+ for (const rec of catalog) {
269
+ if (declared.has(rec.slug)) continue;
270
+ const v = triggerVerdict(rec, facts0);
271
+ if (!v.applies) { skipped.push([rec.slug, v.why]); continue; }
272
+ const { cmd } = await installGate(rec.slug, man, facts0);
273
+ put.push([rec.slug, cmd, rec.intent || ""]);
274
+ declared.add(rec.slug);
275
+ added++;
276
+ man = await readManifest();
277
+ }
278
+ if (!added) break;
279
+ facts0 = await detectFacts(man);
280
+ }
281
+
282
+ if (put.length) {
283
+ console.log(c.green(` Поставлено сторожей дня 0: ${put.length}\n`));
284
+ for (const [slug, , intent] of put) console.log(` ${c.green("✔")} ${slug.padEnd(22)} ${c.dim(intent)}`);
285
+ console.log(c.dim("\n Долга нет: на пустом проекте им нечего пропускать. Тот же сторож, поставленный"));
286
+ console.log(c.dim(" через полгода, покраснел бы на всём старом коде — и его бы выключили.\n"));
287
+ } else {
288
+ console.log(c.dim(" Все применимые записи уже объявлены.\n"));
289
+ }
290
+ if (skipped.length) {
291
+ console.log(c.dim(" Не применимо пока:"));
292
+ for (const [slug, why] of skipped.slice(0, 6)) console.log(c.dim(` ${slug.padEnd(22)} ${why}`));
293
+ console.log(c.dim(" Появится признак — запись покажется сама.\n"));
294
+ }
295
+
296
+ // --- порядок работы --------------------------------------------------------
297
+ console.log(`${c.bold("Порядок, в котором это делают:")}
298
+
299
+ 1. ${c.bold("Задача словами.")} Что и кому, без единого технического слова.
300
+ ${c.dim("Пока задача не описана словами, любая архитектура защищает неизвестно что.")}
301
+ 2. ${c.bold("Ограничения.")} Сроки, деньги, нагрузка, чем нельзя пользоваться.
302
+ ${c.dim("Ограничения выбирают решение куда чаще, чем вкус: без них выбирают вкусом.")}
303
+ 3. ${c.bold("Сайзинг.")} Сколько данных, запросов, людей — числами, хотя бы порядком.
304
+ ${c.dim("Число отделяет «нужна очередь» от «хватит таблицы». Без него спорят словами.")}
305
+ 4. ${c.bold("Архитектура.")} И только теперь — из первых трёх, а не до них.
306
+
307
+ ${c.dim("Этот порядок программа не проверяет: он в .aqk/docs/, и агент может его")}
308
+ ${c.dim("проигнорировать. Машина держит другое — сторожей выше. Разница между")}
309
+ ${c.dim("мягким и жёстким тут ровно такая: текст просят, команду выполняют.")}
310
+
311
+ ${c.bold("Дальше:")} ${c.bold(`${SELF} doctor --run`)} ${c.dim("— прогнать всё, что объявлено")}
312
+ `);
313
+ await maybeAskFeedback();
314
+ }
315
+
316
+ export { cmdInit, cmdNote, cmdBlob, cmdStart };
@@ -0,0 +1,98 @@
1
+ // tool/lib/core.mjs — пути, вывод и выход. То, что нужно всем остальным файлам.
2
+ //
3
+ // ЗАЧЕМ ОТДЕЛЬНО. Программа перестала помещаться в один файл: 1357 строк при собственном
4
+ // пределе в 500. Разделена по назначению, а не пополам — так требует наше же правило про
5
+ // размер файла. Зависимостей по-прежнему нет ни одной: только встроенные модули Node.
6
+
7
+ import { access, readdir, mkdir, copyFile, writeFile } from "node:fs/promises";
8
+ import { constants } from "node:fs";
9
+ import { fileURLToPath } from "node:url";
10
+ import { dirname, join, resolve, relative } from "node:path";
11
+ import { homedir } from "node:os";
12
+
13
+ const HERE = dirname(fileURLToPath(import.meta.url));
14
+ // Два уровня вверх: файл лежит в tool/lib/. Ошибка здесь тихая — программа стала бы искать
15
+ // комплект внутри tool/ и сообщала бы «методичек нет» на исправной установке.
16
+ const PKG_ROOT = resolve(HERE, "..", "..");
17
+ const CWD = process.cwd();
18
+
19
+ const DOCS_SRC = join(PKG_ROOT, "kit", "docs");
20
+ const RULES_SRC = join(PKG_ROOT, "kit", "rules");
21
+ const TARGET_DIR = ".aqk";
22
+
23
+ const c = {
24
+ bold: (s) => `${s}`,
25
+ dim: (s) => `${s}`,
26
+ green: (s) => `${s}`,
27
+ yellow: (s) => `${s}`,
28
+ red: (s) => `${s}`,
29
+ };
30
+
31
+ const exists = async (p) => access(p, constants.F_OK).then(() => true, () => false);
32
+
33
+ // Как звать программу — зависит от того, как её запустили. Через npx команды `aqk` в системе
34
+ // нет: подсказка «aqk doctor» отправляет человека в «команда не найдена» на первом же шаге.
35
+ // Печатаем то, что можно скопировать и выполнить прямо сейчас.
36
+ const REPO = "github:arsen-ask-lx/Agent_Quality_Kit";
37
+
38
+ function selfCmd() {
39
+ const p = process.argv[1] || "";
40
+ if (/[\\/]_npx[\\/]/.test(p)) return `npx ${REPO}`;
41
+ if (/[\\/]node_modules[\\/]\.bin[\\/]/.test(p) || /[\\/]aqk$/.test(p)) return "aqk";
42
+ const rel = relative(CWD, p);
43
+ return `node ${rel && !rel.startsWith("..") ? rel : p}`;
44
+ }
45
+
46
+ const SELF = selfCmd();
47
+
48
+ function die(msg) {
49
+ console.error(c.red(msg));
50
+ process.exit(1);
51
+ }
52
+
53
+ // Имя манифеста и каталоги, которые программа знает по именам. Собраны здесь, а не разбросаны
54
+ // по файлам команд: путь, записанный в двух местах, однажды разъедется.
55
+ const MANIFEST = ".aqk.yml";
56
+ const GATES_SRC = join(PKG_ROOT, "kit", "gates");
57
+ const PROJECT_GATES = "gates";
58
+ const RATCHET_DIR = "ratchets";
59
+ // Обёртка лежит рядом с реестрами, которые она читает, а не среди гейтов: храповик — это
60
+ // обёртка плюс реестр, и разносить их по разным каталогам значит прятать половину механизма.
61
+ const RATCHET_LIB = `${RATCHET_DIR}/_ratchet.sh`;
62
+
63
+ // Отметка «просьбу про звезду уже показали» — вне репозитория, в доме пользователя. Внутри
64
+ // .aqk/ она либо закоммитится в чужой проект как наш мусор, либо пропадёт при init --force:
65
+ // то и другое врёт о том, видел человек просьбу или нет.
66
+ const FEEDBACK_MARK = join(homedir(), ".config", "aqk", "feedback-shown");
67
+
68
+ async function copyDir(src, dst, { force }) {
69
+ await mkdir(dst, { recursive: true });
70
+ const entries = await readdir(src, { withFileTypes: true });
71
+ const written = [];
72
+ for (const e of entries) {
73
+ const from = join(src, e.name);
74
+ const to = join(dst, e.name);
75
+ if (e.isDirectory()) {
76
+ written.push(...(await copyDir(from, to, { force })));
77
+ continue;
78
+ }
79
+ if (!force && (await exists(to))) continue;
80
+ await copyFile(from, to);
81
+ written.push(to);
82
+ }
83
+ return written;
84
+ }
85
+
86
+ async function writeIfAbsent(path, content, { force }) {
87
+ if (!force && (await exists(path))) return false;
88
+ await mkdir(dirname(path), { recursive: true });
89
+ await writeFile(path, content, "utf8");
90
+ return true;
91
+ }
92
+
93
+ export {
94
+ copyDir, writeIfAbsent,
95
+ PKG_ROOT, CWD, DOCS_SRC, RULES_SRC, TARGET_DIR,
96
+ MANIFEST, GATES_SRC, PROJECT_GATES, RATCHET_DIR, RATCHET_LIB,
97
+ SELF, REPO, c, exists, die, FEEDBACK_MARK,
98
+ };
@@ -0,0 +1,140 @@
1
+ // tool/lib/manifest.mjs — чтение .aqk.yml и вычисление ступени соответствия.
2
+
3
+ import { readFile } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { CWD, MANIFEST, PROJECT_GATES, exists } from "./core.mjs";
6
+
7
+ // СТАНДАРТ. Уровень — не самооценка и не галочка в README, а вычисляемое утверждение:
8
+ // каждая ступень проверяется файлами на диске. Утверждение, которое нельзя проверить
9
+ // машиной, в стандарт не входит — иначе значок в README означает только доверие к автору.
10
+
11
+ // Разбор ограниченного подмножества YAML: ключ, вложенный на один уровень ключ, список.
12
+ // НАМЕРЕННО без библиотеки: манифест обязан быть настолько простым, чтобы его разбирал
13
+ // кусок кода, который читается за минуту. Сложный манифест никто не заполнит.
14
+ function parseManifest(text) {
15
+ const out = {};
16
+ let section = null;
17
+ for (const raw of text.split("\n")) {
18
+ const line = raw.replace(/#.*$/, "").replace(/\s+$/, "");
19
+ if (!line.trim()) continue;
20
+ const indented = /^\s/.test(line);
21
+ const listItem = line.trim().startsWith("- ");
22
+
23
+ if (listItem && section) {
24
+ // Ключ вида `entry:` без значения уже создал пустой объект — под список его надо
25
+ // заменить массивом, иначе push падает и весь манифест читается как отсутствующий.
26
+ if (!Array.isArray(out[section])) out[section] = [];
27
+ out[section].push(line.trim().slice(2).trim().replace(/^["']|["']$/g, ""));
28
+ continue;
29
+ }
30
+ const m = line.trim().match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
31
+ if (!m) continue;
32
+ const [, key, value] = m;
33
+ const clean = value.trim().replace(/^["']|["']$/g, "");
34
+
35
+ if (indented && section) {
36
+ if (typeof out[section] !== "object" || Array.isArray(out[section])) out[section] = {};
37
+ out[section][key] = clean;
38
+ continue;
39
+ }
40
+ section = key;
41
+ // Список в одну строку: entry: [AGENTS.md, docs/START.md]. Люди пишут именно так —
42
+ // и раньше манифест молча читался как пустой, а проект получал вердикт «нет AQK-0».
43
+ // Неверный вердикт хуже отсутствия вердикта: ему верят.
44
+ if (clean.startsWith("[") && clean.endsWith("]")) {
45
+ out[key] = clean
46
+ .slice(1, -1)
47
+ .split(",")
48
+ .map((v) => v.trim().replace(/^["']|["']$/g, ""))
49
+ .filter(Boolean);
50
+ continue;
51
+ }
52
+ out[key] = clean === "" ? {} : clean;
53
+ }
54
+ return out;
55
+ }
56
+
57
+ async function readManifest() {
58
+ const p = join(CWD, MANIFEST);
59
+ if (!(await exists(p))) return null;
60
+ try {
61
+ return parseManifest(await readFile(p, "utf8"));
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ // Каждая ступень: что требуется, как проверяется, и что это даёт человеку.
68
+ async function assessLevel(man) {
69
+ const has = async (rel) => Boolean(rel) && (await exists(join(CWD, String(rel))));
70
+ const isUrl = (v) => typeof v === "string" && /^https?:\/\//.test(v);
71
+
72
+ const entries = Array.isArray(man?.entry) ? man.entry : [];
73
+ const entriesExist = entries.length > 0 && (await Promise.all(entries.map(has))).every(Boolean);
74
+
75
+ const gates = man?.gates && typeof man.gates === "object" && !Array.isArray(man.gates) ? man.gates : {};
76
+ const filledGates = Object.entries(gates).filter(([, cmd]) => String(cmd || "").trim());
77
+
78
+ const steps = [
79
+ {
80
+ level: 0,
81
+ title: "манифест и точка входа",
82
+ ok: Boolean(man?.aqk) && entriesExist,
83
+ need: "создай .aqk.yml и укажи в entry файл, который агент читает первым (AGENTS.md)",
84
+ gives: "любой инструмент понимает, что читать в этом репозитории",
85
+ },
86
+ {
87
+ level: 1,
88
+ title: "правила и работающие гейты",
89
+ ok: (await has(man?.rules)) && filledGates.length > 0,
90
+ need: "укажи rules (каталог стандартов) и заполни хотя бы один гейт в gates реальной командой",
91
+ gives: "проверки объявлены командами, а не описаны словами",
92
+ },
93
+ {
94
+ level: 2,
95
+ title: "гейты доказаны, долг под храповиком",
96
+ ok: (await has(man?.samples)) && (await has(man?.ratchets)),
97
+ need: "заведи samples (красные и зелёные образцы гейтов) и ratchets (реестры долга)",
98
+ gives: "гейт доказал, что ловит брак и молчит на исправном коде",
99
+ },
100
+ {
101
+ level: 3,
102
+ title: "уроки возвращаются в работу",
103
+ ok: isUrl(man?.lessons) || (await has(man?.lessons)),
104
+ need: "укажи lessons — путь или адрес журнала, где каждый инцидент даёт вывод",
105
+ gives: "проект учится: одна и та же шишка не набивается дважды",
106
+ },
107
+ ];
108
+
109
+ let reached = -1;
110
+ for (const s of steps) {
111
+ if (!s.ok) break;
112
+ reached = s.level;
113
+ }
114
+ return { reached, steps };
115
+ }
116
+
117
+ // Вписать гейт в манифест, не тронув комментарии: правим текст, а не пересобираем YAML.
118
+ function manifestWithGate(text, slug, cmd) {
119
+ const lines = text.split("\n");
120
+ const entry = ` ${slug}: "${cmd}"`;
121
+
122
+ const gi = lines.findIndex((l) => /^gates:\s*$/.test(l));
123
+ if (gi === -1) return { text: null, why: "в .aqk.yml нет блока gates:" };
124
+ if (lines.some((l) => new RegExp(`^\\s+${slug}:`).test(l))) return { text: null, why: "уже объявлен" };
125
+
126
+ let last = gi;
127
+ for (let i = gi + 1; i < lines.length; i++) {
128
+ if (/^\s+\S/.test(lines[i])) last = i;
129
+ else if (lines[i].trim() === "" || lines[i].startsWith("#")) continue;
130
+ else break;
131
+ }
132
+ lines.splice(last + 1, 0, entry);
133
+
134
+ let out = lines.join("\n");
135
+ // Образцы теперь есть — ступень AQK-2 требует, чтобы поле на них указывало.
136
+ out = out.replace(/^samples:\s*""\s*$/m, `samples: ${PROJECT_GATES}`);
137
+ return { text: out, why: null };
138
+ }
139
+
140
+ export { parseManifest, readManifest, assessLevel, manifestWithGate };