diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json index d0da210a9f..02b7d48974 100644 --- a/.github/plugin/marketplace.json +++ b/.github/plugin/marketplace.json @@ -1817,6 +1817,12 @@ "sha": "940979bf3d82be136c4529a0b08aee7198ef178f" } }, + { + "name": "local-coding", + "source": "plugins/local-coding", + "description": "Experimental. Hand large, mechanical sub-tasks (big reads, log and test triage, and the same edit across many files) from a cloud Copilot CLI session to the on-device MAI Code 1.1 Flash model on Windows, through an offload agent (the main session checks every DONE reply) or the on-device-delegate skill. Includes scripts to cap the local model's output tokens and to install or update the plugin.", + "version": "0.3.2" + }, { "name": "markdstage", "description": "Create, edit, present, and export Markdown slides in a GitHub Copilot Canvas.", diff --git a/agents/on-device-offload.agent.md b/agents/on-device-offload.agent.md new file mode 100644 index 0000000000..6902654b5e --- /dev/null +++ b/agents/on-device-offload.agent.md @@ -0,0 +1,58 @@ +--- +name: On-Device Offload +description: 'Experimental. Worker on the local MAI Code 1.1 Flash model. Its tokens do not use GitHub AI Credits. USE IT when the task needs reading more than about 10 KB of text or many files (logs, long docs, test output, source files), even if a short script could compute the answer (the worker writes and runs scripts too), and for the same mechanical edit across several files. SKIP IT and work normally when one search or one or two small file views answer the task, for new code from a spec or design, and for security review or design decisions. HOW: in your FIRST response, before reading anything, call task with name "offload", agent_type "local-coding:on-device-offload" (the plugin id, or plain "on-device-offload" if the task tool lists this agent that way because it was copied into an agents folder), model = the on-device model id named in the session''s routing line, exactly (if there is none: "winml-azureml-mai-code-1.1-flash-on-device/MAI-Code-1.1-Flash-On-Device"), mode "sync", a 3-word description, and prompt = the user''s whole task copied verbatim. It reads the inputs, writes the deliverable, checks it, and replies DONE or UNABLE. Before you accept DONE, verify it cheaply: read the deliverable, then check each fact you will report (every listed item, count, or value) against the source with one targeted grep or a small view of the cited lines, or rerun the worker''s own check. Never re-read the whole input or redo the task. If it all checks out, answer in one sentence that names what it wrote and asks the user to review it. If any fact is wrong or missing, treat the reply as UNABLE. Anything else (UNABLE, an error, no reply): do the task yourself, with no retry. Before you start, run git status and undo only the files the worker changed (never changes that existed before the offload).' +tools: ["read", "search", "edit", "execute"] +strict-tools-list: true +--- + +# On-Device Offload + +You are the on-device worker. You own the whole task in the prompt, end to end. +You run on a local model, so extra checking is cheap, and the main session +will not re-check your work. Be thorough in your checks but brief in your +words. + +## Budget and style + +These rules keep a small local model from stalling: + +- Do all the work yourself with your own tools. Never start a sub-agent or + another worker. If the task is too big for you, reply + `UNABLE: too big - `. +- At most 15 tool calls. If the task is not done by then, reply UNABLE. +- Think in at most three short sentences per response, then make ONE tool + call. Never enumerate cases, lines, or phrasings in your thinking. Put them + in a script or a file instead. +- If you notice that you are repeating yourself, stop and reply + `UNABLE: looping`. +- The shell is PowerShell: no heredocs (`<<`) and no multi-line `python -c`. + Write a script with the create tool, then run `python .py`. Keep + scripts under about 80 lines and print only what you need (counts, matches, + short samples), never whole files. + +## Steps + +1. Find what you need with search, then read only the ranges you need. For + large or structured inputs (logs, test output, many files), write and run + a short script instead of reading everything. +2. Prose with varied wording: search for every line that mentions the items + you are tracking and save that list to a file. Read a sample of it and + write into your script every phrasing that changes the answer (and the + ones that do not: suspicions, proposals, questions, and rejected or + "almost done" items). +3. Compute every extracted, classified, or counted answer TWO independent ways + (for example two differently written scripts, or a script plus a manual + pass over the matching lines). If they disagree, read the source lines + behind each difference and fix the wrong one until both agree. +4. Produce the exact deliverable: write the requested file(s) in exactly the + requested format, or make exactly the requested edits. Change nothing else. +5. Edits: change only the target text, in place (exact or regex replacement + on the original file text). Never re-serialize or reformat a file: no + tokenize/untokenize, AST or JSON round trips, and no formatters. Keep a + copy of the originals and diff them afterwards: every changed line must + differ only in the intended text, and every intended occurrence must be + changed. +6. Verify with a command (the file exists and parses, counts match, the diff + is clean, tests pass). If it fails, fix it and verify again. +7. Reply with ONE line and stop: `DONE: ; + verified: ` or `UNABLE: `. diff --git a/agents/on-device-worker.agent.md b/agents/on-device-worker.agent.md new file mode 100644 index 0000000000..f9ada9bb0f --- /dev/null +++ b/agents/on-device-worker.agent.md @@ -0,0 +1,25 @@ +--- +name: On-Device Worker +description: 'Bounded worker on the local MAI Code 1.1 Flash model, briefed by the on-device-delegate skill. Runs exactly one sub-task with named inputs (search and summarize, extract facts from large files or logs, or make a mechanical edit with an exact spec) and replies in the requested format or UNABLE. Not for security review, design decisions, or multi-step coding.' +tools: ["read", "search", "edit", "execute"] +strict-tools-list: true +--- + +# On-Device Worker + +You are the on-device worker. Do exactly the one sub-task in the prompt. The +prompt has the sections GOAL, INPUTS, RULES, OUTPUT, and ANSWER. Follow them +literally. + +- Read only the INPUTS. Search first, then view small ranges. Do not read a + whole large file when a search answers the question. +- For counting or extracting across many lines or files, write a short + script with the create tool and run it. The shell is PowerShell: no + heredocs and no multi-line `python -c`. +- Write or edit only the file named in OUTPUT. If OUTPUT says "reply only", + edit nothing. +- Never start a sub-agent or another worker. +- Stay within the tool-call limit in RULES. +- Answer in the exact format in ANSWER, nothing else. Cite `path:line` for + every claim you make about the inputs. +- If you cannot finish, answer exactly `UNABLE: `. diff --git a/docs/README.agents.md b/docs/README.agents.md index 6a2963e011..93d09bac5f 100644 --- a/docs/README.agents.md +++ b/docs/README.agents.md @@ -149,6 +149,8 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-agents) for guidelines on how to | [New Relic Kubernetes Agent](../agents/new-relic-kubernetes.agent.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fnew-relic-kubernetes.agent.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode-insiders%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fnew-relic-kubernetes.agent.md) | Kubernetes diagnosis and debugging using New Relic telemetry. Use when investigating pod crashes, CrashLoopBackOff, OOMKills, pod evictions, scheduling failures, container restarts, node pressure, HPA/scaling issues, service disruptions, or other Kubernetes workload problems. Requires a New Relic account with nri-kubernetes / kube-state-metrics data ingested. | | [Next.js Expert](../agents/expert-nextjs-developer.agent.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fexpert-nextjs-developer.agent.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode-insiders%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fexpert-nextjs-developer.agent.md) | Expert Next.js 16 developer specializing in App Router, Server Components, Cache Components, Turbopack, and modern React patterns with TypeScript | | [Octopus Release Notes With Mcp](../agents/octopus-deploy-release-notes-mcp.agent.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Foctopus-deploy-release-notes-mcp.agent.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode-insiders%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Foctopus-deploy-release-notes-mcp.agent.md) | Generate release notes for a release in Octopus Deploy. The tools for this MCP server provide access to the Octopus Deploy APIs. | +| [On Device Offload](../agents/on-device-offload.agent.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fon-device-offload.agent.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode-insiders%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fon-device-offload.agent.md) | Experimental. Worker on the local MAI Code 1.1 Flash model. Its tokens do not use GitHub AI Credits. USE IT when the task needs reading more than about 10 KB of text or many files (logs, long docs, test output, source files), even if a short script could compute the answer (the worker writes and runs scripts too), and for the same mechanical edit across several files. SKIP IT and work normally when one search or one or two small file views answer the task, for new code from a spec or design, and for security review or design decisions. HOW: in your FIRST response, before reading anything, call task with name "offload", agent_type "local-coding:on-device-offload" (the plugin id, or plain "on-device-offload" if the task tool lists this agent that way because it was copied into an agents folder), model = the on-device model id named in the session's routing line, exactly (if there is none: "winml-azureml-mai-code-1.1-flash-on-device/MAI-Code-1.1-Flash-On-Device"), mode "sync", a 3-word description, and prompt = the user's whole task copied verbatim. It reads the inputs, writes the deliverable, checks it, and replies DONE or UNABLE. Before you accept DONE, verify it cheaply: read the deliverable, then check each fact you will report (every listed item, count, or value) against the source with one targeted grep or a small view of the cited lines, or rerun the worker's own check. Never re-read the whole input or redo the task. If it all checks out, answer in one sentence that names what it wrote and asks the user to review it. If any fact is wrong or missing, treat the reply as UNABLE. Anything else (UNABLE, an error, no reply): do the task yourself, with no retry. Before you start, run git status and undo only the files the worker changed (never changes that existed before the offload). | +| [On Device Worker](../agents/on-device-worker.agent.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fon-device-worker.agent.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode-insiders%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fon-device-worker.agent.md) | Bounded worker on the local MAI Code 1.1 Flash model, briefed by the on-device-delegate skill. Runs exactly one sub-task with named inputs (search and summarize, extract facts from large files or logs, or make a mechanical edit with an exact spec) and replies in the requested format or UNABLE. Not for security review, design decisions, or multi-step coding. | | [One Shot Feature Issue Planner](../agents/one-shot-feature-issue-planner.agent.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fone-shot-feature-issue-planner.agent.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode-insiders%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fone-shot-feature-issue-planner.agent.md) | Cloud Agent to Turn a single new-feature request into a complete, issue-ready implementation plan without follow-up questions. | | [OpenAPI to Application Generator](../agents/openapi-to-application.agent.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fopenapi-to-application.agent.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode-insiders%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fopenapi-to-application.agent.md) | Expert assistant for generating working applications from OpenAPI specifications | | [OpenAPI to Application Generator (Spring Boot)](../agents/openapi-to-application-java-spring-boot.agent.md)
[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fopenapi-to-application-java-spring-boot.agent.md)
[![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://aka.ms/awesome-copilot/install/agent?url=vscode-insiders%3Achat-agent%2Finstall%3Furl%3Dhttps%3A%2F%2Fraw.githubusercontent.com%2Fgithub%2Fawesome-copilot%2Fmain%2Fagents%2Fopenapi-to-application-java-spring-boot.agent.md) | Expert assistant for generating Spring Boot applications from OpenAPI specifications, spec-first with openapi-generator | diff --git a/docs/README.plugins.md b/docs/README.plugins.md index 128cbbdc20..4c5b445d15 100644 --- a/docs/README.plugins.md +++ b/docs/README.plugins.md @@ -85,6 +85,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-plugins) for guidelines on how t | [java-modernization-studio](../plugins/java-modernization-studio/README.md) | Drive the GitHub Copilot App Modernization for Java workflow from an interactive canvas: environment readiness, repo assessment, prioritized plan and progress, validation gates, and one-click predefined-task runs grounded in the repo's real artifacts. | 1 items | app-modernization, assessment-dashboard, azure-migration, java-modernization, legacy-java, modernization-cockpit, validation-gates | | [jupyter-notebooks](../plugins/jupyter-notebooks/README.md) | Create, edit, run, save, and checkpoint Jupyter notebooks in an interactive Copilot canvas. | 1 items | data-analysis, interactive-notebooks, jupyter, python | | [kotlin-mcp-development](../plugins/kotlin-mcp-development/README.md) | Complete toolkit for building Model Context Protocol (MCP) servers in Kotlin using the official io.modelcontextprotocol:kotlin-sdk library. Includes instructions for best practices, a prompt for generating servers, and an expert chat mode for guidance. | 2 items | kotlin, mcp, model-context-protocol, kotlin-multiplatform, server-development, ktor | +| [local-coding](../plugins/local-coding/README.md) | Experimental. Hand large, mechanical sub-tasks (big reads, log and test triage, and the same edit across many files) from a cloud Copilot CLI session to the on-device MAI Code 1.1 Flash model on Windows, through an offload agent (the main session checks every DONE reply) or the on-device-delegate skill. Includes scripts to cap the local model's output tokens and to install or update the plugin. | 3 items | on-device, local-models, offload, mai-code, llama-cpp, copilot-cli, windows | | [mcp-m365-copilot](../plugins/mcp-m365-copilot/README.md) | Comprehensive collection for building declarative agents with Model Context Protocol integration for Microsoft 365 Copilot | 4 items | mcp, m365-copilot, declarative-agents, api-plugins, model-context-protocol, adaptive-cards | | [modernization-workflow](../plugins/modernization-workflow/README.md) | Visualize and guide the GitHub Copilot Modernize CLI Assess, Plan, and Execute workflow for .NET, Java, and C++ application repositories. | 1 items | app-modernization, assessment-dashboard, azure-migration, cpp-modernization, dotnet-modernization, java-modernization, modernization-canvas, modernize-cli | | [napkin](../plugins/napkin/README.md) | Visual whiteboard collaboration for Copilot CLI. Opens an interactive whiteboard in your browser where you can draw, sketch, and add sticky notes — then share everything back with Copilot. Copilot sees your drawings and responds with analysis, suggestions, and ideas. | 1 items | whiteboard, visual, collaboration, brainstorming, non-technical, drawing, sticky-notes, accessibility, copilot-cli, ux | diff --git a/docs/README.skills.md b/docs/README.skills.md index 26484ccb62..4c16ceda20 100644 --- a/docs/README.skills.md +++ b/docs/README.skills.md @@ -320,6 +320,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-skills) for guidelines on how to | [noob-mode](../skills/noob-mode/SKILL.md)
`gh skills install github/awesome-copilot noob-mode` | Plain-English translation layer for non-technical Copilot CLI users. Translates every approval prompt, error message, and technical output into clear, jargon-free English with color-coded risk indicators. | `references/examples.md`
`references/glossary.md` | | [npm-trusted-publishing](../skills/npm-trusted-publishing/SKILL.md)
`gh skills install github/awesome-copilot npm-trusted-publishing` | Set up or fix npm trusted publishing (OIDC) in GitHub Actions and remove NPM_TOKEN. Use for ENEEDAUTH, E404 on PUT, E422 repository.url, or migrating off 2FA-bypass tokens before January 2027. | None | | [nuget-manager](../skills/nuget-manager/SKILL.md)
`gh skills install github/awesome-copilot nuget-manager` | Manage NuGet packages in .NET projects/solutions. Use this skill when adding, removing, or updating NuGet package versions. It enforces using `dotnet` CLI for package management and provides strict procedures for direct file edits only when updating versions. | None | +| [on-device-delegate](../skills/on-device-delegate/SKILL.md)
`gh skills install github/awesome-copilot on-device-delegate` | Saves cloud-model context by briefing one bounded, read-heavy sub-task (more than about 25 KB of prose, or many files that no single command can answer) to the local-coding plugin's on-device worker, which runs on the local MAI Code 1.1 Flash model. Use for log and test triage, summarizing many files, or extracting facts from large inputs. When the user names this skill, call the skill tool in the same response as your first other tool call, never as a separate step. | `scripts/Set-LocalOutputCap.ps1`
`scripts/Update-LocalCodingPlugin.ps1` | | [onboard-context-matic](../skills/onboard-context-matic/SKILL.md)
`gh skills install github/awesome-copilot onboard-context-matic` | Interactive onboarding tour for the context-matic MCP server. Walks the user through what the server does, shows all available APIs, lets them pick one to explore, explains it in their project language, demonstrates model_search and endpoint_search live, and ends with a menu of things the user can ask the agent to do. USE FOR: first-time setup; "what can this MCP do?"; "show me the available APIs"; "onboard me"; "how do I use the context-matic server"; "give me a tour". DO NOT USE FOR: actually integrating an API end-to-end (use integrate-context-matic instead). | None | | [oo-component-documentation](../skills/oo-component-documentation/SKILL.md)
`gh skills install github/awesome-copilot oo-component-documentation` | Create or update standardized object-oriented component documentation using a shared template plus mode-specific guidance for new and existing docs. | `assets/documentation-template.md`
`references/create-mode.md`
`references/update-mode.md` | | [openapi-to-application-code](../skills/openapi-to-application-code/SKILL.md)
`gh skills install github/awesome-copilot openapi-to-application-code` | Generate a complete, production-ready application from an OpenAPI specification | None | diff --git a/plugins/local-coding/README.md b/plugins/local-coding/README.md new file mode 100644 index 0000000000..caf70b9c8b --- /dev/null +++ b/plugins/local-coding/README.md @@ -0,0 +1,257 @@ +# Local Coding Plugin + +**Experimental, for dogfooding.** Let a cloud GitHub Copilot CLI session hand large, mechanical +sub-tasks to **MAI Code 1.1 Flash** running on your own Windows PC. The main session keeps +planning and answering. The local model does the bulky reading and the repetitive edits. Those +tokens stay out of the main session's context and, because they run on your device, do not use +GitHub AI Credits. It is about 10x to 11x slower than cloud only, and it has not shown a cost +saving on our task set (see [Measured results](#measured-results)). + +Version 0.3.2: offload agent v12 with the DONE double-check, the `on-device-delegate` skill, and +two setup scripts, `Set-LocalOutputCap.ps1` and `Update-LocalCodingPlugin.ps1`. + +## Installation + +```bash +copilot plugin install local-coding@awesome-copilot +``` + +Then cap the local model's output tokens once (strongly recommended, see +[Cap runaway generations](#cap-runaway-generations)). + +## What's Included + +| Component | Type | Use it when | +|---|---|---| +| `On-Device Offload` (`local-coding:on-device-offload`) | Agent | You want the main session to hand a whole read-heavy or mechanical task to the local model. Start here. | +| `on-device-delegate` | Skill | You want the main session to brief one bounded sub-task to the local model and check the answer once. | +| `On-Device Worker` (`local-coding:on-device-worker`) | Agent | Used by the `on-device-delegate` skill. You do not call it directly. | +| `skills/on-device-delegate/scripts/Set-LocalOutputCap.ps1` | Script | You run it once to cap the local model's output tokens in your `providers.json`. | +| `skills/on-device-delegate/scripts/Update-LocalCodingPlugin.ps1` | Script | You want the install or update and the cap in one command. | + +Neither agent pins a model in its frontmatter. On Copilot CLI 1.0.105 a pinned CLI-managed model +id is rejected, so the main session passes the model on the task call. Both agents limit +themselves to the read, search, edit, and shell tools (no sub-agents), and finish with a one-line +`DONE` or `UNABLE` (or the exact answer format they were asked for). + +What the plugin adds over a plain custom agent with a local model: + +- **When to offload.** The agent description and the skill gate on how much the main session + would otherwise read (about 10 KB for the agent, about 25 KB for the skill), and rule out new + code from a spec, design decisions, and security review. +- **A worker prompt tuned for a small local model.** A tool-call budget, scripts instead of + reading whole files, computing every count or extraction two independent ways, in-place edits + with a diff check, and an explicit stop on loops. +- **DONE double-check.** Before the main session accepts the worker's `DONE`, it reads the + deliverable and checks each item, count, or value it will report against the source with one + targeted search or a small view of the cited lines, or reruns the worker's own check. It never + re-reads the whole input or redoes the task. If any fact is wrong or missing, it treats the reply + as `UNABLE`, undoes the worker's files, and does the task itself. +- **A brief format** (`GOAL`, `INPUTS`, `RULES`, `OUTPUT`, and `ANSWER`) for the skill, so the + main session never reads the big input itself, plus a single cheap check of the result. + +## Prerequisites + +These come from the GitHub Docs page +[Adding LLM models to GitHub Copilot CLI](https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-llm-models#installing-a-local-model-from-the-model-picker), +where local models are in public preview: + +- A Windows PC with an NVIDIA GPU, at least 96 GB of combined RAM, and 60 GB of free storage for + MAI Code 1.1 Flash. +- GitHub Copilot CLI, signed in. +- Experimental features on: start the CLI with `copilot --experimental`, or run + `/experimental on` once. +- MAI Code 1.1 Flash installed from the model picker: + 1. In a session, enter `/model`. + 2. Under **Local and self-hosted**, select **MAI Code 1.1 Flash** and press Enter. + 3. Choose **Download and set up**. The CLI checks your hardware, downloads the model, and tells + you in the conversation when it is done. + 4. Pick your usual cloud model again for the main session. +- Windows PowerShell 5.1 or PowerShell 7 for the two scripts. + +## Cap runaway generations + +Run `Set-LocalOutputCap.ps1` once after you install the model. Copilot CLI 1.0.105 asks the local +model for up to the model row's `maxOutputTokens`, and **Download and set up** writes 128,000 for +MAI Code 1.1 Flash. When the model loops, one reply can run for 25 to 50 minutes. With an 8,192 +cap the loop stops at the cap, the offload agent's reply is not `DONE`, and the main session does +the task itself. In our runs the cap took runaway timeouts from 6 to 0. + +The plugin cannot change that setting by itself. The script backs up `providers.json` first and +changes only that one model row. It ships inside the installed plugin, under +`%USERPROFILE%\.copilot\installed-plugins\`: + +```powershell +$cap = Get-ChildItem "$HOME\.copilot\installed-plugins" -Recurse -Filter Set-LocalOutputCap.ps1 | + Where-Object FullName -match '\\local-coding\\' | Select-Object -First 1 +powershell -ExecutionPolicy Bypass -File $cap.FullName -WhatIf # show the change +powershell -ExecutionPolicy Bypass -File $cap.FullName # cap at 8,192 +powershell -ExecutionPolicy Bypass -File $cap.FullName -Restore # undo +``` + +Restart open sessions afterwards. Running **Download and set up** for the model again puts 128,000 +back, so run the script again then. Options: `-MaxOutputTokens `, `-Model `, and +`-ProvidersJson `. + +## Update or one-shot setup + +`Update-LocalCodingPlugin.ps1` does the install or update and the cap in one go, and is safe to run +again: + +1. It installs or updates `local-coding@awesome-copilot`. If that fails, or the marketplace has an + older version than the copy the script came in, it installs that local copy instead + (`-PluginPath`, default: the plugin folder the script sits in). It then removes the other copy, + so the agents do not load twice. +2. It checks that `copilot plugin list` shows this version or newer, and enables the plugin if it + is disabled. +3. It runs the installed `Set-LocalOutputCap.ps1`. If `providers.json` has no CLI-managed + MAI Code 1.1 Flash row, it says so and skips the cap. +4. It prints the next steps. + +```powershell +$upd = Get-ChildItem "$HOME\.copilot\installed-plugins" -Recurse -Filter Update-LocalCodingPlugin.ps1 | + Where-Object FullName -match '\\local-coding\\' | Select-Object -First 1 +powershell -ExecutionPolicy Bypass -File $upd.FullName -WhatIf # show the plan +powershell -ExecutionPolicy Bypass -File $upd.FullName # do it +``` + +Options: `-Source Marketplace|Local` (default `Auto`), `-PluginPath `, +`-CopilotHome `, `-MaxOutputTokens `, `-SkipCap`, `-Restore` (undo the cap only), and +`-Help`. Restart open CLI sessions afterwards. + +## The model id + +The agent and skill text use the provider-qualified id that Copilot CLI gives the model it +installs: + +```text +winml-azureml-mai-code-1.1-flash-on-device/MAI-Code-1.1-Flash-On-Device +``` + +`/model` shows the same model as **MAI Code 1.1 Flash** under **Local and self-hosted**. If your +CLI shows a different id, put that id in the routing line below. The agent and the skill use the id +the routing line names, and fall back to the id above when there is none. + +## Check the installation + +```bash +copilot plugin list +copilot skill list +``` + +The install message counts only skills ("Installed 1 skill"). The two agents are installed too. +Plugin agents load with the plugin name as a prefix: `local-coding:on-device-offload` and +`local-coding:on-device-worker`. In a session, `/agent` lists **On-Device Offload** and +**On-Device Worker**, and `/skills list` shows `on-device-delegate`. A personal or project agent or +skill with the same id takes precedence over the plugin's copy. Copied agents (for example in +`~/.copilot/agents/`) load without the prefix, as plain `on-device-offload` and +`on-device-worker`, and the agent and skill text fall back to those ids. + +## Use the offload agent + +Start `copilot --experimental` in your repository and keep a cloud model as the main model. +Begin the prompt with this routing line, then your task: + +```text +An on-device offload agent is installed. Use it only when its description says so. When you do, set model winml-azureml-mai-code-1.1-flash-on-device/MAI-Code-1.1-Flash-On-Device on its task calls. +``` + +Example tasks: + +```text +Summarize every failing test in test-output.log into failures.md: one line per test with the test name, file, and first error line. +``` + +```text +Rename the setting retryCount to maxRetries in every file under src\ and docs\. Change nothing else. +``` + +```text +Read every file under docs\ and list each TODO in todos.md as path:line followed by the TODO text. +``` + +When the agent runs, the session shows an offload sub-agent on the local model that ends in +`DONE: ...` or `UNABLE: ...`. On `DONE`, the main session spot-checks the result against the +source, then answers in one sentence that names what was written. On `UNABLE`, an error, or a +failed check, it runs `git status`, undoes only the files the worker changed, and does the task +itself. + +## Use the skill + +Name the skill in the prompt: + +```text +Use the on-device-delegate skill. From the logs under logs\, list every distinct error message with its count, most frequent first. +``` + +The skill briefs one bounded sub-task to the `On-Device Worker` agent and sets the task's `model` +to the local model id, because the worker does not pin one. The worker's block in the session +shows the model it ran on. The skill only delegates when the task means reading a lot of text. For +small tasks, or when the worker agent is not available, the main session does the work itself. + +## Measured results + +For dogfooding and testing, **not a cost win under either main model so far**. Measured on Copilot +CLI 1.0.108 with plugin 0.3.2, 12 tasks x 3, one test PC (RTX, 66 GB), using MAI Code 1.1 Flash +(CLI-managed, 8,192 output cap) as the offload model. The offload set is log triage, +long-document extraction, and multi-file edits. Cost per success charges each failed run a redo +at the pure-cloud price. + +| Main model | Offload agent v12 with the DONE double-check | Pure cloud, same main model | Cost per success | Time per success | +|---|---|---|---|---| +| Claude Sonnet 5.5 | 33/36 | 36/36 | 29.4% higher (0.0756 vs 0.0584 USD), interval spans zero | 11.1x slower (193.6 vs 17.5 s) | +| GPT-6.1 Sol | 35/36 | 36/36 | 4.4% lower (0.0498 vs 0.0521 USD), interval [-19.3, +24.7] spans zero | 10.6x slower (322.4 vs 30.5 s) | + +- **No measurable cost saving on this set.** Both cost intervals span zero. Read the Sol number as + a wash, not as a saving, and the Sonnet number as dearer. +- The double-check costs about 0.02 USD and 9 to 12 s per `DONE` reply. It caught 2 wrong `DONE` + replies in each arm and rejected no correct one. It does not catch them all: 3 wrong `DONE` + replies were kept under Claude Sonnet 5.5, and 1 under GPT-6.1 Sol. +- The output cap keeps a runaway response bounded. Both arms had 0 timeouts in these runs. +- Three runs per task on one task set: most differences between plugin versions are within noise. + +Treat these as one machine's dogfooding numbers, not a benchmark. + +## Fully local instead + +To run the whole session on the local model, with no cloud main model and no plugin, select +**MAI Code 1.1 Flash** in `/model` and choose **Select this model**. + +## Good for and not for + +Good for: reading or summarizing big logs and many files, search and extraction, test and log +triage, and the same mechanical edit across files. + +Not for: multi-step coding, hard or long tasks, design decisions, security review, or anything +where a confident wrong answer is costly. + +## Limits + +- **Experimental.** Treat results as drafts and review the files the agent names. +- **Check the results.** The local model can return a confident but wrong answer. The DONE + double-check catches most of these, not all (one wrong `DONE` was kept in 27 under GPT-6.1 Sol). +- **Slower than cloud only.** Expect minutes, not seconds, per offloaded task. The first offload + in a session waits for the local model to load. +- **Output cap is opt-in.** Without `Set-LocalOutputCap.ps1`, a long or looping response can run + for many minutes before the agent's own loop check stops it. +- **No hooks in this listing.** The plugin has no session-start hook, so type the routing line + yourself, and it does not undo a failed offload's partial edits for you. The agent text tells the + main session to undo only the worker's files with `git status` before it does the task itself. + Changes outside the git work tree are not covered. +- **One surface at a time.** Do not use the local model from the CLI, the Copilot app, and VS Code + at the same time. Each loads its own copy into memory. + +## Related + +- [Delegating focused tasks to a specific model](https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/create-custom-agents-for-cli#delegating-focused-tasks-to-a-specific-model) + describes the custom-agent mechanism these agents use. +- **Copilot Fusion** with **Fusion Standard** in `/model` picks MAI Code 1.1 Flash automatically + for suitable tasks. This plugin is the explicit, per-task alternative. + +## Source + +This plugin is part of [Awesome Copilot](https://github.com/github/awesome-copilot). + +## License + +MIT diff --git a/plugins/local-coding/plugin.json b/plugins/local-coding/plugin.json new file mode 100644 index 0000000000..d741719fcd --- /dev/null +++ b/plugins/local-coding/plugin.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "name": "local-coding", + "description": "Experimental. Hand large, mechanical sub-tasks (big reads, log and test triage, and the same edit across many files) from a cloud Copilot CLI session to the on-device MAI Code 1.1 Flash model on Windows, through an offload agent (the main session checks every DONE reply) or the on-device-delegate skill. Includes scripts to cap the local model's output tokens and to install or update the plugin.", + "version": "0.3.2", + "keywords": [ + "on-device", + "local-models", + "offload", + "mai-code", + "llama-cpp", + "copilot-cli", + "windows" + ], + "author": { + "name": "Chris Dern (Microsoft)", + "url": "https://github.com/chrisdMSFT" + }, + "repository": "https://github.com/github/awesome-copilot", + "license": "MIT", + "extensions": { + "com.github.awesome-copilot": { + "agents": [ + "./agents/on-device-offload.md", + "./agents/on-device-worker.md" + ], + "skills": [ + "./skills/on-device-delegate/" + ] + } + } +} diff --git a/skills/on-device-delegate/SKILL.md b/skills/on-device-delegate/SKILL.md new file mode 100644 index 0000000000..7bc8123a63 --- /dev/null +++ b/skills/on-device-delegate/SKILL.md @@ -0,0 +1,84 @@ +--- +name: on-device-delegate +description: 'Saves cloud-model context by briefing one bounded, read-heavy sub-task (more than about 25 KB of prose, or many files that no single command can answer) to the local-coding plugin''s on-device worker, which runs on the local MAI Code 1.1 Flash model. Use for log and test triage, summarizing many files, or extracting facts from large inputs. When the user names this skill, call the skill tool in the same response as your first other tool call, never as a separate step.' +compatibility: 'GitHub Copilot CLI on Windows with experimental features on, MAI Code 1.1 Flash installed from /model, and the local-coding plugin installed (it provides the on-device worker agent).' +--- + +# On-Device Delegate + +Send the bulky reading in a task to the on-device worker, then check its answer +once. The main session keeps planning and answering. The worker reads the big +inputs on the local model, so they never enter the main context. + +## Steps + +1. **Gate on what you would read.** If one command or a short script can + compute the answer, do it yourself now: no plan, no notes about this + skill. Delegate only when you would otherwise READ more than about 25 KB + of text into your context (prose, many files). Never delegate security + review or design decisions. +2. **Check the worker.** The worker is the custom agent + `local-coding:on-device-worker` (the plugin id). If the task tool lists it + as plain `on-device-worker` instead (copied into an agents folder), use + that. If neither is available, work normally. +3. **Brief.** Do not read the big input first. Make ONE task call: + - `name`: "on-device-" + - `description`: 3-5 words + - `agent_type`: the worker id from step 2 + - `mode`: "sync" + - `model`: the on-device model id named in the session's routing line, + exactly (if there is none: + "winml-azureml-mai-code-1.1-flash-on-device/MAI-Code-1.1-Flash-On-Device"). + The worker does not pin a model, so without this it runs on your own + model. + - `prompt`, exactly: + + ```text + GOAL: + INPUTS: + RULES: Read only INPUTS. At most 8 tool calls. Edit nothing but OUTPUT. No questions. + OUTPUT: + ANSWER: Reply ONCE with only , then stop. + If you cannot, reply only: UNABLE: + ``` + +4. **Check once.** At most ONE cheap check (format, a count, or one spot + check). Never recompute the answer or re-read the input. If the answer is + wrong, UNABLE, or the task call fails: do it yourself. Then finish. + +## Example + +User: "Use the on-device-delegate skill. From the logs under `logs\`, list +every distinct error message with its count, most frequent first." + +Task prompt: + +```text +GOAL: Count each distinct error message in the logs. +INPUTS: logs\ +RULES: Read only INPUTS. At most 8 tool calls. Edit nothing but OUTPUT. No questions. +OUTPUT: reply only +ANSWER: Reply ONCE with only lines of the form " ", most frequent first, then stop. +If you cannot, reply only: UNABLE: +``` + +Check: confirm the lines match the format and spot-check one count with a +single search. Then answer the user. + +## Limits + +- The worker can return a confident but wrong answer. The single check + catches format errors, not every wrong fact. Tell the user what you checked. +- Expect minutes, not seconds, per delegated task, especially on the first + call while the local model loads. + +## Setup scripts + +[`scripts/`](scripts/) holds two setup scripts for the user, described in the +local-coding plugin README. They are not part of a task: never run them while +you work on the user's request. + +- [`scripts/Set-LocalOutputCap.ps1`](scripts/Set-LocalOutputCap.ps1) caps the + local model's output tokens in the user's `providers.json`. +- [`scripts/Update-LocalCodingPlugin.ps1`](scripts/Update-LocalCodingPlugin.ps1) + installs or updates the plugin, then runs the cap script. diff --git a/skills/on-device-delegate/scripts/Set-LocalOutputCap.ps1 b/skills/on-device-delegate/scripts/Set-LocalOutputCap.ps1 new file mode 100644 index 0000000000..cbe25b795c --- /dev/null +++ b/skills/on-device-delegate/scripts/Set-LocalOutputCap.ps1 @@ -0,0 +1,82 @@ +<# +.SYNOPSIS + Cap the on-device model's output tokens in your Copilot CLI providers.json (opt-in and reversible). + +.DESCRIPTION + Copilot CLI 1.0.105 sends max_tokens equal to the model row's maxOutputTokens on every request to an on-device + model. "Download and set up" writes 128000 for MAI Code 1.1 Flash, so a looping generation can run for 25-50 + minutes. With a lower cap the server stops the generation at the cap; the CLI then asks the model to continue + at most twice and hands the result back, so the offload agent reports a non-DONE reply and the main session does + the task itself after a few minutes instead of waiting for the loop. + + The script changes only the one model row you name (maxOutputTokens and capabilities.limits.max_output_tokens), + writes a backup next to the file first, and leaves every other row and key alone. The CLI reads providers.json + when a session starts, so restart open sessions. Running "Download and set up" for the model again rewrites the + row with the default. Run this script again afterwards. + +.PARAMETER MaxOutputTokens + The cap. Default 8192: our measured runs of MAI Code 1.1 Flash never produced a useful reply longer than about + 3,300 tokens, and an 8,192 cap had no runaway timeouts. + +.PARAMETER Model + The provider-qualified model id, as /model shows it. Default: the CLI-managed MAI Code 1.1 Flash id. + +.PARAMETER ProvidersJson + The providers.json to change. Default: providers.json in COPILOT_HOME, else in %USERPROFILE%\.copilot. + +.PARAMETER Restore + Put back the value saved by the last run of this script (from the backup file). + +.EXAMPLE + pwsh -File Set-LocalOutputCap.ps1 -WhatIf + pwsh -File Set-LocalOutputCap.ps1 + pwsh -File Set-LocalOutputCap.ps1 -Restore +#> +[CmdletBinding(SupportsShouldProcess)] +param( + [ValidateRange(1024, 262144)][int]$MaxOutputTokens = 8192, + [string]$Model = 'winml-azureml-mai-code-1.1-flash-on-device/MAI-Code-1.1-Flash-On-Device', + [string]$ProvidersJson = '', + [switch]$Restore +) +$ErrorActionPreference = 'Stop' +if (-not $ProvidersJson) { + $homeDir = if ($env:COPILOT_HOME) { $env:COPILOT_HOME } else { Join-Path $HOME '.copilot' } + $ProvidersJson = Join-Path $homeDir 'providers.json' +} +if (-not (Test-Path -LiteralPath $ProvidersJson)) { throw "No providers.json at $ProvidersJson. Install the model from /model first." } +$slash = $Model.IndexOf('/') +if ($slash -lt 1) { throw "-Model must be provider/id, for example $Model" } +$provider = $Model.Substring(0, $slash); $id = $Model.Substring($slash + 1) +$backup = "$ProvidersJson.local-coding.bak" + +$doc = [IO.File]::ReadAllText($ProvidersJson) | ConvertFrom-Json +$rows = @($doc.models | Where-Object { $_.provider -eq $provider -and $_.id -eq $id }) +if ($rows.Count -ne 1) { throw "Expected one model row for $Model in $ProvidersJson; found $($rows.Count)." } +$row = $rows[0] +$old = $row.maxOutputTokens + +if ($Restore) { + if (-not (Test-Path -LiteralPath $backup)) { throw "No backup at $backup." } + $bdoc = [IO.File]::ReadAllText($backup) | ConvertFrom-Json + $brow = @($bdoc.models | Where-Object { $_.provider -eq $provider -and $_.id -eq $id }) + if ($brow.Count -ne 1) { throw "The backup has no single row for $Model." } + $MaxOutputTokens = [int]$brow[0].maxOutputTokens +} + +if ($old -eq $MaxOutputTokens -and $row.capabilities.limits.max_output_tokens -eq $MaxOutputTokens) { + Write-Host "$Model already has maxOutputTokens $MaxOutputTokens in $ProvidersJson; nothing to do." + return +} +if ($PSCmdlet.ShouldProcess($ProvidersJson, "set $Model maxOutputTokens $old -> $MaxOutputTokens")) { + if (-not $Restore) { Copy-Item -LiteralPath $ProvidersJson -Destination $backup -Force } + $row | Add-Member -Force -NotePropertyName maxOutputTokens -NotePropertyValue $MaxOutputTokens + if ($row.capabilities -and $row.capabilities.limits) { + $row.capabilities.limits | Add-Member -Force -NotePropertyName max_output_tokens -NotePropertyValue $MaxOutputTokens + } + $tmp = "$ProvidersJson.tmp" + [IO.File]::WriteAllText($tmp, ($doc | ConvertTo-Json -Depth 32), (New-Object System.Text.UTF8Encoding($false))) + $null = [IO.File]::ReadAllText($tmp) | ConvertFrom-Json + Move-Item -LiteralPath $tmp -Destination $ProvidersJson -Force + Write-Host "$Model maxOutputTokens $old -> $MaxOutputTokens in $ProvidersJson$(if (-not $Restore) { " (backup: $backup)" }). Restart open Copilot CLI sessions." +} diff --git a/skills/on-device-delegate/scripts/Update-LocalCodingPlugin.ps1 b/skills/on-device-delegate/scripts/Update-LocalCodingPlugin.ps1 new file mode 100644 index 0000000000..45a0f9566c --- /dev/null +++ b/skills/on-device-delegate/scripts/Update-LocalCodingPlugin.ps1 @@ -0,0 +1,292 @@ +<# +.SYNOPSIS + Install or update the local-coding plugin, then cap the on-device model's output tokens. One shot, and safe to re-run. + +.DESCRIPTION + 1. Installs or updates the local-coding plugin in your Copilot home (%USERPROFILE%\.copilot, or COPILOT_HOME). + By default it uses the awesome-copilot marketplace (github/awesome-copilot, included in Copilot CLI by default). + It adds the marketplace if it is missing, refreshes its catalog, then installs the plugin or updates it. If that + fails, or the marketplace has an older version than the local copy, it installs the local copy of the plugin + folder instead: -PluginPath, or the installed plugin folder this script sits in. It removes the other copy + (marketplace or direct) so the agents do not load twice. + 2. Checks that "copilot plugin list" shows the expected version, and enables the plugin if it is disabled. + 3. Runs Set-LocalOutputCap.ps1 from the installed plugin, which backs up providers.json and caps the CLI-managed + MAI Code 1.1 Flash row at 8,192 output tokens. If providers.json has no such row, it says why and skips. + 4. Prints the next steps. + +.PARAMETER Source + Auto (default): marketplace, then the local folder if that fails. Marketplace: marketplace only. + Local: the local folder only. + +.PARAMETER PluginPath + A local copy of the local-coding plugin folder (the one with plugin.json). Default: the plugin folder this script + sits in (the script is at skills\on-device-delegate\scripts\ inside the plugin). + +.PARAMETER Marketplace + The marketplace repository. Default github/awesome-copilot (marketplace name awesome-copilot). + +.PARAMETER ExpectedVersion + The version "copilot plugin list" must show (or newer). Default: the version in the plugin.json next to this script, + or in -PluginPath. + +.PARAMETER CopilotHome + The Copilot home to update. Default: COPILOT_HOME, else %USERPROFILE%\.copilot. + +.PARAMETER CopilotExe + The Copilot CLI to run. Default: copilot on PATH. + +.PARAMETER MaxOutputTokens + Passed to Set-LocalOutputCap.ps1. Default 8192. + +.PARAMETER Model + Passed to Set-LocalOutputCap.ps1: the provider-qualified model id. Default: the CLI-managed MAI Code 1.1 Flash id. + +.PARAMETER Restore + Only undo the output cap (Set-LocalOutputCap.ps1 -Restore); the plugin is left as it is. + +.PARAMETER SkipCap + Install or update the plugin only; leave providers.json alone. + +.PARAMETER Help + Show this help. + +.EXAMPLE + powershell -ExecutionPolicy Bypass -File .\local-coding\scripts\Update-LocalCodingPlugin.ps1 -WhatIf + powershell -ExecutionPolicy Bypass -File .\local-coding\scripts\Update-LocalCodingPlugin.ps1 + powershell -ExecutionPolicy Bypass -File .\local-coding\scripts\Update-LocalCodingPlugin.ps1 -Source Local + powershell -ExecutionPolicy Bypass -File .\local-coding\scripts\Update-LocalCodingPlugin.ps1 -Restore +#> +[CmdletBinding(SupportsShouldProcess)] +param( + [ValidateSet('Auto', 'Marketplace', 'Local')][string]$Source = 'Auto', + [string]$PluginPath = '', + [string]$Marketplace = 'github/awesome-copilot', + [string]$ExpectedVersion = '', + [string]$CopilotHome = '', + [string]$CopilotExe = 'copilot', + [ValidateRange(1024, 262144)][int]$MaxOutputTokens = 8192, + [string]$Model = 'winml-azureml-mai-code-1.1-flash-on-device/MAI-Code-1.1-Flash-On-Device', + [switch]$Restore, + [switch]$SkipCap, + [switch]$Help +) +$ErrorActionPreference = 'Stop' +if ($Help) { Get-Help -Detailed $PSCommandPath; return } + +$PluginName = 'local-coding' +$MarketplaceName = ($Marketplace -split '/')[-1] +$MarketplaceId = "$PluginName@$MarketplaceName" + +function Write-Step([string]$Text) { Write-Host ''; Write-Host "== $Text" } +function Write-Note([string]$Text) { Write-Host " $Text" } + +function Read-PluginManifest([string]$Folder) { + if (-not $Folder) { return $null } + $manifest = Join-Path $Folder 'plugin.json' + if (-not (Test-Path -LiteralPath $manifest)) { return $null } + try { $json = [IO.File]::ReadAllText($manifest) | ConvertFrom-Json } catch { return $null } + if ($json.name -ne $PluginName) { return $null } + return $json +} + +function Invoke-Copilot([string[]]$Arguments) { + # Native stderr must not become a terminating error under Windows PowerShell 5.1. + $saved = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + try { + $lines = & $script:CopilotPath @Arguments 2>&1 | ForEach-Object { + if ($_ -is [System.Management.Automation.ErrorRecord]) { $_.Exception.Message } else { "$_" } + } + $code = $LASTEXITCODE + } finally { $ErrorActionPreference = $saved } + return [pscustomobject]@{ Code = $code; Output = @($lines) } +} + +function Get-InstalledCopies { + $result = Invoke-Copilot @('plugin', 'list') + $copies = @() + foreach ($line in $result.Output) { + if ($line -match "\s($([regex]::Escape($PluginName))(@[^\s(]+)?)\s+\(v([0-9][0-9A-Za-z.\-]*)\)(.*)$") { + $copies += [pscustomobject]@{ Id = $Matches[1]; Version = $Matches[3]; Disabled = ($Matches[4] -match 'disabled') } + } + } + return $copies +} + +function Test-VersionAtLeast([string]$Have, [string]$Want) { + if (-not $Want) { return $true } + $h = $null; $w = $null + if ([version]::TryParse(($Have -replace '-.*$', ''), [ref]$h) -and [version]::TryParse(($Want -replace '-.*$', ''), [ref]$w)) { + return $h -ge $w + } + return $Have -eq $Want +} + +# --- Resolve the Copilot home, the local plugin copy and the expected version --- +if (-not $CopilotHome) { $CopilotHome = if ($env:COPILOT_HOME) { $env:COPILOT_HOME } else { Join-Path $HOME '.copilot' } } +$CopilotHome = [IO.Path]::GetFullPath($CopilotHome) +# The script sits at \skills\on-device-delegate\scripts\, so the plugin folder is three levels up. +$scriptPlugin = $PSScriptRoot +foreach ($n in 1..3) { $scriptPlugin = Split-Path -Parent $scriptPlugin; if (Read-PluginManifest $scriptPlugin) { break } } +if (-not $PluginPath -and (Read-PluginManifest $scriptPlugin)) { $PluginPath = $scriptPlugin } +if ($PluginPath) { + $PluginPath = [IO.Path]::GetFullPath($PluginPath) + if (-not (Read-PluginManifest $PluginPath)) { throw "-PluginPath $PluginPath is not a local-coding plugin folder (no plugin.json named $PluginName)." } +} +if (-not $ExpectedVersion) { + $m = Read-PluginManifest $PluginPath + if ($m) { $ExpectedVersion = [string]$m.version } +} +if ($Source -eq 'Local' -and -not $PluginPath) { throw '-Source Local needs -PluginPath (or run the copy of this script inside the plugin folder).' } + +Write-Host "local-coding plugin setup" +Write-Note "Copilot home: $CopilotHome" +Write-Note "Source: $Source$(if ($PluginPath) { " (local copy: $PluginPath)" })" +Write-Note "Expected version: $(if ($ExpectedVersion) { "$ExpectedVersion or newer" } else { 'any' })" +if ($WhatIfPreference) { Write-Note 'WhatIf: nothing is changed.' } + +$savedHome = $env:COPILOT_HOME; $savedPrompt = $env:GIT_TERMINAL_PROMPT; $savedGcm = $env:GCM_INTERACTIVE +$env:COPILOT_HOME = $CopilotHome +# Fail fast instead of waiting on a credential prompt when the marketplace repository is not reachable. +$env:GIT_TERMINAL_PROMPT = '0'; $env:GCM_INTERACTIVE = 'never' +$installedOk = $false +try { + # --- 1. Install or update the plugin --- + $cmd = Get-Command $CopilotExe -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 + $script:CopilotPath = if ($cmd) { $cmd.Source } else { $null } + + if ($Restore) { + Write-Step 'Plugin: left as it is (-Restore only undoes the output cap)' + } elseif (-not $script:CopilotPath) { + Write-Step 'Plugin: Copilot CLI not found' + Write-Note "'$CopilotExe' is not on PATH. Install the CLI (winget install --id GitHub.Copilot) and run this again." + } else { + Write-Step "Plugin: install or update ($script:CopilotPath)" + $before = @(Get-InstalledCopies) + foreach ($c in $before) { Write-Note "Installed now: $($c.Id) v$($c.Version)$(if ($c.Disabled) { ' [disabled]' })" } + if ($before.Count -eq 0) { Write-Note 'Installed now: none' } + + $installedFrom = $null + if ($Source -ne 'Local') { + if ($PSCmdlet.ShouldProcess($MarketplaceId, 'install or update from the marketplace')) { + $mkts = Invoke-Copilot @('plugin', 'marketplace', 'list') + $haveMkt = @($mkts.Output | Where-Object { $_ -match "\s$([regex]::Escape($MarketplaceName))\s" }).Count -gt 0 + $ok = $true + if (-not $haveMkt) { + $r = Invoke-Copilot @('plugin', 'marketplace', 'add', $Marketplace) + $r.Output | Where-Object { $_ } | ForEach-Object { Write-Note $_ } + $ok = ($r.Code -eq 0) + } else { + $r = Invoke-Copilot @('plugin', 'marketplace', 'update', $MarketplaceName) + $ok = ($r.Code -eq 0) + if (-not $ok) { $r.Output | Where-Object { $_ } | ForEach-Object { Write-Note $_ } } + } + if ($ok) { + $verb = if (@($before | Where-Object { $_.Id -eq $MarketplaceId }).Count) { 'update' } else { 'install' } + $r = Invoke-Copilot @('plugin', $verb, $MarketplaceId) + $r.Output | Where-Object { $_ } | ForEach-Object { Write-Note $_ } + $ok = ($r.Code -eq 0) + } + if (-not $ok) { + if ($Source -eq 'Auto' -and $PluginPath) { Write-Note 'The marketplace did not work; installing from the local copy instead.' } + elseif ($Source -eq 'Auto') { Write-Note 'The marketplace did not work and there is no local copy (-PluginPath).' } + } elseif ($Source -eq 'Auto' -and $PluginPath) { + $mcopy = @(Get-InstalledCopies | Where-Object { $_.Id -eq $MarketplaceId }) + if ($mcopy.Count -eq 1 -and -not (Test-VersionAtLeast $mcopy[0].Version $ExpectedVersion)) { + Write-Note "The marketplace has v$($mcopy[0].Version), older than the local copy ($ExpectedVersion); installing the local copy instead." + $ok = $false + } + } + if ($ok) { $installedFrom = 'Marketplace' } + } else { + $installedFrom = 'Marketplace' + } + } + if (-not $installedFrom -and $Source -ne 'Marketplace' -and $PluginPath) { + if ($PSCmdlet.ShouldProcess($PluginPath, "install $PluginName from the local folder")) { + $r = Invoke-Copilot @('plugin', 'install', $PluginPath) + $r.Output | Where-Object { $_ -and $_ -notmatch 'deprecated' } | ForEach-Object { Write-Note $_ } + if ($r.Code -eq 0) { $installedFrom = 'Local' } + } else { + $installedFrom = 'Local' + } + } + + if (-not $installedFrom) { + Write-Note 'Install or update FAILED; see the messages above.' + } elseif (-not $WhatIfPreference) { + # Keep one copy: a direct and a marketplace copy would both load their agents and skill. + $other = if ($installedFrom -eq 'Marketplace') { $PluginName } else { $MarketplaceId } + if (@(Get-InstalledCopies | Where-Object { $_.Id -eq $other }).Count) { + $r = Invoke-Copilot @('plugin', 'uninstall', $other) + Write-Note "Removed the other copy ($other): exit $($r.Code)" + } + # --- 2. Verify --- + $after = @(Get-InstalledCopies | Where-Object { $_.Id -eq $(if ($installedFrom -eq 'Marketplace') { $MarketplaceId } else { $PluginName }) }) + if ($after.Count -eq 1) { + $copy = $after[0] + if ($copy.Disabled) { + $r = Invoke-Copilot @('plugin', 'enable', $copy.Id) + Write-Note "Enabled $($copy.Id): exit $($r.Code)" + } + if (Test-VersionAtLeast $copy.Version $ExpectedVersion) { + Write-Note "OK: copilot plugin list shows $($copy.Id) v$($copy.Version)." + $installedOk = $true + } else { + Write-Note "WARNING: copilot plugin list shows $($copy.Id) v$($copy.Version); expected $ExpectedVersion or newer." + if ($installedFrom -eq 'Marketplace') { Write-Note 'The marketplace may not have the new version yet; try -Source Local with a fresh copy.' } + } + } else { + Write-Note "WARNING: copilot plugin list does not show $PluginName after the install." + } + } + } + + # --- 3. Output cap --- + if ($SkipCap) { + Write-Step 'Output cap: skipped (-SkipCap)' + } else { + Write-Step "Output cap: $(if ($Restore) { 'restore' } else { "$MaxOutputTokens tokens" })" + $root = Join-Path $CopilotHome 'installed-plugins' + $cap = $null + if (Test-Path -LiteralPath $root) { + $cap = Get-ChildItem -LiteralPath $root -Recurse -Filter 'Set-LocalOutputCap.ps1' -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -match "[\\/]$([regex]::Escape($PluginName))[\\/]skills[\\/]" } | + Sort-Object LastWriteTime -Descending | Select-Object -First 1 + } + $capPath = if ($cap) { $cap.FullName } elseif (Test-Path -LiteralPath (Join-Path $PSScriptRoot 'Set-LocalOutputCap.ps1')) { Join-Path $PSScriptRoot 'Set-LocalOutputCap.ps1' } else { $null } + $providers = if ($env:COPILOT_PROVIDERS_CONFIG) { $env:COPILOT_PROVIDERS_CONFIG } else { Join-Path $CopilotHome 'providers.json' } + $slash = $Model.IndexOf('/') + $rowCount = 0 + if ((Test-Path -LiteralPath $providers) -and $slash -gt 0) { + $doc = [IO.File]::ReadAllText($providers) | ConvertFrom-Json + $p = $Model.Substring(0, $slash); $i = $Model.Substring($slash + 1) + $rowCount = @($doc.models | Where-Object { $_.provider -eq $p -and $_.id -eq $i }).Count + } + if (-not $capPath) { + Write-Note 'Set-LocalOutputCap.ps1 not found in installed-plugins or next to this script; skipped.' + } elseif (-not (Test-Path -LiteralPath $providers)) { + Write-Note "No providers.json at $providers; skipped." + Write-Note 'The CLI writes it when you choose "Download and set up" for MAI Code 1.1 Flash in /model.' + } elseif ($rowCount -ne 1) { + Write-Note "providers.json has $rowCount rows for $Model; skipped." + Write-Note 'Set up MAI Code 1.1 Flash through the CLI /model first, or pass -Model with the id /model shows.' + } else { + Write-Note "Using $capPath" + $capArgs = @{ ProvidersJson = $providers; Model = $Model; MaxOutputTokens = $MaxOutputTokens } + if ($Restore) { $capArgs.Restore = $true } + try { & $capPath @capArgs -WhatIf:$WhatIfPreference } catch { Write-Note "Set-LocalOutputCap.ps1: $($_.Exception.Message)" } + } + } +} finally { + $env:COPILOT_HOME = $savedHome; $env:GIT_TERMINAL_PROMPT = $savedPrompt; $env:GCM_INTERACTIVE = $savedGcm +} + +# --- 4. Next steps --- +Write-Step 'Next steps' +Write-Note '1. Restart open Copilot CLI sessions so they load the plugin.' +Write-Note '2. Run copilot --experimental, keep a cloud main model, and type your task. Start the prompt with the routing' +Write-Note ' line from the plugin README, and the main session sends big, mechanical sub-tasks to the' +Write-Note ' local-coding:on-device-offload agent.' +Write-Note 'Check: copilot plugin list, and in a session /agent lists local-coding:on-device-offload. Undo the cap: -Restore.' +if (-not $Restore -and -not $WhatIfPreference -and $script:CopilotPath -and -not $installedOk) { exit 1 }