From d45d07881286322da0b6085bdd5405a2c0a7fd06 Mon Sep 17 00:00:00 2001 From: Ittai Zeidman Date: Fri, 12 Jun 2026 08:05:39 +0300 Subject: [PATCH] Expose workspace env vars to project hooks and run cleanup on every worktree destruction Project hook scripts (setupScript / devScript / cleanupScript) now receive DEV3_PROJECT_PATH, DEV3_PROJECT_NAME, DEV3_TASK_ID, DEV3_TASK_TITLE, DEV3_WORKTREE_PATH and DEV3_BRANCH_NAME. DEV3_PROJECT_PATH is the load-bearing one: a git-ignored .dev3/config.local.json exists only at the project root, so hooks it references must be resolvable as "$DEV3_PROJECT_PATH/.dev3/.sh" from inside a fresh worktree. This mirrors the Superset workspace-hook contract (SUPERSET_ROOT_PATH / SUPERSET_WORKSPACE_NAME / SUPERSET_WORKSPACE_PATH) so tooling like the b44 CLI can drive per-worktree environments on both runners with the same scripts. The cleanup hook now fires on every worktree-destruction path, not just completed/cancelled column moves: deleting an active task runs it with DEV3_TASK_STATUS=deleted, and cancelling task preparation runs it with DEV3_TASK_STATUS=todo. Task deletion also releases allocated ports and stops the task dev server (both previously leaked). Docs updated: dev3-project-config skill (new step 3b documenting hook env vars and the git-ignored-hooks pattern) and the cleanup script description in all three locales. --- .../feature-workspace-lifecycle-hooks-env.md | 1 + src/bun/__tests__/rpc-handlers.test.ts | 83 +++++++++++++++++++ src/bun/agent-skills.ts | 22 ++++- src/bun/rpc-handlers/shared-pure.ts | 35 +++++++- src/bun/rpc-handlers/shared.ts | 1 + src/bun/rpc-handlers/task-lifecycle.ts | 55 ++++++++++-- src/bun/rpc-handlers/tmux-pty.ts | 18 +++- src/mainview/i18n/translations/en/settings.ts | 2 +- src/mainview/i18n/translations/es/settings.ts | 2 +- src/mainview/i18n/translations/ru/settings.ts | 2 +- 10 files changed, 203 insertions(+), 18 deletions(-) create mode 100644 change-logs/2026/06/27/feature-workspace-lifecycle-hooks-env.md diff --git a/change-logs/2026/06/27/feature-workspace-lifecycle-hooks-env.md b/change-logs/2026/06/27/feature-workspace-lifecycle-hooks-env.md new file mode 100644 index 000000000..524580ffc --- /dev/null +++ b/change-logs/2026/06/27/feature-workspace-lifecycle-hooks-env.md @@ -0,0 +1 @@ +Project hook scripts (setup/dev/cleanup) now receive workspace env vars (DEV3_PROJECT_PATH, DEV3_PROJECT_NAME, DEV3_TASK_TITLE, DEV3_WORKTREE_PATH, DEV3_BRANCH_NAME) so git-ignored hooks in .dev3/config.local.json can be resolved from the project root, mirroring the Superset workspace-hook contract. The cleanup script now also runs when an active task is deleted (DEV3_TASK_STATUS=deleted) or its preparation is cancelled (todo), and task deletion releases allocated ports and stops the dev server — per-worktree resources like dev containers no longer leak on those paths. diff --git a/src/bun/__tests__/rpc-handlers.test.ts b/src/bun/__tests__/rpc-handlers.test.ts index 2e01f7866..665d3ca41 100644 --- a/src/bun/__tests__/rpc-handlers.test.ts +++ b/src/bun/__tests__/rpc-handlers.test.ts @@ -404,6 +404,7 @@ describe("runCleanupScript", () => { DEV3_PROJECT_NAME: "Alpha Project", DEV3_PROJECT_PATH: "/tmp/project-root", DEV3_WORKTREE_PATH: "/tmp/test-worktree", + DEV3_BRANCH_NAME: "dev3/task-test", DEV3_TASK_STATUS: "completed", DEV3_TASK_FROM_STATUS: "in-progress", DEV3_TASK_TO_STATUS: "completed", @@ -411,6 +412,25 @@ describe("runCleanupScript", () => { }); }); + it("reports status 'deleted' when the worktree dies via task deletion", async () => { + const project = makeProject({ path: "/tmp/project-root", cleanupScript: "echo cleanup" }); + const task = makeTask({ + id: "task-123", + worktreePath: "/tmp/test-worktree", + status: "in-progress", + }); + + await runCleanupScript(task, project, { fromStatus: "in-progress", toStatus: "deleted" }); + + expect(mockSpawn.mock.calls[0]?.[1]).toMatchObject({ + env: expect.objectContaining({ + DEV3_TASK_STATUS: "deleted", + DEV3_TASK_FROM_STATUS: "in-progress", + DEV3_TASK_TO_STATUS: "deleted", + }), + }); + }); + it("passes lifecycle env vars to tmux new-session via -e flags (no leak)", async () => { // Without -e KEY=VAL on new-session, the tmux server's global env (from // whichever task started the server) leaks into the cleanup script — @@ -2166,6 +2186,69 @@ describe("handlers.deleteTask", () => { expect(git.removeWorktree).toHaveBeenCalledWith(project, task); expect(data.deleteTask).toHaveBeenCalledWith(project, "task-1"); }); + + it("runs the cleanup script (status 'deleted') before removing the worktree", async () => { + // Deleting an active task destroys its worktree just like completing it — + // the teardown hook must fire here too, or per-worktree resources (e.g. + // dev containers brought up by the setup hook) leak. + const project = makeProject({ cleanupScript: "echo cleanup" }); + const task = makeTask({ status: "in-progress" }); + vi.mocked(data.getProject).mockResolvedValue(project); + vi.mocked(data.getTask).mockResolvedValue(task); + vi.mocked(pty.destroySession).mockImplementation(() => {}); + vi.mocked(existsSync).mockReturnValue(true); + + const callOrder: string[] = []; + mockSpawn.mockImplementation((args: string[]) => { + if (Array.isArray(args) && args.includes("new-session")) callOrder.push("cleanup"); + return { stdout: new Response(""), stderr: new Response(""), exited: Promise.resolve(0) }; + }); + vi.mocked(git.removeWorktree).mockImplementation(async () => { + callOrder.push("removeWorktree"); + }); + + await handlers.deleteTask({ taskId: "task-1", projectId: "proj-1" }); + + const cleanupCall = mockSpawn.mock.calls.find( + ([args]) => Array.isArray(args) && args.includes("DEV3_TASK_STATUS=deleted"), + ); + expect(cleanupCall).toBeDefined(); + expect(callOrder.indexOf("cleanup")).toBeGreaterThanOrEqual(0); + expect(callOrder.indexOf("cleanup")).toBeLessThan(callOrder.indexOf("removeWorktree")); + expect(data.deleteTask).toHaveBeenCalledWith(project, "task-1"); + }); + + it("tolerates cleanup script failure: still removes worktree and task", async () => { + const project = makeProject({ cleanupScript: "echo cleanup" }); + const task = makeTask({ status: "in-progress" }); + vi.mocked(data.getProject).mockResolvedValue(project); + vi.mocked(data.getTask).mockResolvedValue(task); + vi.mocked(pty.destroySession).mockImplementation(() => {}); + vi.mocked(git.removeWorktree).mockResolvedValue(undefined); + vi.mocked(existsSync).mockReturnValue(true); + mockSpawn.mockImplementation(() => { + throw new Error("tmux unavailable"); + }); + + await handlers.deleteTask({ taskId: "task-1", projectId: "proj-1" }); + + expect(git.removeWorktree).toHaveBeenCalledWith(project, task); + expect(data.deleteTask).toHaveBeenCalledWith(project, "task-1"); + }); + + it("releases allocated ports on delete", async () => { + const project = makeProject(); + const task = makeTask({ status: "todo", worktreePath: null }); + vi.mocked(data.getProject).mockResolvedValue(project); + vi.mocked(data.getTask).mockResolvedValue(task); + vi.mocked(pty.destroySession).mockImplementation(() => {}); + const portPool = await import("../port-pool"); + const releaseSpy = vi.spyOn(portPool, "releasePorts"); + + await handlers.deleteTask({ taskId: "task-1", projectId: "proj-1" }); + + expect(releaseSpy).toHaveBeenCalledWith("task-1"); + }); }); // ================================================================ diff --git a/src/bun/agent-skills.ts b/src/bun/agent-skills.ts index 71d144428..3beeecec2 100644 --- a/src/bun/agent-skills.ts +++ b/src/bun/agent-skills.ts @@ -483,7 +483,7 @@ is genuinely ambiguous (e.g., multiple possible dev servers, unclear base branch |-------|-----------------| | \`setupScript\` | Package manager install command. Detect from lockfile: \`bun.lockb\` → \`bun install\`, \`pnpm-lock.yaml\` → \`pnpm install\`, \`yarn.lock\` → \`yarn\`, \`package-lock.json\` → \`npm install\`. For Python: \`pip install -e .\` or \`poetry install\`. For Rust: \`cargo build\`. Chain multiple steps with \`&&\` if needed. | | \`devScript\` | The dev server command. Check \`package.json\` scripts for \`dev\`, \`start\`, \`serve\`. Use the full command: \`bun run dev\`, \`npm run dev\`, etc. If no dev server exists, leave empty. | - | \`cleanupScript\` | Teardown hook that runs before the task worktree is removed after \`completed\` or \`cancelled\`. Useful for copy-back, exports, and cache cleanup. Inside the script you can branch on \`$DEV3_TASK_STATUS\`, \`$DEV3_TASK_FROM_STATUS\`, and \`$DEV3_TASK_TO_STATUS\`. | + | \`cleanupScript\` | Teardown hook that runs before the task worktree is removed — on \`completed\` / \`cancelled\`, when an active task is deleted (\`$DEV3_TASK_STATUS\` = \`deleted\`), or when task preparation is cancelled (\`$DEV3_TASK_STATUS\` = \`todo\`). Useful for copy-back, exports, cache cleanup, and tearing down per-worktree containers. Inside the script you can branch on \`$DEV3_TASK_STATUS\`, \`$DEV3_TASK_FROM_STATUS\`, and \`$DEV3_TASK_TO_STATUS\`, plus the workspace env vars from step 3b. | | \`clonePaths\` | Heavy directories that should be CoW-cloned into new worktrees instead of re-downloaded. Common: \`node_modules\`, \`.venv\`, \`target\`, \`.next\`, \`build\`. Only include dirs that actually exist in the project. | | \`defaultBaseBranch\` | Check \`git symbolic-ref refs/remotes/origin/HEAD\` or look at common branches. Usually \`main\` or \`master\`. | | \`defaultCompareRefMode\` | Default diff comparison target. Use \`"remote"\` for \`origin/\` (recommended default) or \`"local"\` for the local base branch. | @@ -512,6 +512,22 @@ is genuinely ambiguous (e.g., multiple possible dev servers, unclear base branch Before writing the config, briefly state the evidence for each mapping in your response so the user can verify it. If you cannot find an explicit port override mechanism in this project, do NOT guess with a generic \`PORT=\` assignment. Set \`portCount: 0\` and explain why. +3b. **Workspace env vars available to hook scripts.** + Every hook (\`setupScript\`, \`devScript\`, \`cleanupScript\`) runs with cwd = the task worktree and these env vars: + + | Var | Value | + |-----|-------| + | \`$DEV3_PROJECT_PATH\` | Project root directory (the original repo checkout) | + | \`$DEV3_PROJECT_NAME\` | Project name | + | \`$DEV3_TASK_ID\` | Task UUID | + | \`$DEV3_TASK_TITLE\` | Task title | + | \`$DEV3_WORKTREE_PATH\` | This task's worktree directory | + | \`$DEV3_BRANCH_NAME\` | The task's git branch | + + \`setupScript\` and \`devScript\` additionally get the \`$DEV3_PORT*\` vars (step 3a); \`cleanupScript\` additionally gets \`$DEV3_TASK_STATUS\` / \`$DEV3_TASK_FROM_STATUS\` / \`$DEV3_TASK_TO_STATUS\`. + + **Git-ignored hooks pattern:** \`.dev3/config.local.json\` exists only at the project root — a fresh worktree has no copy of it or of any git-ignored script it references. Reference such scripts through the root, e.g. \`"setupScript": "bash \\"$DEV3_PROJECT_PATH/.dev3/setup.sh\\""\` — the script is resolved from the root while cwd stays the worktree. (Scripts committed to the repo can keep plain relative paths.) + 4. **Ask where to save.** Stop and ask clearly: "Repo config (shared, git) or Local config (personal, git-ignored)?" — wait for answer before writing anything. \`\`\`bash @@ -541,9 +557,9 @@ EOF | Field | Type | Description | |-------|------|-------------| -| \`setupScript\` | string | Runs after a new worktree is created (install deps, generate code, etc.) | +| \`setupScript\` | string | Runs after a new worktree is created (install deps, generate code, etc.). Gets the workspace env vars from step 3b | | \`devScript\` | string | Dev server command (powers the "Dev Server" button in the UI) | -| \`cleanupScript\` | string | Runs before the task worktree is removed after \`completed\` or \`cancelled\` | +| \`cleanupScript\` | string | Runs before the task worktree is removed (\`completed\` / \`cancelled\` / task deleted / preparation cancelled) | | \`clonePaths\` | string[] | Dirs to CoW-clone into worktrees (faster than re-downloading) | | \`defaultBaseBranch\` | string | Base branch for new task branches (default: \`main\`) | | \`defaultCompareRefMode\` | \`"remote" \| "local"\` | Default diff comparison target (\`origin/\` vs local base branch) | diff --git a/src/bun/rpc-handlers/shared-pure.ts b/src/bun/rpc-handlers/shared-pure.ts index 121f15f9d..bcd6ef081 100644 --- a/src/bun/rpc-handlers/shared-pure.ts +++ b/src/bun/rpc-handlers/shared-pure.ts @@ -4,7 +4,7 @@ * in standalone bun scripts (e.g. e2e tests) without triggering native deps. */ import { existsSync } from "node:fs"; -import type { TaskStatus } from "../../shared/types"; +import type { Project, Task, TaskStatus } from "../../shared/types"; import { ACTIVE_STATUSES, DEV3_REPO_CONFIG_KEYS } from "../../shared/types"; import { createLogger } from "../logger"; import { DEV3_HOME } from "../paths"; @@ -160,6 +160,39 @@ export function buildAgentEnv(extraEnv: Record, taskId: string): return { ...extraEnv, DEV3_TASK_ID: taskId, PATH: pathWithDev3 }; } +/** + * Workspace env vars injected into every project hook (setup script, dev + * script, cleanup script) and into agent sessions. + * + * `DEV3_PROJECT_PATH` is the load-bearing one for git-ignored hooks: a + * `.dev3/config.local.json` lives only at the project root (a fresh worktree + * checkout has no copy), so any script it references must be resolved from + * the root — `"bash \"$DEV3_PROJECT_PATH/.dev3/setup.sh\""` — while the cwd + * stays the worktree. This mirrors the Superset workspace-hook contract + * (SUPERSET_ROOT_PATH / SUPERSET_WORKSPACE_NAME / SUPERSET_WORKSPACE_PATH), + * which lets tooling such as the b44 CLI target both runners with the same + * per-worktree setup/teardown scripts. + * + * `branchName` wins over `task.branchName` because at first launch the task + * record is persisted only after the PTY starts — callers that just created + * the worktree pass the fresh branch name explicitly. + */ +export function buildTaskLifecycleEnv( + project: Project, + task: Task, + worktreePath: string, + branchName?: string | null, +): Record { + return { + DEV3_PROJECT_PATH: project.path, + DEV3_PROJECT_NAME: project.name, + DEV3_TASK_ID: task.id, + DEV3_TASK_TITLE: task.title, + DEV3_WORKTREE_PATH: worktreePath, + DEV3_BRANCH_NAME: branchName ?? task.branchName ?? "", + }; +} + export function extractConfigFromParams(params: Record): Record { const config: Record = {}; for (const key of DEV3_REPO_CONFIG_KEYS) { diff --git a/src/bun/rpc-handlers/shared.ts b/src/bun/rpc-handlers/shared.ts index 9b4b5b573..116485dcb 100644 --- a/src/bun/rpc-handlers/shared.ts +++ b/src/bun/rpc-handlers/shared.ts @@ -13,6 +13,7 @@ export { getPushMessageLocal, isActive, buildAgentEnv, + buildTaskLifecycleEnv, extractConfigFromParams, } from "./shared-pure"; diff --git a/src/bun/rpc-handlers/task-lifecycle.ts b/src/bun/rpc-handlers/task-lifecycle.ts index effa4350b..f9132d1b9 100644 --- a/src/bun/rpc-handlers/task-lifecycle.ts +++ b/src/bun/rpc-handlers/task-lifecycle.ts @@ -24,7 +24,7 @@ import { import { loadSettings, loadSettingsSync } from "../settings"; import { getUserShell } from "../shell-env"; import { spawn } from "../spawn"; -import { buildScriptRunnerCommand, getPushMessage, isActive, log, notifyWatchedTaskStatusChange } from "./shared"; +import { buildScriptRunnerCommand, buildTaskLifecycleEnv, getPushMessage, isActive, log, notifyWatchedTaskStatusChange } from "./shared"; import { clearMergeNotification, cleanupTaskGitState } from "./git-operations"; import { resolveOperationalProjectConfig } from "./settings-config"; import { cleanupTaskTmuxState, killDevServerSession, launchColumnAgent, launchTaskPty } from "./tmux-pty"; @@ -143,6 +143,22 @@ async function revertPreparingTaskToTodo(project: Project, task: Task): Promise< worktreePath: task.worktreePath ?? `${git.taskDir(project, task)}/worktree`, }; + // Preparation may already have run the setup script (e.g. brought up a + // per-worktree dev container), so give the cleanup hook a chance to tear + // that down before the worktree disappears. Best-effort: the worktree may + // be half-created and runCleanupScript skips it when missing. + try { + await runCleanupScript(cleanupTask, project, { + fromStatus: task.status, + toStatus: "todo", + }); + } catch (err) { + log.warn("Cleanup script failed while cancelling preparation", { + taskId: task.id.slice(0, 8), + error: String(err), + }); + } + try { await git.removeWorktree(project, cleanupTask); } catch (err) { @@ -341,6 +357,8 @@ async function prepareTaskInBackground( options.agentId, options.configId, true, + false, + { branchName: wt.branchName }, ), "launching-pty", ); @@ -437,7 +455,7 @@ export async function activateTask( // (not just the prepareTaskInBackground / Launch Variants flow), so we must // blank it here too — otherwise the placeholder is sent as the prompt. const taskForLaunch = isReopen || task.scratch ? { ...task, description: "" } : task; - await launchTaskPty(resolved, taskForLaunch, wt.worktreePath, task.agentId, task.configId, true, isReopen); + await launchTaskPty(resolved, taskForLaunch, wt.worktreePath, task.agentId, task.configId, true, isReopen, { branchName: wt.branchName }); return { worktreePath: wt.worktreePath, branchName: wt.branchName }; } @@ -510,7 +528,10 @@ const DEFAULT_CLEANUP_SCRIPT = 'echo "Task finished"'; type CleanupTransition = { fromStatus: TaskStatus; - toStatus: Extract; + // Beyond board statuses, the cleanup hook also fires when the worktree is + // destroyed outside a column move: "deleted" (task deleted while active) + // and "todo" (task preparation cancelled, task reverted to To Do). + toStatus: TaskStatus | "deleted"; }; function buildCleanupScriptEnv( @@ -521,11 +542,7 @@ function buildCleanupScriptEnv( return { TERM: "xterm-256color", HOME: process.env.HOME || "/", - DEV3_TASK_TITLE: task.title, - DEV3_TASK_ID: task.id, - DEV3_PROJECT_NAME: project.name, - DEV3_PROJECT_PATH: project.path, - DEV3_WORKTREE_PATH: task.worktreePath || "", + ...buildTaskLifecycleEnv(project, task, task.worktreePath || ""), DEV3_TASK_STATUS: transition.toStatus, DEV3_TASK_FROM_STATUS: transition.fromStatus, DEV3_TASK_TO_STATUS: transition.toStatus, @@ -893,6 +910,7 @@ async function deleteTask(params: { taskId: string; projectId: string }): Promis const project = await data.getProject(params.projectId); const task = await data.getTask(project, params.taskId); cleanupTaskState(task.id); + portPool.releasePorts(task.id); try { pty.destroySession(task.id, task.tmuxSocket ?? undefined); @@ -901,6 +919,27 @@ async function deleteTask(params: { taskId: string; projectId: string }): Promis } if (isActive(task.status) || task.worktreePath) { + killDevServerSession(task.id, task.tmuxSocket ?? pty.DEFAULT_TMUX_SOCKET).catch((err) => { + log.warn("killDevServerSession on task delete failed (best-effort)", { + taskId: task.id.slice(0, 8), error: String(err), + }); + }); + + // Deleting an active task destroys its worktree/work dir just like moving + // it to completed/cancelled does — run the same teardown hook so + // per-worktree resources (dev containers, exports, caches) don't leak. + try { + await runCleanupScript(task, project, { + fromStatus: task.status, + toStatus: "deleted", + }); + } catch (err) { + log.error("Cleanup script failed, continuing with task delete", { + taskId: task.id, + error: String(err), + }); + } + if (project.kind === "virtual") { // SAFETY: only ever remove a MANAGED dir (under ~/.dev3.0/ops/). A // user-chosen fixed folder (e.g. ~ or ~/Downloads) is NEVER auto-removed. diff --git a/src/bun/rpc-handlers/tmux-pty.ts b/src/bun/rpc-handlers/tmux-pty.ts index c84d51de3..62097aefe 100644 --- a/src/bun/rpc-handlers/tmux-pty.ts +++ b/src/bun/rpc-handlers/tmux-pty.ts @@ -12,7 +12,7 @@ import { loadSettings } from "../settings"; import { getUserShell } from "../shell-env"; import { spawn } from "../spawn"; import { setupAgentHooks } from "../agent-hooks"; -import { isActive, buildAgentEnv, buildCmdScript, buildEnvExports, buildScriptRunnerCommand, escapeForDoubleQuotes, log, resolveBinaryPath, shellQuote } from "./shared-pure"; +import { isActive, buildAgentEnv, buildCmdScript, buildEnvExports, buildScriptRunnerCommand, buildTaskLifecycleEnv, escapeForDoubleQuotes, log, resolveBinaryPath, shellQuote } from "./shared-pure"; import { resolveOperationalProjectConfig } from "./settings-config"; const devViewerPaneIds = new Map(); @@ -128,7 +128,7 @@ export async function launchTaskPty( configId?: string | null, runSetup = false, resume = false, - opts?: { sessionId?: string; skipSessionPersist?: boolean }, + opts?: { sessionId?: string; skipSessionPersist?: boolean; branchName?: string }, ): Promise { const sessionId = opts?.sessionId; const skipSessionPersist = opts?.skipSessionPersist ?? false; @@ -222,7 +222,15 @@ export async function launchTaskPty( throw err; } - const env = buildAgentEnv(extraEnv, task.id); + // Lifecycle env first so an explicit agent-config extraEnv can override it. + // These vars reach the agent session, and — crucially — the setup script + // below: a git-ignored hook (e.g. installed by the b44 CLI into + // .dev3/config.local.json) only exists at the project root, so the script + // command must be resolvable as "$DEV3_PROJECT_PATH/.dev3/.sh". + const env = { + ...buildTaskLifecycleEnv(project, task, worktreePath, opts?.branchName), + ...buildAgentEnv(extraEnv, task.id), + }; const userShell = getUserShell(); const portCount = project.portCount ?? 0; @@ -577,9 +585,13 @@ export async function runDevServer(params: { taskId: string; projectId: string } const portExports = devPorts.length > 0 ? buildEnvExports(portPool.buildPortEnv(devPorts)).join("\n") + "\n" : ""; + // Same workspace env the setup/cleanup hooks get, so a devScript can + // reference root-resolved hooks ("$DEV3_PROJECT_PATH/...") too. + const lifecycleExports = buildEnvExports(buildTaskLifecycleEnv(project, task, task.worktreePath)).join("\n") + "\n"; const wrappedScript = [ `#!/bin/bash`, + lifecycleExports, ...(portExports ? [portExports] : []), `set -x`, resolved.devScript, diff --git a/src/mainview/i18n/translations/en/settings.ts b/src/mainview/i18n/translations/en/settings.ts index b20f6a946..cd652502e 100644 --- a/src/mainview/i18n/translations/en/settings.ts +++ b/src/mainview/i18n/translations/en/settings.ts @@ -155,7 +155,7 @@ const settings = { "Runs when starting the dev server for this project", "projectSettings.cleanupScript": "Cleanup Script", "projectSettings.cleanupScriptDesc": - "Runs before the worktree is removed after a task is marked Completed or Cancelled", + "Runs before the worktree is removed — when a task is marked Completed or Cancelled, an active task is deleted, or its preparation is cancelled", "projectSettings.clonePaths": "Clone Paths (Copy-on-Write)", "projectSettings.clonePathsDesc": "Directories and files to clone from the root project into each worktree using Copy-on-Write (instant, no disk space duplication on APFS/btrfs). Falls back to regular copy on unsupported filesystems.", "projectSettings.addClonePath": "Add Path", diff --git a/src/mainview/i18n/translations/es/settings.ts b/src/mainview/i18n/translations/es/settings.ts index 701e63f65..f6c5685da 100644 --- a/src/mainview/i18n/translations/es/settings.ts +++ b/src/mainview/i18n/translations/es/settings.ts @@ -155,7 +155,7 @@ const settings = { "Se ejecuta al iniciar el servidor de desarrollo de este proyecto", "projectSettings.cleanupScript": "Script de limpieza", "projectSettings.cleanupScriptDesc": - "Se ejecuta antes de eliminar el worktree cuando una tarea se marca como Completed o Cancelled", + "Se ejecuta antes de eliminar el worktree — cuando una tarea se marca como Completed o Cancelled, se elimina una tarea activa o se cancela su preparación", "projectSettings.clonePaths": "Rutas de clonación (Copy-on-Write)", "projectSettings.clonePathsDesc": "Directorios y archivos que se clonan del proyecto raíz a cada worktree usando Copy-on-Write (instantáneo, sin duplicar espacio en APFS/btrfs). En sistemas de archivos no compatibles se hace una copia normal.", "projectSettings.addClonePath": "Agregar ruta", diff --git a/src/mainview/i18n/translations/ru/settings.ts b/src/mainview/i18n/translations/ru/settings.ts index d32e10b45..9c8486766 100644 --- a/src/mainview/i18n/translations/ru/settings.ts +++ b/src/mainview/i18n/translations/ru/settings.ts @@ -155,7 +155,7 @@ const settings = { "Запускается при старте dev-сервера для этого проекта", "projectSettings.cleanupScript": "Скрипт очистки", "projectSettings.cleanupScriptDesc": - "Запускается перед удалением worktree, когда задача помечена как Completed или Cancelled", + "Запускается перед удалением worktree — когда задача помечена как Completed или Cancelled, активная задача удалена или её подготовка отменена", "projectSettings.clonePaths": "Clone-пути (Copy-on-Write)", "projectSettings.clonePathsDesc": "Директории и файлы, которые клонируются из корневого проекта в каждый worktree через Copy-on-Write (мгновенно, без дублирования на APFS/btrfs). На неподдерживаемых ФС делается обычная копия.", "projectSettings.addClonePath": "Добавить путь",