docs(router-gate-v4): calibration 5 plan - cosmetic-detector git-approval exemption

This commit is contained in:
Дмитрий
2026-05-31 11:39:20 +03:00
parent d647bf1858
commit f6421fd61c
@@ -0,0 +1,98 @@
# Calibration 5 — cosmetic-detector excludes git-approval AskUser Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:test-driven-development. Steps use checkbox (`- [ ]`) syntax.
**Goal:** Stop `askuser-cosmetic-detector` from counting/blocking git-operation **approval** AskUsers as "cosmetic A/B" — a scope fix that does NOT lower discipline.
**Architecture:** The detector's target is *simple A/B questions used instead of structured ideation* (brainstorming/writing-plans). A git-approval AskUser (one whose option label is a literal git command) is the *sanctioned git-approval channel* (`enforce-askuser-answer-parser` writes `approve_git_operation` from it) — it is never a substitute for ideation. We add `isGitApprovalQuestion()` and short-circuit `decide()` to `allow` with `isSimpleAB:false` for such questions, so they are neither counted toward the session limit nor hard-blocked. Everything else is unchanged.
**Tech Stack:** Node ESM `.mjs`, vitest.
**Why NO discipline hole (adversarial check):**
- The real target (design-clarification A/B like "Вариант A"/"Вариант B") has NON-git labels → still classified simple → still counted → still hard-blocked at >2. Unchanged.
- A git-approval question is identified ONLY by an option label matching a git-command verb. To "disguise" a cosmetic clarification as exempt, the controller would have to put a literal `git …` command as an option label — but then the chosen answer IS a git command, which `enforce-askuser-answer-parser` turns into a real `approve_git_operation` record; it cannot function as a cosmetic ideation-dodge. So there is no usable bypass.
- Exemption is narrow and structural (label is a git command), mirroring calibrations 1 (Skill) / 3 (test-runner) / 4 (user-prompt fallback): scope fix, not a discipline drop.
---
## Task 1: isGitApprovalQuestion + decide() exemption
**Files:**
- Modify: `tools/askuser-cosmetic-detector.mjs`
- Test: `tools/askuser-cosmetic-detector.test.mjs`
- [ ] **Step 1: Write failing tests**
```javascript
import { isGitApprovalQuestion } from './askuser-cosmetic-detector.mjs';
describe('isGitApprovalQuestion (calibration 5)', () => {
it('true when an option label is a git command', () => {
expect(isGitApprovalQuestion([{ options: [{ label: 'git push origin main' }, { label: 'Не пушить' }] }])).toBe(true);
expect(isGitApprovalQuestion([{ options: [{ label: 'git commit -F x -- a b' }, { label: 'Отмена' }] }])).toBe(true);
});
it('false for a non-git A/B', () => {
expect(isGitApprovalQuestion([{ options: [{ label: 'Вариант А' }, { label: 'Вариант Б' }] }])).toBe(false);
});
});
// decide(): git-approval question is exempt — allow, not simple, not counted, never blocked even past the session limit.
describe('decide — git-approval exemption (calibration 5)', () => {
it('allows a git-approval question and does NOT count it even when session is already over the limit', () => {
const r = decide({
questions: [{ options: [{ label: 'git push origin main' }, { label: 'Не пушить' }] }],
simpleCountSession: 5, brainstormingInvoked: false,
});
expect(r.block).toBe(false);
expect(r.action).toBe('allow');
expect(r.isSimpleAB).toBe(false);
expect(r.newSessionCount).toBe(5); // unchanged — not counted
});
it('REGRESSION: a non-git simple A/B past the limit STILL hard-blocks (discipline intact)', () => {
const r = decide({
questions: [{ options: [{ label: 'A' }, { label: 'B' }] }],
simpleCountSession: 5, brainstormingInvoked: false,
});
expect(r.block).toBe(true);
expect(r.action).toBe('hard_block');
});
});
```
- [ ] **Step 2: Run RED**`npx vitest run --root app --config vitest.config.tools.mjs askuser-cosmetic-detector` → fail (isGitApprovalQuestion missing; git-approval not exempt).
- [ ] **Step 3: Implement**
Add near `isSimpleAB`:
```javascript
const GIT_CMD_RE = /\bgit\s+(?:commit|push|add|pull|merge|rebase|reset|checkout|switch|branch|stash|cherry-pick|revert|clean|restore|fetch|tag)\b/i;
/** True if this AskUser is a git-operation approval prompt (an option label is a git command). */
export function isGitApprovalQuestion(questions) {
if (!Array.isArray(questions)) return false;
return questions.some((q) =>
q && Array.isArray(q.options) &&
q.options.some((o) => o && typeof o.label === 'string' && GIT_CMD_RE.test(o.label)));
}
```
In `decide()`, replace `const simple = isSimpleAB(questions);` with:
```javascript
// Calibration 5: git-operation approval prompts are the sanctioned approval
// channel, never cosmetic ideation — exempt from the simple-AB count/block.
if (isGitApprovalQuestion(questions)) {
return { action: 'allow', block: false, reason: null, isSimpleAB: false, newSessionCount: simpleCountSession, newTurnCount: simpleCountTurn };
}
const simple = isSimpleAB(questions);
```
- [ ] **Step 4: Run GREEN** — same command → pass.
- [ ] **Step 5: Full regression**`npx vitest run --root app --config vitest.config.tools.mjs` → all green.
- [ ] **Step 6: Commit** (with git-approval).