* refactor: replace recursive auto-dispatch with linear autoLoop, delete ~3k lines of dead code Replace the complex recursive dispatch system (dispatchNextUnit, reentrancy guards, stall detection, idempotency tracking, skip-depth machinery) with a simple linear while(s.active) loop in auto-loop.ts. Key changes: - New auto-loop.ts with autoLoop(), runUnit(), resolveAgentEnd() - Deleted auto-idempotency.ts, auto-stuck-detection.ts, session-lock.ts, mechanical-completion.ts, progress-score.ts, auto-constants.ts, unit-id.ts - Extracted WorktreeResolver class for worktree path resolution - Added auto-worktree-sync.ts for worktree synchronization - Simplified auto.ts from ~1400 lines to ~400 lines - Fixed 9 TypeScript errors (NotifyCtx type widening, capture typing) - Comprehensive test coverage: 32 auto-loop tests + worktree resolver/DB tests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address 6 audit findings in auto-loop refactor 1. CRITICAL: Move pendingResolve to AutoSession + queue orphaned agent_end events instead of silently dropping them. Prevents permanent stalls when error-recovery sendMessage retries fire between loop iterations. 2. HIGH: Scope pendingResolve per-session via _activeSession ref, preventing concurrent /gsd auto sessions from corrupting each other's promises. 3. HIGH: Replace console.log in dispatchHookUnit with debugLog to prevent hook prompt content (potentially containing secrets) from leaking to stdout. 4. HIGH: Restore parked milestone handling in state.ts — Phase 1 skips parked milestones so they don't satisfy depends_on, Phase 2 registers them as 'parked' status. Add 'parked' to MilestoneRegistryEntry type. 5. MEDIUM: Restore queuePhaseActive parameter in shouldBlockContextWrite and re-export setQueuePhaseActive for guided-flow-queue.ts consumers. 6. MEDIUM: Add MAX_LOOP_ITERATIONS (500) lifetime cap to autoLoop to prevent runaway loops when units alternate between IDs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve build breakers, add correctness fixes, and graduated recovery Build breakers (CRITICAL): - Restore unit-id.ts (deleted but still imported by complexity-classifier.ts, metrics.ts) - Restore progress-score.ts (deleted but still imported by commands.ts, dashboard-overlay.ts, doctor.ts) - Rewrite worktree-sync-milestones.test.ts to use new syncProjectRootToWorktree API Correctness fixes (MEDIUM): - Cap pendingAgentEndQueue to 3 entries to prevent unbounded growth from stale events - Add milestoneId path traversal validation in WorktreeResolver - Clear depthVerificationDone on session_start to prevent cross-session leaks in RPC mode - Add verification gate for non-hook sidecar units (triage, quick-tasks) - Remove dead handleAgentEnd import from index.ts Graduated recovery (Jeremy's feedback): - Blanket try/catch around loop body — one bad iteration no longer kills the session - Graduated stuck recovery: at count 3 try artifact verification + cache invalidation, at count 5 hard stop (was: binary stop at 5 with no recovery attempt) - Graduated error recovery: 1st error retries, 2nd invalidates caches, 3rd stops Test results: 32/32 auto-loop, 28/28 worktree-resolver, 11/11 sidecar-queue, tsc clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: restore copyWorktreeDb/reconcileWorktreeDb exports and fix loadToolApiKeys import Two missing exports caused ~90% of the 120 pre-existing test failures: 1. copyWorktreeDb + reconcileWorktreeDb — imported by auto-worktree.ts but never added to gsd-db.ts. Restored with the original implementations. 2. loadToolApiKeys — moved to commands-config.ts but index.ts still imported from commands.ts. Fixed the import path. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: move loadToolApiKeys import to commands-config.js loadToolApiKeys was moved to commands-config.ts but index.ts still imported it from commands.ts, causing runtime failures in all tests that transitively load the extension entry point. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: fix provider error assertion on windows --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
133 lines
3.6 KiB
TypeScript
133 lines
3.6 KiB
TypeScript
import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, rmSync, statSync, symlinkSync, unlinkSync } from "node:fs";
|
|
import { delimiter, join } from "node:path";
|
|
|
|
type ManagedTool = "fd" | "rg";
|
|
|
|
interface ToolSpec {
|
|
targetName: string;
|
|
candidates: string[];
|
|
}
|
|
|
|
const TOOL_SPECS: Record<ManagedTool, ToolSpec> = {
|
|
fd: {
|
|
targetName: process.platform === "win32" ? "fd.exe" : "fd",
|
|
candidates: process.platform === "win32" ? ["fd.exe", "fd", "fdfind.exe", "fdfind"] : ["fd", "fdfind"],
|
|
},
|
|
rg: {
|
|
targetName: process.platform === "win32" ? "rg.exe" : "rg",
|
|
candidates: process.platform === "win32" ? ["rg.exe", "rg"] : ["rg"],
|
|
},
|
|
};
|
|
|
|
function splitPath(pathValue: string | undefined): string[] {
|
|
if (!pathValue) return [];
|
|
return pathValue.split(delimiter).map((segment) => segment.trim()).filter(Boolean);
|
|
}
|
|
|
|
function getCandidateNames(name: string): string[] {
|
|
if (process.platform !== "win32") return [name];
|
|
const lower = name.toLowerCase();
|
|
if (lower.endsWith(".exe") || lower.endsWith(".cmd") || lower.endsWith(".bat")) return [name];
|
|
return [name, `${name}.exe`, `${name}.cmd`, `${name}.bat`];
|
|
}
|
|
|
|
function isRegularFile(path: string): boolean {
|
|
try {
|
|
const stat = lstatSync(path);
|
|
return stat.isFile() || stat.isSymbolicLink();
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function pathExistsIncludingBrokenSymlink(path: string): boolean {
|
|
try {
|
|
lstatSync(path);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function isBrokenSymlink(path: string): boolean {
|
|
try {
|
|
const stat = lstatSync(path);
|
|
if (!stat.isSymbolicLink()) return false;
|
|
try {
|
|
statSync(path);
|
|
return false;
|
|
} catch {
|
|
return true;
|
|
}
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function removeTargetPath(path: string): void {
|
|
try {
|
|
const stat = lstatSync(path);
|
|
if (stat.isSymbolicLink()) {
|
|
unlinkSync(path);
|
|
return;
|
|
}
|
|
rmSync(path, { force: true });
|
|
} catch {
|
|
// Path already absent.
|
|
}
|
|
}
|
|
|
|
export function resolveToolFromPath(tool: ManagedTool, pathValue: string | undefined = process.env.PATH): string | null {
|
|
const spec = TOOL_SPECS[tool];
|
|
for (const dir of splitPath(pathValue)) {
|
|
for (const candidate of spec.candidates) {
|
|
for (const name of getCandidateNames(candidate)) {
|
|
const fullPath = join(dir, name);
|
|
if (existsSync(fullPath) && isRegularFile(fullPath)) {
|
|
return fullPath;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function provisionTool(targetDir: string, tool: ManagedTool, sourcePath: string): string {
|
|
const targetPath = join(targetDir, TOOL_SPECS[tool].targetName);
|
|
const brokenTarget = isBrokenSymlink(targetPath);
|
|
if (pathExistsIncludingBrokenSymlink(targetPath)) {
|
|
if (!brokenTarget) return targetPath;
|
|
removeTargetPath(targetPath);
|
|
}
|
|
|
|
mkdirSync(targetDir, { recursive: true });
|
|
|
|
if (!brokenTarget) {
|
|
try {
|
|
symlinkSync(sourcePath, targetPath);
|
|
return targetPath;
|
|
} catch {
|
|
// Fall back to copying below.
|
|
}
|
|
}
|
|
|
|
removeTargetPath(targetPath);
|
|
copyFileSync(sourcePath, targetPath);
|
|
chmodSync(targetPath, 0o755);
|
|
|
|
return targetPath;
|
|
}
|
|
|
|
export function ensureManagedTools(targetDir: string, pathValue: string | undefined = process.env.PATH): string[] {
|
|
const provisioned: string[] = [];
|
|
|
|
for (const tool of Object.keys(TOOL_SPECS) as ManagedTool[]) {
|
|
const targetPath = join(targetDir, TOOL_SPECS[tool].targetName);
|
|
if (pathExistsIncludingBrokenSymlink(targetPath) && !isBrokenSymlink(targetPath)) continue;
|
|
const sourcePath = resolveToolFromPath(tool, pathValue);
|
|
if (!sourcePath) continue;
|
|
provisioned.push(provisionTool(targetDir, tool, sourcePath));
|
|
}
|
|
|
|
return provisioned;
|
|
}
|