Merge pull request #12993 from Kilo-Org/fix-agent-manager-multi-project-id

fix(agent-manager): route tool requests by project directory and handle busy sessions
This commit is contained in:
Marius
2026-08-07 14:31:27 +02:00
committed by GitHub
13 changed files with 351 additions and 78 deletions
@@ -0,0 +1,6 @@
---
"kilo-code": patch
"@kilocode/cli": patch
---
Route Agent Manager tool-launched sessions to the project that owns the tool event directory, keep sandboxed worktree sessions inside their active worktree, and wait for busy managed sessions before prompting them.
@@ -53,8 +53,10 @@ import {
import { initContextState, pushProjectSessions, reactivateProject, registerProjectSessions } from "./project/init"
import { createLocalDiff } from "./local-diff"
import { parseToolRequest, startFromTool, type ToolRequest } from "./tool-start"
import { handleToolEvent } from "./tool-project"
import { sandboxSessionMetadata } from "../shared/sandbox-session"
import { AgentManagerOrchestrationBridge } from "./orchestration-bridge"
import { createOrchestrationBridge } from "./orchestration-setup"
import type { AgentManagerOrchestrationBridge } from "./orchestration-bridge"
import { pruneSubagents } from "./prune-subagents"
import { startSession } from "./mcp-warmup"
import { readTerminalFont, watchTerminalFont } from "./terminal-font"
@@ -253,22 +255,20 @@ export class AgentManagerProvider implements Disposable {
this.statsPoller = pollers.stats
this.prBridge = pollers.pr
this.projectPollers = pollers.projects
this.orchestration = new AgentManagerOrchestrationBridge(this.connectionService, {
root: () => this.getRoot(),
state: () => this.state,
ready: async () => {
this.stateReady ??= this.initializeState()
await this.stateReady
return this.state
},
stats: () => this.statsPoller.snapshot(),
prs: () => this.prBridge.snapshot(),
push: () => this.pushState(),
managed: (id) => this.panelSessions.has(id) || !!this.state?.getSession(id),
close: async (id) => {
await this.onCloseSession(id)
this.postToWebview({ type: "agentManager.sessionClosed", sessionId: id })
},
this.orchestration = createOrchestrationBridge({
connectionService: this.connectionService,
contexts: this.contexts,
projectScope: this.projectScope,
getRoot: () => this.getRoot(),
getState: () => this.state,
getStateReady: () => this.stateReady,
initStateReady: () => (this.stateReady = this.initializeState()),
getStats: () => this.statsPoller.snapshot(),
getPrs: () => this.prBridge.snapshot(),
pushState: (ctx) => this.pushState(ctx),
hasPanelSession: (id) => this.panelSessions.has(id),
closeSession: (id) => this.onCloseSession(id),
postSessionClosed: (id) => this.postToWebview({ type: "agentManager.sessionClosed", sessionId: id }),
log: (...args) => this.log(...args),
})
this.unsubTool = this.connectionService.onEventFiltered(
@@ -1083,14 +1083,16 @@ export class AgentManagerProvider implements Disposable {
}
private onToolEvent(event: unknown, directory?: string): void {
const properties = (event as { properties?: unknown }).properties
const req = parseToolRequest(properties)
if (!req) return
if (directory) {
req.directory = directory
req.projectId ??= this.contexts.byDirectory(directory)?.id
}
void this.startToolRequest(req)
handleToolEvent(
event,
directory,
{
byDirectory: (value) => this.contexts.byDirectory(value),
usable: (id) => this.contexts.usable(id),
},
this.projectScope,
(req) => this.startToolRequest(req),
)
}
private async startToolRequest(req: ToolRequest): Promise<void> {
@@ -1829,19 +1831,15 @@ export class AgentManagerProvider implements Disposable {
public async createFromSidebar(baseBranch?: string, branchName?: string): Promise<void> {
this.openPanel()
const panel = this.panel
if (!panel) return
if (!(await this.waitForPanelReady(panel))) return
if (!this.panel || !(await this.waitForPanelReady(this.panel))) return
await this.waitForStateReady("createFromSidebar")
await this.onCreateWorktree(baseBranch, branchName)
}
public async openAdvancedWorktree(): Promise<void> {
this.openPanel()
const panel = this.panel
if (!panel) return
if (!(await this.waitForPanelActive(panel))) return
if (!(await this.waitForPanelReady(panel))) return
if (!this.panel || !(await this.waitForPanelActive(this.panel)) || !(await this.waitForPanelReady(this.panel)))
return
await this.waitForStateReady("openAdvancedWorktree")
queueMicrotask(() => this.postToWebview({ type: "action", action: "advancedWorktree" }))
}
@@ -41,14 +41,15 @@ interface Failure {
}
interface Options {
root(): string | undefined
ready(): Promise<WorktreeStateManager | undefined>
state(): WorktreeStateManager | undefined
stats(): Promise<{ worktrees: WorktreeStats[]; local?: LocalStats }>
prs(): Map<string, PRStatus>
push(): void
managed(sessionID: string): boolean
close(sessionID: string): Promise<void>
root(directory?: string): string | undefined
ready(directory?: string): Promise<WorktreeStateManager | undefined>
state(directory?: string): WorktreeStateManager | undefined
stats(directory?: string): Promise<{ worktrees: WorktreeStats[]; local?: LocalStats }>
prs(directory?: string): Map<string, PRStatus>
push(directory?: string): void
managed(sessionID: string, directory?: string): boolean
close(sessionID: string, directory?: string): Promise<void>
directories?(): string[]
log(...args: unknown[]): void
}
@@ -108,6 +109,7 @@ export class AgentManagerOrchestrationBridge {
})
})
this.unsubscribeDirectories = connection.registerDirectoryProvider(() => {
if (this.options.directories) return this.options.directories()
const root = this.options.root()
const dirs =
this.options
@@ -181,8 +183,8 @@ export class AgentManagerOrchestrationBridge {
}
private async admit(request: Request, directory: string): Promise<void> {
const state = await this.options.ready()
const root = this.options.root()
const state = await this.options.ready(directory)
const root = this.options.root(directory)
if (this.disposed || this.settled.has(request.id)) return
if (!state || !root) {
const accepted = await this.reject(request.id, directory, {
@@ -232,7 +234,7 @@ export class AgentManagerOrchestrationBridge {
private async run(request: Request, origin: Origin, active: Active): Promise<void> {
try {
const outcome = this.outcomes.get(request.id) ?? (await this.execute(request, active))
const outcome = this.outcomes.get(request.id) ?? (await this.execute(request, origin, active))
if (!outcome || this.disposed || active.cancelled) return
this.rememberOutcome(request.id, outcome)
const accepted =
@@ -248,10 +250,10 @@ export class AgentManagerOrchestrationBridge {
}
}
private async execute(request: Request, active: Active): Promise<Outcome | undefined> {
private async execute(request: Request, origin: Origin, active: Active): Promise<Outcome | undefined> {
try {
const state = await this.options.ready()
const root = this.options.root()
const state = await this.options.ready(origin.directory)
const root = this.options.root(origin.directory)
if (!state || !root)
throw new OrchestrationError("workspace_unavailable", "Agent Manager requires an open workspace")
if (this.disposed || active.cancelled) return
@@ -260,7 +262,7 @@ export class AgentManagerOrchestrationBridge {
// Git stats are refreshed by the poller independently. A forced refresh
// here can spawn one diff/ahead-behind pair per worktree and exceed the
// host request timeout before the overview can return its IDs.
const stats = await this.options.stats()
const stats = await this.options.stats(origin.directory)
if (this.disposed || active.cancelled) return
const result = await overview({
client,
@@ -269,7 +271,7 @@ export class AgentManagerOrchestrationBridge {
titles: this.titles,
filter: request.filter,
stats,
prs: this.options.prs(),
prs: this.options.prs(origin.directory),
})
return { result: { operation: "overview", overview: result } }
}
@@ -288,7 +290,7 @@ export class AgentManagerOrchestrationBridge {
}
if (request.operation === "move") {
move({ state, sessionID: request.targetSessionID, sectionID: request.sectionID })
this.options.push()
this.options.push(origin.directory)
if (this.disposed || active.cancelled) return
return {
result: {
@@ -299,10 +301,10 @@ export class AgentManagerOrchestrationBridge {
},
}
}
if (!this.options.managed(request.targetSessionID)) {
if (!this.options.managed(request.targetSessionID, origin.directory)) {
throw new OrchestrationError("unknown_session", "The session is not managed by this Agent Manager workspace")
}
await this.options.close(request.targetSessionID)
await this.options.close(request.targetSessionID, origin.directory)
if (this.disposed || active.cancelled) return
return { result: { operation: "stop", sessionID: request.targetSessionID, stopped: true } }
} catch (error) {
@@ -319,6 +319,7 @@ export async function prompt(input: {
text: string
messageID: string
signal?: AbortSignal
idleTimeoutMs?: number
}): Promise<void> {
if (input.signal?.aborted) return
const managed = input.state.getSession(input.sessionID)
@@ -342,15 +343,7 @@ export async function prompt(input: {
if (!(await sameManagedDirectory(response.data.directory, dir))) {
throw new OrchestrationError("cross_workspace", "The managed session belongs to a different workspace directory")
}
const status = await input.client.session.status({ directory: dir })
if (status.error) throw new OrchestrationError("host_error", "The managed session status could not be read")
const activity = status.data?.[input.sessionID]?.type ?? "idle"
if (activity !== "idle") {
throw new OrchestrationError(
"unavailable_session",
`The managed session is ${activity}; only idle sessions can be prompted`,
)
}
await waitForIdle(input.client, dir, input.sessionID, input.signal, input.idleTimeoutMs ?? 30_000)
if (input.signal?.aborted) return
await input.client.session.promptAsync(
{
@@ -364,6 +357,29 @@ export async function prompt(input: {
)
}
async function waitForIdle(
client: KiloClient,
directory: string,
sessionID: string,
signal: AbortSignal | undefined,
timeout: number,
start = Date.now(),
): Promise<void> {
if (signal?.aborted) return
const status = await client.session.status({ directory })
if (status.error) throw new OrchestrationError("host_error", "The managed session status could not be read")
const activity = status.data?.[sessionID]?.type ?? "idle"
if (activity === "idle") return
if (Date.now() - start >= timeout) {
throw new OrchestrationError(
"unavailable_session",
`The managed session is still ${activity}; only idle sessions can be prompted`,
)
}
await new Promise<void>((resolve) => setTimeout(resolve, 250))
return waitForIdle(client, directory, sessionID, signal, timeout, start)
}
export function move(input: { state: WorktreeStateManager; sessionID: string; sectionID: string | null }): void {
const session = input.state.getSession(input.sessionID)
if (!session)
@@ -0,0 +1,78 @@
import type { KiloConnectionService } from "../services/cli-backend/connection-service"
import { AgentManagerOrchestrationBridge } from "./orchestration-bridge"
import type { ProjectContexts } from "./project/contexts"
import type { ProjectContext } from "./project/context"
import type { ProjectScope } from "./project/scope"
import type { WorktreeStateManager } from "./WorktreeStateManager"
import type { WorktreeStats, LocalStats } from "./GitStatsPoller"
import type { PRStatus } from "./types"
import { initContextState } from "./project/init"
export interface OrchestrationBridgeDeps {
connectionService: KiloConnectionService
contexts: ProjectContexts
projectScope: ProjectScope
getRoot: () => string | undefined
getState: () => WorktreeStateManager | undefined
getStateReady: () => Promise<void> | undefined
initStateReady: () => Promise<void>
getStats: () => Promise<{ worktrees: WorktreeStats[]; local?: LocalStats }>
getPrs: () => Map<string, PRStatus>
pushState: (ctx?: ProjectContext) => void
hasPanelSession: (id: string) => boolean
closeSession: (id: string) => Promise<unknown>
postSessionClosed: (id: string) => void
log: (...args: unknown[]) => void
}
export function createOrchestrationBridge(deps: OrchestrationBridgeDeps): AgentManagerOrchestrationBridge {
return new AgentManagerOrchestrationBridge(deps.connectionService, {
root: (dir) => (dir ? deps.contexts.byDirectory(dir)?.root : undefined) ?? deps.getRoot(),
state: (dir) => (dir ? deps.contexts.byDirectory(dir)?.peekState() : undefined) ?? deps.getState(),
ready: async (dir) => {
const ctx = dir ? deps.contexts.byDirectory(dir) : undefined
if (ctx && ctx.id !== deps.contexts.active()?.id) {
await initContextState(ctx, (...args) => deps.log(...args))
return ctx.stateManager()
}
const ready = deps.getStateReady() ?? deps.initStateReady()
await ready
return deps.getState()
},
stats: () => deps.getStats(),
prs: () => deps.getPrs(),
push: (dir) => {
const ctx = dir ? deps.contexts.byDirectory(dir) : undefined
deps.pushState(ctx)
},
managed: (id, dir) => {
const ctx = dir ? deps.contexts.byDirectory(dir) : undefined
if (ctx) return ctx.hasLiveSession(id) || !!ctx.peekState()?.getSession(id)
return deps.hasPanelSession(id) || !!deps.getState()?.getSession(id)
},
close: async (id, dir) => {
const ctx = dir ? deps.contexts.byDirectory(dir) : undefined
if (ctx) {
await deps.projectScope.run(ctx, () => deps.closeSession(id))
} else {
await deps.closeSession(id)
}
deps.postSessionClosed(id)
},
directories: () => {
const all: string[] = []
for (const ctx of deps.contexts.values()) {
all.push(ctx.root)
for (const wt of ctx.peekState()?.getWorktrees() ?? []) {
if (wt.path) all.push(wt.path)
}
}
if (all.length === 0) {
const root = deps.getRoot()
if (root) all.push(root)
}
return all
},
log: (...args) => deps.log(...args),
})
}
@@ -100,6 +100,10 @@ export class ProjectContexts {
return this.contexts.get(id)
}
values(): IterableIterator<ProjectContext> {
return this.contexts.values()
}
/** The context that owns a directory: its root or one of its worktree paths. */
byDirectory(dir: string): ProjectContext | undefined {
for (const ctx of this.contexts.values()) {
@@ -0,0 +1,32 @@
import type { ToolRequest } from "./tool-start"
import { parseToolRequest } from "./tool-start"
export function routeToolRequest<T extends { projectId?: string; directory?: string }, C extends { id: string }>(
input: T,
directory: string | undefined,
deps: { byDirectory: (value: string) => C | undefined; usable: (id: string) => C | undefined },
): { request: T; owner?: C } {
const request = directory ? { ...input, directory } : input
const owner =
(directory && deps.byDirectory(directory)) ?? (request.projectId ? deps.usable(request.projectId) : undefined)
if (!owner) return { request }
return { request: { ...request, projectId: owner.id }, owner }
}
export function handleToolEvent<C extends { id: string }>(
event: unknown,
directory: string | undefined,
contexts: { byDirectory: (value: string) => C | undefined; usable: (id: string) => C | undefined },
scope: { run: <T>(owner: C, fn: () => Promise<T>) => Promise<T> },
start: (req: ToolRequest) => Promise<void>,
): void {
const properties = (event as { properties?: unknown }).properties
const req = parseToolRequest(properties)
if (!req) return
const routed = routeToolRequest(req, directory, contexts)
if (routed.owner) {
void scope.run(routed.owner, () => start(routed.request))
return
}
void start(routed.request)
}
@@ -33,7 +33,9 @@ describe("AgentManagerOrchestrationBridge", () => {
fs.rmSync(root, { recursive: true, force: true })
})
function harness() {
function harness(
overrides?: Partial<Parameters<(typeof AgentManagerOrchestrationBridge.prototype)["constructor"]>[1]>,
) {
const replies: unknown[] = []
const rejections: unknown[] = []
const lists = new Map<string, AgentManagerRequest[]>()
@@ -49,8 +51,8 @@ describe("AgentManagerOrchestrationBridge", () => {
const push = mock(() => undefined)
const client = {
session: {
get: mock(async () => ({
data: { id: "ses_target", directory: dir, title: "Target" } as Session,
get: mock(async ({ sessionID, directory }: { sessionID?: string; directory?: string }) => ({
data: { id: sessionID ?? "ses_target", directory: directory ?? dir, title: "Target" } as Session,
})),
status: mock(async () => ({ data: {} })),
promptAsync,
@@ -100,17 +102,17 @@ describe("AgentManagerOrchestrationBridge", () => {
getClient: () => client,
}
const bridge = new AgentManagerOrchestrationBridge(connection as never, {
root: () => root,
ready: async () => state,
state: () => state,
stats: async () => {
root: (dir) => (overrides?.root ? overrides.root(dir) : root),
ready: async (dir) => (overrides?.ready ? overrides.ready(dir) : state),
state: (dir) => (overrides?.state ? overrides.state(dir) : state),
stats: async (dir) => {
statsCalls.push(1)
return { worktrees: [] }
return overrides?.stats ? overrides.stats(dir) : { worktrees: [] }
},
prs: () => new Map(),
push,
managed: (id) => managed.has(id),
close,
prs: (dir) => (overrides?.prs ? overrides.prs(dir) : new Map()),
push: (dir) => (overrides?.push ? overrides.push(dir) : push()),
managed: (id, dir) => (overrides?.managed ? overrides.managed(id, dir) : managed.has(id)),
close: async (id, dir) => (overrides?.close ? overrides.close(id, dir) : close(id, dir)),
log: () => undefined,
})
const request = (value: AgentManagerRequest, directory = root) =>
@@ -195,7 +197,7 @@ describe("AgentManagerOrchestrationBridge", () => {
await waitFor(() => test.replies.length === 2)
expect(test.close).toHaveBeenCalledTimes(1)
expect(test.close).toHaveBeenCalledWith("ses_target")
expect(test.close).toHaveBeenCalledWith("ses_target", root)
expect(test.replies).toEqual([
{
requestID: "amr_stop",
@@ -294,7 +296,7 @@ describe("AgentManagerOrchestrationBridge", () => {
await waitFor(() => test.replies.length === 1)
expect(state.getSession("ses_live")).toBeUndefined()
expect(test.close).toHaveBeenCalledWith("ses_live")
expect(test.close).toHaveBeenCalledWith("ses_live", root)
expect(test.replies[0]).toEqual({
requestID: "amr_stop_live",
directory: root,
@@ -389,4 +391,39 @@ describe("AgentManagerOrchestrationBridge", () => {
expect(test.promptAsync).toHaveBeenCalledTimes(1)
test.bridge.dispose()
})
it("handles requests for secondary project directories in multi-project mode", async () => {
const secondaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), "am-orchestration-secondary-"))
fs.mkdirSync(path.join(secondaryRoot, ".kilo"), { recursive: true })
const secondaryState = new WorktreeStateManager(secondaryRoot, () => undefined)
secondaryState.addSession("ses_secondary", null)
const test = harness({
root: (d) => (d === secondaryRoot ? secondaryRoot : root),
ready: async (d) => (d === secondaryRoot ? secondaryState : state),
state: (d) => (d === secondaryRoot ? secondaryState : state),
})
test.request(
{
id: "amr_secondary",
sessionID: "ses_caller",
operation: "prompt",
targetSessionID: "ses_secondary",
prompt: "Hello from secondary",
},
secondaryRoot,
)
await waitFor(() => test.replies.length === 1)
expect(test.promptAsync).toHaveBeenCalledTimes(1)
expect(test.replies[0]).toEqual({
requestID: "amr_secondary",
directory: secondaryRoot,
result: { operation: "prompt", sessionID: "ses_secondary", delivered: true },
})
test.bridge.dispose()
await secondaryState.flush()
fs.rmSync(secondaryRoot, { recursive: true, force: true })
})
})
@@ -202,6 +202,25 @@ describe("Agent Manager orchestration domain", () => {
)
})
it("waits for a busy managed session to become idle before prompting", async () => {
const managed = state.addWorktree({ branch: "fix/wait", path: worktree, parentBranch: "main" })
state.addSession("ses_wait", managed.id)
let calls = 0
const promptAsync = mock(async () => ({ data: undefined }))
const client = {
session: {
get: mock(async () => ({ data: { id: "ses_wait", directory: worktree, title: "Wait" } as Session })),
status: mock(async () => ({ data: calls++ === 0 ? { ses_wait: { type: "busy" } } : {} })),
promptAsync,
},
} as unknown as KiloClient
await prompt({ client, root, state, sessionID: "ses_wait", text: "Continue", messageID: "amr_wait" })
expect(client.session.status).toHaveBeenCalledTimes(2)
expect(promptAsync).toHaveBeenCalledTimes(1)
})
it("rejects unknown, stale, cross-workspace, and busy targets", async () => {
const managed = state.addWorktree({ branch: "fix/errors", path: worktree, parentBranch: "main" })
state.addSession("ses_target", managed.id)
@@ -231,7 +250,15 @@ describe("Agent Manager orchestration domain", () => {
data: { ses_target: { type: "busy" } },
}))
await expect(
prompt({ client, root, state, sessionID: "ses_target", text: "Continue", messageID: "amr_busy" }),
prompt({
client,
root,
state,
sessionID: "ses_target",
text: "Continue",
messageID: "amr_busy",
idleTimeoutMs: 0,
}),
).rejects.toMatchObject({
code: "unavailable_session",
} satisfies Partial<OrchestrationError>)
@@ -0,0 +1,31 @@
import { describe, expect, it } from "bun:test"
import { routeToolRequest } from "../../src/agent-manager/tool-project"
describe("Agent Manager tool project routing", () => {
it("routes by the event directory before any explicit project id", () => {
const secondary = { id: "prj-secondary" }
const request = routeToolRequest({ requestID: "am-1", projectId: "prj-active", mode: "worktree" }, "/secondary", {
byDirectory: (dir) => (dir === "/secondary" ? secondary : undefined),
usable: () => ({ id: "prj-active" }),
})
expect(request.owner).toBe(secondary)
expect(request.request).toEqual({
requestID: "am-1",
projectId: "prj-secondary",
mode: "worktree",
directory: "/secondary",
})
})
it("uses an explicit usable project when no event directory is available", () => {
const project = { id: "prj-secondary" }
const request = routeToolRequest({ requestID: "am-2", projectId: "prj-secondary", mode: "local" }, undefined, {
byDirectory: () => undefined,
usable: (id) => (id === project.id ? project : undefined),
})
expect(request.owner).toBe(project)
expect(request.request.projectId).toBe("prj-secondary")
})
})
@@ -53,7 +53,7 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@kilocode/AgentManager") {}
export function layer(timeout: Duration.Input = "10 seconds") {
export function layer(timeout: Duration.Input = "60 seconds") {
return Layer.effect(
Service,
Effect.gen(function* () {
@@ -1,4 +1,4 @@
import { readFileSync, statSync } from "node:fs"
import { accessSync, constants, readFileSync, realpathSync, statSync } from "node:fs"
import os from "node:os"
import path from "node:path"
import { Effect, Semaphore } from "effect"
@@ -194,6 +194,37 @@ function isolated(ctx: InstanceContext) {
return linked(path.resolve(ctx.directory), path.resolve(ctx.worktree))
}
function canonical(dir: string) {
try {
return realpathSync.native(dir)
} catch {
return path.resolve(dir)
}
}
function ancestor(value: string, target: string) {
const relative = path.relative(canonical(value), canonical(target))
return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative)
}
function accessible(dir: string) {
try {
accessSync(dir, constants.R_OK)
return true
} catch {
return false
}
}
function filterWritable(ctx: InstanceContext, values: readonly string[]) {
const list = values.filter(accessible)
if (!isolated(ctx)) return list
// A nested macOS sandbox cannot reliably canonicalize an inherited writable
// ancestor of the linked worktree. The active worktree is already writable;
// keep unrelated explicit paths, but do not widen it back to the repository.
return list.filter((value) => !ancestor(value, ctx.directory))
}
export function profile(
ctx: InstanceContext,
mode: Profile["network"]["mode"] = "deny",
@@ -215,7 +246,7 @@ export function profile(
Global.Path.bin,
Global.Path.log,
Global.Path.repos,
...(extraWritable ?? []),
...filterWritable(ctx, extraWritable ?? []),
].map(root)
return {
filesystem: {
@@ -130,6 +130,17 @@ describe("sandbox policy", () => {
expect(actual).not.toContain(dirs.b)
})
test("drops inherited writable ancestors for a managed worktree", async () => {
await using tmp = await fixture()
const dirs = tmp.extra
const policy = profile(context(dirs.a, dirs.main, dirs), "deny", [dirs.main, dirs.approved])
const paths = policy.filesystem.allowWrite.map((rule) => rule.path)
expect(paths).not.toContain(dirs.main)
expect(paths).toContain(dirs.approved)
expect(paths).toContain(dirs.a)
})
posix("fails closed when a worktree marker cannot be resolved", async () => {
await using tmp = await fixture()
const dirs = tmp.extra