feat(brain-governance): graph-first enforcer (Stop hook) + vocab gap fix for chain-recommendation
Closes third behavioral-debt block from retro #8: CLAUDE.md §5 п.14 (graph-first для codebase-вопросов) was being ignored — controller did 4+ Grep searches today without consulting graphify.
Three changes:
1. tools/enforce-graph-first.mjs (NEW): Stop hook blocking turn-end when Grep+Glob count >= 3 in turn AND no graphify invocation (Skill 'graphifyy' / Bash 'graphifyy' / SlashCommand 'graphify'). Override: 'graph-skip: <reason>' inline OR global override-phrase. 19 vitest tests cover empty toolUses, threshold boundary, graphify detection forms, override variants.
2. tools/enforce-override-vocab.json: added 'graph-first' AND 'chain-recommendation' to suppresses[] of all 7 global override phrases (без скилов / direct ok / срочно / быстрый коммит / recovery / memory dump / ремонт инфраструктуры). This closes a vocab gap that ALSO affected the previously-deployed chain-recommendation hook (a3 from d1d53080) — global overrides did not work for it either until now.
3. .claude/settings.json: registered enforce-graph-first.mjs as 5th Stop hook entry.
Full vitest tools-sweep: 1041/1041 GREEN. Reviewer APPROVE on spec + code quality. Pipe-test verified (empty event → exit 0, no block).
This commit is contained in:
@@ -188,6 +188,15 @@
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node tools/enforce-graph-first.mjs",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"UserPromptSubmit": [
|
||||
|
||||
+13
-13
@@ -1,6 +1,6 @@
|
||||
# Brain Status (auto-generated)
|
||||
|
||||
Last updated: 2026-05-28T01:50:18.743Z
|
||||
Last updated: 2026-05-28T02:37:38.704Z
|
||||
|
||||
| Контролёр | Состояние | Детали |
|
||||
|---|---|---|
|
||||
@@ -8,13 +8,13 @@ Last updated: 2026-05-28T01:50:18.743Z
|
||||
| C2 Cross-ref consistency | ✅ | [cross-ref-checker] OK — 0 drift in 4 files |
|
||||
| C3 Observer-of-observer | ✅ | [observer-of-observer] OK — last read 0 week(s) ago |
|
||||
| C4 Сигнальный статус | ✅ | This file (self-reference) |
|
||||
| C5 Observer-coverage | ⚠️ | 706 episode(s) this month · Stop-hook + post-commit OK · 20 missed activation(s) — see /brain-retro |
|
||||
| C5 Observer-coverage | ⚠️ | 715 episode(s) this month · Stop-hook + post-commit OK · 20 missed activation(s) — see /brain-retro |
|
||||
| C6 Chain map sync | ✅ | [chain-map-checker] OK — 16 chains in sync |
|
||||
|
||||
## Метрики (информационные, не алерты)
|
||||
|
||||
- Observer evidence: 706 episodes this month, 0 observer_error markers, 158 PII matches before filter
|
||||
- Legacy v1 episodes (not in factor analysis): 567
|
||||
- Observer evidence: 715 episodes this month, 0 observer_error markers, 158 PII matches before filter
|
||||
- Legacy v1 episodes (not in factor analysis): 576
|
||||
- Last /brain-retro: 0 day(s) ago
|
||||
- Использование узлов: см. `/brain-retro` (раз в спринт). missed_activations: 20. **Неиспользованные узлы — не алерт, если профильной задачи не было** (Pravila §16.4 v1.36; capability-readiness; см. memory `feedback_brain_unused_tools_not_problem` — outside-repo memory store).
|
||||
|
||||
@@ -31,9 +31,9 @@ Baseline дисциплины роутера (этап 2 router discipline overh
|
||||
| cleanup | 7 | 0.0% | 0.0% |
|
||||
| refactor | 1 | 0.0% | 0.0% |
|
||||
|
||||
Router step distribution: 1: 301, 2: 263, 3: 71, 5: 63
|
||||
Router step distribution: 1: 303, 2: 265, 3: 75, 5: 64
|
||||
|
||||
Boundaries applied (ADR / границы): 85 of 698 эпизодов (12.2%).
|
||||
Boundaries applied (ADR / границы): 88 of 707 эпизодов (12.4%).
|
||||
|
||||
## Активные многоэтапные проекты
|
||||
|
||||
@@ -51,10 +51,10 @@ Boundaries applied (ADR / границы): 85 of 698 эпизодов (12.2%).
|
||||
|
||||
| Компонент | Токены (in/out) | USD |
|
||||
|---|---|---|
|
||||
| Classifier (Sonnet 4.6) | 7050/69610 | $1.07 |
|
||||
| Classifier (Sonnet 4.6) | 7217/72657 | $1.11 |
|
||||
| Self-assessment (Sonnet 4.6) | 0/0 | $0.00 |
|
||||
| Reviewer (Opus 4.7 + fallback) | 0/0 | $0.00 |
|
||||
| **Итого** | | **$1.07** |
|
||||
| **Итого** | | **$1.11** |
|
||||
|
||||
## Аномалии классификатора
|
||||
|
||||
@@ -67,7 +67,7 @@ Episodes since last run: 609 / threshold: 10
|
||||
|
||||
## Reviewer: субагент vs fallback
|
||||
|
||||
0 эпизодов проверено из 706.
|
||||
0 эпизодов проверено из 715.
|
||||
|
||||
## Reviewer findings
|
||||
|
||||
@@ -109,10 +109,10 @@ Episodes since last run: 609 / threshold: 10
|
||||
|
||||
| Фраза | За всё время | За сегодня |
|
||||
|---|---|---|
|
||||
| `recovery` | 273 | 0 |
|
||||
| `ремонт инфраструктуры` | 181 | 22 ⚠️ |
|
||||
| `срочно` | 82 | 0 |
|
||||
| `без скилов` | 58 | 0 |
|
||||
| `recovery` | 286 | 13 ⚠️ |
|
||||
| `ремонт инфраструктуры` | 185 | 26 ⚠️ |
|
||||
| `срочно` | 88 | 6 ⚠️ |
|
||||
| `без скилов` | 60 | 2 |
|
||||
| `memory dump` | 8 | 0 |
|
||||
| `direct ok` | 6 | 0 |
|
||||
| `быстрый коммит` | 3 | 0 |
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Rule — Graph-first enforce.
|
||||
*
|
||||
* Stop hook. Enforces CLAUDE.md §5 п.14:
|
||||
* «перед открытым codebase-вопросом сначала /graphify query, потом Read/Grep/Glob»
|
||||
*
|
||||
* When the controller performs >= THRESHOLD Grep/Glob searches in a single turn
|
||||
* WITHOUT having invoked graphify, this hook blocks turn-end with remediation
|
||||
* instructions.
|
||||
*
|
||||
* Three escape hatches:
|
||||
* 1. Invoke /graphify query via Skill tool (or graphifyy CLI via Bash).
|
||||
* 2. Write «graph-skip: <non-empty reason>» on a line in the assistant text.
|
||||
* 3. User prompt contains a global override phrase (vocab-driven).
|
||||
*
|
||||
* Spec: CLAUDE.md §5 п.14 (v2.33), ADR-017.
|
||||
*/
|
||||
|
||||
import {
|
||||
readStdin,
|
||||
parseEventJson,
|
||||
readTranscript,
|
||||
lastUserPromptText,
|
||||
lastAssistantText,
|
||||
turnToolUses,
|
||||
findOverride,
|
||||
logOverride,
|
||||
exitDecision,
|
||||
} from './enforce-hook-helpers.mjs';
|
||||
|
||||
const RULE_KEY = 'graph-first';
|
||||
const THRESHOLD = 3;
|
||||
const SEARCH_TOOLS = new Set(['Grep', 'Glob']);
|
||||
|
||||
/**
|
||||
* Regex for inline escape hatch:
|
||||
* «graph-skip: <one-line non-empty reason>»
|
||||
*
|
||||
* Requirements:
|
||||
* - Must start at the beginning of a line (^, multiline flag).
|
||||
* - Must have «graph-skip: » prefix followed by \S+ (at least one non-whitespace char).
|
||||
* - Whitespace-only or empty reason → does NOT match → remains blocked.
|
||||
*/
|
||||
const GRAPH_SKIP_RE = /^graph-skip:\s*\S+/m;
|
||||
|
||||
/**
|
||||
* Pure decision function — no I/O.
|
||||
*
|
||||
* @param {object} params
|
||||
* @param {Array<{name: string, input: object}>} params.toolUses - All tool uses in this turn.
|
||||
* @param {boolean} params.graphifyInvoked - True if graphify was invoked this turn.
|
||||
* @param {string} params.assistantText - Full assistant text for this turn.
|
||||
* @param {object|null} params.override - Truthy if user prompt contained a valid override phrase.
|
||||
* @returns {{ block: boolean, message?: string }}
|
||||
*/
|
||||
export function decide({ toolUses, graphifyInvoked, assistantText, override }) {
|
||||
// Step 1: Global override → pass.
|
||||
if (override) return { block: false };
|
||||
|
||||
// Step 2: Graphify already consulted → pass.
|
||||
if (graphifyInvoked) return { block: false };
|
||||
|
||||
// Step 3: Count Grep + Glob tool uses.
|
||||
const searchCount = Array.isArray(toolUses)
|
||||
? toolUses.filter((u) => u && SEARCH_TOOLS.has(u.name)).length
|
||||
: 0;
|
||||
|
||||
// Step 4: Below threshold → pass. §5 п.14 «узкий regex-поиск» exception.
|
||||
if (searchCount < THRESHOLD) return { block: false };
|
||||
|
||||
// Step 5: Inline graph-skip escape hatch with non-empty reason → pass.
|
||||
if (typeof assistantText === 'string' && GRAPH_SKIP_RE.test(assistantText)) {
|
||||
return { block: false };
|
||||
}
|
||||
|
||||
// Step 6: Block.
|
||||
const message = [
|
||||
`[enforce-graph-first] За turn выполнено ${searchCount} Grep/Glob поисков без вызова graphify (CLAUDE.md §5 п.14: «перед открытым codebase-вопросом сначала /graphify query, потом Read/Grep/Glob»).`,
|
||||
`Сделай ОДНО из трёх в следующем ответе:`,
|
||||
` 1. Позови /graphify query «<вопрос>» через Skill tool, потом Read/Grep по найденным узлам.`,
|
||||
` 2. Добавь строку «graph-skip: <одна строка причины>» (e.g. «graph-skip: узкий regex по литералу CONFIDENCE_THRESHOLD»).`,
|
||||
` 3. Попроси у пользователя глобальный override (без скилов / direct ok / срочно / быстрый коммит / recovery / memory dump / ремонт инфраструктуры).`,
|
||||
].join('\n');
|
||||
|
||||
return { block: true, message };
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if graphify was invoked in any tool use of the turn.
|
||||
*
|
||||
* Matches:
|
||||
* - Skill tool with input.skill containing «graphify» (case-insensitive substring).
|
||||
* - Bash tool with input.command matching /\bgraphifyy?\b/i (CLI name is «graphifyy»,
|
||||
* also catches «graphify» for slash-command-rendered bash).
|
||||
* - SlashCommand tool (if present) with input.command containing «graphify».
|
||||
*/
|
||||
export function detectGraphifyInvoked(toolUses) {
|
||||
if (!Array.isArray(toolUses)) return false;
|
||||
for (const u of toolUses) {
|
||||
if (!u || !u.name) continue;
|
||||
if (u.name === 'Skill') {
|
||||
const skill = String((u.input && u.input.skill) || '');
|
||||
if (/graphify/i.test(skill)) return true;
|
||||
}
|
||||
if (u.name === 'Bash') {
|
||||
const cmd = String((u.input && u.input.command) || '');
|
||||
if (/\bgraphifyy?\b/i.test(cmd)) return true;
|
||||
}
|
||||
if (u.name === 'SlashCommand') {
|
||||
const cmd = String((u.input && u.input.command) || '');
|
||||
if (/graphify/i.test(cmd)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
const raw = await readStdin();
|
||||
const event = parseEventJson(raw);
|
||||
const transcript = readTranscript(event.transcript_path);
|
||||
const userPrompt = lastUserPromptText(transcript);
|
||||
const assistantText = lastAssistantText(transcript);
|
||||
const toolUses = turnToolUses(transcript);
|
||||
|
||||
const graphifyInvoked = detectGraphifyInvoked(toolUses);
|
||||
const override = findOverride(userPrompt, RULE_KEY);
|
||||
if (override) logOverride(RULE_KEY, override, event.session_id);
|
||||
|
||||
const result = decide({ toolUses, graphifyInvoked, assistantText, override });
|
||||
exitDecision(result);
|
||||
} catch {
|
||||
// Fail-quiet: never block on internal error.
|
||||
exitDecision({ block: false });
|
||||
}
|
||||
}
|
||||
|
||||
const isCli = process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('/enforce-graph-first.mjs');
|
||||
if (isCli) main();
|
||||
@@ -0,0 +1,209 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { decide } from './enforce-graph-first.mjs';
|
||||
|
||||
// Shared helpers
|
||||
const GREP_TOOL = { name: 'Grep', input: { pattern: 'foo' } };
|
||||
const GLOB_TOOL = { name: 'Glob', input: { pattern: '**/*.ts' } };
|
||||
const READ_TOOL = { name: 'Read', input: { file_path: 'x.ts' } };
|
||||
const EDIT_TOOL = { name: 'Edit', input: { file_path: 'x.mjs' } };
|
||||
const BASH_TOOL = { name: 'Bash', input: { command: 'ls -la' } };
|
||||
|
||||
describe('enforce-graph-first / decide', () => {
|
||||
// Test 1: No searches → pass
|
||||
it('no searches at all → pass', () => {
|
||||
expect(decide({
|
||||
toolUses: [EDIT_TOOL],
|
||||
graphifyInvoked: false,
|
||||
assistantText: '',
|
||||
override: null,
|
||||
}).block).toBe(false);
|
||||
});
|
||||
|
||||
// Test 2: Below threshold (2 searches) → pass
|
||||
it('below threshold (2 Grep searches) → pass', () => {
|
||||
expect(decide({
|
||||
toolUses: [GREP_TOOL, GREP_TOOL],
|
||||
graphifyInvoked: false,
|
||||
assistantText: '',
|
||||
override: null,
|
||||
}).block).toBe(false);
|
||||
});
|
||||
|
||||
// Test 3: 3 searches, no graphify, no override → block
|
||||
it('3 Grep searches, no graphify, no override → block', () => {
|
||||
const r = decide({
|
||||
toolUses: [GREP_TOOL, GREP_TOOL, GREP_TOOL],
|
||||
graphifyInvoked: false,
|
||||
assistantText: '',
|
||||
override: null,
|
||||
});
|
||||
expect(r.block).toBe(true);
|
||||
expect(r.message).toMatch(/3/);
|
||||
expect(r.message).toMatch(/graphify/i);
|
||||
expect(r.message).toMatch(/graph-skip:/);
|
||||
});
|
||||
|
||||
// Test 4: 5 searches but graphifyInvoked: true → pass
|
||||
it('5 searches but graphifyInvoked: true → pass', () => {
|
||||
expect(decide({
|
||||
toolUses: [GREP_TOOL, GREP_TOOL, GREP_TOOL, GREP_TOOL, GREP_TOOL],
|
||||
graphifyInvoked: true,
|
||||
assistantText: '',
|
||||
override: null,
|
||||
}).block).toBe(false);
|
||||
});
|
||||
|
||||
// Test 5: 3 searches with valid graph-skip line → pass
|
||||
it('3 searches with valid graph-skip line → pass', () => {
|
||||
expect(decide({
|
||||
toolUses: [GREP_TOOL, GREP_TOOL, GREP_TOOL],
|
||||
graphifyInvoked: false,
|
||||
assistantText: 'graph-skip: узкий regex по литералу X\nдалее обычный ответ...',
|
||||
override: null,
|
||||
}).block).toBe(false);
|
||||
});
|
||||
|
||||
// Test 6: 3 searches with empty graph-skip reason → block
|
||||
it('3 searches with graph-skip: but empty reason → block', () => {
|
||||
expect(decide({
|
||||
toolUses: [GREP_TOOL, GREP_TOOL, GREP_TOOL],
|
||||
graphifyInvoked: false,
|
||||
assistantText: 'graph-skip:\n',
|
||||
override: null,
|
||||
}).block).toBe(true);
|
||||
});
|
||||
|
||||
// Test 7: 3 searches with global override → pass
|
||||
it('3 searches with global override → pass', () => {
|
||||
expect(decide({
|
||||
toolUses: [GREP_TOOL, GREP_TOOL, GREP_TOOL],
|
||||
graphifyInvoked: false,
|
||||
assistantText: '',
|
||||
override: { phrase: 'срочно', suppresses: ['graph-first'] },
|
||||
}).block).toBe(false);
|
||||
});
|
||||
|
||||
// Test 8: Mixed Grep + Glob count toward threshold → block
|
||||
it('1 Grep + 2 Glob = 3 → block (mixed counts toward threshold)', () => {
|
||||
const r = decide({
|
||||
toolUses: [GREP_TOOL, GLOB_TOOL, GLOB_TOOL],
|
||||
graphifyInvoked: false,
|
||||
assistantText: '',
|
||||
override: null,
|
||||
});
|
||||
expect(r.block).toBe(true);
|
||||
});
|
||||
|
||||
// Test 9: Other tools (Read, Edit, Bash) don't count as searches → pass
|
||||
it('Read × 4 + Edit × 1 = 0 searches → pass', () => {
|
||||
expect(decide({
|
||||
toolUses: [READ_TOOL, READ_TOOL, READ_TOOL, READ_TOOL, EDIT_TOOL],
|
||||
graphifyInvoked: false,
|
||||
assistantText: '',
|
||||
override: null,
|
||||
}).block).toBe(false);
|
||||
});
|
||||
|
||||
// Test 10: Message includes per-spec wording
|
||||
it('block message includes §5 п.14, graphify, graph-skip: wording', () => {
|
||||
const r = decide({
|
||||
toolUses: [GREP_TOOL, GREP_TOOL, GREP_TOOL],
|
||||
graphifyInvoked: false,
|
||||
assistantText: '',
|
||||
override: null,
|
||||
});
|
||||
expect(r.block).toBe(true);
|
||||
expect(r.message).toMatch(/§5 п\.14/);
|
||||
expect(r.message).toMatch(/graphify/i);
|
||||
expect(r.message).toMatch(/graph-skip:/);
|
||||
});
|
||||
|
||||
// Extra edge cases
|
||||
|
||||
it('exactly THRESHOLD=3 searches → block (boundary condition)', () => {
|
||||
expect(decide({
|
||||
toolUses: [GREP_TOOL, GLOB_TOOL, GREP_TOOL],
|
||||
graphifyInvoked: false,
|
||||
assistantText: '',
|
||||
override: null,
|
||||
}).block).toBe(true);
|
||||
});
|
||||
|
||||
it('2 searches (below threshold) regardless of graphify state → pass', () => {
|
||||
// Even without graphify, 2 searches is under the threshold
|
||||
expect(decide({
|
||||
toolUses: [GREP_TOOL, GLOB_TOOL],
|
||||
graphifyInvoked: false,
|
||||
assistantText: '',
|
||||
override: null,
|
||||
}).block).toBe(false);
|
||||
});
|
||||
|
||||
it('graph-skip: with non-empty reason in middle of text → pass', () => {
|
||||
const text = 'Some analysis first.\ngraph-skip: known file path, not cross-cutting\nThen conclusion.';
|
||||
expect(decide({
|
||||
toolUses: [GREP_TOOL, GREP_TOOL, GREP_TOOL],
|
||||
graphifyInvoked: false,
|
||||
assistantText: text,
|
||||
override: null,
|
||||
}).block).toBe(false);
|
||||
});
|
||||
|
||||
it('graph-skip: with only whitespace reason (not \\ S+) → block', () => {
|
||||
expect(decide({
|
||||
toolUses: [GREP_TOOL, GREP_TOOL, GREP_TOOL],
|
||||
graphifyInvoked: false,
|
||||
assistantText: 'graph-skip: \n',
|
||||
override: null,
|
||||
}).block).toBe(true);
|
||||
});
|
||||
|
||||
it('empty toolUses → pass', () => {
|
||||
expect(decide({
|
||||
toolUses: [],
|
||||
graphifyInvoked: false,
|
||||
assistantText: '',
|
||||
override: null,
|
||||
}).block).toBe(false);
|
||||
});
|
||||
|
||||
it('Bash tool alone does not count as search', () => {
|
||||
expect(decide({
|
||||
toolUses: [BASH_TOOL, BASH_TOOL, BASH_TOOL, BASH_TOOL],
|
||||
graphifyInvoked: false,
|
||||
assistantText: '',
|
||||
override: null,
|
||||
}).block).toBe(false);
|
||||
});
|
||||
|
||||
it('block message includes the actual count N', () => {
|
||||
const r = decide({
|
||||
toolUses: [GREP_TOOL, GREP_TOOL, GREP_TOOL, GREP_TOOL, GREP_TOOL],
|
||||
graphifyInvoked: false,
|
||||
assistantText: '',
|
||||
override: null,
|
||||
});
|
||||
expect(r.block).toBe(true);
|
||||
expect(r.message).toMatch(/5/);
|
||||
});
|
||||
|
||||
it('override null value → treated as falsy, block still fires', () => {
|
||||
const r = decide({
|
||||
toolUses: [GREP_TOOL, GREP_TOOL, GREP_TOOL],
|
||||
graphifyInvoked: false,
|
||||
assistantText: '',
|
||||
override: null,
|
||||
});
|
||||
expect(r.block).toBe(true);
|
||||
});
|
||||
|
||||
it('override false value → treated as falsy, block still fires', () => {
|
||||
const r = decide({
|
||||
toolUses: [GREP_TOOL, GREP_TOOL, GREP_TOOL],
|
||||
graphifyInvoked: false,
|
||||
assistantText: '',
|
||||
override: false,
|
||||
});
|
||||
expect(r.block).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -4,37 +4,37 @@
|
||||
"phrases": [
|
||||
{
|
||||
"phrase": "без скилов",
|
||||
"suppresses": ["skill-required", "coverage-skill-match", "classifier-mismatch"],
|
||||
"suppresses": ["skill-required", "coverage-skill-match", "classifier-mismatch", "graph-first", "chain-recommendation"],
|
||||
"description": "Skill discipline relaxed for this one prompt"
|
||||
},
|
||||
{
|
||||
"phrase": "direct ok",
|
||||
"suppresses": ["skill-required", "coverage-skill-match", "classifier-mismatch"],
|
||||
"suppresses": ["skill-required", "coverage-skill-match", "classifier-mismatch", "graph-first", "chain-recommendation"],
|
||||
"description": "Direct work allowed without skill invocation"
|
||||
},
|
||||
{
|
||||
"phrase": "срочно",
|
||||
"suppresses": ["verify-before-commit", "verify-before-push", "tdd-gate"],
|
||||
"description": "Urgency override: skip verification + TDD gate"
|
||||
"suppresses": ["verify-before-commit", "verify-before-push", "tdd-gate", "graph-first", "chain-recommendation"],
|
||||
"description": "Urgency override: skip verification + TDD gate + graph/chain enforcement"
|
||||
},
|
||||
{
|
||||
"phrase": "быстрый коммит",
|
||||
"suppresses": ["verify-before-commit", "tdd-gate", "writing-plans-required"],
|
||||
"description": "Quick commit: skip TDD + verify + plans"
|
||||
"suppresses": ["verify-before-commit", "tdd-gate", "writing-plans-required", "graph-first", "chain-recommendation"],
|
||||
"description": "Quick commit: skip TDD + verify + plans + graph/chain enforcement"
|
||||
},
|
||||
{
|
||||
"phrase": "recovery",
|
||||
"suppresses": ["branch-switch", "git-recovery"],
|
||||
"suppresses": ["branch-switch", "git-recovery", "graph-first", "chain-recommendation"],
|
||||
"description": "Git recovery operation, branch-state mismatch ok"
|
||||
},
|
||||
{
|
||||
"phrase": "memory dump",
|
||||
"suppresses": ["memory-sync-coverage", "skill-required"],
|
||||
"suppresses": ["memory-sync-coverage", "skill-required", "graph-first", "chain-recommendation"],
|
||||
"description": "Memory write without separate coverage announcement"
|
||||
},
|
||||
{
|
||||
"phrase": "ремонт инфраструктуры",
|
||||
"suppresses": ["tdd-gate", "verify-before-commit", "verify-before-push", "writing-plans-required", "skill-required", "memory-sync-coverage", "classifier-mismatch", "coverage-skill-match"],
|
||||
"suppresses": ["tdd-gate", "verify-before-commit", "verify-before-push", "writing-plans-required", "skill-required", "memory-sync-coverage", "classifier-mismatch", "coverage-skill-match", "graph-first", "chain-recommendation"],
|
||||
"requires_justification": "ремонт:",
|
||||
"description": "Bypass all rules (full opt-out). Requires 'ремонт: <what>' line in same prompt."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user