From 9f6bd2bd46c400aab602c476dfdc52c8672050e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Tue, 2 Jun 2026 13:14:35 +0300 Subject: [PATCH] feat(router-gate): support git -C path for worktree dev Shell resets cwd each call so a worktree cd does not persist; pointing git at the worktree dir is the cwd-independent way to commit there. classifyGitCommand now strips the leading working-dir flag before all checks, so the real subcommand is classified and all hard-patterns (hook-bypass, force-push, force-add, config-injection) plus the push-main-guard still apply. TDD: plus 6 tests; full tools suite 2003 GREEN. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/observer/STATUS.md | 21 +++++++++++---------- tools/shell-content-rules.mjs | 15 ++++++++++++--- tools/shell-content-rules.test.mjs | 22 ++++++++++++++++++++++ 3 files changed, 45 insertions(+), 13 deletions(-) diff --git a/docs/observer/STATUS.md b/docs/observer/STATUS.md index 9d1f7e1d..bb1f6468 100644 --- a/docs/observer/STATUS.md +++ b/docs/observer/STATUS.md @@ -1,6 +1,6 @@ # Brain Status (auto-generated) -Last updated: 2026-06-02T06:32:47.583Z +Last updated: 2026-06-02T10:04:24.915Z | Контролёр | Состояние | Детали | |---|---|---| @@ -8,13 +8,13 @@ Last updated: 2026-06-02T06:32:47.583Z | 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 | ✅ | 130 episode(s) this month · Stop-hook + post-commit OK | +| C5 Observer-coverage | ✅ | 137 episode(s) this month · Stop-hook + post-commit OK | | C6 Chain map sync | ✅ | [chain-map-checker] OK — 16 chains in sync | ## Метрики (информационные, не алерты) -- Observer evidence: 130 episodes this month, 0 observer_error markers, 6 PII matches before filter -- Legacy v1 episodes (not in factor analysis): 130 +- Observer evidence: 137 episodes this month, 0 observer_error markers, 6 PII matches before filter +- Legacy v1 episodes (not in factor analysis): 137 - Last /brain-retro: 2 day(s) ago - Использование узлов: см. `/brain-retro` (раз в спринт). missed_activations: 0. **Неиспользованные узлы — не алерт, если профильной задачи не было** (Pravila §16.4 v1.36; capability-readiness; см. memory `feedback_brain_unused_tools_not_problem` — outside-repo memory store). @@ -29,9 +29,9 @@ Baseline дисциплины роутера (этап 2 router discipline overh | analysis | 2 | 0.0% | 0.0% | | bugfix | 1 | 0.0% | 0.0% | -Router step distribution: 1: 74, 2: 51, 5: 4 +Router step distribution: 1: 81, 2: 51, 5: 4 -Boundaries applied (ADR / границы): 1 of 129 эпизодов (0.8%). +Boundaries applied (ADR / границы): 1 of 136 эпизодов (0.7%). ## Активные многоэтапные проекты @@ -55,10 +55,10 @@ Long sessions correlate with discipline drift. Если % regulated просел | Компонент | Токены (in/out) | USD | |---|---|---| -| Classifier (Sonnet 4.6) | 10454/50220 | $0.78 | +| Classifier (Sonnet 4.6) | 10473/50827 | $0.79 | | Self-assessment (Sonnet 4.6) | 0/0 | $0.00 | | Reviewer (Opus 4.7 + fallback) | 0/0 | $0.00 | -| **Итого** | | **$0.78** | +| **Итого** | | **$0.79** | ## Аномалии классификатора @@ -71,7 +71,7 @@ Episodes since last run: 542 / threshold: 10 ## Reviewer: субагент vs fallback -0 эпизодов проверено из 130. +0 эпизодов проверено из 137. ## Reviewer findings @@ -97,7 +97,8 @@ Episodes since last run: 542 / threshold: 10 | PID | Имя | CPU-время | Возраст | |---|---|---|---| -| 10388 | Code | 2.57ч | 0.0ч | +| 10388 | Code | 3.01ч | NaNч | +| 3220 | MsMpEng | 1.13ч | 0.0ч | ⚠️ Проверь, не «осиротевшие» ли это процессы от завершённых Claude-сессий. diff --git a/tools/shell-content-rules.mjs b/tools/shell-content-rules.mjs index 60dd5854..54780c2f 100644 --- a/tools/shell-content-rules.mjs +++ b/tools/shell-content-rules.mjs @@ -187,14 +187,23 @@ const GIT_HARD_PATTERNS = [ ]; function gitSubcommand(command) { - const m = normalizeCommand(command).match(/\bgit\s+(?:-c\s+\S+\s+)*([a-z][\w-]*)/); + // Skip leading global flags `-c ` and `-C `. `git -C ` is the + // cwd-independent way to operate on a worktree (the shell resets cwd each call), so the + // real subcommand must be found after `-C`. `-C` (uppercase, working-dir) is case-distinct + // from the blocked `-c` config-injection (GIT_HARD_PATTERNS still scans the full command). + const m = normalizeCommand(command).match( + /\bgit\s+(?:(?:-c\s+\S+|-C\s+(?:"[^"]*"|'[^']*'|\S+))\s+)*([a-z][\w-]*)/, + ); return m ? m[1] : null; } export function classifyGitCommand(command, ctx = {}) { - const norm = normalizeCommand(command); + // Strip a leading `git -C ` (worktree-dir flag) so every rule below sees the real + // subcommand+flags. Without this, position-anchored hard-patterns (--no-verify / --force / + // add -f) and the push-main-guard would be bypassed by interposing `-C `. + const norm = normalizeCommand(command).replace(/(\bgit)\s+-C\s+(?:"[^"]*"|'[^']*'|\S+)\s+/, '$1 '); if (!/\bgit\b/.test(norm)) return null; - const sub = gitSubcommand(command); + const sub = gitSubcommand(norm); if (!sub) return null; // 1. git-hard — block безусловно diff --git a/tools/shell-content-rules.test.mjs b/tools/shell-content-rules.test.mjs index 1e5a7e66..7a47c941 100644 --- a/tools/shell-content-rules.test.mjs +++ b/tools/shell-content-rules.test.mjs @@ -220,6 +220,28 @@ describe('classifyGitCommand — push main-guard (owner-authorized 2026-06-02 re }); }); +describe('classifyGitCommand — git -C (worktree dev, 2026-06-02)', () => { + const na = { approvedGitOps: [], now: 4_000_000 }; + // git -C points git at another working tree (cwd resets each shell call, so this is + // the cwd-independent way to commit in a worktree). Classify by the REAL subcommand + // after -C, with all hard-patterns / push-main-guard still applied to the full command. + it.each([ + 'git -C "C:\\моя\\проекты\\портал crm\\worktree-x" commit -m "y"', + 'git -C "C:\\моя\\проекты\\портал crm\\worktree-x" add app/foo.php', + 'git -C "/path/worktree-x" push origin feature-y', + 'git -C /repo status', + ])('classifies by real subcommand after -C: %s', (cmd) => { + expect(classifyGitCommand(cmd, na).result).toBe('allow'); + }); + + it('still blocks push to main even with -C', () => { + expect(classifyGitCommand('git -C /repo push origin main', na).result).toBe('block'); + }); + it('still blocks --no-verify even with -C', () => { + expect(classifyGitCommand('git -C /repo commit --no-verify -m x', na).result).toBe('block'); + }); +}); + describe('classifyGitCommand — git-hard (always block)', () => { it.each([ 'git push --force origin main',