diff --git a/src/headless.ts b/src/headless.ts index 438972c3a..3a0d87fa9 100644 --- a/src/headless.ts +++ b/src/headless.ts @@ -723,6 +723,9 @@ async function runHeadlessOnce( let exitCode = 0; let milestoneReady = false; // tracks "Milestone X ready." for auto-chaining let timedOut = false; // true only when the overall timeout timer fires + // Rolling buffer for milestone-ready detection across split streaming deltas. + // Capped at 200 chars — long enough to bridge any realistic delta boundary. + let milestoneDetectionBuffer = ""; let providerAutoResumePending = false; const recentEvents: TrackedEvent[] = []; const interactiveToolCallIds = new Set(); diff --git a/src/resources/extensions/sf/atomic-write.ts b/src/resources/extensions/sf/atomic-write.ts index 84563c4ad..1356c46d6 100644 --- a/src/resources/extensions/sf/atomic-write.ts +++ b/src/resources/extensions/sf/atomic-write.ts @@ -161,7 +161,7 @@ export function atomicWriteSyncWithOps( const tmpPath = ops.createTempPath?.(filePath) ?? defaultTempPath(filePath); ops.writeFile(tmpPath, content, encoding); - let lastError: unknown = null; + const errors: unknown[] = []; let attempts = 0; for (attempts = 1; attempts <= MAX_RENAME_ATTEMPTS; attempts++) { @@ -169,7 +169,7 @@ export function atomicWriteSyncWithOps( ops.rename(tmpPath, filePath); return; } catch (error) { - lastError = error; + errors.push(error); if (!isTransientLockError(error) || attempts === MAX_RENAME_ATTEMPTS) { break; } @@ -178,7 +178,7 @@ export function atomicWriteSyncWithOps( } cleanupTempFileSync(tmpPath, ops); - throw buildAtomicWriteError(filePath, attempts, lastError); + throw buildAtomicWriteError(filePath, attempts, errors); } const DEFAULT_ASYNC_OPS: AtomicWriteAsyncOps = { diff --git a/src/resources/extensions/sf/doctor-runtime-checks.ts b/src/resources/extensions/sf/doctor-runtime-checks.ts index a99cefc16..be0f1dd8c 100644 --- a/src/resources/extensions/sf/doctor-runtime-checks.ts +++ b/src/resources/extensions/sf/doctor-runtime-checks.ts @@ -345,16 +345,8 @@ export async function checkRuntimeHealth( ); // Check for critical runtime patterns that must be present. - // NOTE: SF_RUNTIME_PATTERNS in gitignore.ts is the canonical source of truth. - // This is a minimal subset for the doctor check. - const criticalPatterns = [ - ".sf/activity/", - ".sf/runtime/", - ".sf/auto.lock", - ".sf/sf.db*", - ".sf/completed-units*.json", - ".sf/event-log.jsonl", - ]; + // Use the canonical SF_RUNTIME_PATTERNS list for consistency. + const criticalPatterns = Array.from(SF_RUNTIME_PATTERNS); // If blanket .sf/ or .sf is present, all patterns are covered const hasBlanketIgnore = diff --git a/src/resources/extensions/sf/doctor.ts b/src/resources/extensions/sf/doctor.ts index d2b56e7fd..428d75c00 100644 --- a/src/resources/extensions/sf/doctor.ts +++ b/src/resources/extensions/sf/doctor.ts @@ -637,6 +637,7 @@ export async function runSFDoctor( const requirementsContent = await loadFile(requirementsPath); issues.push(...auditRequirements(requirementsContent)); + const t0state = Date.now(); const state = await deriveState(basePath); // Provider / auth health checks — only relevant when there is active work to dispatch. diff --git a/src/resources/extensions/sf/jsonl-utils.ts b/src/resources/extensions/sf/jsonl-utils.ts index b7620c177..23017750f 100644 --- a/src/resources/extensions/sf/jsonl-utils.ts +++ b/src/resources/extensions/sf/jsonl-utils.ts @@ -11,6 +11,7 @@ export const MAX_JSONL_BYTES = 10 * 1024 * 1024; // 10 MB /** * Parse a raw JSONL string into an array of parsed objects. + * * If the input exceeds MAX_JSONL_BYTES, only the tail is parsed (most recent entries). */ export function parseJSONL(raw: string): unknown[] { diff --git a/src/resources/extensions/sf/self-feedback.ts b/src/resources/extensions/sf/self-feedback.ts index 15e79ac30..3ec86ec98 100644 --- a/src/resources/extensions/sf/self-feedback.ts +++ b/src/resources/extensions/sf/self-feedback.ts @@ -236,7 +236,7 @@ function escapeCell(text: string): string { function readActiveUnit(basePath: string): SelfFeedbackOccurredIn | undefined { try { - const lockPath = join(basePath, ".sf", "auto.lock"); + const lockPath = join(sfRuntimeRoot(basePath), "auto.lock"); if (!existsSync(lockPath)) return undefined; const lock = JSON.parse(readFileSync(lockPath, "utf-8")); const id: string | undefined = lock?.unitId; diff --git a/src/resources/extensions/sf/workflow-templates.ts b/src/resources/extensions/sf/workflow-templates.ts index f5ff652c2..74b9116f4 100644 --- a/src/resources/extensions/sf/workflow-templates.ts +++ b/src/resources/extensions/sf/workflow-templates.ts @@ -133,6 +133,9 @@ export function loadRegistry(): TemplateRegistry { * * Consumer: `/sf` command completion catalogs and `/sf start` usage rendering. */ +/** + * Get registry-backed command definitions for /sf start completion and help. + */ export function workflowTemplateCommandDefinitions(): WorkflowTemplateCommandDefinition[] { const registry = loadRegistry(); return Object.entries(registry.templates).map(([id, entry]) => ({ diff --git a/src/resources/extensions/sf/worktree-resolver.ts b/src/resources/extensions/sf/worktree-resolver.ts index 43f60cde6..e853e75e5 100644 --- a/src/resources/extensions/sf/worktree-resolver.ts +++ b/src/resources/extensions/sf/worktree-resolver.ts @@ -620,11 +620,13 @@ export class WorktreeResolver { ); // Clean up stale merge state left by failed squash-merge (#1389) - // Use resolveGitDir to handle worktrees where .git is a file (gitdir pointer) + // Use resolveGitDir to handle worktrees where .git is a file (gitdir pointer). + // Use resolve() rather than join() to normalise any backslash separators on + // Windows and avoid mixed-separator paths in downstream comparisons. try { const gitDir = resolveGitDir(originalBase || this.s.basePath); for (const f of ["SQUASH_MSG", "MERGE_HEAD", "MERGE_MSG"]) { - const p = join(gitDir, f); + const p = resolve(gitDir, f); if (existsSync(p)) unlinkSync(p); } } catch {