feat: enhance project root detection with caching and debug logging

This commit is contained in:
catlog22
2026-01-24 10:04:04 +08:00
parent a293a01d85
commit 6c80168612
2 changed files with 120 additions and 47 deletions

View File

@@ -316,65 +316,60 @@ batch.forEach(id => updateTodo(id, 'completed'));
function dispatchExecutor(solutionId, executorType, worktreePath = null) {
// If worktree is provided, executor works in that directory
// No per-solution worktree creation - ONE worktree for entire queue
const cdCommand = worktreePath ? `cd "${worktreePath}"` : '';
// Pre-defined values (replaced at dispatch time, NOT by executor)
const SOLUTION_ID = solutionId;
const WORK_DIR = worktreePath || null;
// Build prompt without markdown code blocks to avoid escaping issues
const prompt = `
## Execute Solution ${solutionId}
${worktreePath ? `
### Step 0: Enter Queue Worktree
\`\`\`bash
cd "${worktreePath}"
\`\`\`
` : ''}
### Step 1: Get Solution (read-only)
\`\`\`bash
ccw issue detail ${solutionId}
\`\`\`
## Execute Solution: ${SOLUTION_ID}
${WORK_DIR ? `Working Directory: ${WORK_DIR}` : ''}
### Step 1: Get Solution Details
Run this command to get the full solution with all tasks:
ccw issue detail ${SOLUTION_ID}
### Step 2: Execute All Tasks Sequentially
The detail command returns a FULL SOLUTION with all tasks.
Execute each task in order (T1 → T2 → T3 → ...):
For each task:
1. Follow task.implementation steps
2. Run task.test commands
3. Verify task.acceptance criteria
(Do NOT commit after each task)
- Follow task.implementation steps
- Run task.test commands
- Verify task.acceptance criteria
- Do NOT commit after each task
### Step 3: Commit Solution (Once)
After ALL tasks pass, commit once with formatted summary:
\`\`\`bash
git add <all-modified-files>
git commit -m "[type](scope): [solution.description]
After ALL tasks pass, commit once with formatted summary.
## Solution Summary
- Solution-ID: ${solutionId}
- Tasks: T1, T2, ...
Command:
git add -A
git commit -m "<type>(<scope>): <description>
## Tasks Completed
- [T1] task1.title: action
- [T2] task2.title: action
Solution: ${SOLUTION_ID}
Tasks completed: <list task IDs>
## Files Modified
- file1.ts
- file2.ts
Changes:
- <file1>: <what changed>
- <file2>: <what changed>
## Verification
- All tests passed
- All acceptance criteria verified"
\`\`\`
Verified: all tests passed"
Replace <type> with: feat|fix|refactor|docs|test
Replace <scope> with: affected module name
Replace <description> with: brief summary from solution
### Step 4: Report Completion
\`\`\`bash
ccw issue done ${solutionId} --result '{"summary": "...", "files_modified": [...], "commit": {"hash": "...", "type": "feat"}, "tasks_completed": N}'
\`\`\`
On success, run:
ccw issue done ${SOLUTION_ID} --result '{"summary": "<brief>", "files_modified": ["<file1>", "<file2>"], "commit": {"hash": "<hash>", "type": "<type>"}, "tasks_completed": <N>}'
If any task failed:
\`\`\`bash
ccw issue done ${solutionId} --fail --reason '{"task_id": "TX", "error_type": "test_failure", "message": "..."}'
\`\`\`
On failure, run:
ccw issue done ${SOLUTION_ID} --fail --reason '{"task_id": "<TX>", "error_type": "<test_failure|build_error|other>", "message": "<error details>"}'
**Note**: Do NOT cleanup worktree after this solution. Worktree is shared by all solutions in the queue.
### Important Notes
- Do NOT cleanup worktree - it is shared by all solutions in the queue
- Replace all <placeholder> values with actual values from your execution
`;
// For CLI tools, pass --cd to set working directory

View File

