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
40 changes: 38 additions & 2 deletions packages/build/src/esbuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,42 @@ import { execSync } from "child_process"

import { ViewsContainer, Views, Menus, Configuration, Keybindings, contributesSchema } from "./types.js"

/**
* Copy a single file with retry logic to handle transient Windows file-locking
* errors (EBUSY, EPERM, EACCES) that occur when antivirus or indexing services
* hold brief locks on files during CI builds.
*/
function copyFileWithRetry(src: string, dst: string, maxRetries: number = 5): void {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
fs.copyFileSync(src, dst)
return
} catch (error) {
const isRetryable =
error instanceof Error &&
"code" in error &&
((error as NodeJS.ErrnoException).code === "EBUSY" ||
(error as NodeJS.ErrnoException).code === "EPERM" ||
(error as NodeJS.ErrnoException).code === "EACCES")

if (!isRetryable || attempt === maxRetries) {
throw error
}

const baseDelay = process.platform === "win32" ? 200 : 100
const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), 2000)
console.warn(`[copyFileWithRetry] Attempt ${attempt} failed for ${src}, retrying in ${delay}ms...`)

// Synchronous sleep (same pattern as rmDir).
const start = Date.now()

while (Date.now() - start < delay) {
/* Busy wait */
}
}
}
}

function copyDir(srcDir: string, dstDir: string, count: number): number {
const entries = fs.readdirSync(srcDir, { withFileTypes: true })

Expand All @@ -16,7 +52,7 @@ function copyDir(srcDir: string, dstDir: string, count: number): number {
count = copyDir(srcPath, dstPath, count)
} else {
count = count + 1
fs.copyFileSync(srcPath, dstPath)
copyFileWithRetry(srcPath, dstPath)
}
}

Expand Down Expand Up @@ -98,7 +134,7 @@ export function copyPaths(copyPaths: [string, string, CopyPathOptions?][], srcDi
const count = copyDir(path.join(srcDir, srcRelPath), path.join(dstDir, dstRelPath), 0)
console.log(`[copyPaths] Copied ${count} files from ${srcRelPath} to ${dstRelPath}`)
} else {
fs.copyFileSync(path.join(srcDir, srcRelPath), path.join(dstDir, dstRelPath))
copyFileWithRetry(path.join(srcDir, srcRelPath), path.join(dstDir, dstRelPath))
console.log(`[copyPaths] Copied ${srcRelPath} to ${dstRelPath}`)
}
} catch (error) {
Expand Down
51 changes: 51 additions & 0 deletions packages/types/src/__tests__/context-handoff.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { contextHandoffSummarySchema } from "../context-handoff.js"

describe("ContextHandoffSummary schema", () => {
it("validates a complete summary", () => {
const summary = {
mode: "code",
filesModified: ["src/app.ts", "src/utils.ts"],
filesRead: ["src/config.ts"],
commandsExecuted: ["npm test"],
toolUsageCounts: { write_to_file: 2, read_file: 1 },
apiRequestCount: 5,
result: "Task completed successfully",
}
const result = contextHandoffSummarySchema.safeParse(summary)
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.filesModified).toEqual(["src/app.ts", "src/utils.ts"])
expect(result.data.mode).toBe("code")
}
})

it("accepts minimal summary with only result", () => {
const summary = { result: "Done" }
const result = contextHandoffSummarySchema.safeParse(summary)
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.filesModified).toEqual([])
expect(result.data.filesRead).toEqual([])
expect(result.data.commandsExecuted).toEqual([])
expect(result.data.toolUsageCounts).toEqual({})
expect(result.data.apiRequestCount).toBe(0)
}
})

it("rejects summary without result", () => {
const summary = { mode: "code", filesModified: [] }
const result = contextHandoffSummarySchema.safeParse(summary)
expect(result.success).toBe(false)
})

it("applies defaults for optional array fields", () => {
const summary = { result: "Done", mode: "debug" }
const result = contextHandoffSummarySchema.safeParse(summary)
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.mode).toBe("debug")
expect(result.data.filesModified).toEqual([])
expect(result.data.commandsExecuted).toEqual([])
}
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { DEFAULT_MODES } from "../mode.js"

