# PR #626 HIGH-Priority Fixes Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Fix 4 HIGH-priority issues from PR #626 code review before merge.

**Architecture:** Minimal targeted fixes — each task is independent. TDD: tests first, then implementation. No refactoring beyond what's needed.

**Tech Stack:** TypeScript, Vitest, Node.js fs/path APIs

**Spec:** `docs/superpowers/specs/2026-04-02-pr626-high-fixes-design.md`

**Paths:** All file paths are relative to the monorepo root (`GitNexus/`). Git commands run from the root. The `gitnexus/` prefix is a package subdirectory, not a separate repo.

---

### Task 1: Path Traversal — Validate Group Name

**Files:**
- Modify: `gitnexus/src/core/group/storage.ts:17-19` (getGroupDir) and `:63-68` (createGroupDir)
- Test: `gitnexus/test/unit/group/storage.test.ts`

- [ ] **Step 1: Write failing tests for validateGroupName**

In `gitnexus/test/unit/group/storage.test.ts`, add `createGroupDir` and `validateGroupName` to the existing import from `'../../../src/core/group/storage.js'` (line 6-11). Then add these describe blocks at the end of the outer `describe('Group storage', ...)`:

```typescript
  describe('validateGroupName', () => {
    it('test_validateGroupName_traversal_path_throws', () => {
      expect(() => validateGroupName('../../evil')).toThrow(/Invalid group name/);
    });

    it('test_validateGroupName_slash_in_name_throws', () => {
      expect(() => validateGroupName('foo/bar')).toThrow(/Invalid group name/);
    });

    it('test_validateGroupName_empty_string_throws', () => {
      expect(() => validateGroupName('')).toThrow(/Invalid group name/);
    });

    it('test_validateGroupName_starts_with_dash_throws', () => {
      expect(() => validateGroupName('-leading-dash')).toThrow(/Invalid group name/);
    });

    it('test_validateGroupName_starts_with_underscore_throws', () => {
      expect(() => validateGroupName('_leading')).toThrow(/Invalid group name/);
    });

    it('test_validateGroupName_dots_throws', () => {
      expect(() => validateGroupName('com.example')).toThrow(/Invalid group name/);
    });

    it('test_validateGroupName_valid_alphanumeric_passes', () => {
      expect(() => validateGroupName('my-group_01')).not.toThrow();
    });

    it('test_validateGroupName_single_char_passes', () => {
      expect(() => validateGroupName('A')).not.toThrow();
    });

    it('test_validateGroupName_all_digits_passes', () => {
      expect(() => validateGroupName('123')).not.toThrow();
    });
  });

  describe('getGroupDir rejects invalid names', () => {
    it('test_getGroupDir_traversal_throws', () => {
      expect(() => getGroupDir(tmpDir, '../../etc')).toThrow(/Invalid group name/);
    });

    it('test_getGroupDir_valid_name_returns_path', () => {
      const dir = getGroupDir(tmpDir, 'company');
      expect(dir).toBe(path.join(tmpDir, 'groups', 'company'));
    });
  });

  describe('createGroupDir rejects invalid names', () => {
    it('test_createGroupDir_traversal_throws', async () => {
      await expect(createGroupDir(tmpDir, '../evil')).rejects.toThrow(/Invalid group name/);
    });
  });
```

- [ ] **Step 2: Run tests to verify they fail**

Run: `cd gitnexus && npx vitest run test/unit/group/storage.test.ts`
Expected: FAIL — `validateGroupName` is not exported, `getGroupDir` does not throw.

- [ ] **Step 3: Implement validateGroupName and wire into getGroupDir and createGroupDir**

In `gitnexus/src/core/group/storage.ts`, add the validation function before `getGroupDir` and call it:

```typescript
const GROUP_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;

export function validateGroupName(name: string): void {
  if (!GROUP_NAME_RE.test(name)) {
    throw new Error(
      `Invalid group name "${name}". Names must start with a letter or digit and contain only [a-zA-Z0-9_-].`,
    );
  }
}

export function getGroupDir(gitnexusDir: string, groupName: string): string {
  validateGroupName(groupName);
  return path.join(gitnexusDir, 'groups', groupName);
}
```

`createGroupDir` already calls `getGroupDir` at line 68, so it inherits validation automatically. No change needed in `createGroupDir`.

