@rt-tools/agent-kit 0.15.0 → 0.16.1

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.
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Возраст записей описания прошлого.
3
+ *
4
+ * Каталог набирает по записи на каждую закрытую работу и не отдаёт обратно ничего: папку
5
+ * задачи разбирает паттерн закрытия работы, и запись оттуда переживает всё дерево. Срок
6
+ * назначает дерево ключом настройки; пакет умолчания не даёт — установка новой версии не
7
+ * вправе начать сносить чужой архив, а снимается там разбор просьбы, которого нет больше нигде.
8
+ *
9
+ * Отбор живёт здесь один на двоих: команда чистки снимает по нему, проверка по нему же
10
+ * краснеет. Разойдясь, они говорили бы о каталоге разное, а заметить это нечем — чистка молча
11
+ * оставляла бы то, на что проверка молча не смотрит.
12
+ *
13
+ * Возраст меряется датой последнего коммита файла. Время файла на диске не годится: свежий
14
+ * чекаут делает все записи одновременными, и чистка на чужой машине не сняла бы ни одной.
15
+ * Шапка записи не годится тоже — день слияния стоит в ней не всегда и не всюду одинаково.
16
+ *
17
+ * Запись, которой в истории ещё нет, считается сегодняшней: она приехала этой же веткой и
18
+ * перестоять не могла.
19
+ */
20
+ import { execFileSync } from 'node:child_process';
21
+ import { existsSync, readdirSync } from 'node:fs';
22
+ import { join } from 'node:path';
23
+
24
+ import { CONFIG } from './rt-kit-checks.config.mjs';
25
+
26
+ /** Каталог описания прошлого — без завершающей косой черты: её несёт настройка. */
27
+ export const ARCHIVE_DIR = CONFIG.archiveDir.replace(/\/$/, '');
28
+
29
+ /**
30
+ * Срок хранения записи в сутках. `null` — срок не назначен, и тогда молчат обе стороны:
31
+ * проверка не краснеет, чистка не снимает. Дерево называет своё число ключом настройки.
32
+ */
33
+ export const RETENTION_DAYS = CONFIG.archiveRetentionDays ?? null;
34
+
35
+ /** Сутки в миллисекундах — считать возраст удобнее в них. */
36
+ const DAY_MS = 24 * 60 * 60 * 1000;
37
+
38
+ /**
39
+ * Дата последнего коммита у каждой записи каталога.
40
+ *
41
+ * Один проход по истории вместо вызова на файл: на трёх сотнях записей это разница между
42
+ * секундой и полуминутой. Лог идёт новыми вперёд, поэтому первая встреченная дата файла и есть
43
+ * последняя.
44
+ */
45
+ function lastCommitDates(root) {
46
+ const log = execFileSync('git', ['log', '--format=%cI', '--name-only', '--', ARCHIVE_DIR], {
47
+ cwd: root,
48
+ encoding: 'utf8',
49
+ maxBuffer: 64 * 1024 * 1024,
50
+ });
51
+
52
+ const dates = new Map();
53
+ let current = null;
54
+
55
+ for (const line of log.split('\n')) {
56
+ if (line === '') {
57
+ continue;
58
+ }
59
+
60
+ if (line.startsWith(`${ARCHIVE_DIR}/`)) {
61
+ if (current !== null && !dates.has(line)) {
62
+ dates.set(line, current);
63
+ }
64
+
65
+ continue;
66
+ }
67
+
68
+ current = line;
69
+ }
70
+
71
+ return dates;
72
+ }
73
+
74
+ /**
75
+ * Записи каталога с их возрастом в сутках.
76
+ *
77
+ * @param root Корень дерева.
78
+ * @param now Момент отсчёта — передаётся, чтобы проверка и чистка судили по одному времени.
79
+ * @returns Записи: путь, дата последнего коммита и возраст в сутках.
80
+ */
81
+ export function archiveRecords(root, now = new Date()) {
82
+ if (!existsSync(join(root, ARCHIVE_DIR))) {
83
+ return [];
84
+ }
85
+
86
+ const dates = lastCommitDates(root);
87
+
88
+ return readdirSync(join(root, ARCHIVE_DIR))
89
+ .filter((name) => name.endsWith('.md'))
90
+ .map((name) => {
91
+ const path = `${ARCHIVE_DIR}/${name}`;
92
+ const committed = dates.get(path);
93
+ const ageDays = committed === undefined ? 0 : (now.getTime() - new Date(committed).getTime()) / DAY_MS;
94
+
95
+ return { path, name, committed, ageDays };
96
+ })
97
+ .sort((one, other) => other.ageDays - one.ageDays);
98
+ }
99
+
100
+ /** Записи, перестоявшие срок. Срок не назначен — перестоявших нет ни одной. */
101
+ export function staleRecords(root, now = new Date()) {
102
+ if (RETENTION_DAYS === null) {
103
+ return [];
104
+ }
105
+
106
+ return archiveRecords(root, now).filter((record) => record.ageDays > RETENTION_DAYS);
107
+ }
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Чистка описания прошлого по сроку.
4
+ *
5
+ * Запись живёт назначенный деревом срок, дальше снимается из дерева и остаётся в истории
6
+ * системы контроля версий — достать её оттуда можно по имени файла, оно же единственный
7
+ * указатель каталога.
8
+ *
9
+ * Сухой прогон — умолчание. Команда сносит разбор просьбы — единственную запись слов
10
+ * владельца, — и снос называется явно: `--apply`. Перечень снимаемого печатается в обоих
11
+ * случаях одинаково, чтобы решение принималось по тому же списку, который потом уедет.
12
+ *
13
+ * Срок и отбор берутся у `archive-age.mjs` — того же модуля, по которому краснеет проверка.
14
+ */
15
+ import { execFileSync } from 'node:child_process';
16
+
17
+ import { RETENTION_DAYS, archiveRecords, staleRecords } from './archive-age.mjs';
18
+ import { ROOT } from './rt-kit-checks.config.mjs';
19
+
20
+ if (RETENTION_DAYS === null) {
21
+ console.log('archive-prune: срок хранения описания прошлого деревом не назначен — снимать нечего');
22
+ process.exit(0);
23
+ }
24
+
25
+ const apply = process.argv.includes('--apply');
26
+ const stale = staleRecords(ROOT);
27
+ const total = archiveRecords(ROOT).length;
28
+
29
+ if (stale.length === 0) {
30
+ console.log(`archive-prune: записей ${total}, перестоявших срок в ${RETENTION_DAYS} суток нет`);
31
+ process.exit(0);
32
+ }
33
+
34
+ console.log(`archive-prune: перестояло ${stale.length} из ${total} при сроке в ${RETENTION_DAYS} суток`);
35
+
36
+ for (const record of stale) {
37
+ console.log(` ${record.name} — ${Math.floor(record.ageDays)} суток, последний коммит ${record.committed.slice(0, 10)}`);
38
+ }
39
+
40
+ if (!apply) {
41
+ console.log('\nЭто сухой прогон: не снято ничего. Снос идёт доводом --apply.');
42
+ process.exit(0);
43
+ }
44
+
45
+ execFileSync('git', ['rm', '--quiet', '--', ...stale.map((record) => record.path)], { cwd: ROOT, stdio: 'inherit' });
46
+ console.log(`\nСнято записей: ${stale.length}. Они остаются в истории — найти их можно по имени файла.`);
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Сверка срока хранения описания прошлого.
4
+ *
5
+ * Одной чистки хватает ровно на срок: каталог набирает по записи на каждую закрытую работу, и
6
+ * первая уцелевшая перестаивает снова. Без сверки срок держался бы памятью того, кто помнит
7
+ * про команду чистки, — то есть не держался бы вовсе.
8
+ *
9
+ * Сверка не чистит: снос — решение, а не следствие проверки. Она называет перестоявшие записи
10
+ * и команду, которой их снимают.
11
+ *
12
+ * FAIL-OPEN: срок деревом не назначен — сверять нечего, нулевой код. Умолчания у срока нет
13
+ * намеренно: пакет, назначивший его за дерево, начал бы сносить чужой архив в день установки.
14
+ * Каталога нет — то же самое. Пустой каталог отказом не считается: он означает, что всё снято
15
+ * по сроку.
16
+ *
17
+ * Ненулевой код возврата и перечень перестоявших записей.
18
+ */
19
+ import { ARCHIVE_DIR, RETENTION_DAYS, archiveRecords, staleRecords } from './archive-age.mjs';
20
+ import { ROOT } from './rt-kit-checks.config.mjs';
21
+
22
+ if (RETENTION_DAYS === null) {
23
+ console.log('check-archive-age: срок хранения описания прошлого деревом не назначен — сверять нечего');
24
+ process.exit(0);
25
+ }
26
+
27
+ const total = archiveRecords(ROOT).length;
28
+ const stale = staleRecords(ROOT);
29
+
30
+ if (stale.length > 0) {
31
+ console.error(`check-archive-age: расхождений ${stale.length}`);
32
+
33
+ for (const record of stale) {
34
+ console.error(` ${record.path}: ${Math.floor(record.ageDays)} суток при сроке в ${RETENTION_DAYS}`);
35
+ }
36
+
37
+ console.error(`\nЗапись живёт ${RETENTION_DAYS} суток и снимается: \`node tools/archive-prune.mjs --apply\`.`);
38
+ console.error('Снятая остаётся в истории — найти её можно по имени файла.');
39
+ process.exit(1);
40
+ }
41
+
42
+ console.log(`check-archive-age: записей ${total} в ${ARCHIVE_DIR}, ни одна не перестояла срок в ${RETENTION_DAYS} суток`);
@@ -36,6 +36,13 @@ const DEFAULTS = {
36
36
  docsDir: 'docs',
37
37
  /** Отложенное: про него проверки молчат — оно описывает прошлое, а не дерево. */
38
38
  archiveDir: 'docs/archive/',
39
+ /**
40
+ * Сколько суток живёт запись описания прошлого. `null` — срок не назначен, и тогда молчат
41
+ * обе стороны: сверка не краснеет, чистка не снимает. Умолчания у срока нет намеренно —
42
+ * пакет, назначивший его за дерево, начал бы сносить чужой архив в день установки, а
43
+ * снимается там разбор просьбы, которого нет больше нигде.
44
+ */
45
+ archiveRetentionDays: null,
39
46
  /**
40
47
  * Каталоги, чей указатель сверяется с содержимым: обзорный документ в них перечисляет
41
48
  * записи таблицей, и читатель ищет по ней, а не обходом. Пусто — сверки указателя нет.
@@ -88,7 +88,7 @@ rt_push_checks_default() {
88
88
 
89
89
  for check in check-doc-paths check-specs check-file-size check-dupes check-styles \
90
90
  check-lib-layers check-reuse check-schema-drift check-states check-state-next \
91
- check-turn-map check-push-gate; do
91
+ check-turn-map check-archive-age check-push-gate; do
92
92
  [ -f "$root/$RT_CHECKS_DIR/$check.mjs" ] && printf '%s\n' "node $RT_CHECKS_DIR/$check.mjs"
93
93
  done
94
94
 
@@ -82,11 +82,27 @@ verdict="$(tail -n 400 "$transcript" 2>/dev/null | jq -s -r --arg asked "$asked_
82
82
 
83
83
  [ "$verdict" = "owe" ] || exit 0
84
84
 
85
+ # Команда отправки называется той, которая в этом дереве исполняется. Дерево, поставившее пакет
86
+ # зависимостью, зовёт бинарь из зависимостей; дерево, где пакет живёт исходниками, бинаря не
87
+ # имеет вовсе — там зовут собранный bin. Названная наугад команда стоит исполнителю хода: отказ
88
+ # читается как указание, и вызов `npx agent-kit` отвечает в таком дереве отказом установки.
89
+ root="${CLAUDE_PROJECT_DIR:-.}"
90
+ if [ -x "$root/node_modules/.bin/agent-kit" ]; then
91
+ propose_cmd="npx agent-kit propose"
92
+ else
93
+ built="$(ls "$root"/dist/*/bin/agent-kit.js 2>/dev/null | head -1)"
94
+ if [ -n "$built" ]; then
95
+ propose_cmd="node ${built#"$root"/} propose"
96
+ else
97
+ propose_cmd="npx agent-kit propose"
98
+ fi
99
+ fi
100
+
85
101
  reason="BLOCKED by proposal-guard: владелец сказал завести или отправить предложение слою правил, а отправки в этом ходе не было. Написанное и не отправленное лежит в дереве неотличимо от отправленного: своей записи в слое правил у него нет, и владелец читает работу сделанной, пока не спросит прямо.
