diff --git a/docs/superpowers/plans/2026-05-31-calibration-5-cosmetic-detector-git-approval.md b/docs/superpowers/plans/2026-05-31-calibration-5-cosmetic-detector-git-approval.md new file mode 100644 index 00000000..4aecca2d --- /dev/null +++ b/docs/superpowers/plans/2026-05-31-calibration-5-cosmetic-detector-git-approval.md @@ -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).