Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **Experimental:** `copilot-app` target deploys prompts (with optional `schedule:` frontmatter) directly into the GitHub Copilot desktop App's `~/.copilot/data.db` workflows table. Gated behind `apm experimental enable copilot-app`; user-scope only (`--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/).

### Fixed

- `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)
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
101 changes: 101 additions & 0 deletions docs/src/content/docs/integrations/copilot-app.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
---
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: 5
---

:::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
```

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 a `schedule:` frontmatter block, `apm install --target copilot-app --global` inserts the prompt as a row in the GitHub Copilot desktop App's SQLite store at `~/.copilot/data.db`. The App reads new rows on next launch (or refresh) and lists them under Workflows.

Prompts that do not carry `schedule:` are skipped silently at this target — they continue to deploy to file-based targets (`copilot`, `vscode`, `claude`, ...) without changes.

## 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 scheduled prompt

Add a `schedule:` block to any `.prompt.md` file in your package's `.apm/prompts/` folder:

```markdown
---
name: Daily Digest
schedule:
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 ...
```

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: prompt body, schedule, mode, model, or reasoning effort) | UPDATE row AND reset `enabled = 0`, clear `next_run_at`. Rationale: you opted in to a specific prompt; any change to what runs or when is a new consent surface. |
| `apm uninstall` | DELETE only APM-namespaced rows (`apm--<owner>--<pkg>--<prompt>`). User-authored rows are never touched. |

## Enable and check

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

## 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`. The current tested version is `13`. If the App ships a newer schema, APM refuses to write and asks you to update APM rather than risk corruption.

## Concurrency

APM opens the DB in WAL mode and retries briefly when the App holds a write lock. 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 (with `schedule:` frontmatter) 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
9 changes: 5 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,8 @@ 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`.

## 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 a `schedule:` frontmatter block can be deployed to the App's SQLite store at `~/.copilot/data.db` with `apm install --target copilot-app --global`. 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
28 changes: 28 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,34 @@ 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 `schedule:` block (GitHub Copilot App, experimental)

When the `copilot_app` experimental flag is enabled and the package is
installed with `apm install --target copilot-app --global`, prompts
that include a `schedule:` block in frontmatter are deployed as rows in
the desktop App's SQLite store at `~/.copilot/data.db`.

```yaml
---
name: "Daily Digest"
schedule:
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. Prompts without a `schedule:` block are skipped at the
`copilot-app` target (they still deploy normally to file-based targets).
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
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
10 changes: 10 additions & 0 deletions src/apm_cli/core/experimental.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@ class ExperimentalFlag:
"See https://microsoft.github.io/apm/integrations/copilot-cowork/"
),
),
"copilot_app": ExperimentalFlag(
name="copilot_app",
description="Deploy prompts as workflows into the GitHub Copilot desktop App.",
default=False,
hint=(
"Add 'schedule:' frontmatter to any .prompt.md and install with "
"'--target copilot-app --global'. Workflows arrive disabled; "
"enable them from the Copilot app's Workflows tab."
),
),
}


Expand Down
2 changes: 1 addition & 1 deletion src/apm_cli/core/target_detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ def get_target_description(target: UserTargetType) -> str:
#: ``is_enabled()`` in ``core/experimental.py`` and ``_flag_gated()`` in
#: ``integration/targets.py``. They are NOT included in the
#: ``parse_target_arg("all")`` expansion -- explicit opt-in only.
EXPERIMENTAL_TARGETS: frozenset[str] = frozenset({"copilot-cowork"})
EXPERIMENTAL_TARGETS: frozenset[str] = frozenset({"copilot-cowork", "copilot-app"})

#: Stable targets excluded from "all" expansion (cross-client deploy
#: locations). Unlike EXPERIMENTAL_TARGETS, these are GA -- they just do
Expand Down
46 changes: 46 additions & 0 deletions src/apm_cli/install/phases/targets.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,52 @@ def run(ctx: InstallContext) -> None:
)
raise SystemExit(1)

# ------------------------------------------------------------------
# GitHub Copilot App target gating (mirrors cowork rules above):
# explicit --target copilot-app with flag OFF must hint at the
# experimental enable command; with flag ON but no ~/.copilot/data.db
# must error with an actionable install instruction; without --global
# must error because copilot-app is user-scope only.
# ------------------------------------------------------------------
_user_asked_copilot_app = False
if _explicit:
if isinstance(_explicit, list):
_user_asked_copilot_app = "copilot-app" in _explicit
else:
_user_asked_copilot_app = _explicit == "copilot-app"

if _user_asked_copilot_app:
_copilot_app_resolved = any(t.name == "copilot-app" for t in _targets)
if not _copilot_app_resolved:
from apm_cli.core.experimental import is_enabled as _is_flag_on

if not _is_flag_on("copilot_app"):
if ctx.logger:
ctx.logger.progress(
"The 'copilot-app' target requires an experimental flag. "
"Run: apm experimental enable copilot-app",
symbol="info",
)
else:
_app_msg = (
"GitHub Copilot desktop App not detected.\n"
"Expected ~/.copilot/data.db but the file is missing.\n"
"Install the app, or omit '--target copilot-app'."
)
if ctx.logger:
ctx.logger.error(_app_msg, symbol="cross")
raise SystemExit(1)

if not _is_user:
_copilot_app_in_set = any(t.name == "copilot-app" for t in _targets)
if _copilot_app_in_set:
if ctx.logger:
ctx.logger.error(
"The 'copilot-app' target requires --global (user scope). "
"Run: apm install --target copilot-app --global"
)
raise SystemExit(1)

# ------------------------------------------------------------------
# v2 resolution (#1154): signal-based provenance and strict errors.
# Runs AFTER the legacy resolver and cowork gates so existing
Expand Down
32 changes: 27 additions & 5 deletions src/apm_cli/install/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,39 @@ def _deployed_path_entry(
If the path is outside the project tree and cannot be
translated to a ``cowork://`` URI via any available target.
"""
if targets:
for _t in targets:
if _t.resolved_deploy_root is None:
continue
try:
target_path.relative_to(_t.resolved_deploy_root)
except ValueError:
continue
if _t.name == "copilot-app":
from apm_cli.integration.copilot_app_db import to_lockfile_uri

return to_lockfile_uri(target_path.name)
from apm_cli.integration.copilot_cowork_paths import to_lockfile_path

return to_lockfile_path(target_path, _t.resolved_deploy_root)
try:
return target_path.relative_to(project_root).as_posix()
except ValueError:
# Path is outside the project tree -- must be a dynamic-root
# target. Find the matching target and translate.
# Path is outside the project tree and no dynamic-root target
# contained it. Fall through to the legacy cowork translation
# which security-validates against deploy_root and raises
# PathTraversalError when out of bounds.
if targets:
for _t in targets:
if _t.resolved_deploy_root is not None:
from apm_cli.integration.copilot_cowork_paths import to_lockfile_path
if _t.resolved_deploy_root is None:
continue
if _t.name == "copilot-app":
from apm_cli.integration.copilot_app_db import to_lockfile_uri

return to_lockfile_uri(target_path.name)
from apm_cli.integration.copilot_cowork_paths import to_lockfile_path

return to_lockfile_path(target_path, _t.resolved_deploy_root)
return to_lockfile_path(target_path, _t.resolved_deploy_root)
Comment on lines 74 to +91
raise RuntimeError( # noqa: B904
f"Cannot translate {target_path!r} to a lockfile path: "
f"path is outside the project tree and no dynamic-root "
Expand Down
Loading
Loading