86
102
 
87
103
  Предложение пишется файлом в \`$proposals_dir/\` и уезжает в тот же ход:
88
104
 
89
- npx agent-kit propose
105
+ $propose_cmd
90
106
 
91
107
  Сухой прогон отправкой не является: он показывает, что уехало бы, и следа наружу не оставляет. Отправка пишет отметки в файлы предложений и делает дерево грязным — при открытом PR они ложатся вторым коммитом в ту же ветку, и это их место, а не повод отложить.
92
108
 
@@ -125,6 +125,17 @@ flowchart TD
125
125
  пишется способом её спросить: команда и то, с чем сверять ответ, вместо снимка ответа.
126
126
  <!-- rt-when: *.md -->
127
127
 
128
+ - **У записи описания прошлого есть срок, и после него запись снимается.** Каталог набирает по
129
+ записи на каждую закрытую работу и не отдаёт обратно ничего. Снятая остаётся в истории —
130
+ достают её тем же именем файла, которым ищут живую. Срок называет дерево ключом настройки;
131
+ умолчания у него нет, потому что снимается там разбор просьбы, которого нет больше нигде.
132
+ <!-- rt-when: *.md -->
133
+
134
+ - **Ссылка на запись описания прошлого в живом тексте живёт ровно до её срока.** Проверка
135
+ адресов архив не читает вовсе, поэтому мёртвая ссылка краснеет не в нём, а в том тексте,
136
+ который сослался. Живой текст называет решение словами, а не адресом записи.
137
+ <!-- rt-when: *.md -->
138
+
128
139
  ## Чего из закона здесь нет
129
140
 
130
141
  Ни одна из формулировочных договорённостей не проверяется: одна фраза на правило, простые
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rt-tools/agent-kit",
3
- "version": "0.15.0",
3
+ "version": "0.16.1",
4
4
  "description": "Переносимый слой правил для агента: законы, хуки, проверки и агенты, раскладываемые в репозиторий одной командой",
5
5
  "author": "RT Team",
6
6
  "license": "Apache-2.0",
Binary file
Binary file