Skip to main content
The ScopeTracker tracks which files are currently being worked on by active tasks to detect and prevent concurrent modifications.

ScopeTracker Class

Constructor

constructor()
Creates an empty scope tracker with no registered scopes.

Methods

register()

Registers a task’s file scope as active.
register(taskId: string, scope: string[]): void
taskId
string
required
Unique task identifier
scope
string[]
required
Array of file paths this task will modify

release()

Releases a task’s scope when the task completes.
release(taskId: string): void
taskId
string
required
ID of the task to release

getOverlaps()

Detects overlapping file scopes between a candidate task and active tasks.
getOverlaps(taskId: string, scope: string[]): ScopeOverlap[]
taskId
string
required
ID of the candidate task (excluded from overlap check)
scope
string[]
required
File paths to check for overlaps
return
ScopeOverlap[]
Array of overlapping scopes with other active tasks
interface ScopeOverlap {
  taskId: string;            // ID of the conflicting task
  overlappingFiles: string[]; // Files that overlap
}

getLockedFiles()

Returns all files currently locked by active tasks.
getLockedFiles(): string[]
return
string[]
Sorted array of all file paths currently being modified

Usage Example

import { ScopeTracker } from "@longshot/orchestrator";

const tracker = new ScopeTracker();

// Register task scopes
tracker.register("task-1", ["src/auth.ts", "src/middleware/auth.ts"]);
tracker.register("task-2", ["src/api/users.ts", "src/types/user.ts"]);

// Check for overlaps before dispatching a new task
const candidateScope = ["src/auth.ts", "src/utils.ts"];
const overlaps = tracker.getOverlaps("task-3", candidateScope);

if (overlaps.length > 0) {
  console.warn("Scope overlap detected!");
  for (const overlap of overlaps) {
    console.log(`Conflicts with ${overlap.taskId}:`, overlap.overlappingFiles);
  }
  // Decision: defer task, route to subplanner, or accept risk
} else {
  tracker.register("task-3", candidateScope);
  // Safe to dispatch
}

// When task completes
tracker.release("task-1");

// View all locked files
console.log("Currently locked:", tracker.getLockedFiles());
// Output: ["src/api/users.ts", "src/types/user.ts", "src/auth.ts", "src/utils.ts"]

Conflict Detection Strategy

The scope tracker enables different conflict handling strategies:

1. Strict Blocking

Reject any task with overlapping scope:
const overlaps = tracker.getOverlaps(taskId, scope);
if (overlaps.length > 0) {
  throw new Error(`Cannot dispatch: conflicts with ${overlaps[0].taskId}`);
}

2. Deferred Dispatch

Queue task until conflicting scopes are released:
const overlaps = tracker.getOverlaps(taskId, scope);
if (overlaps.length > 0) {
  deferredQueue.push({ taskId, scope, blockedBy: overlaps });
  return;
}
tracker.register(taskId, scope);

3. Subplanner Routing

Route large overlapping scopes to the subplanner for decomposition:
const overlaps = tracker.getOverlaps(taskId, scope);
if (overlaps.length > 0 && scope.length > SUBPLANNER_THRESHOLD) {
  // Subplanner can break it into non-overlapping subtasks
  return routeToSubplanner(task);
}

4. Risk-Aware Dispatch

Accept low overlap risks, escalate high overlap:
const overlaps = tracker.getOverlaps(taskId, scope);
const overlapCount = overlaps.reduce((sum, o) => sum + o.overlappingFiles.length, 0);

if (overlapCount > 3) {
  return routeToSubplanner(task);
} else if (overlapCount > 0) {
  logger.warn(`Dispatching task with ${overlapCount} file overlaps`);
}

tracker.register(taskId, scope);

Scope Granularity

File paths should be normalized before registration:
function normalizeScope(scope: string[]): string[] {
  return scope
    .map(p => p.replace(/\\/g, "/")) // Normalize separators
    .map(p => p.replace(/^\.?\//, "")) // Remove leading ./
    .filter(p => p.length > 0);       // Remove empty
}

const normalizedScope = normalizeScope(task.scope);
tracker.register(task.id, normalizedScope);

Cleanup Pattern

Always release scopes in a finally block:
tracker.register(taskId, scope);
try {
  await executeTask(task);
} finally {
  tracker.release(taskId);
}

Build docs developers (and LLMs) love