- [ ] **Step 4: Run tests to verify they pass**

Run: `cd gitnexus && npx vitest run test/unit/group/storage.test.ts`
Expected: ALL PASS

- [ ] **Step 5: Commit**

```bash
cd gitnexus && git add src/core/group/storage.ts test/unit/group/storage.test.ts
git commit -m "fix(group): validate group name to prevent path traversal

Add validateGroupName() with regex [a-zA-Z0-9][a-zA-Z0-9_-]*.
Called in getGroupDir (defense in depth) which covers all CLI entry
points: create, add, remove, status, sync.

Addresses PR #626 review item 1 (HIGH).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>"
```

---

### Task 2: Directory Exclusions in Service Boundary Detector

**Files:**
- Modify: `gitnexus/src/core/group/service-boundary-detector.ts:24-51` (add constant), `:78` (walkForBoundaries), `:130` (hasSourceFilesInSubdirs)
- Test: `gitnexus/test/unit/group/service-boundary-detector.test.ts`

- [ ] **Step 1: Write failing tests for excluded directories**

Add this describe block inside the existing `detectServiceBoundaries` describe in `gitnexus/test/unit/group/service-boundary-detector.test.ts`:

```typescript
    it('test_detect_skips_vendor_directory', async () => {
      writeFile('services/auth/package.json', '{}');
      writeFile('services/auth/src/index.ts', '');
      // vendor should be skipped — its contents should not create a boundary
      writeFile('vendor/some-dep/package.json', '{}');
      writeFile('vendor/some-dep/src/lib.go', '');

      const boundaries = await detectServiceBoundaries(tmpDir);

      const paths = boundaries.map((b) => b.servicePath);
      expect(paths).toContain('services/auth');
      expect(paths).not.toContain('vendor/some-dep');
    });

    it('test_detect_skips_target_directory', async () => {
      writeFile('services/api/go.mod', 'module api');
      writeFile('services/api/main.go', '');
      writeFile('target/classes/Main.java', '');
      writeFile('target/pom.xml', '<project/>');

      const boundaries = await detectServiceBoundaries(tmpDir);

      const paths = boundaries.map((b) => b.servicePath);
      expect(paths).toContain('services/api');
      expect(paths).not.toContain('target');
    });

    it('test_detect_skips_pycache_directory', async () => {
      writeFile('services/ml/pyproject.toml', '[project]');
      writeFile('services/ml/model.py', '');
      // __pycache__ with a marker + source files — would be detected as
      // a boundary if not excluded, since it has package.json + .py file
      writeFile('__pycache__/package.json', '{}');
      writeFile('__pycache__/cached.py', '');

      const boundaries = await detectServiceBoundaries(tmpDir);

      const paths = boundaries.map((b) => b.servicePath);
      expect(paths).toContain('services/ml');
      expect(paths.every((p) => !p.includes('__pycache__'))).toBe(true);
    });

    it('test_detect_skips_dotfile_directories_regression', async () => {
      writeFile('services/api/package.json', '{}');
      writeFile('services/api/src/index.ts', '');
      writeFile('.hidden/package.json', '{}');
      writeFile('.hidden/src/index.ts', '');

      const boundaries = await detectServiceBoundaries(tmpDir);

      const paths = boundaries.map((b) => b.servicePath);
      expect(paths).toContain('services/api');
      expect(paths).not.toContain('.hidden');
    });

    it('test_detect_does_not_skip_regular_source_directories', async () => {
      writeFile('services/api/package.json', '{}');
      writeFile('services/api/src/index.ts', '');

      const boundaries = await detectServiceBoundaries(tmpDir);

      expect(boundaries).toHaveLength(1);
      expect(boundaries[0].serviceName).toBe('api');
    });
```

- [ ] **Step 2: Run tests to verify `vendor` and `target` tests fail**

Run: `cd gitnexus && npx vitest run test/unit/group/service-boundary-detector.test.ts`
Expected: `test_detect_skips_vendor_directory` and `test_detect_skips_target_directory` FAIL (vendor/target not excluded). Other new tests may pass since dotfile exclusion already exists.

- [ ] **Step 3: Add EXCLUDED_DIRS constant and update both walking functions**

In `gitnexus/src/core/group/service-boundary-detector.ts`:

