agent-quality-kit 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +45 -2
  2. package/README.ru.md +45 -2
  3. package/kit/gates/_skip.sh +61 -1
  4. package/kit/gates/ci-not-hijackable/README.md +56 -0
  5. package/kit/gates/ci-not-hijackable/check.sh +73 -0
  6. package/kit/gates/ci-not-hijackable/gate.yml +19 -0
  7. package/kit/gates/ci-not-hijackable/green/.github/workflows/triage.yml +19 -0
  8. package/kit/gates/ci-not-hijackable/red/.github/workflows/triage.yml +18 -0
  9. package/kit/gates/color-from-token/check.sh +6 -2
  10. package/kit/gates/color-from-token/green/Button.tsx +2 -0
  11. package/kit/gates/complexity-limit/check.sh +6 -7
  12. package/kit/gates/duplicate-code/check.sh +5 -1
  13. package/kit/gates/entry-links-exist/check.sh +4 -1
  14. package/kit/gates/entry-links-exist/green/AGENTS.md +2 -0
  15. package/kit/gates/file-size-limit/check.sh +1 -1
  16. package/kit/gates/secrets-not-in-code/check.sh +16 -3
  17. package/kit/gates/secrets-not-in-code/green/testdata/certificate/key.pem +3 -0
  18. package/kit/gates/todo-without-task/check.sh +1 -1
  19. package/kit/gates/todo-without-task/green/app.py +1 -0
  20. package/llms.txt +22 -1
  21. package/package.json +4 -1
  22. package/tool/commands/context.mjs +260 -0
  23. package/tool/commands/doctor.mjs +25 -18
  24. package/tool/commands/learn.mjs +159 -0
  25. package/tool/commands/project.mjs +1 -0
  26. package/tool/commands/report.mjs +33 -1
  27. package/tool/i18n/en-docs.mjs +85 -1
  28. package/tool/i18n/en.mjs +21 -36
  29. package/tool/i18n/ru-docs.mjs +87 -1
  30. package/tool/i18n/ru.mjs +21 -36
  31. package/tool/lib/core.mjs +30 -1
  32. package/tool/lib/evidence.mjs +124 -0
  33. package/tool/lib/manifest.mjs +29 -2
  34. package/tool/lib/prove.mjs +13 -1
  35. package/tool/lib/scope.mjs +10 -1
  36. package/tool/lib/templates.mjs +1 -0
  37. package/tool/program.mjs +19 -23
  38. package/tool/selfcheck/smoke.sh +242 -2
  39. package/tool/selfcheck/units-context.mjs +186 -0
  40. package/tool/selfcheck/units-evidence.mjs +83 -0
  41. package/tool/selfcheck/units-learn.mjs +88 -0
  42. package/tool/selfcheck/units-level.mjs +65 -3
  43. package/tool/selfcheck/units.mjs +1 -0
@@ -7,6 +7,90 @@
7
7
  // the second for one second in a terminal.
8
8
 
