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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- `apm install` enforces org `apm-policy.yml` at install time (deps deny/allow/require, MCP deny/transport/trust-transitive, `compilation.target.allow`, `extends:` chains, `policy.fetch_failure` knob, `policy.hash` pin); `--no-policy` / `APM_POLICY_DISABLE=1` escape hatch; `--dry-run` previews verdicts; failed package installs roll back `apm.yml`. New `apm policy status` diagnostic (table / `--json`, exit-0 by default, `--check` for CI). `apm audit --ci` auto-discovers org policy. **Migration**: orgs publishing `enforcement: block` may see installs that previously succeeded now fail -- preview with `apm install --dry-run`. Closes #827, #829, #831, #834 (#832)
- `apm experimental` command group - a feature-flag registry with `list` / `enable` / `disable` / `reset` subcommands. Opt in to new behaviour before it graduates to default. Ships with one built-in flag (`verbose-version`) and a contributor recipe for proposing new flags (#845)
- `pr-review-panel` gh-aw workflow: runs the `apm-review-panel` skill on PRs labelled `panel-review` and posts a synthesized verdict (#824)

### Changed
Expand Down
40 changes: 39 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,44 @@ uv run isort .

If your changes affect how users interact with the project, update the documentation accordingly.

## Extending APM

### How to add an experimental feature flag

Use an experimental flag to de-risk rollout of a user-visible behavioural change that may need early adopter feedback. Do not add a flag for a bug fix, internal refactor, or any change that should simply ship as the default behaviour.

Experimental flags MUST NOT gate security-critical behaviour (content scanning, path validation, lockfile integrity, token handling, MCP trust, collision detection). Flags are ergonomic/UX toggles only.

When adding a new experimental flag:

1. Register it in `src/apm_cli/core/experimental.py` in the `FLAGS` dict with a frozen `ExperimentalFlag(name=..., description=..., default=False, hint=...)`.
2. Gate the code path with a function-scope import (avoids import cycles):
```python
def my_function():
from apm_cli.core.experimental import is_enabled
if is_enabled("my_flag"):
...
```
3. Add tests that cover both the enabled and disabled code paths.
4. Update the experimental command reference page at `docs/src/content/docs/reference/experimental.md`.

Naming rules:

- Use `snake_case` in the registry and config.
- Use `kebab-case` for display and other user-facing strings.
- The CLI accepts both forms on input.

Graduation and retirement:

1. When a flag becomes the default, remove the gate and remove the matching `FLAGS` entry in the same PR.
2. Add a `CHANGELOG.md` entry under `Changed` with a migration note if the previous default differed.

Avoid these anti-patterns:

- Do not gate security-critical behaviour behind an experimental flag.
- Do not read `is_enabled()` at module import time.
- Do not persist flag state anywhere other than `~/.apm/config.json` via `update_config`.

## License

By contributing to this project, you agree that your contributions will be licensed under the project's [MIT License](LICENSE).
Expand All @@ -152,4 +190,4 @@ By contributing to this project, you agree that your contributions will be licen

If you have any questions, feel free to open an issue or reach out to the maintainers.

Thank you for your contributions!
Thank you for your contributions!
4 changes: 4 additions & 0 deletions docs/src/content/docs/reference/cli-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -1713,3 +1713,7 @@ apm runtime status
- Runtime preference order (copilot → codex → llm)
- Currently active runtime
- Next steps if no runtime is available

## Experimental Features

`apm experimental` manages opt-in flags that gate new or changing behaviour. Subcommands: `list`, `enable`, `disable`, `reset`. `apm experimental list` also supports `--json`, and `-v` / `--verbose` works on each subcommand. See the full reference in [Experimental Flags](../experimental/).
188 changes: 188 additions & 0 deletions docs/src/content/docs/reference/experimental.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
---
title: "apm experimental"
description: "Manage opt-in experimental feature flags. Evaluate new or changing behaviour without affecting APM defaults."
sidebar:
order: 5
label: "Experimental Flags"
---

`apm experimental` manages opt-in feature flags that gate new or changing behaviour. Flags let you evaluate a capability before it graduates to default, and can be toggled at any time without reinstalling APM.

Default APM behaviour never changes based on what is available here. A flag must be explicitly enabled to take effect, and every flag ships disabled.

:::caution[Scope]
Experimental flags are ergonomic and UX toggles only. They MUST NOT gate security-critical behaviour -- content scanning, path validation, lockfile integrity, token handling, MCP trust, or collision detection are never placed behind a flag. See [Security Model](../../enterprise/security/).
:::

## Subcommands

### `apm experimental list`

List every registered flag with its current state. This is the default when no subcommand is given. Normal output is just the table; add `--verbose` to also print the config path and the introductory preamble.

```bash
apm experimental list [OPTIONS]
```

**Options:**
- `--enabled` - Show only flags that are currently enabled.
- `--disabled` - Show only flags that are currently disabled.
- `--json` - Emit a JSON array to stdout with `name`, `enabled`, `default`, `description`, and `source` fields.
- `-v, --verbose` - Print the config file path used for overrides and the introductory preamble.

**Example:**

```bash
$ apm experimental list
Experimental Features
Flag Status Description
verbose-version disabled Show Python version, platform, and install path in 'apm --version'.
Tip: apm experimental enable <name>
```

Verbose output keeps the same table and adds the extra context lines:

```bash
$ apm experimental list --verbose
Config file: ~/.apm/config.json
Experimental features let you try new behaviour before it becomes default.
...table output...
```

Use `--json` for scripts and automation. It suppresses the table, colour, and intro preamble, and still honours `--enabled` / `--disabled` filters:

```bash
$ apm experimental list --json
[
{
"name": "verbose_version",
"enabled": false,
"default": false,
"description": "Show Python version, platform, and install path in 'apm --version'.",
"source": "default"
}
]
```

The JSON `name` field uses the canonical registry key. For command arguments, APM still accepts either kebab-case (`verbose-version`) or snake_case (`verbose_version`). For clean machine-readable stdout, use `--json` without `--verbose`.

### `apm experimental enable`

Enable a flag. The override is persisted immediately.

```bash
apm experimental enable NAME [OPTIONS]
```

**Arguments:**
- `NAME` - Flag name. Accepted in either kebab-case (`verbose-version`) or snake_case (`verbose_version`).

**Options:**
- `-v, --verbose` - Print the config file path used for overrides.

**Example:**

```bash
$ apm experimental enable verbose-version
[+] Enabled experimental feature: verbose-version
Run 'apm --version' to see the new output.
```

Unknown names produce an error with suggestions drawn from the registered flag list:

```bash
$ apm experimental enable verbose-versio
[x] Unknown experimental feature: verbose-versio
Did you mean: verbose-version?
Run 'apm experimental list' to see all available features.
```

### `apm experimental disable`

Disable a flag. If the flag was not enabled, this is a no-op.

```bash
apm experimental disable NAME [OPTIONS]
```

**Options:**
- `-v, --verbose` - Print the config file path used for overrides.

**Example:**

```bash
$ apm experimental disable verbose-version
[+] Disabled experimental feature: verbose-version
```

### `apm experimental reset`

Remove overrides and restore default state. With no argument, all overrides are cleared; a confirmation prompt lists exactly what will change. Bulk reset also removes malformed overrides for registered flags, such as a string value where a boolean is expected.

```bash
apm experimental reset [NAME] [OPTIONS]
```

**Arguments:**
- `NAME` - Optional. Reset a single flag rather than all of them.

**Options:**
- `-y, --yes` - Skip the confirmation prompt (bulk reset only).
- `-v, --verbose` - Print the config file path used for overrides.

**Example:**

```bash
$ apm experimental reset
This will reset 1 experimental feature to its default:
verbose-version (currently enabled -> disabled)
Proceed? [y/N]: y
[+] Reset all experimental features to defaults
```

Single-flag reset does not prompt:

```bash
$ apm experimental reset verbose-version
[+] Reset verbose-version to default (disabled)
```

## Example workflow

Try a flag, confirm its effect, then revert:

```bash
# 1. See what is available
apm experimental list

# 2. Opt in to verbose version output
apm experimental enable verbose-version

# 3. Observe the new behaviour
apm --version

# 4. Revert to default
apm experimental reset verbose-version
```

## Available flags

| Name | Description |
|-------------------|----------------------------------------------------------------------------------|
| `verbose-version` | Show Python version, platform, and install path in `apm --version`. |

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.

## Storage and scope

Overrides are written to `~/.apm/config.json` under the `experimental` key and persist across CLI invocations. They are global to the user account and do not vary per project or per shell session. The canonical way to clear overrides is `apm experimental reset`; editing the file by hand is supported but unnecessary.

Pass `-v` / `--verbose` to any subcommand after the subcommand name (for example `apm experimental list --verbose`) to print the config file path in use.

When a flag's behaviour is considered stable, it graduates: the gated code becomes the default path and the flag is removed from the registry in a future release.

## Troubleshooting

- **"Unknown experimental feature"** - the name is not in the registry. Run `apm experimental list` to see the current set. Suggestions printed below the error use fuzzy matching on registered names.
- **Unknown keys in config** - a flag that was enabled on a previous APM version may have been removed or renamed. `apm experimental list` surfaces a note when stale keys are present; `apm experimental reset` clears them.
- **Malformed values in config** - if a registered flag has a non-boolean override in `~/.apm/config.json`, `apm experimental reset --yes` removes the bad value and restores the default.
12 changes: 12 additions & 0 deletions packages/apm-guide/.apm/skills/apm-usage/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,18 @@ Set `MCP_REGISTRY_URL` (default `https://api.mcp.github.com`) to point all `apm
| `apm runtime remove {copilot\|codex\|llm}` | Remove a runtime | `--yes` |
| `apm runtime status` | Show active runtime | -- |

## Experimental features

| Command | Purpose | Key flags |
|---------|---------|-----------|
| `apm experimental` | Default to `apm experimental list` | `-v` verbose |
| `apm experimental list` | List registered experimental flags or emit JSON for automation | `--enabled`, `--disabled`, `--json`, `-v` verbose |
| `apm experimental enable NAME` | Enable an opt-in experimental flag | `-v` verbose |
| `apm experimental disable NAME` | Disable an opt-in experimental flag | `-v` verbose |
| `apm experimental reset [NAME]` | Reset one flag or all flags to defaults; also cleans malformed overrides during bulk reset | `-y` skip confirm, `-v` verbose |

Experimental flags MUST NOT gate security-critical behaviour (content scanning, path validation, lockfile integrity, token handling, MCP trust, collision detection). Flags are ergonomic/UX toggles only.

## Configuration and updates

| Command | Purpose | Key flags |
Expand Down
2 changes: 2 additions & 0 deletions src/apm_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from apm_cli.commands.compile import compile as compile_cmd
from apm_cli.commands.config import config
from apm_cli.commands.deps import deps
from apm_cli.commands.experimental import experimental
from apm_cli.commands.view import view as view_cmd
from apm_cli.commands.init import init
from apm_cli.commands.install import install
Expand Down Expand Up @@ -81,6 +82,7 @@ def cli(ctx):
cli.add_command(preview)
cli.add_command(list_cmd, name="list")
cli.add_command(config)
cli.add_command(experimental)
cli.add_command(runtime)
cli.add_command(mcp)
cli.add_command(policy)
Expand Down
19 changes: 19 additions & 0 deletions src/apm_cli/commands/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,25 @@ def print_version(ctx, param, value):
f"{TITLE}Agent Package Manager (APM) CLI{RESET} version {version_str}"
)

# Gated verbose-version output (experimental flag)
try:
from ..core.experimental import is_enabled

if is_enabled("verbose_version"):
import platform
import sys

python_ver = platform.python_version()
plat = f"{sys.platform}-{platform.machine()}"
install_path = str(Path(__file__).resolve().parent.parent)

_rich_echo(f" {'Python:':<14}{python_ver}", color="dim")
_rich_echo(f" {'Platform:':<14}{plat}", color="dim")
_rich_echo(f" {'Install path:':<14}{install_path}", color="dim")
except Exception:
# Never let experimental flag logic break --version
pass

ctx.exit()


Expand Down
Loading
Loading