After `SOURCE_EXTENSIONS` (after line 51), add:

```typescript
const EXCLUDED_DIRS = new Set([
  'node_modules',
  'vendor',
  'target',
  'build',
  'dist',
  '__pycache__',
  '.venv',
  'venv',
  '.tox',
  '.mypy_cache',
  '.gradle',
  '.mvn',
  'out',
  'bin',
]);
```

In `walkForBoundaries`, replace line 78:
```typescript
    if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
```
with:
```typescript
    if (entry.name.startsWith('.') || EXCLUDED_DIRS.has(entry.name)) continue;
```

In `hasSourceFilesInSubdirs`, replace line 130:
```typescript
      if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
```
with:
```typescript
      if (entry.isDirectory() && !entry.name.startsWith('.') && !EXCLUDED_DIRS.has(entry.name)) {
```

- [ ] **Step 4: Run tests to verify they pass**

Run: `cd gitnexus && npx vitest run test/unit/group/service-boundary-detector.test.ts`
Expected: ALL PASS

- [ ] **Step 5: Commit**

```bash
cd gitnexus && git add src/core/group/service-boundary-detector.ts test/unit/group/service-boundary-detector.test.ts
git commit -m "fix(group): add directory exclusions to service boundary detector

Add EXCLUDED_DIRS set: vendor, target, build, dist, __pycache__,
.venv, venv, .tox, .mypy_cache, .gradle, .mvn, out, bin.
Applied in walkForBoundaries and hasSourceFilesInSubdirs.
Replaces inline node_modules check.

Addresses PR #626 review item 3 (HIGH).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>"
```

---

### Task 3: Remove Double-Close of LadybugDB Pools

**Files:**
- Modify: `gitnexus/src/cli/group.ts:160` (remove import), `:187-189` (remove finally block body)
- Test: `gitnexus/test/unit/group/sync.test.ts` (add pool cleanup test)
- Test: `gitnexus/test/integration/group/group-cli.test.ts` (verify no blanket close in source)

- [ ] **Step 1: Write unit tests for per-id pool cleanup in sync.ts**

Add to `gitnexus/test/unit/group/sync.test.ts`, inside the existing `describe('syncGroup', ...)`:

```typescript
  it('test_syncGroup_closes_only_opened_pools', async () => {
    const config = makeConfig({
      'app/backend': 'backend-repo',
      'app/frontend': 'frontend-repo',
    });

    const closedIds: string[] = [];

    // Mock initLbug/closeLbug via per-repo override that tracks pool lifecycle
    const { vi } = await import('vitest');
    const poolAdapter = await import('../../../src/core/lbug/pool-adapter.js');
    const initSpy = vi.spyOn(poolAdapter, 'initLbug').mockResolvedValue(undefined);
    const closeSpy = vi.spyOn(poolAdapter, 'closeLbug').mockImplementation(async (id?: string) => {
      if (id) closedIds.push(id);
    });

    try {
      await syncGroup(config, {
        resolveRepoHandle: async (_name, groupPath) => ({
          id: groupPath.replace(/\//g, '-'),
          path: groupPath,
          repoPath: '/tmp/' + groupPath,
          storagePath: '/tmp/' + groupPath + '/.gitnexus',
        }),
        skipWrite: true,
      }).catch(() => {});
      // Regardless of extraction errors, closeLbug should be called per id
      // closeLbug should only receive specific pool ids, never undefined/empty
      for (const id of closedIds) {
        expect(id).toBeTruthy();
        expect(typeof id).toBe('string');
      }
      // No blanket close (no-arg call)
      const blanketCalls = closeSpy.mock.calls.filter((args) => args.length === 0 || !args[0]);
      expect(blanketCalls).toHaveLength(0);
    } finally {
      initSpy.mockRestore();
      closeSpy.mockRestore();
    }
  });
```

- [ ] **Step 2: Run sync unit test to verify it passes (sync.ts already does per-id cleanup)**

Run: `cd gitnexus && npx vitest run test/unit/group/sync.test.ts`
Expected: PASS — sync.ts already cleans up correctly. This test locks the behavior.

- [ ] **Step 3: Write test verifying CLI source has no blanket closeLbug()**

Add to `gitnexus/test/integration/group/group-cli.test.ts`:

