@rt-tools/agent-kit 0.5.0 → 0.5.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.
- package/README.md +73 -0
- package/assets/checks/check-doc-paths.mjs +200 -30
- package/assets/checks/check-specs.mjs +42 -7
- package/assets/checks/rt-kit-checks.config.mjs +12 -0
- package/assets/commands/agent-kit-digest.md +83 -0
- package/assets/commands/next-session.md +122 -0
- package/assets/commands/skill-curator.md +33 -1
- package/assets/defaults/gate-map.sh +23 -1
- package/assets/defaults/project.sh +37 -1
- package/assets/docs/GLOSSARY.md +77 -0
- package/assets/hooks/docs-guard.sh +18 -1
- package/assets/hooks/git-guard-delivery.sh +30 -3
- package/assets/hooks/git-guard-main.sh +8 -0
- package/assets/hooks/git-guard-push-tests.sh +8 -1
- package/assets/hooks/grill-gate.sh +38 -16
- package/assets/hooks/lint-after-edit.sh +10 -3
- package/assets/hooks/observe.sh +90 -0
- package/assets/hooks/postmortem-guard.sh +92 -0
- package/assets/hooks/profile-check.sh +43 -0
- package/assets/hooks/qa-dataid-guard.sh +9 -2
- package/assets/hooks/reuse-first-guard.sh +9 -2
- package/assets/hooks/skill-gate.sh +15 -0
- package/assets/hooks/skill-loaded.sh +7 -0
- package/assets/hooks/task-context-load.sh +16 -2
- package/assets/hooks/task-flow-guard.sh +9 -2
- package/assets/hooks/window-fill-guard.sh +157 -0
- package/assets/laws/code-structure.md +10 -0
- package/assets/laws/delivery.md +8 -1
- package/assets/laws/project-documentation.md +9 -0
- package/assets/laws/verifiability.md +9 -0
- package/assets/laws/work-conduct.md +47 -0
- package/assets/patterns/git-workflow-commit.azure.md +16 -12
- package/assets/patterns/git-workflow-commit.github.md +16 -12
- package/assets/patterns/git-workflow-commit.gitlab.md +16 -12
- package/assets/patterns/spec-driven-domain.md +19 -0
- package/assets/patterns/task-flow-close.md +4 -4
- package/assets/patterns/task-flow-handoff.md +115 -0
- package/assets/patterns/task-flow-resume.md +14 -2
- package/assets/patterns/task-flow-start.md +40 -2
- package/assets/rules/doc-style.md +39 -1
- package/assets/rules/git-workflow.azure.md +39 -0
- package/assets/rules/git-workflow.github.md +38 -0
- package/assets/rules/git-workflow.gitlab.md +38 -0
- package/assets/rules/spec-driven.md +14 -0
- package/assets/rules/task-flow.md +70 -3
- package/assets/skills/agent-kit.md +32 -0
- package/assets/templates/postmortem.md +32 -0
- package/assets/templates/proposal.md +39 -0
- package/bin/agent-kit.d.ts.map +1 -1
- package/bin/agent-kit.js +50 -1
- package/bin/agent-kit.js.map +1 -1
- package/lib/catalog.d.ts +33 -0
- package/lib/catalog.d.ts.map +1 -1
- package/lib/catalog.js +55 -1
- package/lib/catalog.js.map +1 -1
- package/lib/commands.d.ts +41 -0
- package/lib/commands.d.ts.map +1 -1
- package/lib/commands.js +315 -3
- package/lib/commands.js.map +1 -1
- package/lib/config.d.ts +11 -1
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +6 -0
- package/lib/config.js.map +1 -1
- package/lib/hooks-map.d.ts +15 -3
- package/lib/hooks-map.d.ts.map +1 -1
- package/lib/hooks-map.js +47 -11
- package/lib/hooks-map.js.map +1 -1
- package/lib/observations.d.ts +72 -0
- package/lib/observations.d.ts.map +1 -0
- package/lib/observations.js +126 -0
- package/lib/observations.js.map +1 -0
- package/lib/proposals.d.ts +48 -0
- package/lib/proposals.d.ts.map +1 -0
- package/lib/proposals.js +111 -0
- package/lib/proposals.js.map +1 -0
- package/lib/submit.d.ts +24 -0
- package/lib/submit.d.ts.map +1 -0
- package/lib/submit.js +26 -0
- package/lib/submit.js.map +1 -0
- package/lib/sync.d.ts +9 -1
- package/lib/sync.d.ts.map +1 -1
- package/lib/sync.js +4 -6
- package/lib/sync.js.map +1 -1
- package/package.json +1 -1
- package/rt-tools-agent-kit-0.5.2.tgz +0 -0
- package/rt-tools-agent-kit-0.5.0.tgz +0 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** Куда `observe.sh` пишет наблюдения. Путь от корня дерева. */
|
|
2
|
+
export declare const OBSERVATIONS_DIR: string;
|
|
3
|
+
/** Сколько дней наблюдения лежат. Каталог, который только растёт, через год перестают открывать. */
|
|
4
|
+
export declare const KEEP_DAYS: number;
|
|
5
|
+
/** За сколько дней сводка отвечает, когда отрезок не назван. */
|
|
6
|
+
export declare const DEFAULT_DAYS: number;
|
|
7
|
+
/** Роды событий, которые пишут гарды. */
|
|
8
|
+
export type TEvent = 'skill-load' | 'gate-deny' | 'guard-deny';
|
|
9
|
+
export interface IObservation {
|
|
10
|
+
readonly event: string;
|
|
11
|
+
/** Имя правила или гарда — имя ресурса пакета, и только оно. */
|
|
12
|
+
readonly resource: string;
|
|
13
|
+
/** Род правки: расширение, `command` или `browser`. Пути в наблюдении нет. */
|
|
14
|
+
readonly kind: string;
|
|
15
|
+
/** Признак сессии: считает заходы, имени сессии не выдаёт. */
|
|
16
|
+
readonly session: string;
|
|
17
|
+
readonly version: string;
|
|
18
|
+
}
|
|
19
|
+
/** Сколько раз что-то встретилось. Порядок — по убыванию, при равенстве по имени. */
|
|
20
|
+
export interface ICount {
|
|
21
|
+
readonly name: string;
|
|
22
|
+
readonly count: number;
|
|
23
|
+
}
|
|
24
|
+
export interface ISummary {
|
|
25
|
+
readonly days: number;
|
|
26
|
+
/** Сколько заходов оставило хоть одно наблюдение. */
|
|
27
|
+
readonly sessions: number;
|
|
28
|
+
readonly loads: readonly ICount[];
|
|
29
|
+
/** Отбития гейта по правилам. */
|
|
30
|
+
readonly denials: readonly ICount[];
|
|
31
|
+
/** Род правки, на котором гейт отбивал. */
|
|
32
|
+
readonly kinds: readonly ICount[];
|
|
33
|
+
readonly guards: readonly ICount[];
|
|
34
|
+
/**
|
|
35
|
+
* Правила, разложенные в дерево и не загруженные за отрезок ни разу. Мёртвый ресурс иначе
|
|
36
|
+
* неотличим от работающего — и это единственная строка сводки, которую нельзя получить из
|
|
37
|
+
* самих наблюдений.
|
|
38
|
+
*/
|
|
39
|
+
readonly unused: readonly string[];
|
|
40
|
+
readonly versions: readonly string[];
|
|
41
|
+
readonly total: number;
|
|
42
|
+
}
|
|
43
|
+
/** День, отстоящий от названного на столько-то суток назад. Считается в UTC — как и пишется. */
|
|
44
|
+
export declare function dayBefore(today: string, days: number): string;
|
|
45
|
+
/**
|
|
46
|
+
* Одна строка наблюдения. Битая пропускается молча: наблюдение — побочная запись гарда, и
|
|
47
|
+
* оборванная на полуслове строка не имеет права уронить сводку.
|
|
48
|
+
*/
|
|
49
|
+
export declare function parseObservation(line: string): IObservation | null;
|
|
50
|
+
export interface IReadResult {
|
|
51
|
+
readonly observations: readonly IObservation[];
|
|
52
|
+
/** Файлы, снятые по сроку хранения: их называют вслух, а не убирают молча. */
|
|
53
|
+
readonly swept: readonly string[];
|
|
54
|
+
/** Каталога нет вовсе — записи не велось ни разу. */
|
|
55
|
+
readonly silent: boolean;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Наблюдения за отрезок и уборка того, что старше срока хранения.
|
|
59
|
+
*
|
|
60
|
+
* Уборка идёт здесь, а не отдельной командой: каталог растёт от работы, а не от запусков, и
|
|
61
|
+
* команда, о которой надо помнить, до второго месяца не доживает.
|
|
62
|
+
*/
|
|
63
|
+
export declare function readObservations(root: string, today: string, days: number): IReadResult;
|
|
64
|
+
/**
|
|
65
|
+
* Сводка за отрезок.
|
|
66
|
+
*
|
|
67
|
+
* `known` — имена правил, разложенных в это дерево. Без них сводка отвечает только на вопрос
|
|
68
|
+
* «чем пользовались», а самое ценное — чем не пользовались ни разу — сказать нечем: в самих
|
|
69
|
+
* наблюдениях незагруженного правила нет по определению.
|
|
70
|
+
*/
|
|
71
|
+
export declare function summarize(observations: readonly IObservation[], known: readonly string[], days: number): ISummary;
|
|
72
|
+
//# sourceMappingURL=observations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observations.d.ts","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/observations.ts"],"names":[],"mappings":"AAeA,gEAAgE;AAChE,eAAO,MAAM,gBAAgB,EAAE,MAAsC,CAAC;AAEtE,oGAAoG;AACpG,eAAO,MAAM,SAAS,EAAE,MAAW,CAAC;AAEpC,gEAAgE;AAChE,eAAO,MAAM,YAAY,EAAE,MAAU,CAAC;AAEtC,yCAAyC;AACzC,MAAM,MAAM,MAAM,GAAG,YAAY,GAAG,WAAW,GAAG,YAAY,CAAC;AAE/D,MAAM,WAAW,YAAY;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,gEAAgE;IAChE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,8EAA8E;IAC9E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,8DAA8D;IAC9D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC5B;AAED,qFAAqF;AACrF,MAAM,WAAW,MAAM;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,QAAQ;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,qDAAqD;IACrD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,iCAAiC;IACjC,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,2CAA2C;IAC3C,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CAC1B;AAOD,gGAAgG;AAChG,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAI7D;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAoBlE;AAED,MAAM,WAAW,WAAW;IACxB,QAAQ,CAAC,YAAY,EAAE,SAAS,YAAY,EAAE,CAAC;IAC/C,8EAA8E;IAC9E,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,qDAAqD;IACrD,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC5B;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,WAAW,CAiCvF;AAoBD;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,YAAY,EAAE,SAAS,YAAY,EAAE,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,QAAQ,CAgBjH"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Наблюдения и сводка по ним.
|
|
3
|
+
*
|
|
4
|
+
* Наблюдения пишет `hooks/observe.sh` — по строке на событие, по файлу на день. Здесь они
|
|
5
|
+
* читаются и сводятся: чем пользовались, чем не пользовались ни разу, обо что спотыкались.
|
|
6
|
+
*
|
|
7
|
+
* Разбор отделён от чтения диска намеренно: сводка — то, ради чего наблюдения ведутся, и
|
|
8
|
+
* проверять её на подложенных файлах значило бы проверять файловую систему.
|
|
9
|
+
*
|
|
10
|
+
* Дата приходит доводом, а не берётся из часов: сводка за отрезок дней иначе не проверяется
|
|
11
|
+
* спекой — вчерашний файл фикстуры завтра оказывается позавчерашним.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readFileSync, readdirSync, rmSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
/** Куда `observe.sh` пишет наблюдения. Путь от корня дерева. */
|
|
16
|
+
export const OBSERVATIONS_DIR = '.claude/rt-kit/observations';
|
|
17
|
+
/** Сколько дней наблюдения лежат. Каталог, который только растёт, через год перестают открывать. */
|
|
18
|
+
export const KEEP_DAYS = 30;
|
|
19
|
+
/** За сколько дней сводка отвечает, когда отрезок не назван. */
|
|
20
|
+
export const DEFAULT_DAYS = 3;
|
|
21
|
+
/** Дата файла наблюдений: `2026-08-12.jsonl`. Чужое имя в каталоге не наблюдение. */
|
|
22
|
+
const DAY_FILE = /^(\d{4}-\d{2}-\d{2})\.jsonl$/;
|
|
23
|
+
const dayOf = (name) => DAY_FILE.exec(name)?.[1] ?? null;
|
|
24
|
+
/** День, отстоящий от названного на столько-то суток назад. Считается в UTC — как и пишется. */
|
|
25
|
+
export function dayBefore(today, days) {
|
|
26
|
+
const at = Date.parse(`${today}T00:00:00Z`);
|
|
27
|
+
return Number.isNaN(at) ? today : new Date(at - days * 86_400_000).toISOString().slice(0, 10);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Одна строка наблюдения. Битая пропускается молча: наблюдение — побочная запись гарда, и
|
|
31
|
+
* оборванная на полуслове строка не имеет права уронить сводку.
|
|
32
|
+
*/
|
|
33
|
+
export function parseObservation(line) {
|
|
34
|
+
if (!line.trim()) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const found = JSON.parse(line);
|
|
39
|
+
const event = found['ev'];
|
|
40
|
+
return typeof event === 'string' && event
|
|
41
|
+
? {
|
|
42
|
+
event,
|
|
43
|
+
resource: typeof found['res'] === 'string' ? found['res'] : '',
|
|
44
|
+
kind: typeof found['kind'] === 'string' ? found['kind'] : '',
|
|
45
|
+
session: typeof found['sid'] === 'string' ? found['sid'] : '',
|
|
46
|
+
version: typeof found['v'] === 'string' ? found['v'] : '',
|
|
47
|
+
}
|
|
48
|
+
: null;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Наблюдения за отрезок и уборка того, что старше срока хранения.
|
|
56
|
+
*
|
|
57
|
+
* Уборка идёт здесь, а не отдельной командой: каталог растёт от работы, а не от запусков, и
|
|
58
|
+
* команда, о которой надо помнить, до второго месяца не доживает.
|
|
59
|
+
*/
|
|
60
|
+
export function readObservations(root, today, days) {
|
|
61
|
+
const dir = join(root, OBSERVATIONS_DIR);
|
|
62
|
+
if (!existsSync(dir)) {
|
|
63
|
+
return { observations: [], swept: [], silent: true };
|
|
64
|
+
}
|
|
65
|
+
const since = dayBefore(today, days - 1);
|
|
66
|
+
const keepFrom = dayBefore(today, KEEP_DAYS);
|
|
67
|
+
const observations = [];
|
|
68
|
+
const swept = [];
|
|
69
|
+
for (const name of readdirSync(dir).sort()) {
|
|
70
|
+
const day = dayOf(name);
|
|
71
|
+
if (!day) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (day < keepFrom) {
|
|
75
|
+
rmSync(join(dir, name), { force: true });
|
|
76
|
+
swept.push(name);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (day < since) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
for (const line of readFileSync(join(dir, name), 'utf8').split('\n')) {
|
|
83
|
+
const found = parseObservation(line);
|
|
84
|
+
if (found) {
|
|
85
|
+
observations.push(found);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return { observations, swept, silent: false };
|
|
90
|
+
}
|
|
91
|
+
const countBy = (values) => {
|
|
92
|
+
const counted = new Map();
|
|
93
|
+
for (const value of values) {
|
|
94
|
+
if (value) {
|
|
95
|
+
counted.set(value, (counted.get(value) ?? 0) + 1);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return [...counted]
|
|
99
|
+
.map(([name, count]) => ({ name, count }))
|
|
100
|
+
.sort((left, right) => right.count - left.count || left.name.localeCompare(right.name));
|
|
101
|
+
};
|
|
102
|
+
const of = (observations, event) => observations.filter((entry) => entry.event === event);
|
|
103
|
+
/**
|
|
104
|
+
* Сводка за отрезок.
|
|
105
|
+
*
|
|
106
|
+
* `known` — имена правил, разложенных в это дерево. Без них сводка отвечает только на вопрос
|
|
107
|
+
* «чем пользовались», а самое ценное — чем не пользовались ни разу — сказать нечем: в самих
|
|
108
|
+
* наблюдениях незагруженного правила нет по определению.
|
|
109
|
+
*/
|
|
110
|
+
export function summarize(observations, known, days) {
|
|
111
|
+
const loads = of(observations, 'skill-load');
|
|
112
|
+
const denials = of(observations, 'gate-deny');
|
|
113
|
+
const loaded = new Set(loads.map((entry) => entry.resource));
|
|
114
|
+
return {
|
|
115
|
+
days,
|
|
116
|
+
sessions: new Set(observations.map((entry) => entry.session).filter(Boolean)).size,
|
|
117
|
+
loads: countBy(loads.map((entry) => entry.resource)),
|
|
118
|
+
denials: countBy(denials.map((entry) => entry.resource)),
|
|
119
|
+
kinds: countBy(denials.map((entry) => entry.kind)),
|
|
120
|
+
guards: countBy(of(observations, 'guard-deny').map((entry) => entry.resource)),
|
|
121
|
+
unused: known.filter((name) => !loaded.has(name)).sort(),
|
|
122
|
+
versions: [...new Set(observations.map((entry) => entry.version).filter(Boolean))].sort(),
|
|
123
|
+
total: observations.length,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=observations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observations.js","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/observations.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Наблюдения и сводка по ним.\n *\n * Наблюдения пишет `hooks/observe.sh` — по строке на событие, по файлу на день. Здесь они\n * читаются и сводятся: чем пользовались, чем не пользовались ни разу, обо что спотыкались.\n *\n * Разбор отделён от чтения диска намеренно: сводка — то, ради чего наблюдения ведутся, и\n * проверять её на подложенных файлах значило бы проверять файловую систему.\n *\n * Дата приходит доводом, а не берётся из часов: сводка за отрезок дней иначе не проверяется\n * спекой — вчерашний файл фикстуры завтра оказывается позавчерашним.\n */\nimport { existsSync, readFileSync, readdirSync, rmSync } from 'node:fs';\nimport { join } from 'node:path';\n\n/** Куда `observe.sh` пишет наблюдения. Путь от корня дерева. */\nexport const OBSERVATIONS_DIR: string = '.claude/rt-kit/observations';\n\n/** Сколько дней наблюдения лежат. Каталог, который только растёт, через год перестают открывать. */\nexport const KEEP_DAYS: number = 30;\n\n/** За сколько дней сводка отвечает, когда отрезок не назван. */\nexport const DEFAULT_DAYS: number = 3;\n\n/** Роды событий, которые пишут гарды. */\nexport type TEvent = 'skill-load' | 'gate-deny' | 'guard-deny';\n\nexport interface IObservation {\n readonly event: string;\n /** Имя правила или гарда — имя ресурса пакета, и только оно. */\n readonly resource: string;\n /** Род правки: расширение, `command` или `browser`. Пути в наблюдении нет. */\n readonly kind: string;\n /** Признак сессии: считает заходы, имени сессии не выдаёт. */\n readonly session: string;\n readonly version: string;\n}\n\n/** Сколько раз что-то встретилось. Порядок — по убыванию, при равенстве по имени. */\nexport interface ICount {\n readonly name: string;\n readonly count: number;\n}\n\nexport interface ISummary {\n readonly days: number;\n /** Сколько заходов оставило хоть одно наблюдение. */\n readonly sessions: number;\n readonly loads: readonly ICount[];\n /** Отбития гейта по правилам. */\n readonly denials: readonly ICount[];\n /** Род правки, на котором гейт отбивал. */\n readonly kinds: readonly ICount[];\n readonly guards: readonly ICount[];\n /**\n * Правила, разложенные в дерево и не загруженные за отрезок ни разу. Мёртвый ресурс иначе\n * неотличим от работающего — и это единственная строка сводки, которую нельзя получить из\n * самих наблюдений.\n */\n readonly unused: readonly string[];\n readonly versions: readonly string[];\n readonly total: number;\n}\n\n/** Дата файла наблюдений: `2026-08-12.jsonl`. Чужое имя в каталоге не наблюдение. */\nconst DAY_FILE: RegExp = /^(\\d{4}-\\d{2}-\\d{2})\\.jsonl$/;\n\nconst dayOf: (name: string) => string | null = (name: string): string | null => DAY_FILE.exec(name)?.[1] ?? null;\n\n/** День, отстоящий от названного на столько-то суток назад. Считается в UTC — как и пишется. */\nexport function dayBefore(today: string, days: number): string {\n const at: number = Date.parse(`${today}T00:00:00Z`);\n\n return Number.isNaN(at) ? today : new Date(at - days * 86_400_000).toISOString().slice(0, 10);\n}\n\n/**\n * Одна строка наблюдения. Битая пропускается молча: наблюдение — побочная запись гарда, и\n * оборванная на полуслове строка не имеет права уронить сводку.\n */\nexport function parseObservation(line: string): IObservation | null {\n if (!line.trim()) {\n return null;\n }\n try {\n const found: Record<string, unknown> = JSON.parse(line) as Record<string, unknown>;\n const event: unknown = found['ev'];\n\n return typeof event === 'string' && event\n ? {\n event,\n resource: typeof found['res'] === 'string' ? found['res'] : '',\n kind: typeof found['kind'] === 'string' ? found['kind'] : '',\n session: typeof found['sid'] === 'string' ? found['sid'] : '',\n version: typeof found['v'] === 'string' ? found['v'] : '',\n }\n : null;\n } catch {\n return null;\n }\n}\n\nexport interface IReadResult {\n readonly observations: readonly IObservation[];\n /** Файлы, снятые по сроку хранения: их называют вслух, а не убирают молча. */\n readonly swept: readonly string[];\n /** Каталога нет вовсе — записи не велось ни разу. */\n readonly silent: boolean;\n}\n\n/**\n * Наблюдения за отрезок и уборка того, что старше срока хранения.\n *\n * Уборка идёт здесь, а не отдельной командой: каталог растёт от работы, а не от запусков, и\n * команда, о которой надо помнить, до второго месяца не доживает.\n */\nexport function readObservations(root: string, today: string, days: number): IReadResult {\n const dir: string = join(root, OBSERVATIONS_DIR);\n if (!existsSync(dir)) {\n return { observations: [], swept: [], silent: true };\n }\n\n const since: string = dayBefore(today, days - 1);\n const keepFrom: string = dayBefore(today, KEEP_DAYS);\n const observations: IObservation[] = [];\n const swept: string[] = [];\n\n for (const name of readdirSync(dir).sort()) {\n const day: string | null = dayOf(name);\n if (!day) {\n continue;\n }\n if (day < keepFrom) {\n rmSync(join(dir, name), { force: true });\n swept.push(name);\n continue;\n }\n if (day < since) {\n continue;\n }\n for (const line of readFileSync(join(dir, name), 'utf8').split('\\n')) {\n const found: IObservation | null = parseObservation(line);\n if (found) {\n observations.push(found);\n }\n }\n }\n\n return { observations, swept, silent: false };\n}\n\nconst countBy: (values: readonly string[]) => readonly ICount[] = (values: readonly string[]): readonly ICount[] => {\n const counted: Map<string, number> = new Map();\n for (const value of values) {\n if (value) {\n counted.set(value, (counted.get(value) ?? 0) + 1);\n }\n }\n\n return [...counted]\n .map(([name, count]: [string, number]): ICount => ({ name, count }))\n .sort((left: ICount, right: ICount): number => right.count - left.count || left.name.localeCompare(right.name));\n};\n\nconst of: (observations: readonly IObservation[], event: TEvent) => readonly IObservation[] = (\n observations: readonly IObservation[],\n event: TEvent\n): readonly IObservation[] => observations.filter((entry: IObservation): boolean => entry.event === event);\n\n/**\n * Сводка за отрезок.\n *\n * `known` — имена правил, разложенных в это дерево. Без них сводка отвечает только на вопрос\n * «чем пользовались», а самое ценное — чем не пользовались ни разу — сказать нечем: в самих\n * наблюдениях незагруженного правила нет по определению.\n */\nexport function summarize(observations: readonly IObservation[], known: readonly string[], days: number): ISummary {\n const loads: readonly IObservation[] = of(observations, 'skill-load');\n const denials: readonly IObservation[] = of(observations, 'gate-deny');\n const loaded: Set<string> = new Set(loads.map((entry: IObservation): string => entry.resource));\n\n return {\n days,\n sessions: new Set(observations.map((entry: IObservation): string => entry.session).filter(Boolean)).size,\n loads: countBy(loads.map((entry: IObservation): string => entry.resource)),\n denials: countBy(denials.map((entry: IObservation): string => entry.resource)),\n kinds: countBy(denials.map((entry: IObservation): string => entry.kind)),\n guards: countBy(of(observations, 'guard-deny').map((entry: IObservation): string => entry.resource)),\n unused: known.filter((name: string): boolean => !loaded.has(name)).sort(),\n versions: [...new Set(observations.map((entry: IObservation): string => entry.version).filter(Boolean))].sort(),\n total: observations.length,\n };\n}\n"]}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Куда главный агент кладёт предложения. Путь от корня дерева. */
|
|
2
|
+
export declare const PROPOSALS_DIR: string;
|
|
3
|
+
/** Адрес предложения: куда правка идёт. Наружу уезжает только первый. */
|
|
4
|
+
export declare const TO_PACKAGE: string;
|
|
5
|
+
export declare const TO_COMPANION: string;
|
|
6
|
+
export declare const TO_TREE: string;
|
|
7
|
+
export interface IProposal {
|
|
8
|
+
readonly address: string;
|
|
9
|
+
readonly resource: string;
|
|
10
|
+
/** Тело блока без заголовка: место, повод и готовый текст. */
|
|
11
|
+
readonly body: string;
|
|
12
|
+
/** Ссылка на заведённую запись; пусто — не отправлялось. */
|
|
13
|
+
readonly sent: string;
|
|
14
|
+
/** Файл, в котором блок лежит, путём от корня дерева. */
|
|
15
|
+
readonly file: string;
|
|
16
|
+
/** Строка заголовка в файле, считая с единицы. */
|
|
17
|
+
readonly line: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Блоки файла предложений.
|
|
21
|
+
*
|
|
22
|
+
* Незаполненный образец пропускается: в шаблоне ресурс стоит скобками — `rules/<правило>.md`, —
|
|
23
|
+
* и отправить его значило бы завести запись о правке несуществующего файла.
|
|
24
|
+
*/
|
|
25
|
+
export declare function parseProposals(text: string, file: string): readonly IProposal[];
|
|
26
|
+
/** Все файлы предложений дерева, в порядке имён. */
|
|
27
|
+
export declare function readProposals(root: string): readonly IProposal[];
|
|
28
|
+
export interface ILeak {
|
|
29
|
+
/** Строка блока, считая с единицы от заголовка. */
|
|
30
|
+
readonly line: number;
|
|
31
|
+
readonly text: string;
|
|
32
|
+
readonly why: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Что в тексте называет дерево, из которого предложение уезжает.
|
|
36
|
+
*
|
|
37
|
+
* `marks` — имена, которые знает только это дерево: корень, его последнее звено, адрес его
|
|
38
|
+
* удалённого репозитория. Пакет их не угадывает — они приходят доводом с края.
|
|
39
|
+
*/
|
|
40
|
+
export declare function leaksIn(text: string, marks: readonly string[]): readonly ILeak[];
|
|
41
|
+
/**
|
|
42
|
+
* Чем дерево себя выдаёт. Последнее звено пути берётся отдельно от полного: в тексте оно
|
|
43
|
+
* встречается чаще, чем путь целиком.
|
|
44
|
+
*/
|
|
45
|
+
export declare function marksOf(root: string, remote: string): readonly string[];
|
|
46
|
+
/** Пометка об отправке дописывается в блок, а не в конец файла: блоков в файле несколько. */
|
|
47
|
+
export declare function markSent(text: string, proposal: IProposal, url: string): string;
|
|
48
|
+
//# sourceMappingURL=proposals.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proposals.d.ts","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/proposals.ts"],"names":[],"mappings":"AAcA,mEAAmE;AACnE,eAAO,MAAM,aAAa,EAAE,MAAmC,CAAC;AAEhE,yEAAyE;AACzE,eAAO,MAAM,UAAU,EAAE,MAAgB,CAAC;AAC1C,eAAO,MAAM,YAAY,EAAE,MAAoB,CAAC;AAChD,eAAO,MAAM,OAAO,EAAE,MAAiB,CAAC;AAUxC,MAAM,WAAW,SAAS;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,yDAAyD;IACzD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,kDAAkD;IAClD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACzB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,SAAS,EAAE,CAiC/E;AAED,oDAAoD;AACpD,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,SAAS,EAAE,CAUhE;AAED,MAAM,WAAW,KAAK;IAClB,mDAAmD;IACnD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACxB;AAKD;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,KAAK,EAAE,CAgBhF;AAED;;;GAGG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAIvE;AAED,6FAA6F;AAC7F,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAO/E"}
|
package/lib/proposals.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Предложения по слою правил: разбор файла, отбор того, что уезжает наружу, и проверка на адрес
|
|
3
|
+
* дерева.
|
|
4
|
+
*
|
|
5
|
+
* Файл пишет главный агент после разбора закрытой задачи — по блоку на предложение, с адресом в
|
|
6
|
+
* заголовке. Адрес роль проставляет и так; здесь он впервые становится тем, что читает машина.
|
|
7
|
+
*
|
|
8
|
+
* Проверка на утечку живёт в этом же модуле не для удобства: предложение уезжает в чужой
|
|
9
|
+
* репозиторий целиком, и запрет называть чужое дерево обязан держаться проверкой, а не памятью
|
|
10
|
+
* того, кто пишет.
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
13
|
+
import { basename, join } from 'node:path';
|
|
14
|
+
/** Куда главный агент кладёт предложения. Путь от корня дерева. */
|
|
15
|
+
export const PROPOSALS_DIR = '.claude/rt-kit/proposals';
|
|
16
|
+
/** Адрес предложения: куда правка идёт. Наружу уезжает только первый. */
|
|
17
|
+
export const TO_PACKAGE = 'пакет';
|
|
18
|
+
export const TO_COMPANION = 'компаньон';
|
|
19
|
+
export const TO_TREE = 'дерево';
|
|
20
|
+
const ADDRESSES = [TO_PACKAGE, TO_COMPANION, TO_TREE];
|
|
21
|
+
/** Заголовок блока: `## <адрес> · <ресурс>`. Разделитель — тот же, что в шаблоне. */
|
|
22
|
+
const HEADING = /^##\s+(\S+)\s+·\s+(\S.*?)\s*$/;
|
|
23
|
+
/** Пометка об отправке. По ней же предложение узнаётся отправленным. */
|
|
24
|
+
const SENT = /^-\s+\*\*отправлено:\*\*\s*(\S+)/m;
|
|
25
|
+
/**
|
|
26
|
+
* Блоки файла предложений.
|
|
27
|
+
*
|
|
28
|
+
* Незаполненный образец пропускается: в шаблоне ресурс стоит скобками — `rules/<правило>.md`, —
|
|
29
|
+
* и отправить его значило бы завести запись о правке несуществующего файла.
|
|
30
|
+
*/
|
|
31
|
+
export function parseProposals(text, file) {
|
|
32
|
+
const lines = text.split('\n');
|
|
33
|
+
const found = [];
|
|
34
|
+
let at = -1;
|
|
35
|
+
const close = (end) => {
|
|
36
|
+
if (at < 0) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const heading = HEADING.exec(lines[at]);
|
|
40
|
+
const body = lines
|
|
41
|
+
.slice(at + 1, end)
|
|
42
|
+
.join('\n')
|
|
43
|
+
.trim();
|
|
44
|
+
const address = heading[1];
|
|
45
|
+
const resource = heading[2];
|
|
46
|
+
if (ADDRESSES.includes(address) && !resource.includes('<')) {
|
|
47
|
+
found.push({ address, resource, body, sent: SENT.exec(body)?.[1] ?? '', file, line: at + 1 });
|
|
48
|
+
}
|
|
49
|
+
at = -1;
|
|
50
|
+
};
|
|
51
|
+
lines.forEach((line, index) => {
|
|
52
|
+
if (line.startsWith('## ')) {
|
|
53
|
+
close(index);
|
|
54
|
+
if (HEADING.test(line)) {
|
|
55
|
+
at = index;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
close(lines.length);
|
|
60
|
+
return found;
|
|
61
|
+
}
|
|
62
|
+
/** Все файлы предложений дерева, в порядке имён. */
|
|
63
|
+
export function readProposals(root) {
|
|
64
|
+
const dir = join(root, PROPOSALS_DIR);
|
|
65
|
+
if (!existsSync(dir)) {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
return readdirSync(dir)
|
|
69
|
+
.filter((name) => name.endsWith('.md'))
|
|
70
|
+
.sort()
|
|
71
|
+
.flatMap((name) => parseProposals(readFileSync(join(dir, name), 'utf8'), join(PROPOSALS_DIR, name)));
|
|
72
|
+
}
|
|
73
|
+
/** Абсолютный путь машины: он не бывает общим ни для одного дерева, кроме этого. */
|
|
74
|
+
const ABSOLUTE = /(^|[\s(`'"])(~\/|\/(Users|home|var|opt|srv|mnt)\/)\S+/;
|
|
75
|
+
/**
|
|
76
|
+
* Что в тексте называет дерево, из которого предложение уезжает.
|
|
77
|
+
*
|
|
78
|
+
* `marks` — имена, которые знает только это дерево: корень, его последнее звено, адрес его
|
|
79
|
+
* удалённого репозитория. Пакет их не угадывает — они приходят доводом с края.
|
|
80
|
+
*/
|
|
81
|
+
export function leaksIn(text, marks) {
|
|
82
|
+
const found = [];
|
|
83
|
+
text.split('\n').forEach((line, index) => {
|
|
84
|
+
if (ABSOLUTE.test(line)) {
|
|
85
|
+
found.push({ line: index + 1, text: line.trim(), why: 'абсолютный путь этой машины' });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const mark = marks.find((value) => Boolean(value) && line.includes(value));
|
|
89
|
+
if (mark) {
|
|
90
|
+
found.push({ line: index + 1, text: line.trim(), why: `имя этого дерева: ${mark}` });
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
return found;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Чем дерево себя выдаёт. Последнее звено пути берётся отдельно от полного: в тексте оно
|
|
97
|
+
* встречается чаще, чем путь целиком.
|
|
98
|
+
*/
|
|
99
|
+
export function marksOf(root, remote) {
|
|
100
|
+
const name = basename(root);
|
|
101
|
+
return [root, name, remote].filter(Boolean);
|
|
102
|
+
}
|
|
103
|
+
/** Пометка об отправке дописывается в блок, а не в конец файла: блоков в файле несколько. */
|
|
104
|
+
export function markSent(text, proposal, url) {
|
|
105
|
+
const lines = text.split('\n');
|
|
106
|
+
// Пометка встаёт сразу под заголовок: конец блока определяется следующим заголовком, а его
|
|
107
|
+
// может и не быть — тогда «конец» пришлось бы искать по пустым строкам в хвосте файла.
|
|
108
|
+
lines.splice(proposal.line, 0, `- **отправлено:** ${url}`);
|
|
109
|
+
return lines.join('\n');
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=proposals.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proposals.js","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/proposals.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAChE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAE3C,mEAAmE;AACnE,MAAM,CAAC,MAAM,aAAa,GAAW,0BAA0B,CAAC;AAEhE,yEAAyE;AACzE,MAAM,CAAC,MAAM,UAAU,GAAW,OAAO,CAAC;AAC1C,MAAM,CAAC,MAAM,YAAY,GAAW,WAAW,CAAC;AAChD,MAAM,CAAC,MAAM,OAAO,GAAW,QAAQ,CAAC;AAExC,MAAM,SAAS,GAAsB,CAAC,UAAU,EAAE,YAAY,EAAE,OAAO,CAAC,CAAC;AAEzE,qFAAqF;AACrF,MAAM,OAAO,GAAW,+BAA+B,CAAC;AAExD,wEAAwE;AACxE,MAAM,IAAI,GAAW,mCAAmC,CAAC;AAezD;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,IAAY;IACrD,MAAM,KAAK,GAAsB,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAClD,MAAM,KAAK,GAAgB,EAAE,CAAC;IAC9B,IAAI,EAAE,GAAW,CAAC,CAAC,CAAC;IAEpB,MAAM,KAAK,GAA0B,CAAC,GAAW,EAAQ,EAAE;QACvD,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC;YACT,OAAO;QACX,CAAC;QACD,MAAM,OAAO,GAAoB,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAoB,CAAC;QAC5E,MAAM,IAAI,GAAW,KAAK;aACrB,KAAK,CAAC,EAAE,GAAG,CAAC,EAAE,GAAG,CAAC;aAClB,IAAI,CAAC,IAAI,CAAC;aACV,IAAI,EAAE,CAAC;QACZ,MAAM,OAAO,GAAW,OAAO,CAAC,CAAC,CAAC,CAAC;QACnC,MAAM,QAAQ,GAAW,OAAO,CAAC,CAAC,CAAC,CAAC;QACpC,IAAI,SAAS,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YACzD,KAAK,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;QAClG,CAAC;QACD,EAAE,GAAG,CAAC,CAAC,CAAC;IACZ,CAAC,CAAC;IAEF,KAAK,CAAC,OAAO,CAAC,CAAC,IAAY,EAAE,KAAa,EAAQ,EAAE;QAChD,IAAI,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;YACzB,KAAK,CAAC,KAAK,CAAC,CAAC;YACb,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;gBACrB,EAAE,GAAG,KAAK,CAAC;YACf,CAAC;QACL,CAAC;IACL,CAAC,CAAC,CAAC;IACH,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAEpB,OAAO,KAAK,CAAC;AACjB,CAAC;AAED,oDAAoD;AACpD,MAAM,UAAU,aAAa,CAAC,IAAY;IACtC,MAAM,GAAG,GAAW,IAAI,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC;IAC9C,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACnB,OAAO,EAAE,CAAC;IACd,CAAC;IAED,OAAO,WAAW,CAAC,GAAG,CAAC;SAClB,MAAM,CAAC,CAAC,IAAY,EAAW,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;SACvD,IAAI,EAAE;SACN,OAAO,CAAC,CAAC,IAAY,EAAwB,EAAE,CAAC,cAAc,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,EAAE,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;AAC3I,CAAC;AASD,oFAAoF;AACpF,MAAM,QAAQ,GAAW,uDAAuD,CAAC;AAEjF;;;;;GAKG;AACH,MAAM,UAAU,OAAO,CAAC,IAAY,EAAE,KAAwB;IAC1D,MAAM,KAAK,GAAY,EAAE,CAAC;IAE1B,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,IAAY,EAAE,KAAa,EAAQ,EAAE;QAC3D,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACtB,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,GAAG,EAAE,6BAA6B,EAAE,CAAC,CAAC;YAEvF,OAAO;QACX,CAAC;QACD,MAAM,IAAI,GAAuB,KAAK,CAAC,IAAI,CAAC,CAAC,KAAa,EAAW,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;QAChH,IAAI,IAAI,EAAE,CAAC;YACP,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,GAAG,EAAE,qBAAqB,IAAI,EAAE,EAAE,CAAC,CAAC;QACzF,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,OAAO,KAAK,CAAC;AACjB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,OAAO,CAAC,IAAY,EAAE,MAAc;IAChD,MAAM,IAAI,GAAW,QAAQ,CAAC,IAAI,CAAC,CAAC;IAEpC,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;AAChD,CAAC;AAED,6FAA6F;AAC7F,MAAM,UAAU,QAAQ,CAAC,IAAY,EAAE,QAAmB,EAAE,GAAW;IACnE,MAAM,KAAK,GAAa,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACzC,2FAA2F;IAC3F,uFAAuF;IACvF,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,EAAE,qBAAqB,GAAG,EAAE,CAAC,CAAC;IAE3D,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC5B,CAAC","sourcesContent":["/**\n * Предложения по слою правил: разбор файла, отбор того, что уезжает наружу, и проверка на адрес\n * дерева.\n *\n * Файл пишет главный агент после разбора закрытой задачи — по блоку на предложение, с адресом в\n * заголовке. Адрес роль проставляет и так; здесь он впервые становится тем, что читает машина.\n *\n * Проверка на утечку живёт в этом же модуле не для удобства: предложение уезжает в чужой\n * репозиторий целиком, и запрет называть чужое дерево обязан держаться проверкой, а не памятью\n * того, кто пишет.\n */\nimport { existsSync, readdirSync, readFileSync } from 'node:fs';\nimport { basename, join } from 'node:path';\n\n/** Куда главный агент кладёт предложения. Путь от корня дерева. */\nexport const PROPOSALS_DIR: string = '.claude/rt-kit/proposals';\n\n/** Адрес предложения: куда правка идёт. Наружу уезжает только первый. */\nexport const TO_PACKAGE: string = 'пакет';\nexport const TO_COMPANION: string = 'компаньон';\nexport const TO_TREE: string = 'дерево';\n\nconst ADDRESSES: readonly string[] = [TO_PACKAGE, TO_COMPANION, TO_TREE];\n\n/** Заголовок блока: `## <адрес> · <ресурс>`. Разделитель — тот же, что в шаблоне. */\nconst HEADING: RegExp = /^##\\s+(\\S+)\\s+·\\s+(\\S.*?)\\s*$/;\n\n/** Пометка об отправке. По ней же предложение узнаётся отправленным. */\nconst SENT: RegExp = /^-\\s+\\*\\*отправлено:\\*\\*\\s*(\\S+)/m;\n\nexport interface IProposal {\n readonly address: string;\n readonly resource: string;\n /** Тело блока без заголовка: место, повод и готовый текст. */\n readonly body: string;\n /** Ссылка на заведённую запись; пусто — не отправлялось. */\n readonly sent: string;\n /** Файл, в котором блок лежит, путём от корня дерева. */\n readonly file: string;\n /** Строка заголовка в файле, считая с единицы. */\n readonly line: number;\n}\n\n/**\n * Блоки файла предложений.\n *\n * Незаполненный образец пропускается: в шаблоне ресурс стоит скобками — `rules/<правило>.md`, —\n * и отправить его значило бы завести запись о правке несуществующего файла.\n */\nexport function parseProposals(text: string, file: string): readonly IProposal[] {\n const lines: readonly string[] = text.split('\\n');\n const found: IProposal[] = [];\n let at: number = -1;\n\n const close: (end: number) => void = (end: number): void => {\n if (at < 0) {\n return;\n }\n const heading: RegExpExecArray = HEADING.exec(lines[at]) as RegExpExecArray;\n const body: string = lines\n .slice(at + 1, end)\n .join('\\n')\n .trim();\n const address: string = heading[1];\n const resource: string = heading[2];\n if (ADDRESSES.includes(address) && !resource.includes('<')) {\n found.push({ address, resource, body, sent: SENT.exec(body)?.[1] ?? '', file, line: at + 1 });\n }\n at = -1;\n };\n\n lines.forEach((line: string, index: number): void => {\n if (line.startsWith('## ')) {\n close(index);\n if (HEADING.test(line)) {\n at = index;\n }\n }\n });\n close(lines.length);\n\n return found;\n}\n\n/** Все файлы предложений дерева, в порядке имён. */\nexport function readProposals(root: string): readonly IProposal[] {\n const dir: string = join(root, PROPOSALS_DIR);\n if (!existsSync(dir)) {\n return [];\n }\n\n return readdirSync(dir)\n .filter((name: string): boolean => name.endsWith('.md'))\n .sort()\n .flatMap((name: string): readonly IProposal[] => parseProposals(readFileSync(join(dir, name), 'utf8'), join(PROPOSALS_DIR, name)));\n}\n\nexport interface ILeak {\n /** Строка блока, считая с единицы от заголовка. */\n readonly line: number;\n readonly text: string;\n readonly why: string;\n}\n\n/** Абсолютный путь машины: он не бывает общим ни для одного дерева, кроме этого. */\nconst ABSOLUTE: RegExp = /(^|[\\s(`'\"])(~\\/|\\/(Users|home|var|opt|srv|mnt)\\/)\\S+/;\n\n/**\n * Что в тексте называет дерево, из которого предложение уезжает.\n *\n * `marks` — имена, которые знает только это дерево: корень, его последнее звено, адрес его\n * удалённого репозитория. Пакет их не угадывает — они приходят доводом с края.\n */\nexport function leaksIn(text: string, marks: readonly string[]): readonly ILeak[] {\n const found: ILeak[] = [];\n\n text.split('\\n').forEach((line: string, index: number): void => {\n if (ABSOLUTE.test(line)) {\n found.push({ line: index + 1, text: line.trim(), why: 'абсолютный путь этой машины' });\n\n return;\n }\n const mark: string | undefined = marks.find((value: string): boolean => Boolean(value) && line.includes(value));\n if (mark) {\n found.push({ line: index + 1, text: line.trim(), why: `имя этого дерева: ${mark}` });\n }\n });\n\n return found;\n}\n\n/**\n * Чем дерево себя выдаёт. Последнее звено пути берётся отдельно от полного: в тексте оно\n * встречается чаще, чем путь целиком.\n */\nexport function marksOf(root: string, remote: string): readonly string[] {\n const name: string = basename(root);\n\n return [root, name, remote].filter(Boolean);\n}\n\n/** Пометка об отправке дописывается в блок, а не в конец файла: блоков в файле несколько. */\nexport function markSent(text: string, proposal: IProposal, url: string): string {\n const lines: string[] = text.split('\\n');\n // Пометка встаёт сразу под заголовок: конец блока определяется следующим заголовком, а его\n // может и не быть — тогда «конец» пришлось бы искать по пустым строкам в хвосте файла.\n lines.splice(proposal.line, 0, `- **отправлено:** ${url}`);\n\n return lines.join('\\n');\n}\n"]}
|
package/lib/submit.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface IIssueInput {
|
|
2
|
+
/** `владелец/репозиторий` — читается из манифеста пакета, а не зашито в код. */
|
|
3
|
+
readonly repository: string;
|
|
4
|
+
readonly title: string;
|
|
5
|
+
readonly body: string;
|
|
6
|
+
readonly label: string;
|
|
7
|
+
}
|
|
8
|
+
/** Чем заводится запись. Двойник в спеке — того же вида. */
|
|
9
|
+
export type TSubmit = (input: IIssueInput) => string;
|
|
10
|
+
/** Метка, по которой сведение находит предложения среди прочих записей. */
|
|
11
|
+
export declare const FEEDBACK_LABEL: string;
|
|
12
|
+
/**
|
|
13
|
+
* `владелец/репозиторий` из адреса, записанного в манифесте пакета. Форм адреса несколько —
|
|
14
|
+
* `git+https://…`, `git@…:…`, — и все они ведут к одной паре имён.
|
|
15
|
+
*/
|
|
16
|
+
export declare function repositoryOf(url: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Запись в очереди работ через помощника хостинга.
|
|
19
|
+
*
|
|
20
|
+
* Помощник может быть подменён оболочкой пользователя, поэтому зовётся он именем, а не строкой
|
|
21
|
+
* оболочки: подмена принимает те же доводы, но уходит в чужую учётную запись.
|
|
22
|
+
*/
|
|
23
|
+
export declare const ghIssue: TSubmit;
|
|
24
|
+
//# sourceMappingURL=submit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"submit.d.ts","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/submit.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,WAAW;IACxB,gFAAgF;IAChF,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CAC1B;AAED,4DAA4D;AAC5D,MAAM,MAAM,OAAO,GAAG,CAAC,KAAK,EAAE,WAAW,KAAK,MAAM,CAAC;AAErD,2EAA2E;AAC3E,eAAO,MAAM,cAAc,EAAE,MAA6B,CAAC;AAE3D;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAIhD;AAED;;;;;GAKG;AACH,eAAO,MAAM,OAAO,EAAE,OAKV,CAAC"}
|
package/lib/submit.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Отправка предложения в очередь работ репозитория пакета.
|
|
3
|
+
*
|
|
4
|
+
* Единственное место пакета, которое ходит наружу, и ходит оно только по команде человека:
|
|
5
|
+
* ни один гард в сеть не ходит вовсе. Вынесено отдельно, чтобы команда `propose` осталась
|
|
6
|
+
* проверяемой: спека подставляет двойник и не заводит записей в живом репозитории.
|
|
7
|
+
*/
|
|
8
|
+
import { execFileSync } from 'node:child_process';
|
|
9
|
+
/** Метка, по которой сведение находит предложения среди прочих записей. */
|
|
10
|
+
export const FEEDBACK_LABEL = 'agent-kit-feedback';
|
|
11
|
+
/**
|
|
12
|
+
* `владелец/репозиторий` из адреса, записанного в манифесте пакета. Форм адреса несколько —
|
|
13
|
+
* `git+https://…`, `git@…:…`, — и все они ведут к одной паре имён.
|
|
14
|
+
*/
|
|
15
|
+
export function repositoryOf(url) {
|
|
16
|
+
const found = /(?:github\.com[/:])([^/]+)\/([^/.]+)/.exec(url);
|
|
17
|
+
return found ? `${found[1]}/${found[2]}` : '';
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Запись в очереди работ через помощника хостинга.
|
|
21
|
+
*
|
|
22
|
+
* Помощник может быть подменён оболочкой пользователя, поэтому зовётся он именем, а не строкой
|
|
23
|
+
* оболочки: подмена принимает те же доводы, но уходит в чужую учётную запись.
|
|
24
|
+
*/
|
|
25
|
+
export const ghIssue = (input) => execFileSync('gh', ['issue', 'create', '--repo', input.repository, '--title', input.title, '--label', input.label, '--body-file', '-'], { input: input.body, encoding: 'utf8' }).trim();
|
|
26
|
+
//# sourceMappingURL=submit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"submit.js","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/submit.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAalD,2EAA2E;AAC3E,MAAM,CAAC,MAAM,cAAc,GAAW,oBAAoB,CAAC;AAE3D;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,GAAW;IACpC,MAAM,KAAK,GAA2B,sCAAsC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAEvF,OAAO,KAAK,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;AAClD,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,OAAO,GAAY,CAAC,KAAkB,EAAU,EAAE,CAC3D,YAAY,CACR,IAAI,EACJ,CAAC,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,CAAC,UAAU,EAAE,SAAS,EAAE,KAAK,CAAC,KAAK,EAAE,SAAS,EAAE,KAAK,CAAC,KAAK,EAAE,aAAa,EAAE,GAAG,CAAC,EACnH,EAAE,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,CAC1C,CAAC,IAAI,EAAE,CAAC","sourcesContent":["/**\n * Отправка предложения в очередь работ репозитория пакета.\n *\n * Единственное место пакета, которое ходит наружу, и ходит оно только по команде человека:\n * ни один гард в сеть не ходит вовсе. Вынесено отдельно, чтобы команда `propose` осталась\n * проверяемой: спека подставляет двойник и не заводит записей в живом репозитории.\n */\nimport { execFileSync } from 'node:child_process';\n\nexport interface IIssueInput {\n /** `владелец/репозиторий` — читается из манифеста пакета, а не зашито в код. */\n readonly repository: string;\n readonly title: string;\n readonly body: string;\n readonly label: string;\n}\n\n/** Чем заводится запись. Двойник в спеке — того же вида. */\nexport type TSubmit = (input: IIssueInput) => string;\n\n/** Метка, по которой сведение находит предложения среди прочих записей. */\nexport const FEEDBACK_LABEL: string = 'agent-kit-feedback';\n\n/**\n * `владелец/репозиторий` из адреса, записанного в манифесте пакета. Форм адреса несколько —\n * `git+https://…`, `git@…:…`, — и все они ведут к одной паре имён.\n */\nexport function repositoryOf(url: string): string {\n const found: RegExpExecArray | null = /(?:github\\.com[/:])([^/]+)\\/([^/.]+)/.exec(url);\n\n return found ? `${found[1]}/${found[2]}` : '';\n}\n\n/**\n * Запись в очереди работ через помощника хостинга.\n *\n * Помощник может быть подменён оболочкой пользователя, поэтому зовётся он именем, а не строкой\n * оболочки: подмена принимает те же доводы, но уходит в чужую учётную запись.\n */\nexport const ghIssue: TSubmit = (input: IIssueInput): string =>\n execFileSync(\n 'gh',\n ['issue', 'create', '--repo', input.repository, '--title', input.title, '--label', input.label, '--body-file', '-'],\n { input: input.body, encoding: 'utf8' }\n ).trim();\n"]}
|
package/lib/sync.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IGapOfVariant } from './catalog.js';
|
|
1
|
+
import { IBrokenLink, IGapOfVariant } from './catalog.js';
|
|
2
2
|
import { ICompanion } from './companion.js';
|
|
3
3
|
import { IConfig } from './config.js';
|
|
4
4
|
import { IHookBinding } from './hooks-map.js';
|
|
@@ -33,6 +33,14 @@ export interface ISyncResult {
|
|
|
33
33
|
* молчать нельзя: гард, который не зовут, неотличим от гарда, который всё пропускает.
|
|
34
34
|
*/
|
|
35
35
|
readonly unbound: readonly IHookBinding[];
|
|
36
|
+
/**
|
|
37
|
+
* Выбранные ресурсы, чьи требования в дерево не поехали.
|
|
38
|
+
*
|
|
39
|
+
* Предупреждение, а не отказ: дерево вправе закрыть требование своим средством, но обязано
|
|
40
|
+
* знать, что закрывает. Молчание же оставляет его исправным на вид — сверка зелена на любом
|
|
41
|
+
* подмножестве, сколько бы связок ни было разорвано.
|
|
42
|
+
*/
|
|
43
|
+
readonly broken: readonly IBrokenLink[];
|
|
36
44
|
readonly written: readonly string[];
|
|
37
45
|
}
|
|
38
46
|
export declare function planSync(config: IConfig, root: string, version: string, assetsDir: string): ISyncResult;
|
package/lib/sync.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/sync.ts"],"names":[],"mappings":"AAWA,OAAO,EAAmB,aAAa,EAA4B,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/sync.ts"],"names":[],"mappings":"AAWA,OAAO,EAAe,WAAW,EAAmB,aAAa,EAA4B,MAAM,cAAc,CAAC;AAClH,OAAO,EAAE,UAAU,EAAyB,MAAM,gBAAgB,CAAC;AACnE,OAAO,EAAE,OAAO,EAAiB,MAAM,aAAa,CAAC;AACrD,OAAO,EAA4B,YAAY,EAAgB,MAAM,gBAAgB,CAAC;AACtF,OAAO,EAAE,QAAQ,EAAkC,MAAM,WAAW,CAAC;AAMrE,MAAM,WAAW,WAAW;IACxB,QAAQ,CAAC,OAAO,EAAE,SAAS,QAAQ,EAAE,CAAC;IACtC,oFAAoF;IACpF,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC;IACzD,+EAA+E;IAC/E,QAAQ,CAAC,UAAU,EAAE,SAAS,UAAU,EAAE,CAAC;IAC3C;;;;;;OAMG;IACH,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC;;;;;;;OAOG;IACH,QAAQ,CAAC,IAAI,EAAE,SAAS,aAAa,EAAE,CAAC;IACxC;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,CAAC;IAC1C;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,EAAE,SAAS,WAAW,EAAE,CAAC;IACxC,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC;AAwDD,wBAAgB,QAAQ,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,WAAW,CA8BvG;AAED,mGAAmG;AACnG,wBAAgB,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,WAAW,CA0CtG;AAED,eAAO,MAAM,SAAS,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,SAAS,QAAQ,EACe,CAAC"}
|
package/lib/sync.js
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
9
9
|
import { dirname, join } from 'node:path';
|
|
10
10
|
import { collectAssets, targetOf } from './assets.js';
|
|
11
|
-
import { readCatalog, variantGaps } from './catalog.js';
|
|
11
|
+
import { brokenLinks, readCatalog, variantGaps } from './catalog.js';
|
|
12
12
|
import { pathOf, planCompanion } from './companion.js';
|
|
13
13
|
import { OVERRIDES_DIR } from './config.js';
|
|
14
|
-
import {
|
|
14
|
+
import { bindingsOf as declaredIn, unboundHooks } from './hooks-map.js';
|
|
15
15
|
import { isPending, isRefusal, planFile } from './plan.js';
|
|
16
16
|
import { mergeDocuments, parseDocument, renderDocument } from './sections.js';
|
|
17
17
|
import { readStamped } from './stamp.js';
|
|
@@ -58,10 +58,7 @@ function bindingsOf(config, assetsDir) {
|
|
|
58
58
|
if (asset.kind !== 'hooks') {
|
|
59
59
|
continue;
|
|
60
60
|
}
|
|
61
|
-
|
|
62
|
-
if (binding) {
|
|
63
|
-
bindings.push(binding);
|
|
64
|
-
}
|
|
61
|
+
bindings.push(...declaredIn(asset.text, asset.target));
|
|
65
62
|
}
|
|
66
63
|
return bindings;
|
|
67
64
|
}
|
|
@@ -88,6 +85,7 @@ export function planSync(config, root, version, assetsDir) {
|
|
|
88
85
|
abandoned: abandonedOf(config, root, assetsDir),
|
|
89
86
|
gaps: variantGaps(readCatalog(assetsDir), config),
|
|
90
87
|
unbound: unboundHooks(bindingsOf(config, assetsDir), root),
|
|
88
|
+
broken: brokenLinks(readCatalog(assetsDir), config),
|
|
91
89
|
written: [],
|
|
92
90
|
};
|
|
93
91
|
}
|