9
9
  const enDocs = {
10
+ // THE STATE BLOCK — the one text of ours whose reader is a machine, not a person.
11
+ // It goes into the agent's context via a SessionStart hook, so it is written as claims of
12
+ // fact: no politeness, no preamble, every line either a fact or an honest "unknown".
13
+ context: {
14
+ title: "AQK — the state of this repository right now. Only what a machine computed; where it\ndoes not know, it says \"unknown\" — which is not the same as \"fine\".",
15
+ level: (r, top, missing) =>
16
+ `Level: AQK-${r} of ${top}.` + (missing ? ` AQK-${r + 1} is missing: ${missing}.` : ""),
17
+ levelUnknown: "Level: not computed — there is no .aqk.yml here. The standard is not set up.",
18
+ rules: (total, machine, human) =>
19
+ `Rules in the entry point: ${total}. Held by a machine ${machine}, by a human ${human}.`,
20
+ rulesNobody: "A rule held by a human is held by nobody: no machine checks it.",
21
+ runNone:
22
+ "No run has been made — which checks are red is UNKNOWN. This is not \"clean\": `aqk doctor --run`.",
23
+ runStale: (when) =>
24
+ `The last run ${when} is OLDER than the last commit — it describes different code.`,
25
+ runClean: (when) => `Last run ${when} — nothing red.`,
26
+ runRed: (when, names) => `Last run ${when} — RED: ${names}.`,
27
+ andMore: (n) => `and ${n} more`,
28
+ skipped: (n) => `Not run: ${n} — the tool is absent on this machine, their state is unknown.`,
29
+ ratchets: (list) => `Ratchets: ${list}. The list may only get shorter, never longer.`,
30
+ where: (entry) => `The rulebook: ${entry}. What proves a diff: \`aqk report --since main\`.`,
31
+ mapTitle: "WHAT THIS TOOL CAN DO. The full list of commands — not a retelling, the same list\nthe help is built from:",
32
+ rulesTitle: (e) => `THE RULEBOOK OF THIS PROJECT (${e}) — verbatim, in full. This is not an invitation\nto read it: it is already here.`,
33
+ hookAlready: (p) => `the hook is already in ${p} — changing nothing.`,
34
+ hookAdded: (p) => `SessionStart hook appended to ${p}:`,
35
+ hookCreated: (p) => `created ${p} with a SessionStart hook:`,
36
+ hookBadJson: (p) => `${p} does not parse as JSON — leaving it alone. Fix it and retry.`,
37
+ hookWhat:
38
+ "the project state now reaches the agent's context before its first action, not at its discretion.",
39
+ },
40
+ // Форма отчёта переехала сюда из терминального каталога 2026-09-08: это текст, который
41
+ // программа ПИШЕТ в .aqk/report.md, а не говорит в терминал. Повод — тот же гейт
42
+ // file-size-limit, что развёл эти файлы в первый раз: терминальный каталог снова перерос
43
+ // 500 строк. Шов по смыслу, а не пополам.
44
+ report2: {
45
+ evidenceNamed: (who) => `named by: ${who}`,
46
+ evidenceSilent: (n) => `${n} check${n === 1 ? "" : "s"} walked past it and said nothing`,
47
+ evidenceTitle: "What proves this diff",
48
+ evidenceUncovered: "no check named this file",
49
+ evidenceBase: "base of comparison",
50
+ evidenceHash: "evidence fingerprint",
51
+ evidenceBadRef: (r) => `ref "${r}" could not be resolved — coverage not computed`,
52
+ evidenceNoFiles: (r) => `no code files in the diff against "${r}" — nothing to prove`,
53
+ evidenceWarn:
54
+ "◻️ means a check walked the directory and said nothing. \"Scanned and clean\" and " +
55
+ "\"never looked\" are indistinguishable from the output, and neither may be passed off " +
56
+ "as the other. " +
57
+ "The fingerprint covers the base, the set of commands and the contents of the files. " +
58
+ "Change any of them and this report is stale, so \"ran it, then edited three more files\" " +
59
+ "stops being indistinguishable from \"ran it\". Mechanism taken from donecheck (MIT).",
60
+ title: "AQK report",
61
+ noManifest: (cmd) => `No .aqk.yml — nothing to report on. Start with ${cmd}`,
62
+ level: "Level",
63
+ holdsTitle: "What a machine holds (from a run, not from the manifest)",
64
+ nothingRuns: "⬜ no gate is declared",
65
+ native: (prog) => `native recipe: ${prog}`,
66
+ portable: "portable check",
67
+ weakerTitle: "Weaker than it could be",
68
+ weaker: (progs) => `${progs} is available on this system, but the gate uses the portable check — it catches less`,
69
+ missingTitle: "What is missing",
70
+ nothingMissing: "✅ every applicable entry is installed",
71
+ needsTool: (prog) => `needs ${prog} — not on this system`,
72
+ notInstalled: "applicable, but not installed",
73
+ hiddenTitle: "Not applicable to this repository",
74
+ readTitle: "What the kit told you to read",
75
+ readWarn:
76
+ "The mark only means the file is on disk. Whether it was read, the machine does not know " +
77
+ "and does not pretend to: that is answered by whoever is reporting.",
78
+ ignoreTitle: "What .aqkignore hides",
79
+ ignoreNone: "no .aqkignore file — nothing is hidden",
80
+ ignoreWarn:
81
+ "Hiding things silently is the same class as a silent gate: the gates do not look at these " +
82
+ "paths at all. A line here means there is no protection there, and will not be.",
83
+ whyTitle: "Why this matters — briefly",
84
+ whyNothing: "nothing to add: everything applicable is in place",
85
+ saved: (path) => `Saved: ${path}`,
86
+ docs: {
87
+ baseline: "the minimum a project needs, independent of language",
88
+ readyMade: "the map of off-the-shelf rules: look for a ready one before writing your own",
89
+ rulesGeneral: "general working rules",
90
+ rulesTesting: "rules about tests",
91
+ rulesSecurity: "rules about security",
92
+ },
93
+ },
10
94
  manifestDoc: {
11
95
  head: [
12
96
  "# .aqk.yml — the Agent Quality Kit manifest",
@@ -15,7 +99,7 @@ const enDocs = {
15
99
  "# and that is honest: filling it with placeholders is pointless, files are checked, not words.",
16
100
  ],
17
101
  entry: "# AQK-0 — what the agent reads first.",
18
- rules: "# AQK-1 — where the standards are and which checks are mandatory.",
102
+ rules: "# AQK-1 — where the standards are and which checks are mandatory. docs — where the\n# guides are: doctor reads both, so the kit may live anywhere you like.",
19
103
  gates: [
20
104
  " # name: a command returning 0 or non-zero. An empty declaration protects nothing and is",
21
105
  ' # rejected by the "a declared gate runs" check — hence examples here, not placeholders.',
package/tool/i18n/en.mjs CHANGED
@@ -8,6 +8,18 @@ import { enDocs } from "./en-docs.mjs";
8
8
  import { templates } from "./templates-en.mjs";
9
9
 
10
10
  export const en = {
11
+ learn: {
12
+ title: "Said out loud, never written down",
13
+ noLogs: (p) => `no logs for this project: ${p}\n The command reads Claude Code transcripts on this machine. Empty means nobody worked here.`,
14
+ counted: (s, typed, said, fresh) =>
15
+ `sessions: ${s} · typed by a human: ${typed} · looks like an instruction: ${said} · not in the entry point: ${fresh}`,
16
+ nothing: "everything that looks like a rule is already in the entry point",
17
+ andMore: (n) => `… and ${n} more`,
18
+ warn:
19
+ "These are CANDIDATES, not findings: measured on 1619 messages, the markers returned 79, and " +
20
+ "about half of those are real rules. The human decides. Nothing was written to disk — the " +
21
+ "command reads transcripts and prints to the terminal only.",
22
+ },
11
23
  ...enDocs,
12
24
  templates,
13
25
  help: {
@@ -27,13 +39,20 @@ export const en = {
27
39
  new: "scaffold your own gate for the catalogue",
28
40
  note: "record a lesson in the shared bruise journal",
29
41
  blob: "assemble the guides into a single GOD_AI.md",
30
- report: "the mandatory report form: what is in place, what is not, what was not read",
42
+ learn: "rule candidates from local transcripts: said out loud, never written down",
43
+ context: "the project state in one block — for an agent's context, not for reading",
44
+ contextInstall: "the same in full — the map and the rulebook — installed as a hook",
45
+ report: "the mandatory report form: what is in place, what is not, what was not read; --since <ref> adds what proves the diff",
31
46
  badge: "a level badge for your README — and a check that it does not lie",
32
47
  noInstall: "Without installing: npx agent-quality-kit init",
33
48
  language: "Output language: AQK_LANG=ru (or en), otherwise your system locale",
34
49
  },
35
50
 
36
51
  doctor: {
52
+ baselineClash: (f) =>
53
+ `"--baseline" is an inspection, not a run: it always exits 0, so together with "${f}" it\n` +
54
+ " gives you a pipeline that cannot go red.\n" +
55
+ " fix: split it in two — \"doctor --baseline\" and \"doctor --run --min 1\".",
37
56
  docsKit: "guides — the originals live here, not a copy",
38
57
  docs: "guides",
39
58
  rulesKit: "standards — the originals live here, not a copy",
@@ -341,6 +360,7 @@ export const en = {
341
360
  n4c: ". A gate guards an artefact that exists:",
342
361
  n4d: "a check for code that is not written yet is a dead rule.",
343
362
  burned: (cmd) => `Got burned by something — write it down: ${cmd}`,
363
+ hookHint: (cmd) => `Claude Code: ${cmd} — the project state will reach the agent's context\n by itself, before its first action. Other agents have no hook, and that is not an omission.`,
344
364
  },
345
365
 
346
366
  feedback: {
@@ -446,41 +466,6 @@ export const en = {
446
466
  checkOk: (level, where) => `Badge matches the run: AQK-${level} — ${where}`,
447
467
  },
448
468
 
449
- report2: {
450
- title: "AQK report",
451
- noManifest: (cmd) => `No .aqk.yml — nothing to report on. Start with ${cmd}`,
452
- level: "Level",
453
- holdsTitle: "What a machine holds (from a run, not from the manifest)",
454
- nothingRuns: "⬜ no gate is declared",
455
- native: (prog) => `native recipe: ${prog}`,
456
- portable: "portable check",
457
- weakerTitle: "Weaker than it could be",
458
- weaker: (progs) => `${progs} is available on this system, but the gate uses the portable check — it catches less`,
459
- missingTitle: "What is missing",
460
- nothingMissing: "✅ every applicable entry is installed",
461
- needsTool: (prog) => `needs ${prog} — not on this system`,
462
- notInstalled: "applicable, but not installed",
463
- hiddenTitle: "Not applicable to this repository",
464
- readTitle: "What the kit told you to read",
465
- readWarn:
466
- "The mark only means the file is on disk. Whether it was read, the machine does not know " +
467
- "and does not pretend to: that is answered by whoever is reporting.",
468
- ignoreTitle: "What .aqkignore hides",
469
- ignoreNone: "no .aqkignore file — nothing is hidden",
470
- ignoreWarn:
471
- "Hiding things silently is the same class as a silent gate: the gates do not look at these " +
472
- "paths at all. A line here means there is no protection there, and will not be.",
473
- whyTitle: "Why this matters — briefly",
474
- whyNothing: "nothing to add: everything applicable is in place",
475
- saved: (path) => `Saved: ${path}`,
476
- docs: {
477
- baseline: "the minimum a project needs, independent of language",
478
- readyMade: "the map of off-the-shelf rules: look for a ready one before writing your own",
479
- rulesGeneral: "general working rules",
480
- rulesTesting: "rules about tests",
481
- rulesSecurity: "rules about security",
482
- },
483
- },
484
469
 
485
470
 
486
471
  };
@@ -7,6 +7,92 @@
7
7
  // вторые — он же в терминале, одну секунду.
8
8
 
9
9
  const ruDocs = {
10
+ // БЛОК СОСТОЯНИЯ — единственный наш текст, у которого читатель машина, а не человек.
11
+ // Уходит в контекст агента хуком SessionStart, поэтому пишется как утверждения о факте:
12
+ // без вежливости, без вводных, каждая строка либо факт, либо честное «неизвестно».
13
+ // Замер 2026-09-08: блок ≈147 токенов против ≈3348 у AGENTS.md — и несёт то, чего в файле
14
+ // нет и быть не может, потому что меняется каждый день.
15
+ context: {
16
+ title: "AQK — состояние этого репозитория на сейчас. Ниже только вычисленное машиной; где\nмашина не знает — сказано «неизвестно», и это не «всё хорошо».",
17
+ level: (r, top, missing) =>
18
+ `Уровень: AQK-${r} из ${top}.` + (missing ? ` До AQK-${r + 1} не хватает: ${missing}.` : ""),
19
+ levelUnknown: "Уровень: не вычислен — в репозитории нет .aqk.yml. Стандарт здесь не заведён.",
20
+ rules: (total, machine, human) =>
21
+ `Правил в точке входа: ${total}. Держит машина ${machine}, держит человек ${human}.`,
22
+ rulesNobody: "Правило, которое держит человек, не держит никто: машина его не проверяет.",
23
+ runNone:
24
+ "Прогон не делался — какие проверки красные, НЕИЗВЕСТНО. Это не «чисто»: `aqk doctor --run`.",
25
+ runStale: (when) =>
26
+ `Последний прогон ${when} СТАРЕЕ последнего коммита — он описывает не тот код, что здесь.`,
27
+ runClean: (when) => `Последний прогон ${when} — красных нет.`,
28
+ runRed: (when, names) => `Последний прогон ${when} — КРАСНЫЕ: ${names}.`,
29
+ andMore: (n) => `и ещё ${n}`,
30
+ skipped: (n) => `Не запускались: ${n} — инструмента нет на этой машине, их состояние неизвестно.`,
31
+ ratchets: (list) => `Храповики: ${list}. Список может только укорачиваться, увеличивать нельзя.`,
32
+ where: (entry) => `Свод правил: ${entry}. Чем доказан диф: \`aqk report --since main\`.`,
33
+ mapTitle: "ЧТО УМЕЕТ ЭТОТ ИНСТРУМЕНТ. Полный список команд — не пересказ, а тот же список,\nиз которого собрана справка:",
34
+ rulesTitle: (e) => `СВОД ПРАВИЛ ЭТОГО ПРОЕКТА (${e}) — дословно, целиком. Это не приглашение\nпрочитать: он уже здесь.`,
35
+ hookAlready: (p) => `хук уже стоит в ${p} — ничего не меняю.`,
36
+ hookAdded: (p) => `хук SessionStart дописан в ${p}:`,
37
+ hookCreated: (p) => `создан ${p} с хуком SessionStart:`,
38
+ hookBadJson: (p) => `${p} не разбирается как JSON — не трогаю его. Почини файл и повтори.`,
39
+ hookWhat:
40
+ "теперь состояние проекта попадает в контекст агента до первого действия, а не по его желанию.",
41
+ },
42
+ // Форма отчёта переехала сюда из терминального каталога 2026-09-08: это текст, который
43
+ // программа ПИШЕТ в .aqk/report.md, а не говорит в терминал. Повод — тот же гейт
44
+ // file-size-limit, что развёл эти файлы в первый раз: терминальный каталог снова перерос
45
+ // 500 строк. Шов по смыслу, а не пополам.
46
+ report2: {
47
+ evidenceNamed: (who) => `назван проверками: ${who}`,
48
+ evidenceSilent: (n) => `${n} ${n === 1 ? "проверка прошла" : "проверок прошли"} мимо и промолчали`,
49
+ evidenceTitle: "Чем доказан этот диф",
50
+ evidenceUncovered: "ни одна проверка не назвала этот файл",
51
+ evidenceBase: "база сравнения",
52
+ evidenceHash: "отпечаток доказательства",
53
+ evidenceBadRef: (r) => `ссылка «${r}» не разобрана — покрытие не считано`,
54
+ evidenceNoFiles: (r) => `файлов кода в дифе с «${r}» нет — доказывать нечего`,
55
+ evidenceWarn:
56
+ "◻️ означает: проверка обошла каталог и ничего не сказала. «Просмотрен и чист» и «не " +
57
+ "просмотрен вовсе» по выводу неразличимы — и выдавать одно за другое нельзя ни в ту, ни " +
58
+ "в другую сторону. " +
59
+ "Отпечаток считается по базе, набору команд и содержимому файлов. Изменилось что-то из " +
60
+ "этого — отчёт устарел, и «прогнал, потом поправил ещё три файла» перестаёт быть " +
61
+ "неотличимым от «прогнал». Механизм взят у donecheck (MIT).",
62
+ title: "Отчёт AQK",
63
+ noManifest: (cmd) => `Нет .aqk.yml — отчитываться не о чем. Начни с ${cmd}`,
64
+ level: "Уровень",
65
+ holdsTitle: "Что держит машина (прогон, а не манифест)",
66
+ nothingRuns: "⬜ ни один гейт не объявлен",
67
+ native: (prog) => `родной рецепт: ${prog}`,
68
+ portable: "переносимая проверка",
69
+ weakerTitle: "Стоит слабее возможного",
70
+ weaker: (progs) => `в системе есть ${progs}, но гейт стоит на переносимой проверке — она ловит меньше`,
71
+ missingTitle: "Чего нет",
72
+ nothingMissing: "✅ все применимые записи поставлены",
73
+ needsTool: (prog) => `нужен ${prog} — его нет в системе`,
74
+ notInstalled: "применимо, но не поставлено",
75
+ hiddenTitle: "Не применимо к этому репозиторию",
76
+ readTitle: "Что комплект велел прочитать",
77
+ readWarn:
78
+ "Значок означает только наличие файла на диске. Прочитан он или нет, машина не знает и " +
79
+ "не притворяется, что знает: это отвечает тот, кто отчитывается.",
80
+ ignoreTitle: "Что спрятано .aqkignore",
81
+ ignoreNone: "файла .aqkignore нет — ничего не спрятано",
82
+ ignoreWarn:
83
+ "Скрытое молча — тот же класс, что молчащий гейт: гейты по этим путям не смотрят вовсе. " +
84
+ "Строка здесь означает, что защиты там нет и не будет.",
85
+ whyTitle: "Зачем это нужно — коротко",
86
+ whyNothing: "нечего добавлять: применимое уже стоит",
87
+ saved: (path) => `Сохранено: ${path}`,
88
+ docs: {
89
+ baseline: "обязательный минимум проекта, без привязки к языку",
90
+ readyMade: "карта готовых правил: сперва ищи готовое, потом пиши своё",
91
+ rulesGeneral: "общие правила работы",
92
+ rulesTesting: "правила про тесты",
93
+ rulesSecurity: "правила про безопасность",
94
+ },
95
+ },
10
96
  manifestDoc: {
11
97
  head: [
12
98
  "# .aqk.yml — манифест Agent Quality Kit",
@@ -15,7 +101,7 @@ const ruDocs = {
15
101
  "# и это честно: заполнять заглушками бессмысленно, проверяются файлы, а не слова.",
16
102
  ],
17
103
  entry: "# AQK-0 — что агент читает первым.",
18
- rules: "# AQK-1 — где стандарты и какие проверки обязательны.",
104
+ rules: "# AQK-1 — где стандарты и какие проверки обязательны. docs — где методички:\n# оба пути читает doctor, так что комплект можно разложить куда угодно.",
19
105
  gates: [
20
106
  " # Имя: команда, возвращающая 0 или не 0. Пустое объявление защиты не даёт и бракуется",
21
107
  " # проверкой «объявленный гейт запускается» — поэтому здесь примеры, а не заготовки.",
package/tool/i18n/ru.mjs CHANGED
@@ -9,6 +9,18 @@ import { ruDocs } from "./ru-docs.mjs";
9
9
  import { templates } from "./templates-ru.mjs";
10
10
 
11
11
  export const ru = {
12
+ learn: {
13
+ title: "Сказано вслух и не записано",
14
+ noLogs: (p) => `логов этого проекта нет: ${p}\n Команда читает переписку Claude Code на этой машине. Пусто — значит здесь не работали.`,
15
+ counted: (s, typed, said, fresh) =>
16
+ `сессий: ${s} · напечатано человеком: ${typed} · похоже на наставление: ${said} · нет в точке входа: ${fresh}`,
17
+ nothing: "всё, что похоже на правило, уже стоит в точке входа",
18
+ andMore: (n) => `… и ещё ${n}`,
19
+ warn:
20
+ "Это КАНДИДАТЫ, а не находки: на замере из 1619 реплик маркеры дали 79, и настоящих правил " +
21
+ "среди них примерно половина. Решает человек. Ничего не записано на диск — команда читает " +
22
+ "переписку и печатает только в терминал.",
23
+ },
12
24
  ...ruDocs,
13
25
  templates,
14
26
  help: {
@@ -28,13 +40,20 @@ export const ru = {
28
40
  new: "заготовка своего гейта для каталога",
29
41
  note: "записать урок в общий журнал шишек",
30
42
  blob: "собрать методички в один файл GOD_AI.md",
31
- report: "обязательная форма отчёта: что стоит, что нет, что не прочитано",
43
+ learn: "кандидаты в правила из локальной переписки: сказано вслух и не записано",
44
+ context: "состояние проекта одним блоком — для контекста агента, а не для чтения",
45
+ contextInstall: "то же самое, но целиком — карта и свод правил — и хуком в контекст",
46
+ report: "обязательная форма отчёта: что стоит, что нет, что не прочитано; --since <ссылка> — ещё и чем доказан диф",
32
47
  badge: "значок уровня для README — и проверка, что он не врёт",
33
48
  noInstall: "Без установки: npx agent-quality-kit init",
34
49
  language: "Язык вывода: AQK_LANG=en (или ru), иначе по системной локали",
35
50
  },
36
51
 
37
52
  doctor: {
53
+ baselineClash: (f) =>
54
+ `«--baseline» — осмотр, а не прогон: он выходит с нулём всегда, и вместе с «${f}» даёт\n` +
55
+ " конвейер, который не может покраснеть.\n" +
56
+ " почини: раздели на две команды — «doctor --baseline» и «doctor --run --min 1».",
38
57
  docsKit: "методички — здесь оригиналы, а не копия",
39
58
  docs: "методички",
40
59
  rulesKit: "стандарты — здесь оригиналы, а не копия",
@@ -347,6 +366,7 @@ export const ru = {
347
366
  n4c: ". Гейт стережёт существующий артефакт:",
348
367
  n4d: "проверка на код, которого ещё нет, — мёртвое правило.",
349
368
  burned: (cmd) => `Обжёгся на чём-то — запиши: ${cmd}`,
369
+ hookHint: (cmd) => `Claude Code: ${cmd} — состояние проекта будет попадать в контекст агента\n само, до первого действия. Для других агентов хука нет, и это не недоделка.`,
350
370
  },
351
371
 
352
372
  feedback: {
@@ -452,41 +472,6 @@ export const ru = {
452
472
  checkOk: (level, where) => `Значок совпал с прогоном: AQK-${level} — ${where}`,
453
473
  },
454
474
 
455
- report2: {
456
- title: "Отчёт AQK",
457
- noManifest: (cmd) => `Нет .aqk.yml — отчитываться не о чем. Начни с ${cmd}`,
458
- level: "Уровень",
459
- holdsTitle: "Что держит машина (прогон, а не манифест)",
460
- nothingRuns: "⬜ ни один гейт не объявлен",
461
- native: (prog) => `родной рецепт: ${prog}`,
462
- portable: "переносимая проверка",
463
- weakerTitle: "Стоит слабее возможного",
464
- weaker: (progs) => `в системе есть ${progs}, но гейт стоит на переносимой проверке — она ловит меньше`,
465
- missingTitle: "Чего нет",
466
- nothingMissing: "✅ все применимые записи поставлены",
467
- needsTool: (prog) => `нужен ${prog} — его нет в системе`,
468
- notInstalled: "применимо, но не поставлено",
469
- hiddenTitle: "Не применимо к этому репозиторию",
470
- readTitle: "Что комплект велел прочитать",
471
- readWarn:
472
- "Значок означает только наличие файла на диске. Прочитан он или нет, машина не знает и " +
473
- "не притворяется, что знает: это отвечает тот, кто отчитывается.",
474
- ignoreTitle: "Что спрятано .aqkignore",
475
- ignoreNone: "файла .aqkignore нет — ничего не спрятано",
476
- ignoreWarn:
477
- "Скрытое молча — тот же класс, что молчащий гейт: гейты по этим путям не смотрят вовсе. " +
478
- "Строка здесь означает, что защиты там нет и не будет.",
479
- whyTitle: "Зачем это нужно — коротко",
480
- whyNothing: "нечего добавлять: применимое уже стоит",
481
- saved: (path) => `Сохранено: ${path}`,
482
- docs: {
483
- baseline: "обязательный минимум проекта, без привязки к языку",
484
- readyMade: "карта готовых правил: сперва ищи готовое, потом пиши своё",
485
- rulesGeneral: "общие правила работы",
486
- rulesTesting: "правила про тесты",
487
- rulesSecurity: "правила про безопасность",
488
- },
489
- },
490
475
 
491
476
 
492
477
  };
package/tool/lib/core.mjs CHANGED
@@ -57,6 +57,35 @@ function selfCmd() {
57
57
 
58
58
  const SELF = selfCmd();
59
59
 
60
+ // КАРТА КОМАНД — ОДИН список на справку в терминале и на карту, уходящую в контекст агента.
61
+ // Два списка рядом через месяц врут по-разному, и непонятно, какой настоящий: это записано
62
+ // у нас в CLAUDE.md про своды правил и верно здесь буквально так же. Модульная проверка
63
+ // сторожит, что список не отстал от диспетчера в program.mjs.
64
+ function commandRows(L) {
65
+ const h = L.help;
66
+ return [
67
+ { name: "init", args: "", text: h.init },
68
+ { name: "init", args: "--force", text: h.initForce },
69
+ { name: "start", args: "", text: h.start },
70
+ { name: "doctor", args: "", text: h.doctor },
71
+ { name: "doctor", args: "--run", text: h.doctorRun },
72
+ { name: "doctor", args: "--run --since main", text: h.doctorSince },
73
+ { name: "prove", args: "", text: h.prove },
74
+ { name: "add", args: h.name, text: h.add },
75
+ { name: "find", args: '"…"', text: h.find },
76
+ { name: "why", args: '"…"', text: h.why },
77
+ { name: "ratchet", args: h.name, text: h.ratchet },
78
+ { name: "new", args: h.name, text: h.new },
79
+ { name: "note", args: '"…"', text: h.note },
80
+ { name: "blob", args: "", text: h.blob },
81
+ { name: "report", args: "", text: h.report },
82
+ { name: "learn", args: "", text: h.learn },
83
+ { name: "context", args: "", text: h.context },
84
+ { name: "context", args: "--full --install", text: h.contextInstall },
85
+ { name: "badge", args: "", text: h.badge },
86
+ ];
87
+ }
88
+
60
89
  function die(msg) {
61
90
  console.error(c.red(msg));
62
91
  process.exit(1);
@@ -114,5 +143,5 @@ export {
114
143
  copyDir, writeIfAbsent,
115
144
  PKG_ROOT, CWD, DOCS_SRC, RULES_SRC, TARGET_DIR, docPath,
116
145
  MANIFEST, GATES_SRC, PROJECT_GATES, RATCHET_DIR, RATCHET_LIB,
117
- SELF, REPO_URL, c, exists, die, FEEDBACK_MARK,
146
+ SELF, REPO_URL, c, exists, die, FEEDBACK_MARK, commandRows,
118
147
  };
@@ -0,0 +1,124 @@
1
+ // tool/lib/evidence.mjs — привязка доказательства к дифу.
2
+ //
3
+ // ЗАЧЕМ. «Готово = доказано» — центральное правило свода, и оно единственное из четырнадцати,
4
+ // за которым не следила машина: в `AGENTS.md` его сторожем честно записан человек. Цена этого
5
+ // измерена 2026-09-08 прогоном чужого инструмента по восьми нашим коммитам: `publish.yml`
6
+ // менялся и не был назван ни одной командой проверки — и именно он оказался сломан. Коммит при
7
+ // этом говорил «прогон: units 51, smoke 81». Утверждение было правдой и не относилось к делу.
8
+ //
9
+ // Механизм взят у [donecheck](https://github.com/AtharvaMaik/donecheck) (MIT, ноль
10
+ // зависимостей). Своего разбора команд не пишем — берём идею, а не код: donecheck САМ запускает
11
+ // команды проверки, а `doctor --run` их уже запустил, и обёртка означала бы двойной прогон
12
+ // всего набора гейтов. Здесь данные уже есть.
13
+ //
14
+ // ОДНО ОТЛИЧИЕ, И ОНО НАМЕРЕННОЕ. donecheck считает файл покрытым и по голому имени. В нашем
15
+ // каталоге имя `check.sh` носят два десятка разных файлов: мягкое сравнение объявило бы
16
+ // покрытым каждый из них, стоит любому гейту напечатать это слово. Ошибка в сторону «покрыто»
17
+ // — это тишина, а тишина здесь и есть предмет спора. Сравниваем строго, по полному пути.
18
+
19
+ import { createHash } from "node:crypto";
20
+ import { readFileSync } from "node:fs";
21
+ import { join } from "node:path";
22
+ import { changedFiles, pathsIn, normPath } from "./scope.mjs";
23
+
24
+ // Расширения, для которых «никто не проверил» — утверждение о деле. Документ не проверяется
25
+ // прогоном по своей природе, и требовать этого значило бы красить каждую правку README.
26
+ const CODE_EXT = new Set([
27
+ "c", "cjs", "cpp", "cs", "css", "go", "h", "java", "js", "json", "jsx", "kt", "mjs", "mts",
28
+ "php", "pl", "py", "rb", "rs", "scala", "sh", "sql", "swift", "toml", "ts", "tsx", "vue",
29
+ "yaml", "yml",
30
+ ]);
31
+
32
+ // Образцы гейтов исключены по той же причине, по какой их исключает каждая сканирующая
33
+ // проверка каталога: они существуют, чтобы быть неправильными, и командой проверки покрыты
34
+ // быть не могут. Без этого замер дал восемь ложных находок из шестнадцати.
35
+ function isSample(p) {
36
+ return /(^|\/)gates\/[^/]+\/(red|green)(\/|$)/.test(p);
37
+ }
38
+
39
+ function ext(p) {
40
+ const m = /\.([A-Za-z0-9]+)$/.exec(p);
41
+ return m ? m[1].toLowerCase() : "";
42
+ }
43
+
44
+ // Файлы кода, которые внёс диф. null — если ссылка не разобрана: «сравнили не с тем» обязано
45
+ // отличаться от «изменений нет».
46
+ function changedCode(ref, cwd) {
47
+ const all = changedFiles(ref, cwd);
48
+ if (all === null) return null;
49
+ return [...all].map(normPath).filter((p) => CODE_EXT.has(ext(p)) && !isSample(p)).sort();
50
+ }
51
+
52
+ // Куда гейт был НАПРАВЛЕН: цели из его команды. Проверка, обошедшая каталог и не нашедшая
53
+ // ничего, файла не назовёт — и «просмотрен и чист» стало бы неотличимо от «никто не смотрел».
54
+ // Различить это по выводу нельзя, а по команде можно: она говорит, куда гейт направляли.
55
+ function targetsOf(cmd, isDir) {
56
+ const out = [];
57
+ for (const tok of String(cmd || "").split(/\s+/)) {
58
+ if (tok.startsWith("-")) continue;
59
+ const t = normPath(tok);
60
+ if (tok === "." || tok === "./") { out.push(""); continue; }
61
+ if (t && !t.includes("*") && isDir(t)) out.push(t.replace(/\/$/, ""));
62
+ }
63
+ return out;
64
+ }
65
+
66
+ // Три состояния, а не два, и это главное в этой функции.
67
+ //
68
+ // named — гейт напечатал путь файла: он его точно видел и что-то о нём сказал;
69
+ // silent — гейт был направлен в каталог с этим файлом, но ничего не напечатал. Просмотрен и
70
+ // чист либо не просмотрен вовсе — по выводу это неразличимо, и выдавать одно за
71
+ // другое нельзя ни в ту, ни в другую сторону;
72
+ // none — ни одна команда даже не была направлена туда, где файл лежит.
73
+ //
74
+ // Двух состояний хватило ровно до первого прогона: `tool/commands/doctor.mjs` попал в «никем
75
+ // не проверен», хотя его обходят пять проверок — они просто промолчали, потому что нашли
76
+ // чисто. Замеряно 2026-09-08.
77
+ function coverage(files, results, isDir = () => false) {
78
+ const seen = results.map((r) => ({
79
+ name: r.name,
80
+ paths: pathsIn(`${r.cmd || ""}\n${r.out || ""}`),
81
+ targets: targetsOf(r.cmd, isDir),
82
+ }));
83
+ const covered = new Map();
84
+ const silent = new Map();
85
+ const uncovered = [];
86
+ for (const raw of files) {
87
+ const f = normPath(raw);
88
+ const by = seen.filter((n) => n.paths.has(f)).map((n) => n.name);
89
+ if (by.length) { covered.set(raw, by); continue; }
90
+ const aimed = seen
91
+ .filter((n) => n.targets.some((t) => t === "" || f === t || f.startsWith(`${t}/`)))
92
+ .map((n) => n.name);
93
+ if (aimed.length) silent.set(raw, aimed);
94
+ else uncovered.push(raw);
95
+ }
96
+ return { covered, silent, uncovered };
97
+ }
98
+
99
+ // Отпечаток того, о чём отчитывается расписка: базовый коммит, набор команд, содержимое файлов.
100
+ // Изменилось что угодно из этого — расписка устарела, и «прогнал, потом поправил ещё три файла»
101
+ // перестаёт быть неотличимым от «прогнал».
102
+ //
103
+ // files — пары [путь, содержимое]. Содержимое передаётся, а не читается здесь: вызывающий уже
104
+ // держит файлы в руках, а функция без ввода-вывода проверяется модульно.
105
+ function evidenceHash(base, commands, files) {
106
+ const h = createHash("sha256");
107
+ h.update(`base:${base || ""}\0`);
108
+ for (const c of commands) h.update(`cmd:${c?.cmd ?? c}\0`);
109
+ for (const [p, body] of [...files].sort((a, b) => String(a[0]).localeCompare(String(b[0])))) {
110
+ h.update(`path:${normPath(p)}\0`);
111
+ h.update(body === null || body === undefined ? "<missing>" : String(body));
112
+ h.update("\0");
113
+ }
114
+ return h.digest("hex");
115
+ }
116
+
117
+ // Читает содержимое для отпечатка. Отсутствующий файл — тоже факт: удаление меняет расписку.
118
+ function readForHash(files, cwd) {
119
+ return files.map((p) => {
120
+ try { return [p, readFileSync(join(cwd, p), "utf8")]; } catch { return [p, null]; }
121
+ });
122
+ }
123
+
124
+ export { changedCode, coverage, evidenceHash, readForHash };
@@ -79,7 +79,34 @@ function parseManifest(text) {
79
79
  // Список обязан совпадать с тем, что программа РЕАЛЬНО читает (`man?.<поле>` в tool/):
80
80
  // лишнее имя здесь молча узаконивает поле, которое ни на что не влияет, — та же тишина,
81
81
  // только с другой стороны. Сверено обходом: aqk, entry, rules, gates, samples, ratchets, lessons.
82
- const KNOWN_KEYS = ["aqk", "entry", "rules", "gates", "samples", "ratchets", "lessons", "advisory"];
82
+ const KNOWN_KEYS = ["aqk", "entry", "rules", "docs", "gates", "samples", "ratchets", "lessons", "advisory"];
83
+
84
+ // ГДЕ У ПРОЕКТА ЛЕЖИТ РАЗЛОЖЕННЫЙ КОМПЛЕКТ. Список для шапки `doctor`. До 2026-09-08 он был
85
+ // литеральным: `.aqk/rules`, `.aqk/docs`, `AGENTS.md` — независимо от того, что написано в
86
+ // манифесте. Второй пользователь прислал разбор: у него `rules: .temper/rules`, правила на
87
+ // месте, гейт entry-links-exist их видит, СТУПЕНЬ считается по манифесту и берётся — а шапка
88
+ // рисует два креста и советует сделать сделанное. Вывод расходился с собственным вердиктом
89
+ // программы; это хуже, чем просто неверный вывод, потому что оба напечатаны рядом.
90
+ // Поля `docs:` не существовало вовсе: методички было некуда перенести, и крест за них снять
91
+ // было нельзя ничем. Умолчания остаются для тех, кто полей не завёл, — это большинство.
92
+ function layoutChecks(man, inKit) {
93
+ const field = (name, dflt) => {
94
+ const v = man && typeof man === "object" && !Array.isArray(man) ? man[name] : null;
95
+ return typeof v === "string" && v.trim() ? v.trim() : dflt;
96
+ };
97
+ // Точка входа — тоже поле манифеста, и по той же причине: проект на `CLAUDE.md` получал крест
98
+ // за `AGENTS.md`, которого у него намеренно нет. Класс дефекта один, чинится он один раз.
99
+ const entries = (Array.isArray(man?.entry) ? man.entry : [])
100
+ .filter((e) => typeof e === "string" && e.trim())
101
+ .map((e) => e.trim());
102
+ return [
103
+ [field("docs", inKit ? "kit/docs" : ".aqk/docs"), inKit ? L.doctor.docsKit : L.doctor.docs],
104
+ [field("rules", inKit ? "kit/rules" : ".aqk/rules"), inKit ? L.doctor.rulesKit : L.doctor.rules],
105
+ ...(entries.length ? entries : ["AGENTS.md"]).map((e) => [e, L.doctor.agents]),
106
+ [".gitignore", L.doctor.gitignore],
107
+ [".git", L.doctor.git],
108
+ ];
109
+ }
83
110
 
84
111
  function unknownKeys(man) {
85
112
  if (!man || typeof man !== "object" || Array.isArray(man)) return [];
@@ -215,5 +242,5 @@ function manifestWithGate(text, slug, cmd) {
215
242
 
216
243
  export {
217
244
  parseManifest, readManifest, assessLevel, manifestWithGate, unknownKeys, KNOWN_KEYS,
218
- entryLifecycle, advisorySet,
245
+ entryLifecycle, advisorySet, layoutChecks,
219
246
  };
@@ -63,7 +63,19 @@ function targetIsLast(parts) {
63
63
  return last === "." || last === "./";
64
64
  }
65
65
 
66
- function commandFor(cmd, dir) {
66
+ // Путь уходит в СТРОКУ КОМАНДЫ, а её исполняет `sh` — не Node. Для `sh` обратный слэш это
67
+ // экранирование, а не разделитель: `gates\\x\\red` превращается в `gatesxred`, каталога с таким
68
+ // именем нет, обход молчит, гейт выходит с нулём — и доказательство объявляет ИСПРАВНЫЙ гейт
69
+ // сломанным. Найдено вторым пользователем 2026-09-08 на Windows: `path.join` там даёт `\\`,
70
+ // и падали ровно те записи, что обходят дерево через `find`; на `grep -r` выживали, потому что
71
+ // Windows разбирает слэши сам. Худший из возможных отказов: комплект против «зелёного, потому
72
+ // что ничего не проверялось» сам выдал зелёное за красное и отобрал у проекта ступень.
73
+ // Нормализация стоит ЗДЕСЬ, а не у сборщика пути: это единственная дверь из мира путей Node
74
+ // в мир оболочки, и закрывать её надо один раз, кто бы путь ни собрал.
75
+ const forShell = (p) => String(p).replace(/\\/g, "/");
76
+
77
+ function commandFor(cmd, dirRaw) {
78
+ const dir = forShell(dirRaw);
67
79
  const { parts, nativeAt } = unwrap(cmd);
68
80
  const out = parts.slice();
69
81
  out[out.length - 1] = dir;