```typescript
  it('test_sync_command_source_does_not_call_blanket_closeLbug', () => {
    const cliGroupPath = path.join(repoRoot, 'src', 'cli', 'group.ts');
    const source = fs.readFileSync(cliGroupPath, 'utf-8');

    // closeLbug() without arguments (blanket close) must not appear.
    // closeLbug(id) with argument is fine (that's in sync.ts, not here).
    // Match closeLbug() but not closeLbug(someArg)
    const blanketClosePattern = /closeLbug\s*\(\s*\)/;
    expect(source).not.toMatch(blanketClosePattern);
  });
```

- [ ] **Step 4: Run test to verify it fails**

Run: `cd gitnexus && npx vitest run test/integration/group/group-cli.test.ts`
Expected: FAIL — `closeLbug()` (no args) exists at line 188.

- [ ] **Step 5: Remove blanket closeLbug() from cli/group.ts**

In `gitnexus/src/cli/group.ts`:

Remove the `closeLbug` import at line 160:
```typescript
      const { closeLbug } = await import('../core/lbug/pool-adapter.js');
```

Replace the try/finally wrapper (lines 162-189):
```typescript
      try {
        const groupDir = getGroupDir(getDefaultGitnexusDir(), name);
        const config = await loadGroupConfig(groupDir);

        console.log(`Syncing group "${name}" (${Object.keys(config.repos).length} repos)...\n`);

        const result = await syncGroup(config, {
          groupDir,
          allowStale: Boolean(opts.allowStale),
          verbose: Boolean(opts.verbose),
          skipEmbeddings: Boolean(opts.skipEmbeddings),
          exactOnly: Boolean(opts.exactOnly),
        });

        if (opts.json) {
          console.log(JSON.stringify(result, null, 2));
        } else {
          console.log(`\nMatching cascade:`);
          const exactLinks = result.crossLinks.filter((l) => l.matchType === 'exact');
          console.log(`  exact:     ${exactLinks.length} cross-links (confidence 1.0)`);
          console.log(`  unmatched: ${result.unmatched.length} contracts`);
          console.log(
            `\nWrote contracts.json (${result.contracts.length} contracts, ${result.crossLinks.length} cross-links)`,
          );
        }
      } finally {
        await closeLbug().catch(() => {});
      }
```

Becomes (remove try/finally entirely, since sync.ts handles its own cleanup):
```typescript
        const groupDir = getGroupDir(getDefaultGitnexusDir(), name);
        const config = await loadGroupConfig(groupDir);

        console.log(`Syncing group "${name}" (${Object.keys(config.repos).length} repos)...\n`);

        const result = await syncGroup(config, {
          groupDir,
          allowStale: Boolean(opts.allowStale),
          verbose: Boolean(opts.verbose),
          skipEmbeddings: Boolean(opts.skipEmbeddings),
          exactOnly: Boolean(opts.exactOnly),
        });

        if (opts.json) {
          console.log(JSON.stringify(result, null, 2));
        } else {
          console.log(`\nMatching cascade:`);
          const exactLinks = result.crossLinks.filter((l) => l.matchType === 'exact');
          console.log(`  exact:     ${exactLinks.length} cross-links (confidence 1.0)`);
          console.log(`  unmatched: ${result.unmatched.length} contracts`);
          console.log(
            `\nWrote contracts.json (${result.contracts.length} contracts, ${result.crossLinks.length} cross-links)`,
          );
        }
```

- [ ] **Step 6: Run tests to verify they pass**

Run: `cd gitnexus && npx vitest run test/integration/group/group-cli.test.ts test/unit/group/sync.test.ts`
Expected: ALL PASS

- [ ] **Step 7: Commit**

```bash
cd gitnexus && git add src/cli/group.ts test/integration/group/group-cli.test.ts test/unit/group/sync.test.ts
git commit -m "fix(group): remove blanket closeLbug() from CLI sync command

sync.ts already closes pools per-id in its finally block.
The blanket closeLbug() in cli/group.ts tears down ALL active pools
including unrelated ones in MCP server context.

Addresses PR #626 review item 4 (HIGH).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>"
```

---

### Task 4: gRPC Proto Regex — Brace-Depth Counter

