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) <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-06-02 13:14:35 +03:00
parent 22e7ac278b
commit 9f6bd2bd46
3 changed files with 45 additions and 13 deletions
+11 -10
View File
@@ -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-сессий.
+12 -3
View File
@@ -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 <val>` and `-C <path>`. `git -C <dir> <sub>` 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 <path>` (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 <dir>`.
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 безусловно
+22
View File
@@ -220,6 +220,28 @@ describe('classifyGitCommand — push main-guard (owner-authorized 2026-06-02 re
});
});
describe('classifyGitCommand — git -C <path> (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',