basant307/AI_Governance_Project
048
1/**2 * @license3 * Copyright 2025 Qwen4 * SPDX-License-Identifier: Apache-2.05 */6 7import type { ToolResult } from './tools.js';8import { BaseDeclarativeTool, BaseToolInvocation, Kind } from './tools.js';9import type { Config } from '../config/config.js';10import { ToolDisplayNames, ToolNames } from './tool-names.js';11import {12 GitWorktreeService,13 writeWorktreeSessionMarker,14} from '../services/gitWorktreeService.js';15import { writeWorktreeSession } from '../services/worktreeSessionService.js';16import { createDebugLogger } from '../utils/debugLogger.js';17 18const debugLogger = createDebugLogger('ENTER_WORKTREE');19 20export interface EnterWorktreeParams {21 /**22 * Optional name (slug) for the worktree. Allowed characters:23 * letters, digits, dot, underscore, hyphen. Maximum 64 characters.24 * If omitted, an auto-generated `{adj}-{noun}-{4hex}` slug is used.25 */26 name?: string;27}28 29const enterWorktreeDescription = `Creates an isolated git worktree at \`<projectRoot>/.qwen/worktrees/<slug>\` and returns its absolute path so subsequent file edits, shell commands, and other tools can operate inside it.30 31## When to Use32 33Only invoke this tool when the user **explicitly asks for a worktree** — e.g. "start a worktree", "use a worktree", "work in a worktree", "create a worktree".34 35## When NOT to Use36 37Do NOT call this tool when the user simply asks to fix a bug, implement a feature, create a branch, or check out code — those tasks belong to the regular working directory unless the user specifically mentions worktrees.38 39## Behavior40 41- Requires the current project to be a git repository.42- Creates a new branch \`worktree-<slug>\` based on the current branch.43- Returns the absolute \`worktreePath\`. From that point on, route every file path you create or edit through this directory; absolute paths are recommended.44- The worktree persists across the session until \`exit_worktree\` is invoked.45`;46 47interface EnterWorktreeOutput {48 worktreePath: string;49 worktreeBranch: string;50 message: string;51}52 53class EnterWorktreeInvocation extends BaseToolInvocation<54 EnterWorktreeParams,55 ToolResult56> {57 constructor(58 private readonly config: Config,59 params: EnterWorktreeParams,60 ) {61 super(params);62 }63 64 getDescription(): string {65 return this.params.name66 ? `Enter worktree "${this.params.name}"`67 : 'Enter a new worktree';68 }69 70 async execute(_signal: AbortSignal): Promise<ToolResult> {71 const cwd = this.config.getTargetDir();72 73 // Refuse nested worktree creation. If the caller's cwd is itself74 // already inside `.qwen/worktrees/<slug>/`, a fresh worktree would75 // be provisioned at `<repo>/.qwen/worktrees/<new>/` — but the76 // model's mental model and inherited file paths would still77 // reference the outer worktree. The resulting handle confusion78 // typically leaves the inner worktree orphaned on exit.79 //80 // The check is conservative: any path component named81 // `.qwen/worktrees` somewhere in cwd qualifies. We also forbid82 // sentinel "inside a worktree" markers that an `enter_worktree`83 // session leaves behind (writeSessionMarker, below).84 if (/\.qwen[\\/]worktrees[\\/]/.test(cwd)) {85 const reason =86 'Already inside a git worktree. Call exit_worktree first, ' +87 'or return to the main repository checkout before creating a ' +88 'new worktree.';89 debugLogger.warn(`enter_worktree: ${reason} (cwd=${cwd})`);90 return errorResult(reason);91 }92 93 // First-pass service rooted at cwd, only to find the repo top-level.94 // We can't use cwd as the worktree anchor because launching from a95 // monorepo subdirectory would scatter `.qwen/worktrees/` under each96 // package's directory, and the startup sweep at `Config.initialize`97 // would never find them.98 const probe = new GitWorktreeService(cwd);99 100 const gitCheck = await probe.checkGitAvailable();101 if (!gitCheck.available) {102 const reason = gitCheck.error ?? 'Git is not available.';103 debugLogger.warn(`enter_worktree: ${reason}`);104 return errorResult(reason);105 }106 107 const isRepo = await probe.isGitRepository();108 if (!isRepo) {109 const reason = `Cannot create a worktree: ${cwd} is not a git repository. Initialize the repo with \`git init\` first.`;110 debugLogger.warn(`enter_worktree: ${reason}`);111 return errorResult(reason);112 }113 114 // Resolve to the repo's top-level so worktrees always live under115 // `<repoRoot>/.qwen/worktrees/`, regardless of which subdirectory116 // the user invoked the tool from.117 const projectRoot = (await probe.getRepoTopLevel()) ?? cwd;118 const service =119 projectRoot === cwd ? probe : new GitWorktreeService(projectRoot);120 121 // Treat an empty `name` ('') the same as undefined — some models pass122 // `{ name: '' }` when the schema marks `name` as optional, expecting123 // the auto-generated slug. Without this, validation would reject the124 // empty string before reaching the auto-slug path.125 const requested =126 this.params.name && this.params.name.length > 0127 ? this.params.name128 : undefined;129 const slug = requested ?? GitWorktreeService.generateAutoSlug();130 const validation = GitWorktreeService.validateUserWorktreeSlug(slug);131 if (validation) {132 debugLogger.warn(`enter_worktree: invalid slug ${slug}: ${validation}`);133 return errorResult(validation);134 }135 136 // Anchor at the parent session's currently checked-out branch.137 // Without an explicit base, `createUserWorktree` falls back to138 // whichever branch the main working tree has checked out, which is139 // not necessarily where the user is working (e.g. they invoked140 // qwen from a feature branch but the main working tree still has141 // `main` checked out).142 let baseBranch: string | undefined;143 try {144 baseBranch = await service.getCurrentBranch();145 } catch (error) {146 debugLogger.warn(147 `enter_worktree: getCurrentBranch failed at ${projectRoot}: ${error}`,148 );149 }150 151 // Capture HEAD before creating the branch so WorktreeExitDialog can152 // count new commits created inside the worktree. Empty string when153 // rev-parse fails (e.g. unborn HEAD) — the dialog treats empty as154 // "unknown" and skips the commit-count display.155 let originalHeadCommit = '';156 try {157 originalHeadCommit = await service.getCurrentCommitHash();158 } catch (error) {159 debugLogger.warn(160 `enter_worktree: getCurrentCommitHash failed at ${projectRoot}: ${error}`,161 );162 }163 164 const result = await service.createUserWorktree(slug, baseBranch, {165 symlinkDirectories: this.config.getWorktreeSymlinkDirectories(),166 });167 if (!result.success || !result.worktree) {168 const reason = result.error ?? 'Failed to create worktree.';169 debugLogger.warn(`enter_worktree: createUserWorktree failed: ${reason}`);170 return errorResult(reason);171 }172 173 // Tag the worktree with the current session id so a future174 // `exit_worktree action='remove'` from a different session refuses175 // to drop someone else's work. Best-effort: a write failure does176 // not abort the creation (the worktree is still usable; ownership177 // checks will treat unmarked worktrees as "owner unknown").178 try {179 await writeWorktreeSessionMarker(180 result.worktree.path,181 this.config.getSessionId(),182 );183 } catch (error) {184 debugLogger.warn(185 `enter_worktree: failed to write session marker at ${result.worktree.path}: ${error}`,186 );187 }188 189 // Persist worktree session state so --resume can restore context,190 // the Footer can display the active worktree, and WorktreeExitDialog191 // knows what to operate on. Best-effort: a write failure does not192 // abort the creation (the worktree is still usable; the CLI just193 // loses visibility into it across resume).194 try {195 await writeWorktreeSession(196 this.config197 .getSessionService()198 .getWorktreeSessionPath(this.config.getSessionId()),199 {200 slug,201 worktreePath: result.worktree.path,202 worktreeBranch: result.worktree.branch,203 originalCwd: projectRoot,204 originalBranch: baseBranch ?? 'HEAD',205 originalHeadCommit,206 },207 );208 } catch (error) {209 debugLogger.warn(210 `enter_worktree: failed to write WorktreeSession sidecar: ${error}`,211 );212 }213 214 const output: EnterWorktreeOutput = {215 worktreePath: result.worktree.path,216 worktreeBranch: result.worktree.branch,217 message:218 `Created worktree "${slug}" at ${result.worktree.path} on branch ${result.worktree.branch}. ` +219 `Use this absolute path for all subsequent file operations until you call ${ToolNames.EXIT_WORKTREE}.`,220 };221 222 debugLogger.debug(223 `Created user worktree: ${output.worktreePath} (branch=${output.worktreeBranch})`,224 );225 226 return {227 llmContent: JSON.stringify(output),228 returnDisplay:229 `Worktree **${slug}** created on branch \`${result.worktree.branch}\`\n` +230 `\`${result.worktree.path}\``,231 };232 }233}234 235function errorResult(message: string): ToolResult {236 return {237 llmContent: `Error: ${message}`,238 returnDisplay: `Error: ${message}`,239 error: { message },240 };241}242 243export class EnterWorktreeTool extends BaseDeclarativeTool<244 EnterWorktreeParams,245 ToolResult246> {247 static readonly Name: string = ToolNames.ENTER_WORKTREE;248 249 constructor(private readonly config: Config) {250 super(251 EnterWorktreeTool.Name,252 ToolDisplayNames.ENTER_WORKTREE,253 enterWorktreeDescription,254 Kind.Other,255 {256 type: 'object',257 properties: {258 name: {259 type: 'string',260 description:261 'Optional slug (letters, digits, dot, underscore, hyphen; max 64 chars). Auto-generated when omitted.',262 },263 },264 additionalProperties: false,265 $schema: 'http://json-schema.org/draft-07/schema#',266 },267 true, // isOutputMarkdown268 false, // canUpdateOutput269 true, // shouldDefer — only invoked when the user explicitly asks for a worktree270 false, // alwaysLoad271 'worktree git isolated branch new',272 );273 }274 275 override validateToolParams(params: EnterWorktreeParams): string | null {276 if (params.name !== undefined) {277 if (typeof params.name !== 'string') {278 return 'Parameter "name" must be a string.';279 }280 // Empty string is treated as "not provided" — `execute` falls back281 // to an auto-generated slug. Skip slug-format validation here so282 // the auto-slug path is reachable.283 if (params.name.length === 0) {284 return null;285 }286 const error = GitWorktreeService.validateUserWorktreeSlug(params.name);287 if (error) return error;288 }289 return null;290 }291 292 protected createInvocation(params: EnterWorktreeParams) {293 return new EnterWorktreeInvocation(this.config, params);294 }295}296 