**Files:**
- Modify: `gitnexus/src/core/group/extractors/grpc-extractor.ts:101-130` (parseProtoFile)
- Test: `gitnexus/test/unit/group/grpc-extractor.test.ts`

- [ ] **Step 1: Write failing tests for nested braces in proto services**

Add this describe block inside the existing `proto file parsing` describe in `gitnexus/test/unit/group/grpc-extractor.test.ts`:

```typescript
    it('test_extract_proto_with_google_api_http_nested_braces', async () => {
      writeFile(
        'api/gateway.proto',
        `syntax = "proto3";
package gateway.v1;

import "google/api/annotations.proto";

service GatewayService {
  rpc GetUser (GetUserRequest) returns (UserResponse) {
    option (google.api.http) = {
      get: "/v1/users/{user_id}"
    };
  }
  rpc CreateUser (CreateUserRequest) returns (UserResponse) {
    option (google.api.http) = {
      post: "/v1/users"
      body: "*"
    };
  }
}`,
      );

      const contracts = await extractor.extract(null, tmpDir, makeRepo(tmpDir));
      const providers = contracts.filter(
        (c) => c.role === 'provider' && c.symbolRef.filePath === 'api/gateway.proto',
      );

      expect(providers).toHaveLength(2);
      const ids = providers.map((c) => c.contractId).sort();
      expect(ids).toEqual([
        'grpc::gateway.v1.GatewayService/CreateUser',
        'grpc::gateway.v1.GatewayService/GetUser',
      ]);
    });

    it('test_extract_proto_with_multiple_services', async () => {
      writeFile(
        'api/multi.proto',
        `syntax = "proto3";
package multi;

service ServiceA {
  rpc MethodA (Req) returns (Res);
}

service ServiceB {
  rpc MethodB1 (Req) returns (Res);
  rpc MethodB2 (Req) returns (Res);
}`,
      );

      const contracts = await extractor.extract(null, tmpDir, makeRepo(tmpDir));
      const providers = contracts.filter(
        (c) => c.role === 'provider' && c.symbolRef.filePath === 'api/multi.proto',
      );

      expect(providers).toHaveLength(3);
      const ids = providers.map((c) => c.contractId).sort();
      expect(ids).toEqual([
        'grpc::multi.ServiceA/MethodA',
        'grpc::multi.ServiceB/MethodB1',
        'grpc::multi.ServiceB/MethodB2',
      ]);
    });

    it('test_extract_proto_with_nested_option_blocks_in_rpc', async () => {
      writeFile(
        'api/nested.proto',
        `syntax = "proto3";
package nested;

service DeepService {
  rpc DeepMethod (Req) returns (Res) {
    option (google.api.http) = {
      post: "/v1/deep"
      body: "*"
      additional_bindings {
        get: "/v1/deep/{id}"
      }
    };
  }
}`,
      );

      const contracts = await extractor.extract(null, tmpDir, makeRepo(tmpDir));
      const providers = contracts.filter(
        (c) => c.role === 'provider' && c.symbolRef.filePath === 'api/nested.proto',
      );

      expect(providers).toHaveLength(1);
      expect(providers[0].contractId).toBe('grpc::nested.DeepService/DeepMethod');
    });

    it('test_extract_proto_malformed_unclosed_brace_skips_service', async () => {
      writeFile(
        'api/broken.proto',
        `syntax = "proto3";
package broken;

