CoolFace
Datasetpublic

basant307/AI_Governance_Project

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes48downloads
enter-worktree.ts296 linesDownload Raw Back to tools
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 
basant307/AI_Governance_Project · CoolFace