describe("Orchestrator context handoff prompt", () => {
const orchestratorMode = DEFAULT_MODES.find((m: { slug: string }) => m.slug === "orchestrator")

it("should have an orchestrator mode", () => {
expect(orchestratorMode).toBeDefined()
})

it("should include context handoff guidance in customInstructions", () => {
expect(orchestratorMode!.customInstructions).toContain("structured context handoff summary")
})

it("should mention files modified in context handoff guidance", () => {
expect(orchestratorMode!.customInstructions).toContain("files modified")
})

it("should mention passing context to subsequent subtasks", () => {
expect(orchestratorMode!.customInstructions).toContain("subsequent subtasks")
})

it("should mention identifying potential conflicts", () => {
expect(orchestratorMode!.customInstructions).toContain("potential conflicts")
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { describe, it, expect } from "vitest"
import { DEFAULT_MODES } from "../mode.js"
import type { ModeConfig } from "../mode.js"

describe("Orchestrator mode - permissions prompt guidance", () => {
const orchestratorMode = DEFAULT_MODES.find((m: ModeConfig) => m.slug === "orchestrator")

it("should have the orchestrator mode defined", () => {
expect(orchestratorMode).toBeDefined()
})

it("should include permissions guidance in customInstructions", () => {
expect(orchestratorMode!.customInstructions).toContain("permissions")
expect(orchestratorMode!.customInstructions).toContain("filePatterns")
expect(orchestratorMode!.customInstructions).toContain("commandPatterns")
expect(orchestratorMode!.customInstructions).toContain("allowedTools")
expect(orchestratorMode!.customInstructions).toContain("deniedTools")
})

it("should mention most-restrictive-wins semantics", () => {
expect(orchestratorMode!.customInstructions).toContain("most-restrictive-wins")
})

it("should provide example use cases for permissions", () => {
// Guidance about restricting file access
expect(orchestratorMode!.customInstructions).toContain("specific directory")
// Guidance about read-only research tasks
expect(orchestratorMode!.customInstructions).toContain("read-only research")
// Guidance about blocking shell access
expect(orchestratorMode!.customInstructions).toContain("shell access")
})
})
126 changes: 126 additions & 0 deletions packages/types/src/__tests__/task-context.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { describe, it, expect } from "vitest"

import {
taskPermissionsSchema,
taskContextSchema,
mergePermissions,
type TaskPermissions,
type TaskContext,
} from "../task-context.js"

describe("TaskPermissions schema", () => {
it("accepts empty object", () => {
const result = taskPermissionsSchema.parse({})
expect(result).toEqual({})
})

it("accepts full permissions object", () => {
const permissions: TaskPermissions = {
fileReadPatterns: ["docs/**", "src/**"],
fileWritePatterns: ["docs/**"],
allowedCommands: ["npm test"],
blockedCommands: ["rm -rf"],
readOnly: true,
allowedTools: ["read_file", "list_files"],
}
const result = taskPermissionsSchema.parse(permissions)
expect(result).toEqual(permissions)
})

it("accepts partial permissions", () => {
const result = taskPermissionsSchema.parse({ readOnly: true })
expect(result).toEqual({ readOnly: true })
})

it("rejects invalid types", () => {
expect(() => taskPermissionsSchema.parse({ readOnly: "yes" })).toThrow()
expect(() => taskPermissionsSchema.parse({ fileReadPatterns: "docs/**" })).toThrow()
})
})

describe("TaskContext schema", () => {
it("accepts minimal context", () => {
const context: TaskContext = { mode: "code" }
const result = taskContextSchema.parse(context)
expect(result.mode).toBe("code")
})

it("accepts full context", () => {
const context: TaskContext = {
mode: "architect",
apiConfigName: "gpt-4",
permissions: {
readOnly: true,
fileReadPatterns: ["docs/**"],
},
inheritSkills: true,
skillOverrides: ["custom-skill"],
workspacePath: "/workspace/project",
parentTaskId: "parent-123",
rootTaskId: "root-456",
}
const result = taskContextSchema.parse(context)
expect(result).toEqual(context)
})

it("rejects missing mode", () => {
expect(() => taskContextSchema.parse({})).toThrow()
})
})

describe("mergePermissions", () => {
it("returns undefined when both are undefined", () => {
expect(mergePermissions(undefined, undefined)).toBeUndefined()
})

it("returns child when parent is undefined", () => {
const child: TaskPermissions = { readOnly: true }
expect(mergePermissions(undefined, child)).toEqual(child)
})

it("returns parent when child is undefined", () => {
const parent: TaskPermissions = { readOnly: true }
expect(mergePermissions(parent, undefined)).toEqual(parent)
})

it("merges readOnly with OR logic", () => {
expect(mergePermissions({ readOnly: true }, { readOnly: false })).toMatchObject({ readOnly: true })
expect(mergePermissions({ readOnly: false }, { readOnly: true })).toMatchObject({ readOnly: true })
expect(mergePermissions({ readOnly: false }, { readOnly: false })).toMatchObject({})
})

it("intersects fileWritePatterns", () => {
const parent: TaskPermissions = { fileWritePatterns: ["docs/**", "src/**", "package.json"] }
const child: TaskPermissions = { fileWritePatterns: ["docs/**", "package.json"] }
const result = mergePermissions(parent, child)
expect(result?.fileWritePatterns).toEqual(["docs/**", "package.json"])
})

it("intersects allowedTools", () => {
const parent: TaskPermissions = { allowedTools: ["read_file", "list_files", "search_files"] }
const child: TaskPermissions = { allowedTools: ["read_file", "search_files", "write_to_file"] }
const result = mergePermissions(parent, child)
expect(result?.allowedTools).toEqual(["read_file", "search_files"])
})

it("unions blockedCommands", () => {
const parent: TaskPermissions = { blockedCommands: ["rm -rf"] }
const child: TaskPermissions = { blockedCommands: ["git push", "rm -rf"] }
const result = mergePermissions(parent, child)
expect(result?.blockedCommands).toEqual(["rm -rf", "git push"])
})

it("returns defined array when only one side specifies it", () => {
const parent: TaskPermissions = { fileReadPatterns: ["docs/**"] }
const child: TaskPermissions = {}
const result = mergePermissions(parent, child)
expect(result?.fileReadPatterns).toEqual(["docs/**"])
})

it("returns empty array when intersection is empty", () => {
const parent: TaskPermissions = { allowedTools: ["read_file"] }
const child: TaskPermissions = { allowedTools: ["write_to_file"] }
const result = mergePermissions(parent, child)
expect(result?.allowedTools).toEqual([])
})
})
Loading