Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **Experimental:** `copilot-app` target deploys prompts (with optional workflow frontmatter) directly into the GitHub Copilot desktop App's `~/.copilot/data.db` workflows table. Gated behind `apm experimental enable copilot-app`; works in both project scope (`apm install --target copilot-app` from a project's `apm.yml`) and user scope (`--global`). Workflows always install disabled (`enabled = 0`); user opts in from the App. No new CLI surface — `apm install / update / uninstall / list` cover the lifecycle. See [Copilot App integration](https://microsoft.github.io/apm/integrations/copilot-app/).

### Changed

- **Experimental (`copilot-app`):** workflow prompts now use flat top-level frontmatter keys. Only `interval`, `schedule_hour`, `schedule_day` mark a `.prompt.md` as a workflow (dispatched to the Copilot App SQLite store); `mode`, `model`, `reasoning_effort` remain optional fields on a workflow but are NOT shape markers because they overload with plain VSCode / Copilot slash-command prompts. Plain prompts (no workflow keys) continue to deploy to slash-command targets only. `interval` defaults to `manual` when omitted. The redesign collapses what looked like two primitives (prompt vs scheduled prompt) into one shape-dispatched `.prompt.md`. Breaking only for users of the unreleased experimental target.

### Fixed

- **Experimental (`copilot-app`):** workflow-shape `.prompt.md` files no longer leak to slash-command targets. Previously a single scheduled prompt would deploy to the Copilot App DB row AND to `.claude/commands/`, `.cursor/commands/`, `.copilot/prompts/`, and `.gemini/commands/`. Now each prompt belongs to exactly one surface based on frontmatter shape.
- **Experimental (`copilot-app`):** pointing a plain `.prompt.md` (no workflow frontmatter) at `--target copilot-app` is now a hard error with an actionable diagnostic telling the author to add `interval: manual` or unset the target, rather than silently skipping.
- `apm install` honors the SSH user portion of dependency URLs (`ssh://user@host/...` and scp shorthand `user@host:org/repo`) instead of hardcoding `git@`; unblocks EMU accounts and other non-`git` SSH identities. User values are validated against a strict allowlist before composing the clone URL. (#1385, closes #1383)

## [0.14.0] - 2026-05-18
Expand Down
1 change: 1 addition & 0 deletions docs/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ export default defineConfig({
{ label: 'CI/CD pipelines', slug: 'integrations/ci-cd' },
{ label: 'GitHub Agentic Workflows', slug: 'integrations/gh-aw' },
{ label: 'Microsoft 365 Copilot Cowork (Experimental)', slug: 'integrations/copilot-cowork' },
{ label: 'GitHub Copilot App workflows (Experimental)', slug: 'integrations/copilot-app' },
{ label: 'AI runtime compatibility', slug: 'integrations/runtime-compatibility' },
{ label: 'GitHub rulesets', slug: 'integrations/github-rulesets' },
],
Expand Down
114 changes: 114 additions & 0 deletions docs/src/content/docs/integrations/copilot-app.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
---
title: "GitHub Copilot App workflows (Experimental)"
description: "Deploy APM prompts with schedule frontmatter as Copilot App workflows backed by the desktop SQLite store."
sidebar:
order: 6
---

See the [Targets matrix](../../reference/targets-matrix/) for where `copilot-app` fits alongside the other deploy targets.

:::caution[Frontier preview]
This integration is experimental and off by default. You must enable the `copilot-app` flag before using it.

```bash
apm experimental enable copilot-app
```

See the [Experimental flags reference](../../reference/experimental/) for the full `apm experimental` subcommand surface (enable / disable / list).

Until the flag is enabled, the `copilot-app` target stays inert: it is hidden from auto-detection, and explicit `--target copilot-app` installs fail cleanly with the enable hint instead of touching the App's database.
:::

## What it does

When `copilot-app` is enabled and a package ships a prompt with workflow frontmatter (any of `interval`, `schedule_hour`, `schedule_day` at the top level), `apm install --target copilot-app` inserts the prompt as a row in the GitHub Copilot desktop App's SQLite store at `~/.copilot/data.db`. Add `--global` to install from a user-scope `~/.apm/apm.yml`, or omit it to install from a project's `apm.yml` (typical for team-shared scheduled prompts). The App reads new rows on next launch (or refresh) and lists them under Workflows.

Prompts that do not carry workflow frontmatter are plain slash commands: they deploy to file-based targets (`copilot`, `vscode`, `claude`, ...) and APM hard-errors with an actionable diagnostic if you point them at `copilot-app` directly. A single `.prompt.md` belongs to exactly ONE surface — whichever its frontmatter shape selects.

## Why a new target

The `copilot` target writes prompts as files (`.github/prompts/<name>.prompt.md`) for Copilot in IDEs. The desktop App stores workflows in a SQLite database, not on disk. They are different surfaces; `copilot-app` exists so that one APM install can serve both without leakage.

## Authoring a workflow prompt

:::note[Shape predicate]
Only `interval`, `schedule_hour`, and `schedule_day` at the top level mark a `.prompt.md` as a workflow. Setting `mode:`, `model:`, or `reasoning_effort:` alone keeps it a plain VSCode-style prompt (deploys to `copilot`, `claude`, etc.) -- those keys are accepted on workflows but never trigger workflow routing on their own.
:::

Add workflow frontmatter (flat top-level keys) to any `.prompt.md` file in your package's `.apm/prompts/` folder:

```markdown
---
name: Daily Digest
interval: daily # one of: manual, hourly, daily, weekly
schedule_hour: 9 # 0-23, UTC; ignored for manual / hourly
schedule_day: 1 # 0-6 (weekly only)
mode: interactive # one of: interactive, plan
model: claude-opus-4.7 # optional
reasoning_effort: high # optional
---

Summarise yesterday's commits across all open PRs ...
```

Manual-only workflows omit `schedule_hour` / `schedule_day` and set
`interval: manual` (the default when any other execution-shape key is
present). The Copilot App provides a "run now" affordance for every
workflow, so manual-only is a useful shape — no schedule, just a
named, parameterised prompt the user can fire from the App UI.

The Copilot App also defines an `autopilot` mode, but APM intentionally
does NOT accept it via this target. Until package signing ships, a
third-party package could declare `mode: autopilot` and have the App
auto-run the prompt the moment you flip the in-App enable toggle.
Refusing autopilot at the writer is the secure-by-default behaviour;
you can still set autopilot yourself on a per-row basis from the App
UI after install.

## Lifecycle

| `apm` action | Effect on `~/.copilot/data.db` |
|---|---|
| `apm install` | INSERT row with `enabled = 0` (always disabled on install — you opt in). |
| `apm install` (already installed, content unchanged) | UPDATE display fields only. `enabled`, `last_run_at`, `next_run_at` are preserved. |
| `apm install` (already installed, any execution-affecting field changed) | UPDATE row; reset `enabled = 0`; clear `next_run_at`. |
| `apm uninstall` | DELETE only APM-namespaced rows (`apm--<owner>--<pkg>--<prompt>`). User-authored rows are never touched. |

Execution-affecting fields are the prompt body, schedule (`interval` / `schedule_hour` / `schedule_day`), `mode`, `model`, and `reasoning_effort`. The reset is by design: you opted in to a specific prompt, so any change to what runs or when is a new consent surface.

Removing the source `.prompt.md` from a package and re-syncing drops the lockfile entry but does NOT delete the corresponding row from `~/.copilot/data.db` -- it merely orphans it. Run `apm uninstall <pkg>` to remove the row.

## Enable and check

Use `apm experimental enable copilot-app` to turn the target on, `apm experimental list` to see all flags, and `apm experimental disable copilot-app` to turn it off again. See the [Experimental flags reference](../../reference/experimental/) for the complete subcommand surface.

## Database resolution

| Order | Source |
|---|---|
| 1 | `APM_COPILOT_APP_DB` environment variable (absolute path; used as-is). |
| 2 | `~/.copilot/data.db` if it exists. |

If neither resolves, the install fails with `[x] GitHub Copilot desktop App not detected. Expected ~/.copilot/data.db ...` and the command exits 1.

## "Auth" model

There is none. The DB file is local; access is governed by your filesystem permissions. APM never sends credentials or syncs the DB anywhere. Treat the DB as user-scope state.

## Schema compatibility

APM guards writes with `PRAGMA user_version` and accepts the closed range `[13, 13]` today. If the App ships a newer schema, APM refuses to write and asks you to update APM rather than risk corruption.

## Concurrency

The Copilot App owns the DB and keeps it in WAL mode while running. APM coexists with the App's writer connection by issuing `BEGIN IMMEDIATE` with a bounded retry; if a lock cannot be acquired after the retry window, the install fails with a `[!]` warning noting that the Copilot App DB stayed locked and asking you to close the GitHub Copilot app momentarily and retry.

## Lockfile entries

Deployed rows are tracked in the project / user lockfile under the `copilot-app-db://workflows/<namespaced-id>` URI scheme. Standard sync semantics apply: lockfile drift triggers redeploy; removal from lockfile triggers row delete on next install.

## Out of scope (today)

- Package signing (would unlock additional trust-gated capabilities such as `mode: autopilot`).
- Scheduled-execution-on-install (deliberately not implemented — first-run is always manual).
- `gh-aw` outer-loop target (separate roadmap).
1 change: 1 addition & 0 deletions docs/src/content/docs/reference/experimental.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ apm experimental reset verbose-version
|-----------------------|----------------------------------------------------------------------------------|
| `verbose-version` | Show Python version, platform, and install path in `apm --version`. |
| `copilot-cowork` | Deploy APM skills to Microsoft 365 Copilot Cowork via OneDrive. |
| `copilot-app` | Deploy APM prompts that carry workflow frontmatter (any of `interval`, `schedule_hour`, `schedule_day`) as workflows in the GitHub Copilot desktop App (`~/.copilot/data.db`). See [Copilot App integration](../integrations/copilot-app/). |

New flags are proposed via [CONTRIBUTING.md](https://github.com/microsoft/apm/blob/main/CONTRIBUTING.md#how-to-add-an-experimental-feature-flag) and graduate to default when stable. See the contributor recipe for the full lifecycle.
See also: [Cowork integration](../integrations/copilot-cowork/).
Expand Down
11 changes: 7 additions & 4 deletions docs/src/content/docs/reference/targets-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ Skills always deploy to the cross-tool `.agents/skills/` directory by
default (see [Skills convergence](#skills-convergence) below). All other
primitives land under each target's own root.

`copilot-cowork` (Microsoft 365 Copilot) is gated behind an experimental
flag and not listed above. See [Experimental](../experimental/).
`copilot-cowork` (Microsoft 365 Copilot) and `copilot-app` (GitHub
Copilot desktop App) are gated behind experimental flags and not listed
above. See [Experimental](../experimental/).

## Detection and resolution

Expand All @@ -60,8 +61,10 @@ list before `compile` or `install`.
| opencode | `.opencode/` directory |
| windsurf | `.windsurf/` directory |

`agent-skills` and `copilot-cowork` are never auto-detected. Select them
explicitly with `--target`.
`agent-skills`, `copilot-cowork`, and `copilot-app` are never
auto-detected. Select them explicitly with `--target`, or list them in
a project's `apm.yml` `targets:` field so contributors running plain
`apm install` pick them up automatically.

## copilot

Expand Down
2 changes: 2 additions & 0 deletions packages/apm-guide/.apm/skills/apm-usage/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ Set `MCP_REGISTRY_URL` (default `https://api.mcp.github.com`) to point all `apm

Use `apm experimental enable copilot-cowork` to turn on Microsoft 365 Copilot Cowork skill deployment. Once enabled, deploy skills with `apm install --target copilot-cowork --global`.

Use `apm experimental enable copilot-app` to turn on GitHub Copilot desktop App workflow deployment. Once enabled, prompts that carry workflow frontmatter -- any flat top-level key of `interval`, `schedule_hour`, `schedule_day` -- can be deployed to the App's SQLite store at `~/.copilot/data.db` with `apm install --target copilot-app` (project scope) or `--target copilot-app --global` (user scope). A `.prompt.md` belongs to exactly ONE surface: workflow-shape prompts go to the App DB, plain prompts go to slash-command targets. Rows always start `enabled = 0` -- you opt in from the App. `apm install / update / uninstall` preserve user state (`enabled`, `last_run_at`, schedule overrides). Override the database path with `APM_COPILOT_APP_DB=<abs-path>`.

### Cross-client skills (`agent-skills`)

Use `--target agent-skills` to deploy skills to `.agents/skills/` -- the cross-tool standard directory. This is useful when multiple clients (Codex, future tools) read from `.agents/skills/`. Unlike `--target all`, `agent-skills` must be requested explicitly: `apm install --target agent-skills` or `apm install --target all,agent-skills` for both. `apm compile --target agent-skills` is a no-op (skills-only target).
Expand Down
38 changes: 38 additions & 0 deletions packages/apm-guide/.apm/skills/apm-usage/package-authoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,44 @@ When installed as a Claude Code slash command, APM maps `input:` to
Claude's `arguments:` frontmatter and converts `${input:name}` to `$name`
placeholders. An `argument-hint` is auto-generated unless one is already set.

#### Optional workflow frontmatter (GitHub Copilot App, experimental)

When the `copilot_app` experimental flag is enabled and the package is
installed with `apm install --target copilot-app` (project scope) or
`apm install --target copilot-app --global` (user scope), prompts that
carry workflow frontmatter -- any flat top-level key of `interval`,
`schedule_hour`, `schedule_day` -- are deployed as rows in the desktop
App's SQLite store at `~/.copilot/data.db`. ``mode``, ``model``, and
``reasoning_effort`` are optional fields on a workflow but do NOT mark
a plain prompt as a workflow (they overload with plain VSCode / Copilot
slash-command prompts); declare ``interval: manual`` to opt a no-schedule
prompt into the App.

```yaml
---
name: "Daily Digest"
interval: daily # manual | hourly | daily | weekly
schedule_hour: 9 # 0-23 (UTC); ignored for manual / hourly
schedule_day: 1 # 0-6 (weekly only)
mode: interactive # interactive | plan
model: claude-opus-4.7 # optional
reasoning_effort: high # optional
---
```

Rows are always inserted with `enabled = 0`; the user opts in from the
App. A `.prompt.md` belongs to exactly ONE surface: workflow-frontmatter
prompts go ONLY to the App DB, plain prompts go ONLY to file-based
slash-command targets (`copilot`, `claude`, `cursor`, ...). Pointing a
plain prompt at `--target copilot-app` is a hard error with an
actionable diagnostic. `interval` is optional and defaults to `manual`
when any other execution-shape key is present, so a parameterised
prompt with no schedule still works as a manually-fired App workflow.
The App also defines an `autopilot` mode, but APM intentionally does
not accept it via this target -- a third-party package could otherwise
auto-run the moment the user enables the row. Users who want autopilot
can still set it themselves per-row from the App UI after install.

### 5. Agent (`*.agent.md`)

Agent persona and behavior definition.
Expand Down
2 changes: 1 addition & 1 deletion src/apm_cli/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -927,7 +927,7 @@ def _handle_mcp_install(
"target",
type=TargetParamType(),
default=None,
help="Target harness(es) to deploy to. Comma-separated for multiple: --target claude,cursor. Highest-priority entry in the resolution chain (--target > apm.yml targets: > auto-detect). Values: copilot, claude, cursor, opencode, codex, gemini, windsurf, agent-skills, all. 'agent-skills' deploys to .agents/skills/ (cross-client). 'all' = copilot+claude+cursor+opencode+codex+gemini+windsurf (excludes agent-skills); combine with 'agent-skills' for both. 'copilot-cowork' is also accepted when the copilot-cowork experimental flag is enabled (run 'apm experimental enable copilot-cowork'). Note: '--target all' on 'apm compile' is deprecated; use 'apm compile --all' instead.",
help="Target harness(es) to deploy to. Comma-separated for multiple: --target claude,cursor. Repeating the flag (e.g. '-t a -t b') is NOT supported -- only the last value wins; use commas. Highest-priority entry in the resolution chain (--target > apm.yml targets: > auto-detect). Values: copilot, claude, cursor, opencode, codex, gemini, windsurf, agent-skills, all. 'agent-skills' deploys to .agents/skills/ (cross-client). 'all' = copilot+claude+cursor+opencode+codex+gemini+windsurf (excludes agent-skills); combine with 'agent-skills' for both. 'copilot-cowork' is also accepted when the copilot-cowork experimental flag is enabled (run 'apm experimental enable copilot-cowork'). 'copilot-app' is also accepted when the copilot-app experimental flag is enabled (run 'apm experimental enable copilot-app'). Note: '--target all' on 'apm compile' is deprecated; use 'apm compile --all' instead.",
)
@click.option(
"--allow-insecure",
Expand Down
37 changes: 36 additions & 1 deletion src/apm_cli/commands/uninstall/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -515,7 +515,11 @@ def _sync_integrations_after_uninstall(
continue
_effective_root = _mapping.deploy_root or _target.root_dir
_deploy_dir = project_root / _effective_root / _mapping.subdir
if not _deploy_dir.exists():
# Dynamic-root targets (e.g. copilot-app) have no filesystem
# deploy dir; their managed files are URIs that the integrator
# resolves internally. Skip the dir-exists guard for them.
_is_dynamic = _target.resolved_deploy_root is not None
if not _is_dynamic and not _deploy_dir.exists():
continue
_managed_subset = None
if _buckets is not None:
Expand Down Expand Up @@ -579,6 +583,37 @@ def _sync_integrations_after_uninstall(
)
counts["skills"] = result.get("files_removed", 0)

# Scan sync_managed DIRECTLY for copilot-app-db:// entries.
# The copilot-app target is opt-in: resolve_targets() excludes it from the
# default user-scope set unless --target copilot-app was passed at install
# time and recorded on apm_package.target. Without this scan, prompts
# deployed to ~/.copilot/data.db would never be deleted on uninstall
# because the per-target loop above does not iterate copilot-app.
if sync_managed:
from ...integration.copilot_app_db import COPILOT_APP_LOCKFILE_PREFIX

_copilot_app_files = {p for p in sync_managed if p.startswith(COPILOT_APP_LOCKFILE_PREFIX)}
if _copilot_app_files:
# Find or synthesise a user-scope copilot-app TargetProfile.
from ...integration.targets import KNOWN_TARGETS

_ca_target = next(
(t for t in _resolved_targets if t.name == "copilot-app"),
None,
)
if _ca_target is None:
_ca_static = KNOWN_TARGETS.get("copilot-app")
if _ca_static is not None:
_ca_target = _ca_static.for_scope(user_scope=True)
if _ca_target is not None:
result = _integrators["prompts"].sync_for_target(
_ca_target,
apm_package,
project_root,
managed_files=_copilot_app_files,
)
counts["prompts"] += result.get("files_removed", 0)

# Hooks (multi-target sync_integration handles all targets)
result = _integrators["hooks"].sync_integration(
apm_package,
Expand Down
Loading
Loading