@@ -233,6 +233,28 @@ const ISSUES_DIR = '.workflow/issues';
// ============ Storage Layer (JSONL) ============
/**
* Cached project root to avoid repeated git command execution
*/
let cachedProjectRoot: string | null = null;
/**
* Clear cached project root (for testing)
*/
export function clearProjectRootCache(): void {
cachedProjectRoot = null;
}
/**
* Debug logging helper (enabled via CCW_DEBUG=true)
*/
const DEBUG = process.env.CCW_DEBUG === 'true';
function debugLog(msg: string): void {
if (DEBUG) {
console.log(`[ccw:worktree] ${msg}`);
}
}
/**
* Normalize path for comparison (handles Windows case sensitivity)
*/
@@ -271,7 +293,32 @@ function resolveMainRepoFromGitFile(gitFilePath: string): string | null {
* This ensures .workflow/issues/ is always accessed from the main repo.
*/
function getProjectRoot(): string {
// First, try to detect if we're in a git worktree using git commands
// Return cached result if available
if (cachedProjectRoot) {
debugLog(`Using cached project root: ${cachedProjectRoot}`);
return cachedProjectRoot;
}
debugLog(`Detecting project root from cwd: ${process.cwd()}`);
// Priority 1: Check CCW_MAIN_REPO environment variable
const envMainRepo = process.env.CCW_MAIN_REPO;
if (envMainRepo) {
debugLog(`Found CCW_MAIN_REPO env: ${envMainRepo}`);
const hasWorkflow = existsSync(join(envMainRepo, '.workflow'));
const hasGit = existsSync(join(envMainRepo, '.git'));
if (hasWorkflow || hasGit) {
debugLog(`CCW_MAIN_REPO validated (workflow=${hasWorkflow}, git=${hasGit})`);
cachedProjectRoot = envMainRepo;
return envMainRepo;
} else {
console.warn('[ccw] CCW_MAIN_REPO is set but path is invalid (no .workflow or .git)');
console.warn(`[ccw] Path: ${envMainRepo}`);
}
}
// Priority 2: Try to detect if we're in a git worktree using git commands
try {
// Get the common git directory (points to main repo's .git)
const gitCommonDir = execSync('git rev-parse --git-common-dir', {
@@ -287,6 +334,9 @@ function getProjectRoot(): string {
timeout: EXEC_TIMEOUTS.GIT_QUICK,
}).trim();
debugLog(`Git common dir: ${gitCommonDir}`);
debugLog(`Git dir: ${gitDir}`);
// Normalize paths for comparison (Windows case insensitive)
const normalizedCommon = normalizePath(gitCommonDir);
const normalizedGit = normalizePath(gitDir);
@@ -298,8 +348,12 @@ function getProjectRoot(): string {
// .git directory's parent is the repo root
const mainRepoRoot = resolve(absoluteCommonDir, '..');
debugLog(`Detected worktree, main repo: ${mainRepoRoot}`);
// Verify .workflow or .git exists in main repo
if (existsSync(join(mainRepoRoot, '.workflow')) || existsSync(join(mainRepoRoot, '.git'))) {
debugLog(`Main repo validated, returning: ${mainRepoRoot}`);
cachedProjectRoot = mainRepoRoot;
return mainRepoRoot;
}
}
@@ -307,10 +361,11 @@ function getProjectRoot(): string {
if (isExecTimeoutError(err)) {
console.warn(`[issue] git rev-parse timed out after ${EXEC_TIMEOUTS.GIT_QUICK}ms; falling back to filesystem detection`);
}
debugLog(`Git command failed, falling back to filesystem detection`);
// Git command failed - fall through to manual detection
}
// Standard detection with worktree file support: walk up to find .workflow or .git
// Priority 3: Standard detection with worktree file support: walk up to find .workflow or .git
let dir = process.cwd();
while (dir !== resolve(dir, '..')) {
const gitPath = join(dir, '.git');
@@ -322,22 +377,45 @@ function getProjectRoot(): string {
if (gitStat.isFile()) {
// .git is a file - this is a worktree, try to resolve main repo
const mainRepo = resolveMainRepoFromGitFile(gitPath);
if (mainRepo && existsSync(join(mainRepo, '.workflow'))) {
return mainRepo;
debugLog(`Parsed .git file, main repo: ${mainRepo}`);
if (mainRepo) {
// Verify main repo has .git directory (always true for main repo)
// Don't require .workflow - it may not exist yet in a new repo
const hasGit = existsSync(join(mainRepo, '.git'));
const hasWorkflow = existsSync(join(mainRepo, '.workflow'));
if (hasGit || hasWorkflow) {
if (!hasWorkflow) {
console.warn('[ccw] Worktree detected but main repo has no .workflow directory');
console.warn(`[ccw] Main repo: ${mainRepo}`);
console.warn('[ccw] Issue commands may fail until .workflow is created');
console.warn('[ccw] Set CCW_MAIN_REPO environment variable to override detection');
}
debugLog(`Main repo validated via .git file (git=${hasGit}, workflow=${hasWorkflow})`);
cachedProjectRoot = mainRepo;
return mainRepo;
}
}
// If main repo doesn't have .workflow, fall back to current worktree
}
} catch {
// stat failed, continue with normal logic
debugLog(`Failed to stat ${gitPath}, continuing`);
}
}
if (existsSync(join(dir, '.workflow')) || existsSync(gitPath)) {
debugLog(`Found project root at: ${dir}`);
cachedProjectRoot = dir;
return dir;
}
dir = resolve(dir, '..');
}
return process.cwd();
debugLog(`No project root found, using cwd: ${process.cwd()}`);
const fallback = process.cwd();
cachedProjectRoot = fallback;
return fallback;
}
function getIssuesDir(): string {