diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 4286694bb2d1..a2659418df48 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -78,7 +78,7 @@ For complete style guidance, see our [style guide](https://docs.github.com/en/co
-**Make changes in a codespace:** See "[Working in a codespace](https://github.com/github/docs/blob/main/contributing/codespace.md)" for documentation-specific setup.
+**Make changes in a codespace:** See "[Working on GitHub Docs in a codespace](https://docs.github.com/en/contributing/setting-up-your-environment-to-work-on-github-docs/working-on-github-docs-in-a-codespace)" for documentation-specific setup.
**Make changes locally:**
1. Fork the repository (see [official forking guide](https://docs.github.com/en/contributing))
diff --git a/.github/workflows/sync-llms-txt.yml b/.github/workflows/sync-llms-txt.yml
index 5a7519776af2..0c9501cb648b 100644
--- a/.github/workflows/sync-llms-txt.yml
+++ b/.github/workflows/sync-llms-txt.yml
@@ -105,8 +105,17 @@ jobs:
git config user.name "docs-bot"
git config user.email "77750099+docs-bot@users.noreply.github.com"
git add data/llms-txt/docs.md
- git commit -m "Update data/llms-txt/docs.md from popularity data"
- git push "https://x-access-token:${GH_TOKEN}@github.com/github/docs-internal.git" "$BRANCH"
+ # diff_docs compares against main, but the sync branch may already
+ # exist with this exact content (open PR from a prior run). In that
+ # case there is nothing new to stage, and `git commit` would exit 1
+ # and fail the whole workflow. Skip the commit and push when the
+ # branch is already up to date.
+ if git diff --cached --quiet; then
+ echo "Sync branch already has the latest generated docs.md; nothing to commit."
+ else
+ git commit -m "Update data/llms-txt/docs.md from popularity data"
+ git push "https://x-access-token:${GH_TOKEN}@github.com/github/docs-internal.git" "$BRANCH"
+ fi
- name: Create or update docs-internal PR
if: steps.diff_docs.outputs.changed == 'true'
diff --git a/assets/images/README.md b/assets/images/README.md
index 9756be1695c9..73eafdcc09a2 100644
--- a/assets/images/README.md
+++ b/assets/images/README.md
@@ -2,4 +2,4 @@
The `/assets/images` directory holds all the site's images.
-See [imaging and versioning](https://github.com/github/docs/blob/main/contributing/images-and-versioning.md) from the contributing docs for more information.
+See [Creating screenshots](https://docs.github.com/en/contributing/writing-for-github-docs/creating-screenshots) from the contributing docs for more information.
diff --git a/content/copilot/concepts/agents/copilot-cli/about-cli-extensions.md b/content/copilot/concepts/agents/copilot-cli/about-cli-extensions.md
new file mode 100644
index 000000000000..c14b48911591
--- /dev/null
+++ b/content/copilot/concepts/agents/copilot-cli/about-cli-extensions.md
@@ -0,0 +1,99 @@
+---
+title: About extensions for {% data variables.copilot.copilot_cli %}
+shortTitle: CLI extensions
+allowTitleToDifferFromFilename: true
+intro: 'Extensions let you add your own tools and slash commands to {% data variables.copilot.copilot_cli %}, using only the SDK that ships with the CLI.'
+product: '{% data reusables.gated-features.copilot-cli %}'
+versions:
+ feature: copilot
+contentType: concepts
+category:
+ - Learn about Copilot # Copilot discovery page
+ - Learn about Copilot CLI # Copilot CLI bespoke page
+docsTeamMetrics:
+ - copilot-cli
+---
+
+{% data reusables.copilot.copilot-cli.cli-extensions-experimental %}
+
+An extension lets you add your own capabilities to {% data variables.copilot.copilot_cli_short %}. Each extension is a small Node.js module that runs as a separate process alongside your interactive session and connects back to it. Through that connection an extension can add:
+
+* **Tools** that {% data variables.product.prodname_copilot_short %} can call while it works on your behalf.
+* **Slash commands** that you run yourself.
+
+Because an extension runs real code on your machine, you can add capabilities the built-in tools don't have. For example, an extension can watch what happens in a session and keep a running total across tool calls, which a one-off shell command can't do.
+
+This article explains how extensions work and how {% data variables.copilot.copilot_cli_short %} handles them. For a hands-on guide to building your own extensions, see [AUTOTITLE](/copilot/tutorials/create-an-extension).
+
+> [!WARNING]
+> Extensions execute on your computer with your privileges. Only load extension code that you trust, in the same way you would only run any other script you didn't write yourself.
+
+## How extensions are discovered
+
+When {% data variables.copilot.copilot_cli_short %} starts, it looks for extensions in several locations. Each extension lives in its own subdirectory, and that subdirectory must contain an entry file named `extension.mjs`, `extension.cjs`, or `extension.js`. These are all JavaScript files: the {% data variables.copilot.copilot_cli_short %} runs the entry file directly with Node.js, so an extension must be written in JavaScript—TypeScript and other languages are currently not supported.
+
+The name of the subdirectory becomes the name of the extension.
+
+| Source | Location | Availability |
+| --- | --- | --- |
+| **Project** | `.github/extensions/NAME/` in the current repository | Available to anyone working in that repository. |
+| **User** | `~/.copilot/extensions/NAME/` | Available in all your CLI sessions, in every directory. |
+| **Plugin** | An installed plugin | Available wherever the plugin is enabled. |
+
+## Choosing where an extension lives
+
+Where you put the extension directory determines who can use it:
+
+* Put it in **`.github/extensions/`** in a repository when the extension is specific to that project and you want to share it with everyone who works there.
+* Put it in **`~/.copilot/extensions/`** when you want the extension available in all of your own sessions, regardless of which directory you start the CLI in.
+
+The two locations follow exactly the same structure—a named subdirectory containing an `extension.mjs` file—so you can move an extension from one to the other simply by relocating its folder.
+
+## Enabling extensions
+
+Extensions are currently an experimental feature, so you need to turn on experimental features. You can do this by doing either of the following:
+
+* Start the CLI with the `--experimental` flag.
+* Run the `/experimental on` slash command inside an interactive session.
+
+## Changing how the CLI handles extensions
+
+You can limit {% data variables.product.prodname_copilot_short %}'s access to extensions, or turn them off entirely, by using the `/extensions mode` command. When you use this command you get three options:
+
+* **Load & Augment** (the default)—the CLI runs your extensions, _and_ {% data variables.product.prodname_copilot_short %} can manage them.
+* **Load Only**—the CLI runs your extensions, but {% data variables.product.prodname_copilot_short %} cannot manage them.
+* **Disabled**—extensions are turned off entirely in the current session and will remain disabled in future sessions until you switch to one of the other two settings. Other current sessions are not affected.
+
+Any change you make takes effect immediately in the current session:
+
+* Switching **to Disabled** stops any extensions that are currently running. Their processes are shut down and their tools are no longer available.
+* Switching **from Disabled** to either of the other settings starts your extensions.
+* Switching between **Load Only** and **Load & Augment** just changes whether {% data variables.product.prodname_copilot_short %} can manage your extensions.
+
+### What "managing extensions" means
+
+In **Load & Augment** mode, {% data variables.product.prodname_copilot_short %} is given a small set of extra tools that let it work on the extension system directly, as part of carrying out your requests. Using these tools, {% data variables.product.prodname_copilot_short %} can:
+
+* **List** the extensions it has discovered and see their status.
+* **Inspect** an extension, including a tail of its log file, to help diagnose one that has failed or is misbehaving.
+* **Scaffold** a new extension—generate a starter `extension.mjs` file for you to build on.
+* **Reload** extensions, so that code it (or you) has just written takes effect without restarting the session.
+
+This is what makes it possible for you to ask {% data variables.product.prodname_copilot_short %} to build an extension for you. {% data variables.product.prodname_copilot_short %} can create the file, write the code, and reload it, all without leaving the session.
+
+Choose **Load Only** when you want to keep a known, trusted set of extensions running but don't want {% data variables.product.prodname_copilot_short %} creating, reloading, or otherwise changing extension code—which runs on your machine—as a side effect of its work. Your existing extensions still run, and {% data variables.product.prodname_copilot_short %} can still call the tools they provide; it simply can't manage the extensions themselves.
+
+## Extensions compared with plugins
+
+Both extensions and plugins add functionality to {% data variables.copilot.copilot_cli_short %}, but they serve different purposes:
+
+* An **extension** is a single JavaScript module that you write to add tools and slash commands, backed by code that runs in your session.
+* A **plugin** is an installable package that bundles reusable components—such as agents, skills, hooks, and integrations—and can be distributed through a marketplace.
+
+For more information about plugins, see [AUTOTITLE](/copilot/concepts/agents/copilot-cli/about-cli-plugins).
+
+## Further reading
+
+* [AUTOTITLE](/copilot/tutorials/create-an-extension)
+* [AUTOTITLE](/copilot/reference/copilot-cli-reference/cli-command-reference#slash-commands-in-the-interactive-interface)
+* [AUTOTITLE](/copilot/how-tos/copilot-cli)
diff --git a/content/copilot/concepts/agents/copilot-cli/index.md b/content/copilot/concepts/agents/copilot-cli/index.md
index e9eb3de15373..28da0d19082d 100644
--- a/content/copilot/concepts/agents/copilot-cli/index.md
+++ b/content/copilot/concepts/agents/copilot-cli/index.md
@@ -10,6 +10,7 @@ children:
- /comparing-cli-features
- /copilot-cli-in-github-actions
- /cancel-and-roll-back
+ - /context-management
- /about-remote-control
- /about-custom-agents
- /autopilot
@@ -18,7 +19,7 @@ children:
- /chronicle
- /rubber-duck
- /lsp-servers
- - /context-management
+ - /about-cli-extensions
- /tool-search
contentType: concepts
docsTeamMetrics:
diff --git a/content/copilot/get-started/enterprise-ai-governance.md b/content/copilot/get-started/enterprise-ai-governance.md
index 02eb3cb5f590..807bc089c776 100644
--- a/content/copilot/get-started/enterprise-ai-governance.md
+++ b/content/copilot/get-started/enterprise-ai-governance.md
@@ -26,6 +26,7 @@ journeyTracks:
- href: '/copilot/tutorials/roll-out-at-scale/govern-at-scale/maintain-codebase-standards'
- href: '/copilot/how-tos/administer-copilot/manage-for-enterprise/review-audit-logs'
- href: '/copilot/concepts/preparing-for-new-features-and-models'
+ - href: '/copilot/tutorials/roll-out-at-scale/govern-at-scale/pilot-a-feature-or-model'
- id: 'adopting_agents'
title: 'Adopting agents'
description: 'Roll out agentic features within secure guardrails.'
diff --git a/content/copilot/how-tos/copilot-cli/index.md b/content/copilot/how-tos/copilot-cli/index.md
index 098eee57c8c4..3b686d781612 100644
--- a/content/copilot/how-tos/copilot-cli/index.md
+++ b/content/copilot/how-tos/copilot-cli/index.md
@@ -41,6 +41,7 @@ children:
- /content/copilot/concepts/agents/copilot-cli/lsp-servers
- /content/copilot/concepts/agents/copilot-cli/research
- /content/copilot/concepts/agents/copilot-cli/rubber-duck
+ - /content/copilot/concepts/agents/copilot-cli/about-cli-extensions
- /content/copilot/reference/copilot-cli-reference/acp-server
- /content/copilot/reference/copilot-cli-reference/cli-command-reference
- /content/copilot/reference/copilot-cli-reference/cli-plugin-reference
diff --git a/content/copilot/how-tos/copilot-on-github/set-up-copilot/enable-copilot/set-up-a-dedicated-enterprise-for-copilot-business.md b/content/copilot/how-tos/copilot-on-github/set-up-copilot/enable-copilot/set-up-a-dedicated-enterprise-for-copilot-business.md
index 27ab6f36cbe2..a4cc94488870 100644
--- a/content/copilot/how-tos/copilot-on-github/set-up-copilot/enable-copilot/set-up-a-dedicated-enterprise-for-copilot-business.md
+++ b/content/copilot/how-tos/copilot-on-github/set-up-copilot/enable-copilot/set-up-a-dedicated-enterprise-for-copilot-business.md
@@ -22,19 +22,15 @@ With a dedicated enterprise account, you get enterprise-grade identity provider
## Create an enterprise account
-> [!IMPORTANT]
-> If you purchased {% data variables.copilot.copilot_business_short %} through {% data variables.product.company_short %}'s sales team, your enterprise account is already created. Skip to the next section.
+To create an enterprise account, contact {% data variables.product.company_short %}'s [sales team](https://github.com/enterprise/contact?ref_product=copilot&ref_type=purchase). They will provision you with a standard enterprise account with {% data variables.product.prodname_copilot_short %} enabled.
-Start a trial of {% data variables.product.prodname_ghe_cloud %} to create your enterprise account.
-
-Set up a trial of {% data variables.product.prodname_ghe_cloud %} {% octicon "link-external" height:16 aria-label="link-external" %}
-
-Do not create any organizations during setup. Adding users to organizations assigns {% data variables.product.prodname_enterprise %} licenses, while adding users directly to the enterprise keeps your setup limited to {% data variables.copilot.copilot_business_short %}.
## Add users to your enterprise
+Once you have an enterprise account, add the people who will receive {% data variables.copilot.copilot_business_short %} licenses. How you add users depends on your enterprise type.
Once you have an enterprise account, add the people who will receive {% data variables.copilot.copilot_business_short %} licenses. How you add users depends on your enterprise type.
+Do not create any organizations during setup. Adding users to organizations assigns {% data variables.product.prodname_enterprise %} licenses, while adding users directly to the enterprise keeps your setup limited to {% data variables.copilot.copilot_business_short %}.
### Enterprise with personal accounts
Invite users directly to your enterprise. For detailed steps, see [AUTOTITLE](/admin/managing-accounts-and-repositories/managing-users-in-your-enterprise/invite-users-directly).
@@ -58,7 +54,6 @@ To begin using {% data variables.copilot.copilot_business_short %} after your tr
1. Ensure you are signed in as an enterprise administrator on {% data variables.product.github %}.
1. To purchase {% data variables.product.prodname_copilot %} for your enterprise, [contact {% data variables.product.github %}'s Sales team](https://github.com/enterprise/contact?ref_product=copilot&ref_type=engagement&ref_style=text).
-1. A member of the Sales team will work with you to set up {% data variables.product.prodname_copilot_short %} for your enterprise.
## Assign {% data variables.product.prodname_copilot_short %} licenses
diff --git a/content/copilot/reference/copilot-cli-reference/cli-command-reference.md b/content/copilot/reference/copilot-cli-reference/cli-command-reference.md
index a9ae97af371e..c5437c5418c0 100644
--- a/content/copilot/reference/copilot-cli-reference/cli-command-reference.md
+++ b/content/copilot/reference/copilot-cli-reference/cli-command-reference.md
@@ -242,7 +242,7 @@ When diff mode is open (entered via `/diff`):
| `/resume [SESSION-ID]`, `/continue [SESSION-ID]` | Switch to a different session by choosing from a list (optionally specify a session ID). |
| `/review [PROMPT]` | Run the code review agent to analyze changes. See [AUTOTITLE](/copilot/how-tos/copilot-cli/use-copilot-cli/agentic-code-review). |
| `/rubber-duck [PROMPT]` | Consult the rubber duck agent for a second opinion on plans, code, and tests. See [AUTOTITLE](/copilot/concepts/agents/copilot-cli/rubber-duck). |
-| `/sandbox [enable\|disable]` | Configure shell command sandboxing. |
+| `/sandbox [enable\|disable]` | Enable, disable, or configure OS-level sandboxing that restricts filesystem and network access for shell commands, MCP/LSP servers, and built-in file/web tools. Run `/sandbox` with no arguments to open the policy dialog. |
| `/search [QUERY]`, `/find [QUERY]` | Search the conversation timeline. {% data reusables.copilot.experimental %} |
| `/security-review [PROMPT]` | Run the security review agent to analyze changes for vulnerabilities. |
| `/session [info\|checkpoints [n]\|files\|plan\|rename [NAME]\|cleanup\|prune\|delete [ID]\|delete-all]`, `/sessions [info\|checkpoints [n]\|files\|plan\|rename [NAME]\|cleanup\|prune\|delete [ID]\|delete-all]` | Show session information and manage sessions. The `info` subcommand shows session details including the session link (when available). Subcommands: `info`, `checkpoints`, `files`, `plan`, `rename`, `cleanup`, `prune`, `delete`, `delete-all`. |
diff --git a/content/copilot/tutorials/create-an-extension.md b/content/copilot/tutorials/create-an-extension.md
new file mode 100644
index 000000000000..2d4073b2ff61
--- /dev/null
+++ b/content/copilot/tutorials/create-an-extension.md
@@ -0,0 +1,359 @@
+---
+title: Creating extensions for {% data variables.copilot.copilot_cli %}
+shortTitle: Create CLI extensions
+allowTitleToDifferFromFilename: true
+intro: 'Build extensions that add your own tools and slash commands to {% data variables.copilot.copilot_cli_short %}.'
+versions:
+ feature: copilot
+contentType: tutorials
+category:
+ - Author and optimize with Copilot # Copilot discovery page
+ - Build with Copilot CLI # Copilot CLI bespoke page
+docsTeamMetrics:
+ - copilot-cli
+---
+
+{% data reusables.copilot.copilot-cli.cli-extensions-experimental %}
+
+An extension lets you add your own capabilities to {% data variables.copilot.copilot_cli_short %}. Each extension is a small Node.js module that runs as a separate process alongside your interactive session and connects back to it. Through that connection, an extension can add **tools** that {% data variables.product.prodname_copilot_short %} can call while it works on your behalf, and **slash commands** that you run yourself.
+
+In this tutorial, you'll build two simple extensions as examples of what you can do:
+
+* A tool, called `tool-time`, that {% data variables.product.prodname_copilot_short %} can call to report how long its tool calls have taken so far in a session.
+* A slash command, called `/tokencount`, that reports how many tokens you've used since you started counting.
+
+Both examples rely only on the SDK that's bundled with the {% data variables.copilot.copilot_cli_short %}, so there's nothing extra to install. For background on how extensions work, see [AUTOTITLE](/copilot/concepts/agents/copilot-cli/about-cli-extensions).
+
+> [!WARNING]
+> Extensions execute on your computer with your privileges. Only load extension code that you trust, in the same way you would only run any other script you didn't write yourself.
+
+## Prerequisites
+
+* **{% data variables.copilot.copilot_cli %}**: You need {% data variables.copilot.copilot_cli_short %} installed and set up. See [AUTOTITLE](/copilot/how-tos/copilot-cli/cli-getting-started).
+* **Experimental features enabled**: Extensions are currently an experimental feature. The steps in this tutorial turn on experimental features each time you start the CLI, using the `--experimental` command line option.
+* **JavaScript**: Extensions are written in JavaScript, so you'll need to be familiar with this language to create your own extensions.
+* **A repository**: The second example adds a project-level extension, so you'll need a local copy of a Git repository in which to add the extension.
+
+## Example extension 1: A "tool time" tool
+
+This example adds a **user-level** extension called `tool-time`. It adds a new tool—called `session_tool_time`—that {% data variables.product.prodname_copilot_short %} can call to report how long tool calls have taken so far this session, and how many calls that covers.
+
+To ensure a tool is used by the CLI, the tool must do something the CLI can't do on its own. In this case, the `session_tool_time` tool keeps track of how long tool calls take by listening to events from the CLI about when tool calls start and finish, and recording the timings itself. Because the CLI doesn't record these timings anywhere that {% data variables.product.prodname_copilot_short %} can read, the only way for {% data variables.product.prodname_copilot_short %} to know them is to call the tool. The tool's description explains this to the model, which steers it toward using the tool when you ask about tool call timings.
+
+### Step 1: Create the extension file
+
+1. Create the following directory and file in your home directory:
+
+ ```text
+ ~/.copilot/extensions/tool-time/extension.mjs
+ ```
+
+ Because this is under `~/.copilot/extensions/`, the extension is available in **all** your CLI sessions, in every directory—not just one repository.
+
+1. Add this code to `extension.mjs`:
+
+ ```javascript copy
+ // Extension: tool-time
+ // Adds a session_tool_time tool that reports how long Copilot's tool calls
+ // have taken this session. Copilot is never told these timings and they
+ // aren't written anywhere, so calling the tool is the only way to find
+ // them out.
+
+ import { joinSession } from "@github/copilot-sdk/extension";
+
+ // The tool's own name, so it can avoid timing its own calls.
+ const TOOL_NAME = "session_tool_time";
+
+ // Module-level state persists for the whole session, because the extension
+ // runs as a single long-lived process.
+ const startTimes = new Map(); // tool call id -> Date.now() when call started
+ let totalMs = 0; // total milliseconds spent across finished tool calls
+ let callCount = 0; // number of finished tool calls measured
+
+ const session = await joinSession({
+ tools: [
+ {
+ name: TOOL_NAME,
+ description:
+ "Report how long your tool calls have taken in total so far " +
+ "in THIS session, and how many calls that covers. You are " +
+ "never told how long your tool calls take and the timings " +
+ "aren't recorded anywhere you can read, so call this tool " +
+ "whenever you are asked about it rather than estimating.",
+ // Always keep this tool's description in the model's tool list,
+ // even when tool search is active, so Copilot reliably sees it:
+ defer: "never",
+ // Force a permissions approval prompt once, for this extension,
+ // rather than on every call of this tool:
+ skipPermission: true,
+ parameters: { type: "object", properties: {} },
+ handler: async () => {
+ const seconds = (totalMs / 1000).toFixed(1);
+ return `So far this session, Copilot's tool calls have taken ${seconds}s in total across ${callCount} call(s).`;
+ },
+ },
+ ],
+ });
+
+ // When a tool starts, record the time, keyed by the tool call id so the
+ // matching completion can be found later. The tool's own calls are skipped.
+ session.on("tool.execution_start", (event) => {
+ const data = event.data ?? {};
+ if (data.toolName && data.toolName !== TOOL_NAME) {
+ startTimes.set(data.toolCallId, Date.now());
+ }
+ });
+
+ // When a tool finishes, add the elapsed time to the running total.
+ session.on("tool.execution_complete", (event) => {
+ const data = event.data ?? {};
+ const startedAt = startTimes.get(data.toolCallId);
+ if (startedAt === undefined) {
+ return;
+ }
+ startTimes.delete(data.toolCallId);
+ totalMs += Date.now() - startedAt;
+ callCount += 1;
+ });
+ ```
+
+> [!NOTE]
+> * `@github/copilot-sdk/extension` is the extension SDK, which is bundled with the CLI. The CLI resolves this import automatically when it runs your extension, so you don't need to add it to a `package.json` or run a package manager.
+> * The `startTimes`, `totalMs`, and `callCount` values live at module scope. Because the extension runs as a single long-lived process for the whole session, they accumulate for as long as the session is open.
+
+### Step 2: Load the extension
+
+1. Start an interactive session with experimental features enabled:
+
+ ```shell copy
+ copilot --experimental
+ ```
+
+ Because the extension lives under `~/.copilot/extensions/`, you can start the CLI from any directory and the extension will be available.
+
+ If you already had a session open, run `/clear` to start a fresh session, which reloads extensions from disk.
+
+1. Without granting elevated permissions to either the new extension, all extensions, or all tools, you'll be prompted to allow the new extension to skip tool permission prompts. Choose either **Yes** or **Yes, and always allow "user:tool-time" in this directory**.
+
+ > [!NOTE]
+ > For the minimum elevated permissions to prevent seeing this message when you start the CLI, add this to your CLI startup command:
+ >
+ > ```shell copy
+ > --allow-tool='extension-permission-access(user:tool-time)'
+ > ```
+ >
+ > For more information, see [AUTOTITLE](/copilot/how-tos/copilot-cli/use-copilot-cli/allowing-tools).
+
+### Step 3: Confirm the extension is running
+
+Run the `/extensions manage` command to open the extension manager. Your `tool-time` extension should be listed under the **User** group with a status of **running**. Press Esc to close the manager.
+
+### Step 4: Try it out
+
+1. Unlike a slash command, you don't invoke a tool yourself—{% data variables.product.prodname_copilot_short %} calls it when it's useful. First, give {% data variables.product.prodname_copilot_short %} some work that involves a few tool calls, for example:
+
+ ```copilot copy
+ Explore the files in the current directory and give me a short summary of what's here.
+ ```
+
+1. Once {% data variables.product.prodname_copilot_short %} finishes responding, ask:
+
+ ```copilot copy
+ How long have tool calls taken so far this session?
+ ```
+
+ The agent calls the `session_tool_time` tool, from the new extension, and uses it to answer your question.
+
+ > [!TIP]
+ > You can confirm that the agent used the new tool by looking at the start of {% data variables.product.prodname_copilot_short %}'s response. The response should be prefixed by the name of the tool that was used; in this case, `session_tool_time`.
+
+### How the tool works
+
+The single call to `joinSession` is what turns a plain Node.js file into a {% data variables.copilot.copilot_cli_short %} extension. It connects the running process to your session and registers everything the extension adds to the CLI—in this case, a single tool.
+
+A tool is defined by these fields:
+
+* **`name`**—the identifier {% data variables.product.prodname_copilot_short %} uses to call the tool.
+* **`description`**—what the tool does. The model relies on this text to decide when to call the tool, so it's worth being explicit. This description tells the model that it isn't told these timings itself, which steers it toward calling the tool instead of attempting to work out the timings some other way.
+* **`parameters`**—a JSON Schema describing the tool's arguments. This tool takes none, so the schema is an object with no properties.
+* **`handler`**—an async function that runs when {% data variables.product.prodname_copilot_short %} calls the tool. Whatever string it returns becomes the tool's result, which the model reads.
+
+Two further fields shape how the tool is offered to the model:
+
+* **`defer: "never"`**—keeps the tool's description in the model's tool list at all times. By default, when many tools are available, the CLI can defer rarely-used tools and let the model search for them on demand. Setting `defer` to `"never"` opts this tool out of that behavior, so {% data variables.product.prodname_copilot_short %} always sees it.
+
+ > [!IMPORTANT]
+ > `defer: "never"` simply makes the tool _available_ to {% data variables.product.prodname_copilot_short %}. It doesn't _force_ {% data variables.product.prodname_copilot_short %} to call it. There is no way for an extension to mandate that a particular tool should always be used if there is an alternative means of generating an appropriate response to a prompt. The model always decides for itself which tool to use. In this example, the new tool is reliably used because there's no other way for the model to know the information the tool provides.
+
+* **`skipPermission: true`** allows the tool to run without asking you to approve each call. This is appropriate here because the tool only reads totals the extension has already collected; it doesn't touch your files or run commands.
+
+The timings are gathered by watching the session. The extension subscribes to two events the CLI emits around every tool call:
+
+```javascript
+session.on("tool.execution_start", (event) => { /* ... */ });
+session.on("tool.execution_complete", (event) => { /* ... */ });
+```
+
+A `tool.execution_start` event carries the tool's name (`event.data.toolName`). A `tool.execution_complete` event carries a `success` flag. Neither event carries the other's information, so the extension correlates them using the `toolCallId` that appears on each: when a tool starts, it records the current time under that ID. When the matching completion arrives, it adds the elapsed milliseconds to the running total. The tool skips its own calls so the figure reflects {% data variables.product.prodname_copilot_short %}'s real work.
+
+Because the totals live in the long-lived extension process, they cover the whole session. This is the kind of job extensions are good at: observing what's happening in the session and keeping state across calls.
+
+> [!NOTE]
+> * The totals are held in memory, so they reset whenever the extension is reloaded or the session restarts (for example, after `/clear`).
+> * The figure is wall-clock time measured from the extension's point of view—the gap between each tool call's start and completion events—so it includes any time a call spent waiting for you to approve it.
+
+## Example extension 2: A token usage slash command
+
+This example adds a project extension called `token-counter`. The extension adds a `/tokencount` slash command that you can use to check how many tokens you've used when interacting with {% data variables.product.prodname_copilot_short %} in the CLI. You run `/tokencount start` when you want to start measuring your token usage, and then you can run `/tokencount` at any later point to check how many tokens you've used since starting the count.
+
+The extension keeps a running total of tokens used in the session by subscribing to events emitted by the CLI, which is something a one-off shell command can't do.
+
+### Step 1: Create the extension file
+
+1. In the root of the Git repository for your project, create the following directories and file:
+
+ ```text
+ .github/extensions/token-counter/extension.mjs
+ ```
+
+1. Add this code to `extension.mjs`:
+
+ ```javascript copy
+ // Extension: token-counter
+ // Adds a /tokencount slash command that reports how many tokens you've used
+ // since you started counting.
+
+ import { joinSession } from "@github/copilot-sdk/extension";
+
+ // Module-level state. The extension runs as a single long-lived process for
+ // the whole session, so these values persist between command invocations.
+ let tokensUsed = 0; // Running total of tokens used so far this session.
+ let startedAt = null; // tokensUsed when "/tokencount start" was last run.
+ // null = not started.
+ // startedAt allows you to count tokens multiple times in a session.
+
+ const session = await joinSession({
+ commands: [
+ {
+ name: "tokencount",
+ description: "Report how many tokens you've used since " +
+ "'/tokencount start'.",
+ handler: async (ctx) => {
+ const arg = (ctx.args ?? "").trim();
+
+ if (arg === "start") {
+ // Reset: remember the current total as the new baseline.
+ startedAt = tokensUsed;
+ await session.log(
+ "Token counter started. Run '/tokencount' later to " +
+ "see how many tokens you've used.",
+ { level: "info" },
+ );
+ return;
+ }
+
+ if (startedAt === null) {
+ await session.log(
+ "The token counter has not been started. Start by " +
+ "entering '/tokencount start'.",
+ { level: "info" },
+ );
+ return;
+ }
+
+ const used = tokensUsed - startedAt;
+ await session.log(
+ `You have used ${used} tokens since entering ` +
+ "'/tokencount start'.",
+ { level: "info" },
+ );
+ },
+ },
+ ],
+ });
+
+ // The CLI emits an "assistant.usage" event after each assistant turn. Add the
+ // tokens it reports to a running total kept in the extension's memory.
+ session.on("assistant.usage", (event) => {
+ const { inputTokens = 0, outputTokens = 0 } = event.data ?? {};
+ tokensUsed += inputTokens + outputTokens;
+ });
+ ```
+
+### Step 2: Load the extension
+
+Start an interactive session from the same repository, with experimental features enabled:
+
+```shell copy
+copilot --experimental
+```
+
+If you already had a session open, you can run `/clear` to start a fresh session, which reloads extensions from disk.
+
+### Step 3: Confirm the extension is running
+
+Run the `/extensions manage` command to open the extension manager. Your `token-counter` extension should be listed under the **Project** group with a status of **running**. Press Esc to close the manager.
+
+You can also run `/env` to see a summary of everything loaded into the session, including extensions.
+
+### Step 4: Try it out
+
+Unlike a tool, you invoke a slash command yourself. Start the counter:
+
+```copilot copy
+/tokencount start
+```
+
+Send {% data variables.product.prodname_copilot_short %} a prompt or two so that it uses some tokens—for example, ask it to explain a file. Then check how many tokens you've used:
+
+```copilot copy
+/tokencount
+```
+
+If you run `/tokencount start` again, the count restarts from zero.
+
+### How the example works
+
+The call to `joinSession` registers everything the extension adds to the CLI—in this case, a single slash command.
+
+A slash command is defined by three fields:
+
+* **`name`**—the command name, without the leading slash. Registering `tokencount` is what makes `/tokencount` available in the session.
+* **`description`**—the text shown next to the command in the slash command picker.
+* **`handler`**—an async function that runs when you invoke the command. It receives a context object whose `args` property holds the raw text typed after the command name. For `/tokencount start`, `ctx.args` is `"start"`; for a bare `/tokencount`, it is an empty string.
+
+The handler writes its output back to the session with `session.log(message, { level: "info" })`, which prints the message in the transcript.
+
+To know how many tokens have been used, the extension subscribes to a session event:
+
+```javascript
+session.on("assistant.usage", (event) => {
+ const { inputTokens = 0, outputTokens = 0 } = event.data ?? {};
+ tokensUsed += inputTokens + outputTokens;
+});
+```
+
+The CLI emits an `assistant.usage` event after each assistant turn, carrying the input and output token counts for that turn. The extension adds them to the running total in `tokensUsed`. When you run `/tokencount start`, the handler records the current total in `startedAt`. Entering `/tokencount` with no argument later in the session reports the difference. Because both variables live in the long-lived extension process, they persist for the whole session.
+
+> [!NOTE]
+> The totals are held in memory, so they reset whenever the extension is reloaded or the session restarts (for example, after `/clear`).
+
+## Editing and reloading an extension
+
+As you develop an extension, you'll edit `extension.mjs` and want to see your changes. After saving the file, you can pick up the new version in any of these ways:
+
+* Ask {% data variables.product.prodname_copilot_short %} to reload extensions—for example, `Reload my extensions`.
+* Run `/clear` to start a new session, which reloads extensions from disk.
+* Restart the CLI.
+
+If an extension fails to start or behaves unexpectedly, run `/extensions manage` and inspect the extension to see its status and the path to its log file. Each extension writes a log under `~/.copilot/logs/extensions/`, which is the best place to look when something goes wrong.
+
+## Next steps
+
+* Adapt one of these examples to your own needs. In the CLI, ask {% data variables.product.prodname_copilot_short %} to modify the behavior of either of the example extensions.
+* Share a user-level extension. For example, move the `tool-time` extension into a repository's `.github/extensions/` directory to share it with everyone who works in that repository.
+* Ask {% data variables.product.prodname_copilot_short %} to create a new extension for you from scratch. {% data variables.copilot.copilot_cli_short %} extensions are powered by the {% data variables.copilot.copilot_sdk_short %}, so an extension can do anything the SDK makes possible, allowing you to add interactive views with buttons and forms, as well as new tools and commands. See [AUTOTITLE](/copilot/how-tos/copilot-sdk).
+
+## Further reading
+
+* [AUTOTITLE](/copilot/reference/copilot-cli-reference/cli-command-reference)
diff --git a/content/copilot/tutorials/index.md b/content/copilot/tutorials/index.md
index 0cc93395ab81..668bcfbc4d03 100644
--- a/content/copilot/tutorials/index.md
+++ b/content/copilot/tutorials/index.md
@@ -33,6 +33,7 @@ children:
- /upgrade-projects
- /copilot-cli-hooks
- /use-an-ai-sme
+ - /create-an-extension
redirect_from:
- /copilot/using-github-copilot/guides-on-using-github-copilot
contentType: tutorials
diff --git a/content/copilot/tutorials/roll-out-at-scale/govern-at-scale/index.md b/content/copilot/tutorials/roll-out-at-scale/govern-at-scale/index.md
index 291ea985af69..0e38dbcb6c77 100644
--- a/content/copilot/tutorials/roll-out-at-scale/govern-at-scale/index.md
+++ b/content/copilot/tutorials/roll-out-at-scale/govern-at-scale/index.md
@@ -8,6 +8,7 @@ children:
- /resources-for-approval
- /establish-ai-managers
- /govern-for-adoption
+ - /pilot-a-feature-or-model
- /maintain-codebase-standards
contentType: tutorials
---
diff --git a/content/copilot/tutorials/roll-out-at-scale/govern-at-scale/pilot-a-feature-or-model.md b/content/copilot/tutorials/roll-out-at-scale/govern-at-scale/pilot-a-feature-or-model.md
new file mode 100644
index 000000000000..201791cdd8be
--- /dev/null
+++ b/content/copilot/tutorials/roll-out-at-scale/govern-at-scale/pilot-a-feature-or-model.md
@@ -0,0 +1,170 @@
+---
+title: Pilot a new Copilot feature or model in your enterprise
+shortTitle: Pilot a feature or model
+intro: Pilot a new {% data variables.product.prodname_copilot_short %} feature or model to validate adoption, control costs, and gather evidence for a wider rollout.
+permissions: Enterprise owners, organization owners, and billing managers
+versions:
+ feature: copilot
+category:
+ - Roll Copilot out at scale
+contentType: tutorials
+allowTitleToDifferFromFilename: true
+---
+
+This tutorial walks you through running a pilot end to end: setting a budget, enabling the feature for a contained group, monitoring results, and using the evidence to make and report a go or no-go decision.
+
+## Before you start your pilot
+
+Before you enable anything, make sure you've evaluated the feature or model and confirmed it meets your compliance requirements. See [AUTOTITLE](/copilot/concepts/preparing-for-new-features-and-models).
+
+If you're not sure whether a feature or model is safe to enable, check with security and compliance teams at your company before you start the pilot.
+
+### Define what success looks like
+
+Decide what a "go" looks like before you start, so you judge the pilot against a plan instead of after the fact. Write down a few concrete success criteria that cover adoption (for example, a minimum share of active users), cost (staying within the ceiling you set), and qualitative feedback (for example, most participants would recommend the feature). You'll check the pilot's actual results against these criteria in [Make a go or no-go decision](#make-a-go-or-no-go-decision).
+
+Also plan how long the pilot will run. Budgets and the shared pool of {% data variables.product.prodname_ai_credits_short %} reset every billing cycle, so plan for the pilot to span at least one full billing cycle, typically four to six weeks; a shorter run can leave you with too little cost and adoption data to draw a reliable conclusion.
+
+### Choose who to include in your pilot group
+
+Since features and models are enabled at the organization level, everyone who receives their {% data variables.product.prodname_copilot_short %} license through that organization will get access, not just a hand-picked subset, and not necessarily everyone who's a member of it. So instead of selecting individuals, look for an organization whose licensed population already reflects the criteria below:
+
+* **Developers doing real, meaningful work.** Pilot participants should use the feature on the languages, frameworks, and repositories that matter to your organization, not on throwaway projects. Usage and cost signals are only trustworthy if they come from genuine work.
+* **A mix of skill and seniority levels.** Include both experienced engineers and people who are newer to your codebase or to {% data variables.product.prodname_copilot_short %}. Different levels of experience use the feature differently and uncover different value and different problems.
+* **A range of teams and workflows.** A feature that helps one team's workflow may not help another's. Spanning a few teams gives you a more reliable picture of where the feature adds value and where it doesn't.
+* **People willing to give feedback.** Much of a pilot's value comes from qualitative feedback, not just metrics. Choose participants who will engage, report what works and what doesn't, and respond when you ask for their input.
+
+If none of your organizations fit, for example, if the right mix of people is scattered across several organizations, or your organizations are too large to serve as a contained pilot, you can create a dedicated organization and add only your chosen users to it. This gives you precise control over who's included, but it means migrating or duplicating repository access for those users, and it may not work well for features like {% data variables.copilot.copilot_cloud_agent %} that depend on deep repository-level context. This tutorial assumes you're using an existing organization; if you go the dedicated-organization route, treat setting it up and adding members as an extra step before [Enable the feature for your pilot group](#enable-the-feature-for-your-pilot-group).
+
+### Estimate costs and set a budget ceiling
+
+Many of the features and models you'll want to pilot are billed through usage, so the spending isn't fixed by the number of licenses you assign.
+
+A handful of active developers using a frontier model or an agentic feature could consume more than you'd expect. Work out a rough estimate of what the pilot could cost, and decide on a ceiling you're willing to spend before you enable the feature.
+
+Start by understanding how the feature or model is billed. Usage-based features consume {% data variables.product.prodname_ai_credits %}, and the cost of each interaction depends on the model and the number of tokens consumed. To understand which features count toward usage, how included credits are pooled across your enterprise, and how overages are charged, see [AUTOTITLE](/copilot/concepts/billing/usage-based-billing-for-organizations-and-enterprises).
+
+To produce an estimate, combine three numbers:
+
+* **The number of people in your pilot group**, which you defined in the previous section.
+* **How heavily you expect them to use the feature.** You won't know this until people are actually using it, so pick a deliberately high-end estimate rather than trying to predict it precisely. If you've already rolled out a comparable feature or model, use its actual usage as a reference point. If not, assume frequent, heavy use per developer, since agentic features and frontier models cost more per interaction than a quick chat question. A small group doing intensive work can cost more than a large group using the feature occasionally.
+* **The included allowance you can draw on.** Each assigned license comes with a monthly amount of included {% data variables.product.prodname_ai_credits_short %} that are pooled at the enterprise level, so some of the pilot's usage may be covered before any overage is billed.
+
+Use these to set a ceiling: the maximum you're prepared to spend on the pilot before you'd want to pause and reassess. Choose a figure that gives the pilot room to generate meaningful usage data while capping your exposure if consumption runs higher than expected. If you adjust your pilot group size later, revisit this estimate.
+
+For example, suppose you pilot an agentic feature with 20 developers on {% data variables.copilot.copilot_business_short %}. Each license includes {% data variables.copilot.ai_credits_per_user_business %} {% data variables.product.prodname_ai_credits_short %} per month, so your pilot group draws on a shared pool of 20 × {% data variables.copilot.ai_credits_per_user_business %} included {% data variables.product.prodname_ai_credits_short %} before any metered charges begin. If you expect heavy use of roughly 3,000 {% data variables.product.prodname_ai_credits_short %} per developer that month, estimate total usage as 20 × 3,000. Subtract the included pool to estimate metered usage, then multiply by {% data variables.product.prodname_ai_credits_value %} to estimate overage cost and set your ceiling slightly above that. You'll create the budget that enforces this ceiling after you enable the feature, in [Set a budget to cap the pilot's costs](#set-a-budget-to-cap-the-pilots-costs).
+
+## Enable the feature for your pilot group
+
+Now that you've estimated costs and chosen who to include, you can turn the feature on in a contained way. The goal is to give your pilot group a real, correctly governed experience while keeping the rest of your enterprise unaffected.
+
+You'll do this in four steps:
+
+1. Confirm organization membership and license assignment.
+1. Configure the policies that govern the feature.
+1. Enable the feature for that organization only.
+1. Set a budget to cap the pilot's costs.
+
+### Confirm organization membership and license assignment
+
+Membership alone isn't enough: most {% data variables.product.prodname_copilot_short %} policies apply based on which organization assigns a user's {% data variables.product.prodname_copilot_short %} license, not simply which organizations they belong to. If someone belongs to multiple organizations, confirm their license is assigned through the pilot organization specifically. If it's assigned through a different one, the pilot organization's policies won't govern their access, and the feature enablement in this tutorial won't apply to them.
+
+If any of your pilot users aren't yet members, or are members but receive their license elsewhere, add them to the pilot organization and assign their {% data variables.product.prodname_copilot_short %} license through it before you continue.
+
+### Configure policies before you enable the feature
+
+{% data variables.product.prodname_copilot_short %} policies control which features and models your users can access and how their data is handled. Configure these policies for the pilot organization *before* you turn the feature on, so that pilot users get a correctly governed experience from their very first interaction rather than a brief window where the feature is available without the guardrails you intend.
+
+Decide which features and models the pilot should allow, and set the policies at the organization level so the change only affects your pilot organization; policies set at the enterprise level apply to every organization in your enterprise. If your enterprise policy doesn't allow organizations to set their own value, an enterprise owner must change this before you can continue. To understand how policies cascade from the enterprise to organizations and who can override settings at each level, see [AUTOTITLE](/copilot/concepts/policies).
+
+### Enable the feature for a single organization
+
+As an organization owner, enable the specific feature or model for the pilot organization only, from the organization's {% data variables.product.prodname_copilot_short %} policy settings. For the full steps, see [AUTOTITLE](/copilot/how-tos/administer-copilot/manage-for-organization/manage-policies). Confirm that the setting applies to the pilot organization and not to the enterprise as a whole before you save it.
+
+### Set a budget to cap the pilot's costs
+
+Earlier, you decided on a spending ceiling for the pilot. Now you'll create the budget that enforces it.
+
+First, understand what an organization budget does. It caps *metered* charges only, and it becomes active only after the shared pool of {% data variables.product.prodname_ai_credits_short %} is exhausted. It doesn't cap total spend, so set the limit to the amount of overage you're willing to pay beyond the included allowance, not to the pilot's total cost. Critically, a budget is **not a hard stop by default**: charges continue to accrue past the limit unless you enable **Stop usage when budget limit is reached** when you create it.
+
+As an organization owner or billing manager, create a budget scoped to the pilot organization, set its limit to the overage ceiling you chose, and enable **Stop usage when budget limit is reached**.
+
+If you want to prevent any single pilot user from running up consumption, also set a universal user-level budget. This is the only control that is always a hard stop, and it counts a user's consumption from both the shared pool and metered usage toward the same limit. Set it to a modest amount that lets participants do real work but stops any one user from consuming an outsized share of the pilot's budget.
+
+For how budgets meter and block usage at the user, organization, and enterprise levels, see [AUTOTITLE](/copilot/concepts/billing/budgets-for-usage-based-billing).
+
+## Monitor the pilot
+
+Throughout the pilot, track adoption, collect developer feedback, and watch cost and agentic activity against the success criteria you defined at the start. Together, these signals help you decide whether to expand the feature.
+
+### Track adoption and usage with {% data variables.product.prodname_copilot_short %} metrics
+
+Use {% data variables.product.prodname_copilot_short %} usage metrics to see how many of your pilot users are active, how often they use the feature, and how those numbers trend over the pilot period. Adoption that holds steady or grows is a strong signal; usage that spikes and then fades may mean the feature isn't fitting into developers' real workflows.
+
+* To understand what the metrics cover and how to interpret them, see [AUTOTITLE](/copilot/concepts/copilot-usage-metrics/copilot-metrics).
+* To view the dashboard for your organization or enterprise, see [AUTOTITLE](/copilot/how-tos/administer-copilot/view-usage-and-adoption).
+
+### Collect developer feedback from your pilot group
+
+Metrics tell you *whether* people use the feature, but not *why* or how well it works for them. Gather qualitative feedback to fill that gap. Run short surveys, hold regular check-ins, or set up a dedicated channel where pilot users can report what's working and what isn't.
+
+This is where the participants you chose for their willingness to give feedback pay off. Ask specific questions: where the feature saved time, where it produced poor results, and whether it changed how they work. This feedback often surfaces problems and opportunities that the numbers alone won't reveal, and it gives you concrete examples to share with leadership later.
+
+### Watch for cost and agentic activity in budgets and the audit log
+
+Keep an eye on spending against the budget you created throughout the pilot, not just at the end. If usage approaches your ceiling sooner than expected, that's useful early evidence about what a wider rollout would cost. For the steps to monitor what you've spent against the budget, see [AUTOTITLE](/copilot/how-tos/manage-and-track-spending/monitor-ai-usage). For tracking spending over time, see [AUTOTITLE](/copilot/how-tos/manage-and-track-spending/manage-company-spending).
+
+Alongside cost, watch whether the feature is being used safely. This is the third question leadership will ask, and the pilot is your chance to answer it with evidence rather than assurances. For agentic features, review agent sessions to understand what the feature is actually doing on your developers' behalf and to catch any unexpected or unwanted behavior; the audit log records when the feature is enabled or disabled, but doesn't capture that level of detail. Confirm that the policies and budgets you set are behaving as intended, and note anything that would need tighter guardrails at a larger scale. See [AUTOTITLE](/copilot/how-tos/administer-copilot/manage-for-enterprise/manage-agents/monitor-agentic-activity).
+
+## Make a go or no-go decision
+
+At the end of the pilot, check your results against the success criteria you defined up front, and decide whether to expand the feature, hold and gather more data, or roll it back. Make the decision deliberately and document the evidence behind it.
+
+### Confirm you have enough evidence to decide
+
+Before you decide, check that the pilot ran for the duration you planned and produced enough data to support a defensible conclusion.
+
+Pilot usage draws from the shared pool first (at no extra cost) before metered billing begins, so $0 spend early in the pilot is expected and a shorter run can make cost data misleading.
+
+You're ready to decide when you have:
+
+* Adoption metrics that show a stable pattern rather than a single early burst.
+* Actual cost data, spanning at least one billing cycle, that you can compare against your estimate.
+* Feedback from a representative range of your pilot users.
+
+If any of these is thin, for example if usage is still climbing or only a few participants engaged, consider extending the pilot rather than deciding on incomplete evidence. A decision you can back with data is far easier to defend to leadership and far less likely to need reversing.
+
+## Report to leadership
+
+To justify your decision and build a case for a wider rollout, report the pilot's results to leadership.
+
+### Identify the data to share
+
+Pull together the signals you gathered during the pilot:
+
+* **Adoption metrics**, showing how many pilot users were active and how usage trended over time.
+* **Actual cost against budget**, comparing what the pilot spent to the ceiling you set and to your original estimate.
+* **Feedback themes**, summarizing where participants found value and where they ran into problems.
+* **Safety and governance observations**, including anything you learned from the audit log or from how policies and budgets behaved.
+
+### Frame adoption, cost, and safety against ROI
+
+Present the data in business terms rather than as raw figures. Connect adoption and feedback to the value the feature delivered, such as time saved, work unblocked, or quality improved, and weigh that against its actual cost and any governance or risk considerations you observed.
+
+Close with a clear recommendation: expand, hold, or stop, and what it would cost to roll out more widely. Framing the pilot as a question of return on investment, predictable cost, and managed risk gives leadership what they need to approve a wider rollout with confidence.
+
+## Act on your decision
+
+Take the next step based on the evidence you gathered and the decision you made.
+
+### Expand the rollout across your enterprise
+
+If the evidence supports a go, widen the rollout in stages rather than all at once. Extend the feature policy to more organizations, and only then to the rest of your enterprise once each stage looks healthy. As you add more users, raise or add budgets so your spending controls scale with the larger population, and keep monitoring adoption and cost at each step.
+
+For broader guidance on managing a rollout across your enterprise, see [AUTOTITLE](/copilot/tutorials/roll-out-at-scale).
+
+### Roll back and disable the feature cleanly
+
+If the evidence points to a no-go, disable the feature cleanly so there's no lingering cost or confusion. Turn off the feature or model policy for the pilot organization, and tell your participants the pilot has ended and why, so they're not left wondering why the feature disappeared.
+
+Confirm there's no remaining usage that could continue to bill against your budget, and review or remove the pilot budget so it doesn't affect future reporting. Keep the data and feedback you gathered: even a no-go is a useful result to share with leadership, and it may inform a later re-evaluation when the feature matures.
diff --git a/content/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/available-rules-for-rulesets.md b/content/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/available-rules-for-rulesets.md
index ddfa3d8746de..d5003889eeb0 100644
--- a/content/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/available-rules-for-rulesets.md
+++ b/content/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/available-rules-for-rulesets.md
@@ -120,6 +120,8 @@ Optionally, you can choose to dismiss stale pull request approvals when commits
Optionally, you can choose to require reviews from code owners. If you do, any pull request that modifies content with a code owner must be approved by that code owner before the pull request can be merged into the protected branch. Note that if code has multiple owners, an approval from _any_ of the code owners will be sufficient to meet this requirement. For more information, see [AUTOTITLE](/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners).
+Optionally, you can restrict who can dismiss pull request reviews. If you enable this setting, select the users, teams, or {% data variables.product.prodname_github_apps %} that can dismiss reviews on branches targeted by the ruleset. You can configure this setting in the UI or through the REST API or GraphQL API. For more information, see [AUTOTITLE](/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/dismissing-a-pull-request-review).
+
Optionally, you can require an approval from someone other than the last person to push to a branch before a pull request can be merged. This means at least one other authorized reviewer has approved any changes. For example, the "last reviewer" can check that the latest set of changes incorporates feedback from other reviews, and does not add new, unreviewed content.
For complex pull requests that require many reviews, requiring an approval from someone other than the last person to push can be a compromise that avoids the need to dismiss all stale reviews: with this option, "stale" reviews are not dismissed, and the pull request remains approved as long as someone other than the person who made the most recent changes approves it. Users who have already reviewed a pull request can reapprove after the most recent push to meet this requirement. If you are concerned about pull requests being "hijacked" (where unapproved content is added to approved pull requests), it is safer to dismiss stale reviews.
diff --git a/contributing/README.md b/contributing/README.md
index 848e3d1474e8..987a112b9b32 100644
--- a/contributing/README.md
+++ b/contributing/README.md
@@ -14,6 +14,6 @@ Here, you'll find additional information that might be helpful as you work on a
- [troubleshooting](./troubleshooting.md) - some help for troubleshooting failed and stalled status checks
- [Reusables](https://docs.github.com/en/contributing/writing-for-github-docs/creating-reusable-content#about-reusables) - We create reusables and call them from articles to help us keep content that is used in multiple places up to date.
- [Variables](https://docs.github.com/en/contributing/writing-for-github-docs/creating-reusable-content#about-variables) - We use variables the same way we use reusables. Variables are for short strings of reusable text.
-- [Tests](/tests/README.md) - We use tests to ensure content will render correctly on the site. Tests run automatically in your PR, and sometimes it's also helpful to run them locally.
+- [Tests](/src/tests/README.md) - We use tests to ensure content will render correctly on the site. Tests run automatically in your PR, and sometimes it's also helpful to run them locally.
You can also read the READMEs in the `src/` directory to learn more about the features of the docs site.
diff --git a/contributing/development.md b/contributing/development.md
index b7b181740bc4..913fb8df4838 100644
--- a/contributing/development.md
+++ b/contributing/development.md
@@ -32,7 +32,7 @@ Power users may want to read more about [debugging the docs application](./debug
### Using GitHub Codespaces
-As an alternative, you can simply use [GitHub Codespaces](https://docs.github.com/en/codespaces/quickstart). For more information about using a codespace for working on GitHub documentation, see [Working in a codespace](https://github.com/github/docs/blob/main/contributing/codespace.md).
+As an alternative, you can simply use [GitHub Codespaces](https://docs.github.com/en/codespaces/quickstart). For more information about using a codespace for working on GitHub documentation, see [Working on GitHub Docs in a codespace](https://docs.github.com/en/contributing/setting-up-your-environment-to-work-on-github-docs/working-on-github-docs-in-a-codespace).
In a matter of minutes, you will be ready to edit, review and test your changes directly from the comfort of your browser.
diff --git a/data/reusables/copilot/copilot-cli/cli-extensions-experimental.md b/data/reusables/copilot/copilot-cli/cli-extensions-experimental.md
new file mode 100644
index 000000000000..8ce3a4759aae
--- /dev/null
+++ b/data/reusables/copilot/copilot-cli/cli-extensions-experimental.md
@@ -0,0 +1,2 @@
+> [!NOTE]
+> {% data variables.copilot.copilot_cli %} extensions are currently an experimental feature and are subject to change.
diff --git a/package-lock.json b/package-lock.json
index 995dbf7df8e5..eece9396d8c4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2345,6 +2345,7 @@
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz",
"integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@octokit/auth-token": "^6.0.0",
"@octokit/graphql": "^9.0.3",
@@ -2645,6 +2646,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"license": "Apache-2.0",
+ "peer": true,
"engines": {
"node": ">=8.0.0"
}
@@ -5193,6 +5195,7 @@
"integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==",
"devOptional": true,
"license": "Apache-2.0",
+ "peer": true,
"dependencies": {
"playwright": "1.60.0"
},
@@ -5942,6 +5945,7 @@
"integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "^5.0.0",
@@ -6083,6 +6087,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -6093,6 +6098,7 @@
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true,
"license": "MIT",
+ "peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@@ -6252,6 +6258,7 @@
"integrity": "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.58.0",
"@typescript-eslint/types": "8.58.0",
@@ -6925,6 +6932,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -6964,6 +6972,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -7484,6 +7493,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001733",
"electron-to-chromium": "^1.5.199",
@@ -8961,6 +8971,7 @@
"integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -9021,6 +9032,7 @@
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true,
"license": "MIT",
+ "peer": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
@@ -9285,6 +9297,7 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -10429,6 +10442,7 @@
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz",
"integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==",
"dev": true,
+ "peer": true,
"engines": {
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
}
@@ -11832,6 +11846,7 @@
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
"dev": true,
+ "peer": true,
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
@@ -14961,6 +14976,7 @@
"integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==",
"devOptional": true,
"license": "Apache-2.0",
+ "peer": true,
"bin": {
"playwright-core": "cli.js"
},
@@ -15021,6 +15037,7 @@
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -15171,6 +15188,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
"integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -15189,6 +15207,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz",
"integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -15215,7 +15234,8 @@
"version": "19.2.4",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz",
"integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/react-markdown": {
"version": "10.1.0",
@@ -15817,6 +15837,7 @@
"integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==",
"devOptional": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"chokidar": "^4.0.0",
"immutable": "^5.0.2",
@@ -16876,6 +16897,7 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -17212,6 +17234,7 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
"license": "Apache-2.0",
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -17551,6 +17574,7 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"napi-postinstall": "^0.3.0"
},
@@ -17761,6 +17785,7 @@
"integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@@ -17869,6 +17894,7 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -18487,10 +18513,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
- "src/search/vendor/apache-arrow-stub": {
- "name": "apache-arrow-stub",
- "version": "0.0.0"
- },
"src/eslint-rules": {
"name": "eslint-plugin-custom-rules",
"version": "1.0.0",
@@ -18499,6 +18521,9 @@
"peerDependencies": {
"eslint": "^8.0.0 || ^9.0.0"
}
+ },
+ "src/search/vendor/apache-arrow-stub": {
+ "version": "0.0.0"
}
}
}
diff --git a/src/languages/lib/correct-translation-content.ts b/src/languages/lib/correct-translation-content.ts
index 73dee768bd8f..53774987d7ea 100644
--- a/src/languages/lib/correct-translation-content.ts
+++ b/src/languages/lib/correct-translation-content.ts
@@ -1251,6 +1251,19 @@ export function correctTranslatedContentStrings(
'{% ifversion ghec %}аутентификации и{% endif %} провизионирования SCIM{% else %}с помощью Okta',
'{% ifversion ghec %}SCIM{% else %}аутентификации и{% endif %} провизионирования с помощью Okta',
)
+
+ // [SCRAPE-6732] admin/managing-accounts-and-repositories/managing-users-in-your-enterprise/viewing-and-managing-a-users-saml-access-to-your-enterprise.md
+ // (intro): translator scrambled `{% ifversion ghec %}...{% else %}...{% endif %}`
+ // so the `{% else %}` ended up before any opening `{% ifversion %}` (an orphan)
+ // and the `{% ifversion ghec %}` moved into the else branch. This breaks the
+ // admin landing page render (`tag "else" not found`). Reconstruct to match
+ // English: view and revoke an enterprise member's {% ifversion ghec %}linked
+ // identity, active sessions, and authorized credentials{% else %}active SAML
+ // sessions{% endif %}. The corrector runs on the PARSED intro value.
+ content = content.replaceAll(
+ 'связанную личность, активные сессии и авторизованные учетные{% else %}данные {% ifversion ghec %}SAML{% endif %}',
+ '{% ifversion ghec %}связанную личность, активные сессии и авторизованные учетные данные{% else %}активные сессии SAML{% endif %}',
+ )
}
if (context.code === 'fr') {
diff --git a/src/languages/tests/correct-translation-content.ts b/src/languages/tests/correct-translation-content.ts
index 093b0e6a8b9a..acd373748f98 100644
--- a/src/languages/tests/correct-translation-content.ts
+++ b/src/languages/tests/correct-translation-content.ts
@@ -2437,4 +2437,21 @@ Para más información, consulta "[AUTOTITLE](/path)".
expect(fix(broken, 'de')).toBe('auf selbst-gehosteten Runnern ausführen.{% endif %}')
})
})
+
+ // ─── SCRAPE-6732: search-scrape failures ─────────────────────────────
+ // The ru admin landing page failed to scrape with `tag "else" not found`
+ // because the intro of viewing-and-managing-a-users-saml-access-to-your-enterprise.md
+ // had an orphaned `{% else %}` before any opening `{% ifversion %}`
+ // (github/docs-engineering#6732). The corrector runs on the PARSED intro value.
+ describe('SCRAPE-6732 per-file fixes', () => {
+ test('ru: viewing-and-managing-a-users-saml-access intro reorders orphaned else', () => {
+ const broken =
+ 'Вы можете просматривать и отзывать связанную личность, активные сессии и авторизованные учетные{% else %}данные {% ifversion ghec %}SAML{% endif %} участника предприятия.'
+ const fixed =
+ 'Вы можете просматривать и отзывать {% ifversion ghec %}связанную личность, активные сессии и авторизованные учетные данные{% else %}активные сессии SAML{% endif %} участника предприятия.'
+ expect(fix(broken, 'ru')).toBe(fixed)
+ // idempotent: the fix only matches the broken form
+ expect(fix(fixed, 'ru')).toBe(fixed)
+ })
+ })
})