service IncompleteService {
  rpc SomeMethod (Req) returns (Res);
  // Missing closing brace — EOF before depth returns to 0
`,
      );

      // Should not throw; incomplete service is silently skipped
      const contracts = await extractor.extract(null, tmpDir, makeRepo(tmpDir));
      const providers = contracts.filter(
        (c) => c.role === 'provider' && c.symbolRef.filePath === 'api/broken.proto',
      );

      // The old regex would find partial match; the new parser should skip it
      expect(providers).toHaveLength(0);
    });
```

- [ ] **Step 2: Run tests to verify the nested brace test fails**

Run: `cd gitnexus && npx vitest run test/unit/group/grpc-extractor.test.ts`
Expected: `test_extract_proto_with_google_api_http_nested_braces` FAIL — regex stops at first `}` inside the `option` block.

- [ ] **Step 3: Replace serviceRe regex with extractServiceBlocks function**

In `gitnexus/src/core/group/extractors/grpc-extractor.ts`, replace the `parseProtoFile` method (lines 101-130):

```typescript
  private parseProtoFile(content: string, filePath: string): ExtractedContract[] {
    const out: ExtractedContract[] = [];

    const pkgMatch = content.match(/^package\s+([\w.]+)\s*;/m);
    const pkg = pkgMatch ? pkgMatch[1] : '';

    for (const { name: serviceName, body } of extractServiceBlocks(content)) {
      const rpcRe = /rpc\s+(\w+)\s*\(/g;
      let rpcMatch: RegExpExecArray | null;
      while ((rpcMatch = rpcRe.exec(body)) !== null) {
        const methodName = rpcMatch[1];
        const cid = contractId(pkg, serviceName, methodName);
        out.push(
          makeContract(cid, 'provider', filePath, `${serviceName}.${methodName}`, 0.85, {
            package: pkg,
            service: serviceName,
            method: methodName,
            source: 'proto',
          }),
        );
      }
    }

    return out;
  }
```

Add this function before the class (e.g. after `serviceOnlyContractId`, around line 26):

```typescript
function extractServiceBlocks(content: string): Array<{ name: string; body: string }> {
  const results: Array<{ name: string; body: string }> = [];
  const headerRe = /service\s+(\w+)\s*\{/g;
  let headerMatch: RegExpExecArray | null;

  while ((headerMatch = headerRe.exec(content)) !== null) {
    const serviceName = headerMatch[1];
    const bodyStart = headerMatch.index + headerMatch[0].length;
    let depth = 1;
    let pos = bodyStart;

    while (pos < content.length && depth > 0) {
      const ch = content[pos];
      if (ch === '{') depth++;
      else if (ch === '}') depth--;
      pos++;
    }

    // If EOF before depth returns to 0, skip incomplete service
    if (depth !== 0) continue;

    // body is between opening { (consumed by regex) and closing } (pos is one past it)
    const body = content.slice(bodyStart, pos - 1);
    results.push({ name: serviceName, body });
  }

  return results;
}
```

- [ ] **Step 4: Run tests to verify they pass**

Run: `cd gitnexus && npx vitest run test/unit/group/grpc-extractor.test.ts`
Expected: ALL PASS (including existing regression tests)

- [ ] **Step 5: Commit**

```bash
cd gitnexus && git add src/core/group/extractors/grpc-extractor.ts test/unit/group/grpc-extractor.test.ts
git commit -m "fix(group): replace gRPC proto regex with brace-depth counter

The serviceRe regex used [^}]* which stopped at the first '}'.
Proto services with google.api.http annotations contain nested {}
blocks, causing methods to be missed.

New extractServiceBlocks() uses a brace-depth counter (init depth=1
after opening {, scan char-by-char). Malformed protos with unclosed
braces are silently skipped.

Addresses PR #626 review item 2 (HIGH).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>"
```

---

### Task 5: Run Full Test Suite

- [ ] **Step 1: Run all group-related tests**

Run: `cd gitnexus && npx vitest run test/unit/group/ test/integration/group/`
Expected: ALL PASS

- [ ] **Step 2: Run full test suite to catch regressions**

Run: `cd gitnexus && npx vitest run`
Expected: ALL PASS, 0 failures

- [ ] **Step 3: Run typecheck**

Run: `cd gitnexus && npx tsc --noEmit`
Expected: No errors

---

### Task 6: CLI Integration Smoke Test

- [ ] **Step 1: Add CLI smoke test for path traversal**

Add to `gitnexus/test/integration/group/group-cli.test.ts` inside the existing `group CLI` describe:

```typescript
  it('test_create_with_invalid_name_fails', () => {
    const result = runGroup(['create', '../../evil']);
    expect(result.status).not.toBe(0);
    expect(result.stderr).toContain('Invalid group name');
  });
```

- [ ] **Step 2: Run test**

Run: `cd gitnexus && npx vitest run test/integration/group/group-cli.test.ts`
Expected: ALL PASS

- [ ] **Step 3: Commit**

```bash
cd gitnexus && git add test/integration/group/group-cli.test.ts
git commit -m "test(group): add CLI smoke test for path traversal rejection

Verifies that 'group create ../../evil' fails with Invalid group name.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>"
```
