@rt-tools/agent-kit 0.10.0 → 0.11.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 (91) hide show
  1. package/assets/checks/check-state-next.mjs +194 -0
  2. package/assets/checks/check-states.mjs +142 -0
  3. package/assets/checks/check-turn-map.mjs +146 -0
  4. package/assets/commands/agent-kit-digest.md +6 -5
  5. package/assets/defaults/project.sh +58 -6
  6. package/assets/defaults/turn-map.md +46 -0
  7. package/assets/hooks/browser-device-id.sh +2 -0
  8. package/assets/hooks/browser-guard-device-id.sh +2 -0
  9. package/assets/hooks/browser-guard-no-asking.sh +2 -0
  10. package/assets/hooks/browser-guard-no-listing.sh +2 -0
  11. package/assets/hooks/browser-guard-no-other-drivers.sh +2 -0
  12. package/assets/hooks/browser-guard-require-select.sh +2 -0
  13. package/assets/hooks/claim-guard.sh +2 -0
  14. package/assets/hooks/commit-msg.sh +2 -0
  15. package/assets/hooks/conscience-guard.sh +2 -0
  16. package/assets/hooks/constitution-index.sh +2 -0
  17. package/assets/hooks/dev-server-guard.sh +2 -0
  18. package/assets/hooks/docs-guard.sh +2 -0
  19. package/assets/hooks/exam-guard.sh +2 -0
  20. package/assets/hooks/git-guard-delivery-signature.sh +2 -0
  21. package/assets/hooks/git-guard-delivery.sh +2 -0
  22. package/assets/hooks/git-guard-main.sh +2 -0
  23. package/assets/hooks/git-guard-push-tests.sh +2 -0
  24. package/assets/hooks/glossary-load.sh +2 -0
  25. package/assets/hooks/grill-gate.sh +2 -0
  26. package/assets/hooks/handoff-entry-guard.sh +2 -0
  27. package/assets/hooks/handoff-write.sh +103 -0
  28. package/assets/hooks/lint-after-edit.sh +2 -0
  29. package/assets/hooks/observe.sh +2 -0
  30. package/assets/hooks/postmortem-guard.sh +2 -0
  31. package/assets/hooks/proposal-guard.sh +2 -0
  32. package/assets/hooks/prose-style-guard.sh +2 -0
  33. package/assets/hooks/qa-dataid-guard.sh +2 -0
  34. package/assets/hooks/rerun-guard.sh +2 -0
  35. package/assets/hooks/reuse-first-guard.sh +2 -0
  36. package/assets/hooks/roles.sh +2 -0
  37. package/assets/hooks/skill-gate-layers.sh +2 -0
  38. package/assets/hooks/skill-gate-rearm.sh +2 -0
  39. package/assets/hooks/skill-gate.sh +2 -0
  40. package/assets/hooks/skill-loaded.sh +2 -0
  41. package/assets/hooks/sql-guard-parse.sh +2 -0
  42. package/assets/hooks/sql-guard-request.sh +2 -0
  43. package/assets/hooks/sql-guard-target.sh +2 -0
  44. package/assets/hooks/sql-guard-write.sh +2 -0
  45. package/assets/hooks/sql-guard.sh +2 -0
  46. package/assets/hooks/task-context-load.sh +2 -0
  47. package/assets/hooks/task-flow-guard.sh +2 -0
  48. package/assets/hooks/turn-entry-load.sh +62 -0
  49. package/assets/hooks/turn-exit-guard.sh +2 -0
  50. package/assets/hooks/utf8.sh +35 -0
  51. package/assets/hooks/waiting-turn-guard.sh +2 -0
  52. package/assets/hooks/window-fill-guard.sh +31 -1
  53. package/assets/laws/work-conduct.md +29 -0
  54. package/assets/patterns/cargo-triage-mark.md +119 -0
  55. package/assets/patterns/task-flow-close.md +21 -0
  56. package/assets/patterns/task-flow-handoff.md +18 -0
  57. package/assets/patterns/task-flow-resume.md +6 -0
  58. package/assets/patterns/task-flow-start.md +41 -0
  59. package/assets/patterns/turn-entry-map.md +81 -0
  60. package/assets/rules/cargo-triage.md +126 -0
  61. package/assets/rules/task-flow.md +14 -1
  62. package/assets/rules/turn-entry.md +93 -0
  63. package/bin/agent-kit.d.ts.map +1 -1
  64. package/bin/agent-kit.js +42 -1
  65. package/bin/agent-kit.js.map +1 -1
  66. package/lib/cargo-state.d.ts +62 -0
  67. package/lib/cargo-state.d.ts.map +1 -0
  68. package/lib/cargo-state.js +118 -0
  69. package/lib/cargo-state.js.map +1 -0
  70. package/lib/cargo.d.ts +42 -0
  71. package/lib/cargo.d.ts.map +1 -1
  72. package/lib/cargo.js +2 -0
  73. package/lib/cargo.js.map +1 -1
  74. package/lib/commands.d.ts.map +1 -1
  75. package/lib/commands.js +64 -4
  76. package/lib/commands.js.map +1 -1
  77. package/lib/observations.d.ts +35 -1
  78. package/lib/observations.d.ts.map +1 -1
  79. package/lib/observations.js +14 -2
  80. package/lib/observations.js.map +1 -1
  81. package/lib/ship.d.ts +3 -0
  82. package/lib/ship.d.ts.map +1 -1
  83. package/lib/ship.js +56 -0
  84. package/lib/ship.js.map +1 -1
  85. package/lib/thresholds.d.ts +49 -0
  86. package/lib/thresholds.d.ts.map +1 -0
  87. package/lib/thresholds.js +151 -0
  88. package/lib/thresholds.js.map +1 -0
  89. package/package.json +1 -1
  90. package/rt-tools-agent-kit-0.11.0.tgz +0 -0
  91. package/rt-tools-agent-kit-0.10.0.tgz +0 -0
