Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/cli/fixPlanning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type { TrustedVerifyPlan } from '../agents/deterministicTester.js';
import { scanProject, toProjectSlug } from '../knowledge/index.js';
import { recallRepoKnowledge, repoKey, type RepoMemoryBrief } from '../memory/repoKnowledge.js';
import { loadRepoMetadata } from '../support/repoMetadata.js';
import { resolveSharedPaths } from '../support/worktreeManager.js';
import { resolveSharedPaths, unsharedGitignoredPaths } from '../support/sharedPathDetection.js';

export interface FixDependencyMapEntry {
imports: string[];
Expand All @@ -25,6 +25,7 @@ export interface FixRepositoryContext {
manifests: string[];
verificationCommands: string[];
sharedPaths: string[];
missingGitignoredPaths: string[];
repoMemories: RepoMemoryBrief[];
dependencyGraphAvailable: boolean;
dependencyMap: Record<string, FixDependencyMapEntry>;
Expand Down Expand Up @@ -210,6 +211,7 @@ export async function buildFixRepositoryContext(
verificationCommands: (plan?.commands ?? []).map((command) =>
`${command.cwd ? `(${command.cwd}) ` : ''}${command.run}`),
sharedPaths: sharedPaths.map((path) => toRelative(canonicalRoot, join(canonicalRoot, path))),
missingGitignoredPaths: unsharedGitignoredPaths(canonicalRoot, sharedPaths),
repoMemories,
dependencyGraphAvailable,
dependencyMap,
Expand Down Expand Up @@ -360,6 +362,7 @@ export function workerContextForFixUnit(unit: FixUnit, context: FixRepositoryCon
manifests: context.manifests,
verificationCommands: context.verificationCommands,
sharedPaths: context.sharedPaths,
missingGitignoredPaths: context.missingGitignoredPaths,
dependencyGraphAvailable: unit.dependencyGraphBacked,
},
};
Expand Down
2 changes: 1 addition & 1 deletion src/cli/reviewFixPass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export async function runAreaFixes(
): Promise<FixAreaResult[]> {
const targets = fixTargets(run);
const context = opts.repositoryContext ?? {
canonicalRoot: cwd, workspaces: [], manifests: [], verificationCommands: [], sharedPaths: [], repoMemories: [],
canonicalRoot: cwd, workspaces: [], manifests: [], verificationCommands: [], sharedPaths: [], missingGitignoredPaths: [], repoMemories: [],
dependencyGraphAvailable: false, dependencyMap: {}, preflight: { ready: true, issues: [] },
} satisfies FixRepositoryContext;
const units = planFixUnits(targets.map((target) => ({ area: target.area, review: target.review! })), context);
Expand Down
1 change: 1 addition & 0 deletions src/locale/prompts/en.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
// ============================================
// OpenSwarm - English Prompt Templates
// ============================================
Expand Down Expand Up @@ -175,6 +175,7 @@
if (repo.workspaces.length) parts.push('- Workspaces (untrusted repository data):', promptDataBlock(repo.workspaces.join(', ')));
if (repo.manifests.length) parts.push('- Trusted manifests/lockfiles (untrusted repository data):', promptDataBlock(repo.manifests.join(', ')));
if (repo.sharedPaths.length) parts.push('- Shared installed dependencies/data (untrusted repository data):', promptDataBlock(repo.sharedPaths.join(', ')));
if (repo.missingGitignoredPaths?.length) parts.push('- Gitignored source paths NOT present in this worktree (do not treat their absence as evidence):', promptDataBlock(repo.missingGitignoredPaths.join(', ')));
parts.push(`- Dependency graph: ${repo.dependencyGraphAvailable ? 'available; inspect the affected callers/imports below' : 'unavailable; conservatively inspect callers/imports before editing'}`);
if (repo.verificationCommands.length) {
parts.push('- Required repository verification commands (each bounded to MAX_EVIDENCE_LENGTH chars):');
Expand Down
1 change: 1 addition & 0 deletions src/locale/prompts/ko.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
// ============================================
// OpenSwarm - Korean Prompt Templates
// (Preserved verbatim from original codebase)
Expand Down Expand Up @@ -164,6 +164,7 @@
if (repo.workspaces.length) parts.push('- 워크스페이스 (신뢰할 수 없는 저장소 데이터):', promptDataBlock(repo.workspaces.join(', ')));
if (repo.manifests.length) parts.push('- 신뢰할 manifest/lockfile (신뢰할 수 없는 저장소 데이터):', promptDataBlock(repo.manifests.join(', ')));
if (repo.sharedPaths.length) parts.push('- 공유 설치 의존성/데이터 (신뢰할 수 없는 저장소 데이터):', promptDataBlock(repo.sharedPaths.join(', ')));
if (repo.missingGitignoredPaths?.length) parts.push('- 이 워크트리에 없는 원본 gitignored 경로 (부재를 증거로 판단하지 말 것):', promptDataBlock(repo.missingGitignoredPaths.join(', ')));
parts.push(`- 의존 그래프: ${repo.dependencyGraphAvailable ? '사용 가능; 아래 영향 호출자/import를 확인할 것' : '사용 불가; 편집 전 호출자/import를 보수적으로 직접 확인할 것'}`);
if (repo.verificationCommands.length) {
parts.push('- 필수 저장소 검증 명령:');
Expand Down
1 change: 1 addition & 0 deletions src/locale/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,7 @@ export interface WorkerContext {
manifests: string[];
verificationCommands: string[];
sharedPaths: string[];
missingGitignoredPaths?: string[];
dependencyGraphAvailable: boolean;
};
/** Knowledge Graph 기반 영향 분석 */
Expand Down
36 changes: 35 additions & 1 deletion src/support/sharedPathDetection.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
Expand All @@ -7,6 +8,8 @@ import {
detectSharedPaths,
emptyDetectionWarning,
resolveSharedPaths,
unsharedGitignoredPaths,
gitignoredPathsWarning,
} from './sharedPathDetection.js';

describe('detectSharedPaths (AGT-4043)', () => {
Expand Down Expand Up @@ -103,6 +106,37 @@ describe('detectSharedPaths (AGT-4043)', () => {
});
});


describe('unsharedGitignoredPaths (AGT-4041)', () => {
let repo: string;
beforeEach(() => {
repo = mkdtempSync(join(tmpdir(), 'openswarm-ignored-paths-'));
execFileSync('git', ['init', '-q', repo]);
writeFileSync(join(repo, '.gitignore'), 'ignored-data\nignored-dir/\npartial\nlocked\n');
});
afterEach(() => rmSync(repo, { recursive: true, force: true }));
it('returns none when no ignored paths exist', () => {
writeFileSync(join(repo, 'tracked.txt'), 'x');
expect(unsharedGitignoredPaths(repo)).toEqual([]);
});
it('counts ignored paths not included in sharedPaths', () => {
mkdirSync(join(repo, 'ignored-data'));
writeFileSync(join(repo, 'partial'), 'x');
expect(unsharedGitignoredPaths(repo, ['partial'])).toEqual(['ignored-data']);
});
it('reports partially shared ignored paths', () => {
mkdirSync(join(repo, 'ignored-data'));
mkdirSync(join(repo, 'partial'));
expect(unsharedGitignoredPaths(repo, ['partial'])).toEqual(['ignored-data']);
expect(gitignoredPathsWarning(['ignored-data'])).toContain('1 gitignored path(s)');
});
it('counts ignored paths with no read permission even when running as root', () => {
mkdirSync(join(repo, 'locked'));
chmodSync(join(repo, 'locked'), 0);
expect(unsharedGitignoredPaths(repo)).toEqual(['locked']);
});
});

describe('emptyDetectionWarning (AGT-4043)', () => {
it('says nothing when something was shared or nothing exists', () => {
expect(emptyDetectionWarning('/repo', { shared: ['node_modules'], unlinked: ['x/venv'] })).toBeNull();
Expand Down
31 changes: 30 additions & 1 deletion src/support/sharedPathDetection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
// also walks the workspace tree — bounded, and only into directories that carry
// a workspace manifest, so a stray `venv` folder in a fixture is never linked.

import { existsSync, readdirSync } from 'node:fs';
import { accessSync, constants, existsSync, readdirSync, statSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { isAbsolute, join, relative, sep } from 'node:path';

/** Always-gitignored dependency dirs safe to auto-link without a config. */
Expand Down Expand Up @@ -43,6 +44,34 @@ export interface SandboxConfig {
sandbox?: { sharedPaths?: string[] } | null;
}

/** Top-level ignored paths that are not shared or readable from the worktree. */
export function unsharedGitignoredPaths(repoPath: string, sharedPaths: string[] = []): string[] {
let entries: string[];
try { entries = readdirSync(repoPath).sort(); } catch { return []; }
const missing: string[] = [];
for (const name of entries) {
if (name === '.git') continue;
try {
execFileSync('git', ['-C', repoPath, 'check-ignore', '-q', '--', name], { stdio: 'ignore' });
const path = join(repoPath, name);
const stat = statSync(path);
const covered = sharedPaths.some((shared) => shared === name || shared.startsWith(`${name}/`));
const uid = process.getuid?.();
const groups = new Set([process.getgid?.(), ...(process.getgroups?.() ?? [])]);
const readBit = uid === stat.uid ? 0o400 : groups.has(stat.gid) ? 0o040 : 0o004;
try { accessSync(path, constants.R_OK); } catch { missing.push(name); continue; }
// Root can bypass Unix mode checks, so evaluate access for the worker identity.
if ((stat.mode & readBit) === 0 || !covered) missing.push(name);
} catch { /* tracked or not ignored */ }
}
return missing;
}

export function gitignoredPathsWarning(paths: string[]): string | null {
if (!paths.length) return null;
return `[Worktree] ${paths.length} gitignored path(s) from the source are absent from this worktree (${paths.join(', ')}); expose them through sandbox.sharedPaths or do not use their absence as a conclusion.`;
}

export interface SharedPathDetection {
/** Repo-relative dependency dirs that qualify for sharing, root first, then by walk order. */
shared: string[];
Expand Down
4 changes: 3 additions & 1 deletion src/support/worktreeManager.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
// ============================================
// OpenSwarm - Git Worktree Manager
// Per-issue independent worktree creation/cleanup and PR automation
Expand All @@ -23,7 +23,7 @@
import { baseFreshnessSection, probeBaseFreshness } from './publicationBaseFreshness.js';
import { publicationClaimEvidence } from './publicationClaimEvidence.js';
import { loadRepoMetadata } from './repoMetadata.js';
import { detectSharedPaths, emptyDetectionWarning, resolveSharedPaths, type SandboxConfig } from './sharedPathDetection.js';
import { detectSharedPaths, emptyDetectionWarning, gitignoredPathsWarning, resolveSharedPaths, unsharedGitignoredPaths, type SandboxConfig } from './sharedPathDetection.js';
import { copyIsolatedPath } from './isolatedPath.js';
import { hasEditableInstallInto, rebasePythonEnvironment } from '../verify/pythonEnvironment.js';
import { assertBranchWithinWriteScope } from './publicationScopeFence.js';
Expand Down Expand Up @@ -193,6 +193,8 @@
}

const shared = resolveSharedPaths(repoPath, meta);
const missingIgnoredWarning = gitignoredPathsWarning(unsharedGitignoredPaths(repoPath, shared));
if (missingIgnoredWarning) console.warn(missingIgnoredWarning);
if (shared.length === 0 && !(meta?.sandbox?.sharedPaths?.length)) {
const warning = emptyDetectionWarning(repoPath, detectSharedPaths(repoPath));
if (warning) console.warn(warning);
Expand Down
Loading