Back to Gitnexus

PR #626 HIGH-Priority Fixes Implementation Plan

docs/superpowers/plans/2026-04-02-pr626-high-fixes.md

1.6.324.2 KB
Original Source

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) <[email protected]>"

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) <[email protected]>"

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) <[email protected]>"

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) <[email protected]>"

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) <[email protected]>"