@@ -1 +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';\nimport { byText } from './order.js';\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/** Раньше ли день черты. Дни записаны `ГГГГ-ММ-ДД`, и порядок строк у них хронологический. */\nfunction isBefore(day: string, edge: string): boolean {\n return byText(day, edge) < 0;\n}\n\n/** Записи одного дня из его файла. Строка, которая не разбирается, пропускается молча. */\nfunction observationsOfDay(path: string): readonly IObservation[] {\n const found: IObservation[] = [];\n\n for (const line of readFileSync(path, 'utf8').split('\\n')) {\n const one: IObservation | null = parseObservation(line);\n\n if (one) {\n found.push(one);\n }\n }\n\n return found;\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(byText)) {\n const day: string | null = dayOf(name);\n\n if (!day) {\n continue;\n }\n\n if (isBefore(day, keepFrom)) {\n rmSync(join(dir, name), { force: true });\n swept.push(name);\n } else if (isBefore(day, since)) {\n // День старше запрошенного отрезка, но моложе черты хранения: файл остаётся лежать,\n // а в этот отчёт не идёт\n } else {\n observations.push(...observationsOfDay(join(dir, name)));\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(byText),\n versions: [...new Set(observations.map((entry: IObservation): string => entry.version).filter(Boolean))].sort(byText),\n total: observations.length,\n };\n}\n"]}
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';\nimport { byText } from './order.js';\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\n/**\n * Вес ресурса на диске, байтами: имя правила в число.\n *\n * Приходит доводом, а не читается здесь, по той же причине, что и день: разбор отделён от диска,\n * и сводку, читающую каталог правил сама, пришлось бы проверять подложенными файлами.\n */\nexport type TWeights = Readonly<Record<string, number>>;\n\nexport interface ISummary {\n readonly days: number;\n /** Сколько заходов оставило хоть одно наблюдение. */\n readonly sessions: number;\n readonly loads: readonly ICount[];\n /**\n * Вес того, что заходы прочитали за отрезок, байтами. Каждая загрузка считается своим весом:\n * правило, открытое в трёх заходах, стоило места трижды — тем, что оно одно, это не дешевле.\n * Ресурс, веса которого не назвали, считается нулём: врать числом хуже, чем не знать его.\n */\n readonly bytes: number;\n /**\n * Тот же вес, приходящийся на один заход, — цена входа в работу.\n *\n * Ноль заходов даёт ноль, а не отказ: сводка за пустой отрезок печатается наравне с полной,\n * и деление на ноль уронило бы её на том единственном случае, ради которого её и зовут, —\n * «а было ли вообще что-нибудь».\n */\n readonly bytesPerSession: number;\n /** Отбития гейта по правилам. */\n readonly denials: readonly ICount[];\n /** Род правки, на котором гейт отбивал. */\n readonly kinds: readonly ICount[];\n /**\n * Отбития, пришедшие не на правку файла: род правки — команда оболочки или браузер.\n *\n * Считается здесь, а не выводится читателем из разбивки по родам: доля, которую надо\n * сложить глазами из десятка строк, не читается вовсе — а именно она говорит, требует ли\n * гейт правило под то, что правкой не является.\n */\n readonly denialsOffFile: number;\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/** Раньше ли день черты. Дни записаны `ГГГГ-ММ-ДД`, и порядок строк у них хронологический. */\nfunction isBefore(day: string, edge: string): boolean {\n return byText(day, edge) < 0;\n}\n\n/** Записи одного дня из его файла. Строка, которая не разбирается, пропускается молча. */\nfunction observationsOfDay(path: string): readonly IObservation[] {\n const found: IObservation[] = [];\n\n for (const line of readFileSync(path, 'utf8').split('\\n')) {\n const one: IObservation | null = parseObservation(line);\n\n if (one) {\n found.push(one);\n }\n }\n\n return found;\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(byText)) {\n const day: string | null = dayOf(name);\n\n if (!day) {\n continue;\n }\n\n if (isBefore(day, keepFrom)) {\n rmSync(join(dir, name), { force: true });\n swept.push(name);\n } else if (isBefore(day, since)) {\n // День старше запрошенного отрезка, но моложе черты хранения: файл остаётся лежать,\n // а в этот отчёт не идёт\n } else {\n observations.push(...observationsOfDay(join(dir, name)));\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/** Роды правки, которые правкой файла не являются: гейт сработал не на файле. */\nconst OFF_FILE_KINDS: readonly string[] = ['command', 'browser'];\n\n/**\n * Сводка за отрезок.\n *\n * `known` — имена правил, разложенных в это дерево. Без них сводка отвечает только на вопрос\n * «чем пользовались», а самое ценное — чем не пользовались ни разу — сказать нечем: в самих\n * наблюдениях незагруженного правила нет по определению.\n *\n * `weights` — вес этих правил на диске. Без них сводка говорит, сколько раз правило открывали, и\n * молчит о том, во что это обошлось: тридцать загрузок паттерна и тридцать загрузок правила на\n * пятьсот строк стоят разного, а в счёте выглядят одинаково. Не назван — вес считается нулём, и\n * строки о нём в выводе не будет вовсе.\n */\nexport function summarize(observations: readonly IObservation[], known: readonly string[], days: number, weights: TWeights = {}): 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 const sessions: number = new Set(observations.map((entry: IObservation): string => entry.session).filter(Boolean)).size;\n const bytes: number = loads.reduce((found: number, entry: IObservation): number => found + (weights[entry.resource] ?? 0), 0);\n\n return {\n days,\n sessions,\n bytes,\n loads: countBy(loads.map((entry: IObservation): string => entry.resource)),\n bytesPerSession: sessions > 0 ? Math.round(bytes / sessions) : 0,\n denials: countBy(denials.map((entry: IObservation): string => entry.resource)),\n kinds: countBy(denials.map((entry: IObservation): string => entry.kind)),\n denialsOffFile: denials.filter((entry: IObservation): boolean => OFF_FILE_KINDS.includes(entry.kind)).length,\n guards: countBy(of(observations, 'guard-deny').map((entry: IObservation): string => entry.resource)),\n unused: known.filter((name: string): boolean => !loaded.has(name)).sort(byText),\n versions: [...new Set(observations.map((entry: IObservation): string => entry.version).filter(Boolean))].sort(byText),\n total: observations.length,\n };\n}\n"]}
package/lib/ship.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { IIntakeAccepted } from './cargo.js';
2
+ import { TMarkCall } from './cargo-state.js';
2
3
  /**
3
4
  * Предел ожидания приёма. Без него молчащая — не отказавшая — служба держит отправку до
4
5
  * умолчания среды, а это минуты: человек, позвавший команду, видит не отказ, а зависший запуск.
@@ -36,4 +37,6 @@ export declare function intakeUrl(intake: string, operation: string): string;
36
37
  export declare function readToken(root: string, spoken: string): string;
37
38
  /** Запрос приёма. Токен уезжает заголовком и в теле не появляется ни разу. */
38
39
  export declare const httpShip: TShip;
40
+ /** Запрос правки состояния. Токен уезжает заголовком и в теле не появляется ни разу. */
41
+ export declare const httpMark: TMarkCall;
39
42
  //# sourceMappingURL=ship.d.ts.map
