@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
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Сверка того, что раздел состояния называет следующее движение.
4
+ *
5
+ * Раздел, обрывающийся на последнем приёме, читается как конец работы: исполнитель
6
+ * доводит обязательное действие до конца, дочитывает раздел, следующего движения в
7
+ * нём не находит — и отдаёт ход отчётом о сделанном. Так кончились два хода: один
8
+ * на записанном замысле, второй на закрытом разборе просьбы. Сверка состояний этого
9
+ * не видит: она сравнивает имена разделов с перечнем и внутрь не смотрит.
10
+ *
11
+ * Читаются четыре текста:
12
+ * перечень — таблица состояний в правиле ведения работы: имя и ведущий паттерн;
13
+ * разделы — заголовки вида «Состояние `имя`» в этих паттернах;
14
+ * правило — оно же: в нём стоит утверждение о границе состояния;
15
+ * карта и закон — там же стоит то же утверждение, если дерево их разложило.
16
+ *
17
+ * Строка следующего движения узнаётся по зачину, а называет своё: единая дословная
18
+ * строка читается как шаблон и перестаёт замечаться на третьем разделе. Поэтому
19
+ * повтор хвоста считается расхождением наравне с его отсутствием.
20
+ *
21
+ * FAIL-OPEN: правила ведения работы в дереве нет — сверять нечего, нулевой код.
22
+ * Пустая таблица состояний отказом считается: она означает не «нечего сверять», а
23
+ * «перечень сломан». Ведущий паттерн, которого в дереве нет, здесь не судится — его
24
+ * называет сверка состояний, и два отказа об одном промахе читаются как две
25
+ * претензии.
26
+ *
27
+ * Ненулевой код возврата и перечень расхождений.
28
+ */
29
+ import { existsSync, readFileSync } from 'node:fs';
30
+ import { join } from 'node:path';
31
+
32
+ const ROOT = process.cwd();
33
+ const RULE = join(ROOT, '.claude/skills/task-flow/SKILL.md');
34
+ const SKILLS = join(ROOT, '.claude/skills');
35
+ const MAP = join(ROOT, '.claude/rt-kit/defaults/turn-map.md');
36
+ const LAW = join(ROOT, 'docs/constitution/work-conduct.md');
37
+
38
+ /** Зачин строки: по нему её находят, а хвост у каждого раздела свой. */
39
+ const MARKER = '**Следующее движение:**';
40
+
41
+ /** Утверждение о границе состояния. Стоит в правиле, в карте и в законе теми же словами. */
42
+ const BOUNDARY = 'Переход из состояния в состояние';
43
+
44
+ /** Короче этого хвост движения не называет: зачин без движения — та же пустота. */
45
+ const MIN_TAIL = 20;
46
+
47
+ /** Имя состояния и паттерн, который его ведёт, — из таблицы правила. */
48
+ function statesFromRule(text) {
49
+ const states = [];
50
+
51
+ for (const line of text.split('\n')) {
52
+ if (!line.startsWith('|')) {
53
+ continue;
54
+ }
55
+
56
+ const cells = line
57
+ .split('|')
58
+ .slice(1, -1)
59
+ .map((cell) => cell.trim());
60
+
61
+ if (cells.length < 4) {
62
+ continue;
63
+ }
64
+
65
+ const name = cells[0].match(/^`([^`]+)`$/);
66
+ const pattern = cells[3].match(/^`([^`]+)`$/);
67
+
68
+ if (name && pattern) {
69
+ states.push({ name: name[1], pattern: pattern[1] });
70
+ }
71
+ }
72
+
73
+ return states;
74
+ }
75
+
76
+ /** Разделы состояний паттерна: имя, заголовок целиком и найденные в разделе строки движения. */
77
+ function sectionsOf(pattern) {
78
+ const file = join(SKILLS, pattern, 'SKILL.md');
79
+
80
+ if (!existsSync(file)) {
81
+ return null;
82
+ }
83
+
84
+ const sections = [];
85
+ let current = null;
86
+
87
+ for (const line of readFileSync(file, 'utf8').split('\n')) {
88
+ const heading = line.match(/^#+\s+Состояние\s+`([^`]+)`/);
89
+
90
+ if (heading) {
91
+ current = { name: heading[1], heading: line.replace(/^#+\s+/, ''), moves: [] };
92
+ sections.push(current);
93
+ continue;
94
+ }
95
+
96
+ if (current && line.startsWith(MARKER)) {
97
+ current.moves.push(line.slice(MARKER.length).trim());
98
+ }
99
+ }
100
+
101
+ return sections;
102
+ }
103
+
104
+ if (!existsSync(RULE)) {
105
+ console.log('check-state-next: правила ведения работы в дереве нет — сверять нечего');
106
+ process.exit(0);
107
+ }
108
+
109
+ const ruleText = readFileSync(RULE, 'utf8');
110
+ const states = statesFromRule(ruleText);
111
+
112
+ if (states.length === 0) {
113
+ console.error('check-state-next: в правиле ведения работы не нашлось таблицы состояний');
114
+ process.exit(1);
115
+ }
116
+
117
+ const problems = [];
118
+ const patterns = new Map();
119
+ const tails = new Map();
120
+ let counted = 0;
121
+
122
+ for (const state of states) {
123
+ if (!patterns.has(state.pattern)) {
124
+ patterns.set(state.pattern, sectionsOf(state.pattern));
125
+ }
126
+ }
127
+
128
+ for (const [pattern, sections] of patterns) {
129
+ if (sections === null) {
130
+ continue;
131
+ }
132
+
133
+ for (const section of sections) {
134
+ counted += 1;
135
+
136
+ const where = `\`${section.name}\` в паттерне \`${pattern}\``;
137
+
138
+ if (section.moves.length === 0) {
139
+ problems.push(`${where}: в разделе «${section.heading}» нет строки «${MARKER}»`);
140
+ continue;
141
+ }
142
+
143
+ if (section.moves.length > 1) {
144
+ problems.push(`${where}: в разделе «${section.heading}» таких строк ${section.moves.length}, а движение одно`);
145
+ continue;
146
+ }
147
+
148
+ const tail = section.moves[0];
149
+
150
+ if (tail.length < MIN_TAIL) {
151
+ problems.push(`${where}: зачин есть, а движение за ним не названо`);
152
+ continue;
153
+ }
154
+
155
+ const twin = tails.get(tail);
156
+
157
+ if (twin) {
158
+ problems.push(`${where}: движение слово в слово то же, что у ${twin}`);
159
+ continue;
160
+ }
161
+
162
+ tails.set(tail, `\`${section.name}\` в паттерне \`${pattern}\``);
163
+ }
164
+ }
165
+
166
+ if (counted === 0) {
167
+ problems.push('ни одного раздела состояния не нашлось: паттерны не разложены или заголовки в них другие');
168
+ }
169
+
170
+ if (!ruleText.includes(BOUNDARY)) {
171
+ problems.push(`правило ведения работы о границе состояния молчит: строки «${BOUNDARY}» в нём нет`);
172
+ }
173
+
174
+ for (const [file, what] of [
175
+ [MAP, 'карта хода'],
176
+ [LAW, 'закон о ведении работы'],
177
+ ]) {
178
+ if (existsSync(file) && !readFileSync(file, 'utf8').includes(BOUNDARY)) {
179
+ problems.push(`${what} о границе состояния молчит: строки «${BOUNDARY}» в ней нет`);
180
+ }
181
+ }
182
+
183
+ if (problems.length > 0) {
184
+ console.error(`check-state-next: расхождений ${problems.length}`);
185
+
186
+ for (const problem of problems) {
187
+ console.error(` ${problem}`);
188
+ }
189
+
190
+ console.error('\nСтрока следующего движения стоит в каждом разделе состояния: зачин общий, движение своё.');
191
+ process.exit(1);
192
+ }
193
+
194
+ console.log(`check-state-next: разделов состояния ${counted}, у каждого названо следующее движение`);
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Сверка перечня состояний работы с разделами паттернов, которые их ведут.
4
+ *
5
+ * Состояние без ведущего текста читается как конец работы: исполнитель дочитывает
6
+ * паттерн до последнего раздела, следующего движения в нём нет, и ход кончается
7
+ * отчётом. Так простояло состояние «замысел записан» — оно встречалось один раз,
8
+ * клеткой таблицы, и дыру нашло происшествие, а не проверка. Разбор —
9
+ * `docs/postmortems/2026-08-21-turn-ended-at-the-written-plan.md`.
10
+ *
11
+ * Сверяются два множества в обе стороны:
12
+ * перечень — таблица состояний в правиле ведения работы: имя и ведущий паттерн;
13
+ * разделы — заголовки вида «Состояние `имя`» в паттернах.
14
+ *
15
+ * Состояние без раздела — расхождение. Раздел про состояние вне перечня — тоже:
16
+ * переименованное состояние иначе оставляет прежний раздел, и тот выглядит
17
+ * действующим.
18
+ *
19
+ * Ведущий паттерн берётся из самой таблицы, а не из имён файлов: раздел, лежащий
20
+ * не в том паттерне, который назначен, читателя до себя не доводит — он открывает
21
+ * назначенный.
22
+ *
23
+ * FAIL-OPEN: правила ведения работы в дереве нет — сверять нечего, нулевой код.
24
+ * Пустая таблица состояний отказом считается: она означает не «нечего сверять», а
25
+ * «перечень сломан».
26
+ *
27
+ * Ненулевой код возврата и перечень расхождений.
28
+ */
29
+ import { existsSync, readFileSync } from 'node:fs';
30
+ import { join } from 'node:path';
31
+
32
+ const ROOT = process.cwd();
33
+ const RULE = join(ROOT, '.claude/skills/task-flow/SKILL.md');
34
+ const SKILLS = join(ROOT, '.claude/skills');
35
+
36
+ /** Имя состояния и паттерн, который его ведёт, — из таблицы правила. */
37
+ function statesFromRule(text) {
38
+ const states = [];
39
+
40
+ for (const line of text.split('\n')) {
41
+ if (!line.startsWith('|')) {
42
+ continue;
43
+ }
44
+
45
+ const cells = line
46
+ .split('|')
47
+ .slice(1, -1)
48
+ .map((cell) => cell.trim());
49
+
50
+ if (cells.length < 4) {
51
+ continue;
52
+ }
53
+
54
+ const name = cells[0].match(/^`([^`]+)`$/);
55
+ const pattern = cells[3].match(/^`([^`]+)`$/);
56
+
57
+ if (name && pattern) {
58
+ states.push({ name: name[1], pattern: pattern[1] });
59
+ }
60
+ }
61
+
62
+ return states;
63
+ }
64
+
65
+ /** Состояния, про которые в паттерне есть раздел. Заголовок любого уровня. */
66
+ function sectionsOf(pattern) {
67
+ const file = join(SKILLS, pattern, 'SKILL.md');
68
+
69
+ if (!existsSync(file)) {
70
+ return null;
71
+ }
72
+
73
+ const found = new Set();
74
+
75
+ for (const line of readFileSync(file, 'utf8').split('\n')) {
76
+ const heading = line.match(/^#+\s+Состояние\s+`([^`]+)`/);
77
+
78
+ if (heading) {
79
+ found.add(heading[1]);
80
+ }
81
+ }
82
+
83
+ return found;
84
+ }
85
+
86
+ if (!existsSync(RULE)) {
87
+ console.log('check-states: правила ведения работы в дереве нет — сверять нечего');
88
+ process.exit(0);
89
+ }
90
+
91
+ const states = statesFromRule(readFileSync(RULE, 'utf8'));
92
+
93
+ if (states.length === 0) {
94
+ console.error('check-states: в правиле ведения работы не нашлось таблицы состояний');
95
+ process.exit(1);
96
+ }
97
+
98
+ const known = new Set(states.map((state) => state.name));
99
+ const problems = [];
100
+ const seen = new Map();
101
+
102
+ for (const state of states) {
103
+ if (!seen.has(state.pattern)) {
104
+ seen.set(state.pattern, sectionsOf(state.pattern));
105
+ }
106
+
107
+ const sections = seen.get(state.pattern);
108
+
109
+ if (sections === null) {
110
+ problems.push(`состояние \`${state.name}\`: ведущий паттерн \`${state.pattern}\` в дереве не разложен`);
111
+ continue;
112
+ }
113
+
114
+ if (!sections.has(state.name)) {
115
+ problems.push(`состояние \`${state.name}\`: в паттерне \`${state.pattern}\` нет раздела «Состояние \`${state.name}\`»`);
116
+ }
117
+ }
118
+
119
+ for (const [pattern, sections] of seen) {
120
+ if (sections === null) {
121
+ continue;
122
+ }
123
+
124
+ for (const name of sections) {
125
+ if (!known.has(name)) {
126
+ problems.push(`паттерн \`${pattern}\`: раздел про \`${name}\`, а такого состояния в перечне нет`);
127
+ }
128
+ }
129
+ }
130
+
131
+ if (problems.length > 0) {
132
+ console.error(`check-states: расхождений ${problems.length}`);
133
+
134
+ for (const problem of problems) {
135
+ console.error(` ${problem}`);
136
+ }
137
+
138
+ console.error('\nПеречень состояний — правило `task-flow`, разделы — паттерны при нём.');
139
+ process.exit(1);
140
+ }
141
+
142
+ console.log(`check-states: состояний ${states.length}, у каждого есть раздел в ведущем паттерне`);
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Сверка карты хода: её размер и её полнота.
4
+ *
5
+ * Карту кладёт в контекст хук входа в заход — целиком, до первой реплики. Тем она и
6
+ * отличается от правила: правило исполнитель читает сам и платит за это ходом, карта
7
+ * приходит даром. Даром — пока она короткая: карта, выросшая до правила, съедает то
8
+ * самое окно, ради которого её и заводили, и заметить это нечем — она продолжает
9
+ * приходить и продолжает быть верной.
10
+ *
11
+ * Второе, чего не видно без сверки: состояние, заведённое в правиле и забытое в карте.
12
+ * Заход тогда получает карту, не находит в ней своего состояния и идёт читать правило —
13
+ * то есть карта работает ровно до первого нового состояния.
14
+ *
15
+ * Сверяются три вещи:
16
+ * размер — байты разложенной карты против объявленного предела;
17
+ * состояния — имена из таблицы правила против имён в карте, в обе стороны;
18
+ * выходы — все четыре выхода хода названы.
19
+ *
20
+ * FAIL-OPEN: карты в дереве нет — сверять нечего, нулевой код. Дерево может её не
21
+ * раскладывать. Пустая таблица состояний в карте отказом считается: это не «нечего
22
+ * сверять», а «карта сломана».
23
+ *
24
+ * Ненулевой код возврата и перечень расхождений.
25
+ */
26
+ import { existsSync, readFileSync, statSync } from 'node:fs';
27
+ import { join } from 'node:path';
28
+
29
+ const ROOT = process.cwd();
30
+ const MAP = join(ROOT, '.claude/rt-kit/defaults/turn-map.md');
31
+ const RULE = join(ROOT, '.claude/skills/task-flow/SKILL.md');
32
+
33
+ /**
34
+ * Предел размера карты в байтах.
35
+ *
36
+ * Число одно и живёт здесь: карта приходит в каждый заход целиком, и предел ей — свойство
37
+ * приёма, а не дерева. Шесть килобайт взяты замером, а не на глаз: карта в день заведения
38
+ * весит 4495 байт, правило, из которого она выжата, — 63803. Запас в полтора килобайта —
39
+ * это два-три новых состояния; выйдя за него, карта перестаёт быть выжимкой, и делить её
40
+ * тогда надо, а не поднимать предел.
41
+ */
42
+ const LIMIT_BYTES = 6144;
43
+
44
+ /** Имена состояний из таблицы: первая ячейка в обратных кавычках и всё, что за ней. */
45
+ function statesOf(text) {
46
+ const states = [];
47
+
48
+ for (const line of text.split('\n')) {
49
+ if (!line.startsWith('|')) {
50
+ continue;
51
+ }
52
+
53
+ const cells = line
54
+ .split('|')
55
+ .slice(1, -1)
56
+ .map((cell) => cell.trim());
57
+
58
+ if (cells.length < 2) {
59
+ continue;
60
+ }
61
+
62
+ const name = cells[0].match(/^`([^`]+)`$/);
63
+
64
+ if (name) {
65
+ states.push({ name: name[1], rest: cells.slice(1) });
66
+ }
67
+ }
68
+
69
+ return states;
70
+ }
71
+
72
+ /** Выходы хода, названные законом. Ищутся по началу строки таблицы, а не по всему тексту. */
73
+ const EXITS = ['вопрос владельцу', 'отказ гарда', 'заполненное окно', 'работа отдана'];
74
+
75
+ function main() {
76
+ if (!existsSync(MAP)) {
77
+ console.log('check-turn-map: карты хода в дереве нет — сверять нечего');
78
+
79
+ return 0;
80
+ }
81
+
82
+ const text = readFileSync(MAP, 'utf8');
83
+ const bytes = statSync(MAP).size;
84
+ const faults = [];
85
+
86
+ if (bytes > LIMIT_BYTES) {
87
+ faults.push(`карта выросла: ${bytes} байт при пределе ${LIMIT_BYTES}`);
88
+ }
89
+
90
+ const inMap = statesOf(text);
91
+
92
+ if (inMap.length === 0) {
93
+ faults.push('в карте нет ни одного состояния — таблица сломана');
94
+ }
95
+
96
+ for (const state of inMap) {
97
+ if (!state.rest[0]) {
98
+ faults.push(`${state.name}: в карте нет обязательного действия`);
99
+ }
100
+ }
101
+
102
+ if (existsSync(RULE)) {
103
+ const inRule = statesOf(readFileSync(RULE, 'utf8'));
104
+ const mapNames = new Set(inMap.map((state) => state.name));
105
+ const ruleNames = new Set(inRule.map((state) => state.name));
106
+
107
+ for (const state of ruleNames) {
108
+ if (!mapNames.has(state)) {
109
+ faults.push(`${state}: состояние объявлено правилом и забыто в карте`);
110
+ }
111
+ }
112
+
113
+ for (const state of mapNames) {
114
+ if (!ruleNames.has(state)) {
115
+ faults.push(`${state}: состояние стоит в карте, а правило его не объявляет`);
116
+ }
117
+ }
118
+ }
119
+
120
+ for (const exit of EXITS) {
121
+ if (!text.includes(exit)) {
122
+ faults.push(`выход хода «${exit}» в карте не назван`);
123
+ }
124
+ }
125
+
126
+ if (faults.length > 0) {
127
+ console.log(`check-turn-map: расхождений ${faults.length}, размер ${bytes} байт при пределе ${LIMIT_BYTES}\n`);
128
+
129
+ for (const fault of faults) {
130
+ console.log(` ${fault}`);
131
+ }
132
+
133
+ console.log('\nКарта короче правила — этим она и полезна. Выросшая, она съедает то окно, ради');
134
+ console.log('которого её кладут в контекст. Правится она в ресурсе пакета, а не в разложенной копии.');
135
+
136
+ return 1;
137
+ }
138
+
139
+ console.log(
140
+ `check-turn-map: ${inMap.length} состояний, ${EXITS.length} выхода хода, ` + `${bytes} байт при пределе ${LIMIT_BYTES} — сошлось`
141
+ );
142
+
143
+ return 0;
144
+ }
145
+
146
+ process.exit(main());
@@ -12,9 +12,9 @@ argument-hint: '[пусто | --days N]'
12
12
 
13
13
  ## 1. Собери, что пришло
14
14
 
15
- Груз уезжает в приём, а не в очередь работ: сводка говорит о рабочих привычках команды, и в
16
- открытой очереди это выложено всему свету. Записи приёма читает его админка; пока её нет,
17
- собранное читается запросом к его хранилищу — как именно, сказано в компаньоне этой команды.
15
+ Чем груз читается и что значит взять его в работу, говорит правило `cargo-triage`: здесь этот
16
+ порядок не повторяется, а зовётся. Сведению от него нужен один шаг список неразобранного,
17
+ суженный отбором по состоянию «новое».
18
18
 
19
19
  Записи, заведённые прежним порядком, лежат в очереди работ и никуда не делись:
20
20
 
@@ -66,13 +66,14 @@ npx agent-kit stats --days <отрезок> --json
66
66
  По каждой — ресурс, место, готовый текст и число записей, из которых она вышла. Не правь ничего
67
67
  сам: решение принимает владелец, а работа идёт обычным ходом — задача, ветка, папка задачи.
68
68
 
69
- Заведи задачу на то, что владелец принял:
69
+ Заведи задачу на то, что владелец принял, и тем же ходом отметь вошедшие в неё записи груза —
70
+ как именно, говорит паттерн `cargo-triage-mark`:
70
71
 
71
72
  ```bash
72
73
  npm run task:new -- --title '<что не так>' --slug <короткое-имя> --label enhancement < тело.md
73
74
  ```
74
75
 
75
- Записи, вошедшие в задачу, закрой ссылкой на неё:
76
+ Записи, заведённые в очереди работ прежним порядком, закрываются там же ссылкой на задачу:
76
77
 
77
78
  ```bash
78
79
  gh issue close <номер> --comment 'Вошло в #<номер задачи>.'
@@ -87,7 +87,8 @@ rt_push_checks_default() {
87
87
  [ -x "$root/$RT_HOOKS_TESTS" ] && printf '%s\n' "bash $RT_HOOKS_TESTS"
88
88
 
89
89
  for check in check-doc-paths check-specs check-file-size check-dupes check-styles \
90
- check-lib-layers check-reuse check-schema-drift check-push-gate; do
90
+ check-lib-layers check-reuse check-schema-drift check-states check-state-next \
91
+ check-turn-map check-push-gate; do
91
92
  [ -f "$root/$RT_CHECKS_DIR/$check.mjs" ] && printf '%s\n' "node $RT_CHECKS_DIR/$check.mjs"
92
93
  done
93
94
 
@@ -135,9 +136,27 @@ RT_ARCHIVE_DIR="${RT_ARCHIVE_DIR:-docs/archive}"
135
136
  # Размер окна захода в токенах и пороги стража. Пусто — стража нет: считать долю не от чего, а
136
137
  # выведенный из записи захода размер врал бы — модель записана там без пометки о расширенном
137
138
  # окне. Дерево задаёт его в настройке агента, переменной окружения того же имени.
139
+ #
140
+ # Теми же двумя числами задаётся порог, на котором инструмент сжимает контекст сам, — а значит и
141
+ # порог, на котором пишется передача захода. В настройке агента им отвечает пара: размер окна
142
+ # автосжатия и доля в процентах, при которой оно приходит. Разъехавшись, они дают заход, который
143
+ # либо сжимается до того, как передача написана, либо доживает до предела окна. Сведены они или
144
+ # нет — говорит разбор состояния раскладки; здесь у пакета своих значений нет, потому что окно
145
+ # принадлежит дереву, а не ему.
146
+ #
147
+ # Сведёнными считаются не совпавшие числа, а разведённые. Порог сжатия обязан стоять НИЖЕ порога
148
+ # остановки: первым срабатывает то, что заход продолжает, а не то, что его останавливает.
149
+ # Совпавшая пара — гонка, и выигрывает её страж: он стоит на вызове инструмента, а сжатие
150
+ # приходит между ходами. Ровно так заход и вставал на пороге вместо того, чтобы продолжиться
151
+ # сжатым, — при том что сверка обе стороны считала настроенными.
152
+ #
153
+ # Насколько ниже — своё число дерева: сжатие идёт не мгновенно, и разница в один процент
154
+ # требование «ниже» удовлетворяет, а работу не спасает. Запас объявляется, а не выводится
155
+ # разницей.
138
156
  RT_WINDOW_TOKENS="${RT_WINDOW_TOKENS:-}"
139
157
  RT_WINDOW_WARN_PCT="${RT_WINDOW_WARN_PCT:-40}"
140
158
  RT_WINDOW_STOP_PCT="${RT_WINDOW_STOP_PCT:-50}"
159
+ RT_WINDOW_MARGIN_PCT="${RT_WINDOW_MARGIN_PCT:-5}"
141
160
 
142
161
  # Куда кладётся передача захода. Вне дерева: состояние работы живёт в ходе работы и коммитится,
143
162
  # а передача его пересказывает для вставки в новый заход и в историю не едет.
@@ -214,19 +233,52 @@ rt_is_app_code_default() {
214
233
  # Список намеренно широк, и цена этого названа: команда чтения, в которой стоит имя
215
234
  # интерпретатора, будет отбита наравне с командой правки. Узкий список стоил бы дороже —
216
235
  # пропущенная форма записи возвращает обход целиком, а найти её можно только промахом.
236
+ #
237
+ # Перенаправление в пустое устройство и в поток ошибок снимается до разбора: файла оно не
238
+ # пишет, а выглядит как перенаправление в файл. Так глушат вывод команды чтения, и без этого
239
+ # `grep -rn x libs/ 2>/dev/null` судится наравне с записью — правило требуется на чтение, а
240
+ # отбитий, пришедшихся не на правку файла, набирается большинство. Настоящая запись рядом с
241
+ # заглушённым потоком остаётся видной: снимается перенаправление, а не команда целиком.
217
242
  rt_shell_writes_default() {
218
- printf '%s' "$1" | grep -Eq \
219
- '>>?[[:space:]]*[^|&>[:space:]]|\btee\b|\bsed\b[^|]*-i|\bperl\b[^|]*-i|\bpython3?\b|\bnode\b|\bruby\b|\bdd\b[^|]*of=|\bcp\b|\bmv\b|\brm\b|\btouch\b|\btruncate\b|\binstall\b|\bpatch\b|\bgit[[:space:]]+(checkout|restore|apply|stash)\b'
243
+ printf '%s' "$1" \
244
+ | sed -E 's#(&|[0-9]*)>>?[[:space:]]*/dev/(null|stderr)##g; s#[0-9]*>&[0-9-]##g' \
245
+ | grep -Eq \
246
+ '>>?[[:space:]]*[^|&>[:space:]]|\btee\b|\bsed\b[^|]*-i|\bperl\b[^|]*-i|\bpython3?\b|\bnode\b|\bruby\b|\bdd\b[^|]*of=|\bcp\b|\bmv\b|\brm\b|\btouch\b|\btruncate\b|\binstall\b|\bpatch\b|\bgit[[:space:]]+(checkout|restore|apply|stash)\b'
220
247
  }
221
248
 
222
249
  # Пути, названные командой оболочки. Печатает по одному в строке; судит их зовущий.
223
250
  #
224
251
  # Разбирать оболочку по-настоящему нечем — здесь и не разбирают: из текста вынимается всё, что
225
252
  # похоже на путь, и каждое отдаётся признаку. Лишнее он отсеет сам, а пропущенное вернуло бы
226
- # обход. Кавычки и heredoc снимаются заменой на пробел: путь внутри них тот же самый.
253
+ # обход. Кавычки снимаются заменой на пробел: путь внутри них тот же самый.
254
+ #
255
+ # Тело документа на месте путей не даёт: там лежит текст, который команда кладёт в файл, а
256
+ # чужой путь, названный в нём словами, требовал бы правила под запись, которой нет. Запись
257
+ # замысла так отбивалась трижды подряд, пока пути под каталогом кода не были названы иначе.
258
+ # Судится заголовок команды — именно в нём стоит тот путь, куда команда пишет.
259
+ #
260
+ # Исключение — интерпретатор: ему код приходит телом, и путь записи стоит именно там. Признак
261
+ # ошибается в сторону лишнего чтения тела: имя интерпретатора, стоящее в команде где угодно,
262
+ # возвращает разбор тела целиком.
227
263
  rt_shell_paths_default() {
228
- printf '%s' "$1" \
229
- | tr "\"'\`(),;=" ' ' \
264
+ text="$(printf '%s' "$1" | tr "\"'\`" ' ')"
265
+ if ! printf '%s' "$text" \
266
+ | grep -Eq '(^|[|;&(]|[[:space:]])(python3?|node|ruby|perl|php|deno|bun|bash|sh|zsh)([[:space:]]|$)'; then
267
+ text="$(printf '%s' "$text" | awk '
268
+ function trim(s) { sub(/^[ \t]+/, "", s); sub(/[ \t]+$/, "", s); return s }
269
+ tag != "" { if (trim($0) == tag) tag = ""; next }
270
+ {
271
+ print
272
+ if (match($0, /<<-?[ \t]*[A-Za-z_][A-Za-z0-9_]*/)) {
273
+ t = substr($0, RSTART, RLENGTH)
274
+ sub(/^<<-?[ \t]*/, "", t)
275
+ tag = t
276
+ }
277
+ }')"
278
+ fi
279
+
280
+ printf '%s' "$text" \
281
+ | tr "(),;=" ' ' \
230
282
  | tr '[:space:]' '\n' \
231
283
  | grep -E '^[A-Za-z0-9_@.-]*/[A-Za-z0-9_@./-]+$' \
232
284
  | sed 's|^\./||' \
@@ -0,0 +1,46 @@
1
+ # Карта хода
2
+
3
+ Это не правило, а его короткая выжимка: правило объясняет, карта называет. Полный текст —
4
+ скил `task-flow`; он же называет паттерн, который ведёт каждое состояние.
5
+
6
+ Состояние работы объявлено строкой в разделе «Где стоим» хода работы. Пока обязательное
7
+ действие не сделано, работа стоит в том же состоянии.
8
+
9
+ ## Состояния и обязательные действия
10
+
11
+ | Состояние | Обязательное действие | Ведёт паттерн |
12
+ | ------------------------- | ------------------------------------------------------ | ------------------ |
13
+ | `просьба-не-разобрана` | разведка по дереву, затем вопросы | `task-flow-start` |
14
+ | `разбор-закрыт` | договорённость о продукте либо причина её отсутствия | `task-flow-start` |
15
+ | `договорённость-записана` | завести задачу, ветку и папку | `task-flow-start` |
16
+ | `задача-взята` | написать замысел | `task-flow-start` |
17
+ | `замысел-записан` | делать первый этап | `task-flow-start` |
18
+ | `этап-идёт` | доделать этап и отметить в ходе работы | `task-flow-resume` |
19
+ | `этапы-кончились` | прогнать набор и открыть PR черновиком | `task-flow-close` |
20
+ | `работа-отдана` | взять следующую задачу | `task-flow-resume` |
21
+ | `разбор-кончился` | влить договорённость, привести тексты, разобрать папку | `task-flow-close` |
22
+ | `папка-разобрана` | снять черновик и попросить влить | `task-flow-close` |
23
+ | `влито` | разбор работы правилами и сверка очереди | `task-flow-close` |
24
+
25
+ Ни у одного состояния обязательное действие не звучит как «ждать». Прогон, разбор владельцем и
26
+ слияние идут без исполнителя и от взгляда быстрее не становятся.
27
+
28
+ ## Чем ход кончается
29
+
30
+ Способов четыре, и других нет.
31
+
32
+ | Выход | Чем подтверждается |
33
+ | -------------------------------------------------- | --------------------------------------------------------------- |
34
+ | вопрос владельцу, ответа на который в правилах нет | вопрос задан, и за тот же ход правила читались |
35
+ | отказ гарда | отказ назван владельцу, обход не искался |
36
+ | заполненное окно там, где сжатия нет | ход работы дописан, передача написана |
37
+ | работа отдана, и следующая начата | PR открыт, и по следующей задаче сделано действие, а не сказано |
38
+
39
+ Там, где дерево объявило порог сжатия ниже порога остановки, заполненное окно ход не кончает:
40
+ контекст сжимается, передача приходит входом, и работа идёт дальше тем же заходом. Порог
41
+ остановки там — страховка на случай, когда сжатие не пришло.
42
+
43
+ Всё остальное — продолжение хода. Ходом не кончаются: коммит, записанный замысел, закрытый
44
+ разбор просьбы, прочитанная договорённость, зелёная проверка, сводка о чужом шаге и объявление
45
+ намерения. Переход из состояния в состояние — тем более: обязательное действие сделано, и
46
+ следующее делается тем же ходом. Названо может быть только сделанное.