package/lib/ship.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ship.d.ts","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/ship.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,eAAe,EAAqB,MAAM,YAAY,CAAC;AAEhE;;;GAGG;AACH,eAAO,MAAM,eAAe,EAAE,MAAe,CAAC;AAE9C,4EAA4E;AAC5E,MAAM,WAAW,SAAS;IACtB,gGAAgG;IAChG,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,8EAA8E;IAC9E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CAC1B;AAED,+FAA+F;AAC/F,MAAM,WAAW,QAAQ;IACrB,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,0FAA0F;IAC1F,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,uFAAuF;IACvF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,yFAAyF;IACzF,QAAQ,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,CAAC;CAC7C;AAED,wDAAwD;AACxD,MAAM,MAAM,KAAK,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAa9F,qFAAqF;AACrF,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAEnE;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAQ9D;AAqCD,8EAA8E;AAC9E,eAAO,MAAM,QAAQ,EAAE,KAiBtB,CAAC"}
1
+ {"version":3,"file":"ship.d.ts","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/ship.ts"],"names":[],"mappings":"AAcA,OAAO,EAA2D,eAAe,EAAsC,MAAM,YAAY,CAAC;AAC1I,OAAO,EAAW,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAEtD;;;GAGG;AACH,eAAO,MAAM,eAAe,EAAE,MAAe,CAAC;AAE9C,4EAA4E;AAC5E,MAAM,WAAW,SAAS;IACtB,gGAAgG;IAChG,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,8EAA8E;IAC9E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CAC1B;AAED,+FAA+F;AAC/F,MAAM,WAAW,QAAQ;IACrB,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,0FAA0F;IAC1F,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,uFAAuF;IACvF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,yFAAyF;IACzF,QAAQ,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,CAAC;CAC7C;AAED,wDAAwD;AACxD,MAAM,MAAM,KAAK,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAa9F,qFAAqF;AACrF,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAEnE;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAQ9D;AAqCD,8EAA8E;AAC9E,eAAO,MAAM,QAAQ,EAAE,KAiBtB,CAAC;AAwCF,wFAAwF;AACxF,eAAO,MAAM,QAAQ,EAAE,SAsBtB,CAAC"}
package/lib/ship.js CHANGED
@@ -92,4 +92,60 @@ export const httpShip = async (intake, token, shipment) => {
92
92
  const text = await answer.text();
93
93
  return { ok: answer.ok, status: answer.status, said: saidOf(text), accepted: answer.ok ? acceptedOf(text) : null };
94
94
  };
95
+ /** Отбитые строки из ответа приёма. Не список — пусто: своё устройство приём не пересказывает. */
96
+ function deniedOf(raw) {
97
+ if (!Array.isArray(raw)) {
98
+ return [];
99
+ }
100
+ return raw
101
+ .filter((one) => typeof one === 'object' && one !== null)
102
+ .map((one) => {
103
+ const line = one;
104
+ return {
105
+ at: typeof line['at'] === 'number' ? line['at'] : 0,
106
+ kind: String(line['kind']),
107
+ key: String(line['key']),
108
+ denial: String(line['denial']),
109
+ };
110
+ });
111
+ }
112
+ /** Два числа и отбитые строки из ответа приёма. Ответ не тем — пусто. */
113
+ function stateAcceptedOf(text) {
114
+ try {
115
+ const said = JSON.parse(text);
116
+ return typeof said['tree'] === 'string' && typeof said['changed'] === 'number'
117
+ ? {
118
+ tree: said['tree'],
119
+ changed: said['changed'],
120
+ same: typeof said['same'] === 'number' ? said['same'] : 0,
121
+ denied: deniedOf(said['denied']),
122
+ }
123
+ : null;
124
+ }
125
+ catch {
126
+ return null;
127
+ }
128
+ }
129
+ /** Запрос правки состояния. Токен уезжает заголовком и в теле не появляется ни разу. */
130
+ export const httpMark = async (intake, token, body) => {
131
+ let answer;
132
+ try {
133
+ answer = await fetch(intakeUrl(intake, 'states'), {
134
+ method: 'POST',
135
+ headers: { 'content-type': 'application/json', [TREE_TOKEN_HEADER]: token },
136
+ body: JSON.stringify(body),
137
+ signal: AbortSignal.timeout(SHIP_TIMEOUT_MS),
138
+ });
139
+ }
140
+ catch (error) {
141
+ return { ok: false, status: 0, said: error.message, accepted: null };
142
+ }
143
+ const text = await answer.text();
144
+ return {
145
+ ok: answer.ok,
146
+ status: answer.status,
147
+ said: saidOf(text),
148
+ accepted: answer.ok ? stateAcceptedOf(text) : null,
149
+ };
150
+ };
95
151
  //# sourceMappingURL=ship.js.map
package/lib/ship.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ship.js","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/ship.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Отправка груза в приём: единственное место пакета, которое ходит в сеть.\n *\n * Ходит оно только по команде человека — ни один гард в сеть не ходит вовсе. Вынесено отдельно,\n * чтобы сборка груза осталась проверяемой: спека подставляет двойник и никуда не стучится.\n *\n * Груз уезжает в закрытый приём, а не в открытую очередь работ: сводка говорит о рабочих\n * привычках команды — чем пользуются, обо что спотыкаются, сколько раз признавали промах, — и в\n * открытой очереди это выложено всему свету.\n */\nimport { existsSync, readFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\n\nimport { IIntakeAccepted, TREE_TOKEN_HEADER } from './cargo.js';\n\n/**\n * Предел ожидания приёма. Без него молчащая — не отказавшая — служба держит отправку до\n * умолчания среды, а это минуты: человек, позвавший команду, видит не отказ, а зависший запуск.\n */\nexport const SHIP_TIMEOUT_MS: number = 15_000;\n\n/** Что уезжает одним запросом: род груза словом, операция приёма и тело. */\nexport interface IShipment {\n /** Род груза словами — его называет отказ: дерево шлёт три рода и должно знать, какой отбит. */\n readonly kind: string;\n /** Последнее звено адреса операции: `summary`, `proposals`, `postmortems`. */\n readonly operation: string;\n readonly body: unknown;\n}\n\n/** Чем кончился один запрос. Отказ — такой же ответ, как принятое: его печатает вызывающий. */\nexport interface IShipped {\n readonly ok: boolean;\n /** Код ответа. Ноль — приём не ответил вовсе: не дозвонились, оборвалось, вышло время. */\n readonly status: number;\n /** Что сказал приём. У неотвеченного запроса — причина, по которой он не состоялся. */\n readonly said: string;\n /** Дерево и месяц записи. Пусто у отказа: приём их называет только у принятого груза. */\n readonly accepted: IIntakeAccepted | null;\n}\n\n/** Чем груз уезжает. Двойник в спеке — того же вида. */\nexport type TShip = (intake: string, token: string, shipment: IShipment) => Promise<IShipped>;\n\n/** Адрес без косых черт в конце: их считает сам вызывающий, а не движок разбора образцов. */\nfunction withoutTrailingSlash(address: string): string {\n let end: number = address.length;\n\n while (end > 0 && address[end - 1] === '/') {\n end -= 1;\n }\n\n return address.slice(0, end);\n}\n\n/** Адрес операции приёма. Косая черта в конце адреса приёма второй не становится. */\nexport function intakeUrl(intake: string, operation: string): string {\n return `${withoutTrailingSlash(intake)}/api/intake/${operation}`;\n}\n\n/**\n * Токен дерева из файла, названного настройкой.\n *\n * Путь читается и от корня дерева, и от домашнего каталога: токен обязан лежать вне дерева —\n * конфиг коммитится, а токен переживать историю не должен. Файла нет — пустая строка, и отказ\n * об этом скажет словами.\n */\nexport function readToken(root: string, spoken: string): string {\n if (!spoken) {\n return '';\n }\n\n const path: string = spoken.startsWith('~/') ? join(homedir(), spoken.slice(2)) : join(root, spoken);\n\n return existsSync(path) ? readFileSync(path, 'utf8').trim() : '';\n}\n\n/** Число из ответа приёма. Не число — пусто: поля есть только у предложений и не у всякого приёма. */\nfunction countOf(said: Record<string, unknown>, field: string): number | undefined {\n return typeof said[field] === 'number' ? said[field] : undefined;\n}\n\n/** Дерево и месяц из ответа приёма. Ответ не тем — пусто: своё устройство приём не пересказывает. */\nfunction acceptedOf(text: string): IIntakeAccepted | null {\n try {\n const said: Record<string, unknown> = JSON.parse(text) as Record<string, unknown>;\n\n return typeof said['tree'] === 'string' && typeof said['month'] === 'string'\n ? {\n tree: said['tree'],\n month: said['month'],\n created: said['created'] === true,\n added: countOf(said, 'added'),\n known: countOf(said, 'known'),\n }\n : null;\n } catch {\n return null;\n }\n}\n\n/** Что сказал приём: его собственное сообщение, а если тело не разбирается — тело как приехало. */\nfunction saidOf(text: string): string {\n try {\n const said: Record<string, unknown> = JSON.parse(text) as Record<string, unknown>;\n\n return typeof said['message'] === 'string' ? said['message'] : text.trim();\n } catch {\n return text.trim();\n }\n}\n\n/** Запрос приёма. Токен уезжает заголовком и в теле не появляется ни разу. */\nexport const httpShip: TShip = async (intake: string, token: string, shipment: IShipment): Promise<IShipped> => {\n let answer: Response;\n\n try {\n answer = await fetch(intakeUrl(intake, shipment.operation), {\n method: 'POST',\n headers: { 'content-type': 'application/json', [TREE_TOKEN_HEADER]: token },\n body: JSON.stringify(shipment.body),\n signal: AbortSignal.timeout(SHIP_TIMEOUT_MS),\n });\n } catch (error: unknown) {\n return { ok: false, status: 0, said: (error as Error).message, accepted: null };\n }\n\n const text: string = await answer.text();\n\n return { ok: answer.ok, status: answer.status, said: saidOf(text), accepted: answer.ok ? acceptedOf(text) : null };\n};\n"]}
1
+ {"version":3,"file":"ship.js","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/ship.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Отправка груза в приём: единственное место пакета, которое ходит в сеть.\n *\n * Ходит оно только по команде человека — ни один гард в сеть не ходит вовсе. Вынесено отдельно,\n * чтобы сборка груза осталась проверяемой: спека подставляет двойник и никуда не стучится.\n *\n * Груз уезжает в закрытый приём, а не в открытую очередь работ: сводка говорит о рабочих\n * привычках команды — чем пользуются, обо что спотыкаются, сколько раз признавали промах, — и в\n * открытой очереди это выложено всему свету.\n */\nimport { existsSync, readFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\n\nimport { ICargoStateAccepted, ICargoStateBody, ICargoStateDenied, IIntakeAccepted, TCargoStateKind, TREE_TOKEN_HEADER } from './cargo.js';\nimport { IMarked, TMarkCall } from './cargo-state.js';\n\n/**\n * Предел ожидания приёма. Без него молчащая — не отказавшая — служба держит отправку до\n * умолчания среды, а это минуты: человек, позвавший команду, видит не отказ, а зависший запуск.\n */\nexport const SHIP_TIMEOUT_MS: number = 15_000;\n\n/** Что уезжает одним запросом: род груза словом, операция приёма и тело. */\nexport interface IShipment {\n /** Род груза словами — его называет отказ: дерево шлёт три рода и должно знать, какой отбит. */\n readonly kind: string;\n /** Последнее звено адреса операции: `summary`, `proposals`, `postmortems`. */\n readonly operation: string;\n readonly body: unknown;\n}\n\n/** Чем кончился один запрос. Отказ — такой же ответ, как принятое: его печатает вызывающий. */\nexport interface IShipped {\n readonly ok: boolean;\n /** Код ответа. Ноль — приём не ответил вовсе: не дозвонились, оборвалось, вышло время. */\n readonly status: number;\n /** Что сказал приём. У неотвеченного запроса — причина, по которой он не состоялся. */\n readonly said: string;\n /** Дерево и месяц записи. Пусто у отказа: приём их называет только у принятого груза. */\n readonly accepted: IIntakeAccepted | null;\n}\n\n/** Чем груз уезжает. Двойник в спеке — того же вида. */\nexport type TShip = (intake: string, token: string, shipment: IShipment) => Promise<IShipped>;\n\n/** Адрес без косых черт в конце: их считает сам вызывающий, а не движок разбора образцов. */\nfunction withoutTrailingSlash(address: string): string {\n let end: number = address.length;\n\n while (end > 0 && address[end - 1] === '/') {\n end -= 1;\n }\n\n return address.slice(0, end);\n}\n\n/** Адрес операции приёма. Косая черта в конце адреса приёма второй не становится. */\nexport function intakeUrl(intake: string, operation: string): string {\n return `${withoutTrailingSlash(intake)}/api/intake/${operation}`;\n}\n\n/**\n * Токен дерева из файла, названного настройкой.\n *\n * Путь читается и от корня дерева, и от домашнего каталога: токен обязан лежать вне дерева —\n * конфиг коммитится, а токен переживать историю не должен. Файла нет — пустая строка, и отказ\n * об этом скажет словами.\n */\nexport function readToken(root: string, spoken: string): string {\n if (!spoken) {\n return '';\n }\n\n const path: string = spoken.startsWith('~/') ? join(homedir(), spoken.slice(2)) : join(root, spoken);\n\n return existsSync(path) ? readFileSync(path, 'utf8').trim() : '';\n}\n\n/** Число из ответа приёма. Не число — пусто: поля есть только у предложений и не у всякого приёма. */\nfunction countOf(said: Record<string, unknown>, field: string): number | undefined {\n return typeof said[field] === 'number' ? said[field] : undefined;\n}\n\n/** Дерево и месяц из ответа приёма. Ответ не тем — пусто: своё устройство приём не пересказывает. */\nfunction acceptedOf(text: string): IIntakeAccepted | null {\n try {\n const said: Record<string, unknown> = JSON.parse(text) as Record<string, unknown>;\n\n return typeof said['tree'] === 'string' && typeof said['month'] === 'string'\n ? {\n tree: said['tree'],\n month: said['month'],\n created: said['created'] === true,\n added: countOf(said, 'added'),\n known: countOf(said, 'known'),\n }\n : null;\n } catch {\n return null;\n }\n}\n\n/** Что сказал приём: его собственное сообщение, а если тело не разбирается — тело как приехало. */\nfunction saidOf(text: string): string {\n try {\n const said: Record<string, unknown> = JSON.parse(text) as Record<string, unknown>;\n\n return typeof said['message'] === 'string' ? said['message'] : text.trim();\n } catch {\n return text.trim();\n }\n}\n\n/** Запрос приёма. Токен уезжает заголовком и в теле не появляется ни разу. */\nexport const httpShip: TShip = async (intake: string, token: string, shipment: IShipment): Promise<IShipped> => {\n let answer: Response;\n\n try {\n answer = await fetch(intakeUrl(intake, shipment.operation), {\n method: 'POST',\n headers: { 'content-type': 'application/json', [TREE_TOKEN_HEADER]: token },\n body: JSON.stringify(shipment.body),\n signal: AbortSignal.timeout(SHIP_TIMEOUT_MS),\n });\n } catch (error: unknown) {\n return { ok: false, status: 0, said: (error as Error).message, accepted: null };\n }\n\n const text: string = await answer.text();\n\n return { ok: answer.ok, status: answer.status, said: saidOf(text), accepted: answer.ok ? acceptedOf(text) : null };\n};\n\n/** Отбитые строки из ответа приёма. Не список — пусто: своё устройство приём не пересказывает. */\nfunction deniedOf(raw: unknown): readonly ICargoStateDenied[] {\n if (!Array.isArray(raw)) {\n return [];\n }\n\n return raw\n .filter((one: unknown): boolean => typeof one === 'object' && one !== null)\n .map((one: unknown): ICargoStateDenied => {\n const line: Record<string, unknown> = one as Record<string, unknown>;\n\n return {\n at: typeof line['at'] === 'number' ? line['at'] : 0,\n kind: String(line['kind']) as TCargoStateKind,\n key: String(line['key']),\n denial: String(line['denial']),\n };\n });\n}\n\n/** Два числа и отбитые строки из ответа приёма. Ответ не тем — пусто. */\nfunction stateAcceptedOf(text: string): ICargoStateAccepted | null {\n try {\n const said: Record<string, unknown> = JSON.parse(text) as Record<string, unknown>;\n\n return typeof said['tree'] === 'string' && typeof said['changed'] === 'number'\n ? {\n tree: said['tree'],\n changed: said['changed'],\n same: typeof said['same'] === 'number' ? said['same'] : 0,\n denied: deniedOf(said['denied']),\n }\n : null;\n } catch {\n return null;\n }\n}\n\n/** Запрос правки состояния. Токен уезжает заголовком и в теле не появляется ни разу. */\nexport const httpMark: TMarkCall = async (intake: string, token: string, body: ICargoStateBody): Promise<IMarked> => {\n let answer: Response;\n\n try {\n answer = await fetch(intakeUrl(intake, 'states'), {\n method: 'POST',\n headers: { 'content-type': 'application/json', [TREE_TOKEN_HEADER]: token },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(SHIP_TIMEOUT_MS),\n });\n } catch (error: unknown) {\n return { ok: false, status: 0, said: (error as Error).message, accepted: null };\n }\n\n const text: string = await answer.text();\n\n return {\n ok: answer.ok,\n status: answer.status,\n said: saidOf(text),\n accepted: answer.ok ? stateAcceptedOf(text) : null,\n };\n};\n"]}
@@ -0,0 +1,49 @@
1
+ /** Пороги, объявленные деревом. Размер окна не объявлен — стража нет, и судить нечего. */
2
+ export interface IThresholds {
3
+ /** Размер окна захода в токенах: `RT_WINDOW_TOKENS`. */
4
+ readonly window: number;
5
+ /** Доля окна, на которой страж напоминает: `RT_WINDOW_WARN_PCT`. */
6
+ readonly warnPct: number;
7
+ /** Доля окна, на которой страж отбивает работу: `RT_WINDOW_STOP_PCT`. */
8
+ readonly stopPct: number;
9
+ /** Запас между порогом сжатия и порогом остановки: `RT_WINDOW_MARGIN_PCT`. */
10
+ readonly marginPct: number;
11
+ /** Размер окна автосжатия: настройка `autoCompactWindow`. Не объявлен — `null`. */
12
+ readonly compactWindow: number | null;
13
+ /** Доля, на которой приходит сжатие: `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE`. Не объявлена — `null`. */
14
+ readonly compactPct: number | null;
15
+ }
16
+ /** Расхождение пары: что с чем разъехалось, какими числами и чем это кончится для захода. */
17
+ export interface IThresholdDrift {
18
+ /** Что разошлось, словами читателя. */
19
+ readonly what: string;
20
+ /** Что объявлено стражу. */
21
+ readonly guard: string;
22
+ /** Что объявлено сжатию. */
23
+ readonly compact: string;
24
+ /** Чем это кончится для захода. Пусто у расхождения, которое говорит само за себя. */
25
+ readonly why: string;
26
+ }
27
+ /**
28
+ * Что дерево объявило о порогах.
29
+ *
30
+ * Настройку читаем разбором и только им: пороги лежат полями объекта, и вынуть их из текста
31
+ * поиском значило бы гадать. Не разобралась — молчим целиком, как молчит карта хуков: назвать
32
+ * расхождением непрочитанное хуже, чем не назвать ничего.
33
+ */
34
+ export declare function readThresholds(root: string): IThresholds | null;
35
+ /**
36
+ * Чем пара стража разошлась с парой сжатия.
37
+ *
38
+ * Незаданное расхождением не считается: дерево вправе не задавать порога сжатия вовсе — тогда
39
+ * его назначает инструмент, и сказать об этом надо один раз, а не двумя строками о разнице.
40
+ *
41
+ * Долю судит расстояние, а не равенство. Три случая дают расхождение, и у каждого свой исход
42
+ * для захода: сжатие выше остановки — страж отбивает раньше, чем клиент успевает начать;
43
+ * сжатие вровень с остановкой — гонка, которую страж выигрывает; сжатие ниже, но ближе
44
+ * объявленного запаса — клиент начать успевает, а докончить нет.
45
+ */
46
+ export declare function thresholdDrift(thresholds: IThresholds): readonly IThresholdDrift[];
47
+ /** Раздел разбора состояния: числа порогов и то, что о них известно. */
48
+ export declare function thresholdLines(root: string): string[];
49
+ //# sourceMappingURL=thresholds.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"thresholds.d.ts","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/thresholds.ts"],"names":[],"mappings":"AA0CA,0FAA0F;AAC1F,MAAM,WAAW,WAAW;IACxB,wDAAwD;IACxD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,oEAAoE;IACpE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,yEAAyE;IACzE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,8EAA8E;IAC9E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,mFAAmF;IACnF,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,kGAAkG;IAClG,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;AAED,6FAA6F;AAC7F,MAAM,WAAW,eAAe;IAC5B,uCAAuC;IACvC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,4BAA4B;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,4BAA4B;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,sFAAsF;IACtF,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACxB;AAYD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CA2B/D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,WAAW,GAAG,SAAS,eAAe,EAAE,CA0ClF;AAED,wEAAwE;AACxE,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAkCrD"}
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Пороги окна захода: где заход останавливается и где инструмент сжимает контекст сам.
3
+ *
4
+ * Чисел два, и живут они в разных местах чужой настройки. Размер окна и доля, на которой страж
5
+ * останавливает заход, приходят переменными окружения — их читает профиль дерева. Порог, на
6
+ * котором инструмент сжимает контекст, задаётся своей парой: размером окна автосжатия и долей в
7
+ * процентах. Пакет ни тем ни другим не владеет — окно принадлежит дереву.
8
+ *
9
+ * Разъехавшись, пары дают заход, который либо сжимается раньше, чем передача написана, либо
10
+ * доживает до предела окна и роняет работу. Заметить это нечем: обе стороны по отдельности
11
+ * выглядят настроенными, а расхождение видно только тому, кто положит их рядом. Здесь их и
12
+ * кладут рядом — разбор состояния раскладки печатает числа и называет разошедшееся.
13
+ *
14
+ * Сведёнными считаются РАЗВЕДЁННЫЕ доли, а не совпавшие. Порог сжатия обязан стоять ниже порога
15
+ * остановки: первым обязано срабатывать то, что заход продолжает, а не то, что его
16
+ * останавливает. Совпавшая пара — гонка, и выигрывает её страж: он стоит на вызове инструмента,
17
+ * а сжатие приходит между ходами. Ровно так заход и вставал на пороге вместо того, чтобы
18
+ * продолжиться сжатым, — при том что прежняя сверка обе стороны считала настроенными и молчала.
19
+ *
20
+ * Насколько ниже — своё число дерева, `RT_WINDOW_MARGIN_PCT`. Запас объявляется, а не выводится
21
+ * разницей: сжатие идёт не мгновенно, и разница в один процент требование «ниже» удовлетворяет,
22
+ * а работу не спасает.
23
+ *
24
+ * Размер окна при этом сверяется по-прежнему на равенство: это одно и то же окно, названное в
25
+ * двух местах, и разные числа там означают, что одна из сторон считает долю не от того.
26
+ *
27
+ * Дерево, не объявившее размера окна, стража не получает вовсе, и судить у него нечего: раздел
28
+ * молчит целиком, а не краснеет отсутствием.
29
+ */
30
+ import { existsSync, readFileSync } from 'node:fs';
31
+ import { join } from 'node:path';
32
+ import { SETTINGS_PATH } from './hooks-map.js';
33
+ /** Доля окна, на которой страж напоминает выбрать точку остановки. Умолчание профиля. */
34
+ const WARN_PCT_DEFAULT = 40;
35
+ /** Доля окна, на которой страж отбивает всё, кроме закрытия захода. Умолчание профиля. */
36
+ const STOP_PCT_DEFAULT = 50;
37
+ /** На сколько долей окна порог сжатия обязан стоять ниже порога остановки. Умолчание профиля. */
38
+ const MARGIN_PCT_DEFAULT = 5;
39
+ /**
40
+ * Число из строки настройки. Пусто, не число, ноль и меньше — ничего: значение, которого нельзя
41
+ * посчитать, объявленным не считается, иначе расхождением станет опечатка, а не расхождение.
42
+ */
43
+ function numberOf(value) {
44
+ const parsed = typeof value === 'number' ? value : Number.parseInt(String(value ?? ''), 10);
45
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
46
+ }
47
+ /**
48
+ * Что дерево объявило о порогах.
49
+ *
50
+ * Настройку читаем разбором и только им: пороги лежат полями объекта, и вынуть их из текста
51
+ * поиском значило бы гадать. Не разобралась — молчим целиком, как молчит карта хуков: назвать
52
+ * расхождением непрочитанное хуже, чем не назвать ничего.
53
+ */
54
+ export function readThresholds(root) {
55
+ const path = join(root, SETTINGS_PATH);
56
+ if (!existsSync(path)) {
57
+ return null;
58
+ }
59
+ let settings;
60
+ try {
61
+ settings = JSON.parse(readFileSync(path, 'utf8'));
62
+ }
63
+ catch {
64
+ return null;
65
+ }
66
+ const env = settings['env'] ?? {};
67
+ const windowTokens = numberOf(env['RT_WINDOW_TOKENS']);
68
+ if (windowTokens === null) {
69
+ return null;
70
+ }
71
+ return {
72
+ window: windowTokens,
73
+ warnPct: numberOf(env['RT_WINDOW_WARN_PCT']) ?? WARN_PCT_DEFAULT,
74
+ stopPct: numberOf(env['RT_WINDOW_STOP_PCT']) ?? STOP_PCT_DEFAULT,
75
+ marginPct: numberOf(env['RT_WINDOW_MARGIN_PCT']) ?? MARGIN_PCT_DEFAULT,
76
+ compactWindow: numberOf(settings['autoCompactWindow']),
77
+ compactPct: numberOf(env['CLAUDE_AUTOCOMPACT_PCT_OVERRIDE']),
78
+ };
79
+ }
80
+ /**
81
+ * Чем пара стража разошлась с парой сжатия.
82
+ *
83
+ * Незаданное расхождением не считается: дерево вправе не задавать порога сжатия вовсе — тогда
84
+ * его назначает инструмент, и сказать об этом надо один раз, а не двумя строками о разнице.
85
+ *
86
+ * Долю судит расстояние, а не равенство. Три случая дают расхождение, и у каждого свой исход
87
+ * для захода: сжатие выше остановки — страж отбивает раньше, чем клиент успевает начать;
88
+ * сжатие вровень с остановкой — гонка, которую страж выигрывает; сжатие ниже, но ближе
89
+ * объявленного запаса — клиент начать успевает, а докончить нет.
90
+ */
91
+ export function thresholdDrift(thresholds) {
92
+ const drift = [];
93
+ if (thresholds.compactWindow !== null && thresholds.compactWindow !== thresholds.window) {
94
+ drift.push({
95
+ what: 'размер окна',
96
+ guard: String(thresholds.window),
97
+ compact: String(thresholds.compactWindow),
98
+ why: 'доля считается не от того же окна',
99
+ });
100
+ }
101
+ const compactPct = thresholds.compactPct;
102
+ if (compactPct === null) {
103
+ return drift;
104
+ }
105
+ const gap = thresholds.stopPct - compactPct;
106
+ const tooHigh = 'сжатие стоит выше остановки: страж отбивает заход раньше, чем приходит сжатие';
107
+ const tied = 'пороги совпали: кто сработает первым, не назначено, и выигрывает страж';
108
+ if (gap <= 0) {
109
+ drift.push({
110
+ what: 'доля окна',
111
+ guard: `${thresholds.stopPct}%`,
112
+ compact: `${compactPct}%`,
113
+ why: gap < 0 ? tooHigh : tied,
114
+ });
115
+ return drift;
116
+ }
117
+ if (gap < thresholds.marginPct) {
118
+ drift.push({
119
+ what: 'запас между порогами',
120
+ guard: `${thresholds.marginPct}% объявлено`,
121
+ compact: `${gap}% на деле`,
122
+ why: 'сжатие идёт не мгновенно и может не успеть до порога остановки',
123
+ });
124
+ }
125
+ return drift;
126
+ }
127
+ /** Раздел разбора состояния: числа порогов и то, что о них известно. */
128
+ export function thresholdLines(root) {
129
+ const thresholds = readThresholds(root);
130
+ if (!thresholds) {
131
+ return [];
132
+ }
133
+ const lines = [
134
+ `окно захода: ${thresholds.window} токенов, напоминание ${thresholds.warnPct}%, остановка ${thresholds.stopPct}%`,
135
+ ];
136
+ if (thresholds.compactWindow === null && thresholds.compactPct === null) {
137
+ lines.push(' порог сжатия деревом не задан — его назначает инструмент, и передача пишется когда придётся', ` объяви в \`${SETTINGS_PATH}\`: \`autoCompactWindow\` числом ${thresholds.window}, а \`CLAUDE_AUTOCOMPACT_PCT_OVERRIDE\` — числом ${thresholds.stopPct - thresholds.marginPct}`);
138
+ return lines;
139
+ }
140
+ const drift = thresholdDrift(thresholds);
141
+ if (!drift.length) {
142
+ lines.push(` порог сжатия ниже порога остановки на ${thresholds.marginPct}% и более: сжатие приходит первым, страж остаётся страховкой`);
143
+ return lines;
144
+ }
145
+ lines.push(' порог сжатия разошёлся с порогом остановки:');
146
+ for (const one of drift) {
147
+ lines.push(` ${one.what}: у стража ${one.guard}, у сжатия ${one.compact}`, ` ${one.why}`);
148
+ }
149
+ return lines;
150
+ }
151
+ //# sourceMappingURL=thresholds.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"thresholds.js","sourceRoot":"","sources":["../../../projects/agent-kit/src/lib/thresholds.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Пороги окна захода: где заход останавливается и где инструмент сжимает контекст сам.\n *\n * Чисел два, и живут они в разных местах чужой настройки. Размер окна и доля, на которой страж\n * останавливает заход, приходят переменными окружения — их читает профиль дерева. Порог, на\n * котором инструмент сжимает контекст, задаётся своей парой: размером окна автосжатия и долей в\n * процентах. Пакет ни тем ни другим не владеет — окно принадлежит дереву.\n *\n * Разъехавшись, пары дают заход, который либо сжимается раньше, чем передача написана, либо\n * доживает до предела окна и роняет работу. Заметить это нечем: обе стороны по отдельности\n * выглядят настроенными, а расхождение видно только тому, кто положит их рядом. Здесь их и\n * кладут рядом — разбор состояния раскладки печатает числа и называет разошедшееся.\n *\n * Сведёнными считаются РАЗВЕДЁННЫЕ доли, а не совпавшие. Порог сжатия обязан стоять ниже порога\n * остановки: первым обязано срабатывать то, что заход продолжает, а не то, что его\n * останавливает. Совпавшая пара — гонка, и выигрывает её страж: он стоит на вызове инструмента,\n * а сжатие приходит между ходами. Ровно так заход и вставал на пороге вместо того, чтобы\n * продолжиться сжатым, — при том что прежняя сверка обе стороны считала настроенными и молчала.\n *\n * Насколько ниже — своё число дерева, `RT_WINDOW_MARGIN_PCT`. Запас объявляется, а не выводится\n * разницей: сжатие идёт не мгновенно, и разница в один процент требование «ниже» удовлетворяет,\n * а работу не спасает.\n *\n * Размер окна при этом сверяется по-прежнему на равенство: это одно и то же окно, названное в\n * двух местах, и разные числа там означают, что одна из сторон считает долю не от того.\n *\n * Дерево, не объявившее размера окна, стража не получает вовсе, и судить у него нечего: раздел\n * молчит целиком, а не краснеет отсутствием.\n */\nimport { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { SETTINGS_PATH } from './hooks-map.js';\n\n/** Доля окна, на которой страж напоминает выбрать точку остановки. Умолчание профиля. */\nconst WARN_PCT_DEFAULT: number = 40;\n\n/** Доля окна, на которой страж отбивает всё, кроме закрытия захода. Умолчание профиля. */\nconst STOP_PCT_DEFAULT: number = 50;\n\n/** На сколько долей окна порог сжатия обязан стоять ниже порога остановки. Умолчание профиля. */\nconst MARGIN_PCT_DEFAULT: number = 5;\n\n/** Пороги, объявленные деревом. Размер окна не объявлен — стража нет, и судить нечего. */\nexport interface IThresholds {\n /** Размер окна захода в токенах: `RT_WINDOW_TOKENS`. */\n readonly window: number;\n /** Доля окна, на которой страж напоминает: `RT_WINDOW_WARN_PCT`. */\n readonly warnPct: number;\n /** Доля окна, на которой страж отбивает работу: `RT_WINDOW_STOP_PCT`. */\n readonly stopPct: number;\n /** Запас между порогом сжатия и порогом остановки: `RT_WINDOW_MARGIN_PCT`. */\n readonly marginPct: number;\n /** Размер окна автосжатия: настройка `autoCompactWindow`. Не объявлен — `null`. */\n readonly compactWindow: number | null;\n /** Доля, на которой приходит сжатие: `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE`. Не объявлена — `null`. */\n readonly compactPct: number | null;\n}\n\n/** Расхождение пары: что с чем разъехалось, какими числами и чем это кончится для захода. */\nexport interface IThresholdDrift {\n /** Что разошлось, словами читателя. */\n readonly what: string;\n /** Что объявлено стражу. */\n readonly guard: string;\n /** Что объявлено сжатию. */\n readonly compact: string;\n /** Чем это кончится для захода. Пусто у расхождения, которое говорит само за себя. */\n readonly why: string;\n}\n\n/**\n * Число из строки настройки. Пусто, не число, ноль и меньше — ничего: значение, которого нельзя\n * посчитать, объявленным не считается, иначе расхождением станет опечатка, а не расхождение.\n */\nfunction numberOf(value: unknown): number | null {\n const parsed: number = typeof value === 'number' ? value : Number.parseInt(String(value ?? ''), 10);\n\n return Number.isFinite(parsed) && parsed > 0 ? parsed : null;\n}\n\n/**\n * Что дерево объявило о порогах.\n *\n * Настройку читаем разбором и только им: пороги лежат полями объекта, и вынуть их из текста\n * поиском значило бы гадать. Не разобралась — молчим целиком, как молчит карта хуков: назвать\n * расхождением непрочитанное хуже, чем не назвать ничего.\n */\nexport function readThresholds(root: string): IThresholds | null {\n const path: string = join(root, SETTINGS_PATH);\n if (!existsSync(path)) {\n return null;\n }\n\n let settings: Record<string, unknown>;\n try {\n settings = JSON.parse(readFileSync(path, 'utf8')) as Record<string, unknown>;\n } catch {\n return null;\n }\n\n const env: Record<string, unknown> = (settings['env'] as Record<string, unknown>) ?? {};\n const windowTokens: number | null = numberOf(env['RT_WINDOW_TOKENS']);\n if (windowTokens === null) {\n return null;\n }\n\n return {\n window: windowTokens,\n warnPct: numberOf(env['RT_WINDOW_WARN_PCT']) ?? WARN_PCT_DEFAULT,\n stopPct: numberOf(env['RT_WINDOW_STOP_PCT']) ?? STOP_PCT_DEFAULT,\n marginPct: numberOf(env['RT_WINDOW_MARGIN_PCT']) ?? MARGIN_PCT_DEFAULT,\n compactWindow: numberOf(settings['autoCompactWindow']),\n compactPct: numberOf(env['CLAUDE_AUTOCOMPACT_PCT_OVERRIDE']),\n };\n}\n\n/**\n * Чем пара стража разошлась с парой сжатия.\n *\n * Незаданное расхождением не считается: дерево вправе не задавать порога сжатия вовсе — тогда\n * его назначает инструмент, и сказать об этом надо один раз, а не двумя строками о разнице.\n *\n * Долю судит расстояние, а не равенство. Три случая дают расхождение, и у каждого свой исход\n * для захода: сжатие выше остановки — страж отбивает раньше, чем клиент успевает начать;\n * сжатие вровень с остановкой — гонка, которую страж выигрывает; сжатие ниже, но ближе\n * объявленного запаса — клиент начать успевает, а докончить нет.\n */\nexport function thresholdDrift(thresholds: IThresholds): readonly IThresholdDrift[] {\n const drift: IThresholdDrift[] = [];\n\n if (thresholds.compactWindow !== null && thresholds.compactWindow !== thresholds.window) {\n drift.push({\n what: 'размер окна',\n guard: String(thresholds.window),\n compact: String(thresholds.compactWindow),\n why: 'доля считается не от того же окна',\n });\n }\n\n const compactPct: number | null = thresholds.compactPct;\n if (compactPct === null) {\n return drift;\n }\n\n const gap: number = thresholds.stopPct - compactPct;\n const tooHigh: string = 'сжатие стоит выше остановки: страж отбивает заход раньше, чем приходит сжатие';\n const tied: string = 'пороги совпали: кто сработает первым, не назначено, и выигрывает страж';\n\n if (gap <= 0) {\n drift.push({\n what: 'доля окна',\n guard: `${thresholds.stopPct}%`,\n compact: `${compactPct}%`,\n why: gap < 0 ? tooHigh : tied,\n });\n\n return drift;\n }\n\n if (gap < thresholds.marginPct) {\n drift.push({\n what: 'запас между порогами',\n guard: `${thresholds.marginPct}% объявлено`,\n compact: `${gap}% на деле`,\n why: 'сжатие идёт не мгновенно и может не успеть до порога остановки',\n });\n }\n\n return drift;\n}\n\n/** Раздел разбора состояния: числа порогов и то, что о них известно. */\nexport function thresholdLines(root: string): string[] {\n const thresholds: IThresholds | null = readThresholds(root);\n if (!thresholds) {\n return [];\n }\n\n const lines: string[] = [\n `окно захода: ${thresholds.window} токенов, напоминание ${thresholds.warnPct}%, остановка ${thresholds.stopPct}%`,\n ];\n\n if (thresholds.compactWindow === null && thresholds.compactPct === null) {\n lines.push(\n ' порог сжатия деревом не задан — его назначает инструмент, и передача пишется когда придётся',\n ` объяви в \\`${SETTINGS_PATH}\\`: \\`autoCompactWindow\\` числом ${thresholds.window}, а \\`CLAUDE_AUTOCOMPACT_PCT_OVERRIDE\\` — числом ${thresholds.stopPct - thresholds.marginPct}`\n );\n\n return lines;\n }\n\n const drift: readonly IThresholdDrift[] = thresholdDrift(thresholds);\n if (!drift.length) {\n lines.push(\n ` порог сжатия ниже порога остановки на ${thresholds.marginPct}% и более: сжатие приходит первым, страж остаётся страховкой`\n );\n\n return lines;\n }\n\n lines.push(' порог сжатия разошёлся с порогом остановки:');\n for (const one of drift) {\n lines.push(` ${one.what}: у стража ${one.guard}, у сжатия ${one.compact}`, ` ${one.why}`);\n }\n\n return lines;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rt-tools/agent-kit",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Переносимый слой правил для агента: законы, хуки, проверки и агенты, раскладываемые в репозиторий одной командой",
5
5
  "author": "RT Team",
6
6
  "license": "Apache-2.0",
Binary file
Binary file