# Plan Alm

> >-

- **Type:** Skill
- **Install:** `agentstack add skill-microsoft-power-platform-skills-plan-alm`
- **Verified:** Pending review
- **Seller:** [microsoft](https://agentstack.voostack.com/s/microsoft)
- **Installs:** 0
- **Category:** [Developer Tools](https://agentstack.voostack.com/c/developer-tools)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [microsoft](https://github.com/microsoft)
- **Source:** https://github.com/microsoft/power-platform-skills/tree/main/plugins/power-pages/skills/plan-alm
- **Website:** https://aka.ms/ppskills

## Install

```sh
agentstack add skill-microsoft-power-platform-skills-plan-alm
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

> **Plugin check**: Run `node "${PLUGIN_ROOT}/scripts/check-version.js"` — if it outputs a message, show it to the user before proceeding.

# plan-alm

A 4-phase **planner** that gathers ALM strategy from the user, generates an HTML deployment plan, and gets approval. **It does not execute anything** — execution is delegated to the individual ALM skills, which the user runs afterward.

## Overview

This skill detects the current project state (existing solution, pipeline), asks targeted questions about the desired promotion strategy (Power Platform Pipelines or Manual export/import), generates a visual `docs/alm-plan.html`, and gets user approval. The four phases are: **Phase 1 — Detect**, **Phase 2 — Gather strategy**, **Phase 3 — Generate plan**, **Phase 4 — Approve & save**.

**plan-alm never deploys.** The plan's `steps[]` array records the **recommended execution sequence**. After approval, the user invokes the individual skills — `setup-solution`, `setup-pipeline` (or `export-solution`), and `deploy-pipeline` (or `import-solution`) — in that order. Each of those skills detects the approved plan via its Phase 0 gate, proceeds without re-nagging, and refreshes the plan on completion. This separation is deliberate: it keeps `plan-alm` safe to run unattended (e.g. under autopilot) because no single answer can trigger an irreversible deployment.

**Do NOT create tasks at the start** — strategy is unknown until Phase 2 completes. Create both tasks in Phase 3 once the strategy is determined.

---

## Phase 1 — Detect Project State

**Do NOT create tasks yet.** Use natural language progress reporting only during this phase.

Steps:

0. **Detect prior ALM deferral for this project.** Before any discovery work, check whether the project root contains a `.alm-deferred` marker file. The marker is written by users who explicitly opted ALM-skill validators out of "missing artifacts" warnings (e.g. *"this site is handled separately"* or *"ni-dev — no ALM"*). If a user is now invoking `plan-alm`, we should surface that the marker is present and ask what to do, rather than silently proceeding (which would build a plan the user previously decided not to maintain) or silently removing the marker (which would re-enable nags on every other ALM skill).

   ```bash
   node "${PLUGIN_ROOT}/scripts/lib/check-alm-plan.js" --projectRoot "." --no-heartbeat
   ```

   > Use `--no-heartbeat` here: this is a **read-only** deferral check by the *planner*, not an execution-skill Phase 0 gate. Without it, `check-alm-plan.js` would promote an already-`Approved` plan to `In Execution` (and refresh the heartbeat) just because you re-opened `plan-alm` — but re-planning isn't execution. Execution skills call it *without* `--no-heartbeat` so the first one to run does the `Approved → In Execution` promotion.

   
   > 🚦 **Gate (progress · plan-alm:1.deferral):** `.alm-deferred` marker present — continue and remove, continue and keep marker, or cancel. Determines whether downstream ALM skills resume gate enforcement.

   The helper returns `{ deferred, deferral, ... }`. If `deferred === true`, read the deferral reason (`deferral.reason` or the raw marker text) and ask via `AskUserQuestion`:

   > "This project has an `.alm-deferred` marker — `{reason}`. ALM was previously deferred here, so the other ALM skills (`setup-solution`, `setup-pipeline`, `deploy-pipeline`, …) skip their plan-completeness checks for this project. How would you like to proceed?"

   | Question | Header | Options |
   |---|---|---|
   | How would you like to proceed? | ALM deferral marker | Continue planning and remove the marker (Recommended), Continue planning but keep the marker (record deferral context in plan), Cancel |

   - **Continue and remove marker (Recommended)** → delete `.alm-deferred` (the user is re-engaging with ALM). Set `DEFERRAL_CLEARED = true` and proceed to step 1.
   - **Continue and keep marker** → set `DEFERRAL_PRESERVED = true` and `DEFERRAL_REASON = {reason}`. Proceed to step 1. Surface a one-line note in the Phase 1 step 9 user report (e.g. *"Note: `.alm-deferred` is preserved — other ALM skills will continue to skip plan-completeness checks for this project."*) so the user remembers the marker remains in effect after planning.
   - **Cancel** → exit cleanly (don't touch the marker).

   If `deferred === false`, skip this step silently and proceed to step 0b.

0b. **Offer to approve an existing Draft in place (skip re-planning).** The same `check-alm-plan.js` output from step 0 also carries `exists` and `planStatus`. When `exists === true` **and** `planStatus === "Draft"`, the user already has a saved Draft plan — offer to approve it directly instead of regenerating the whole plan. (This is the only Draft→Approved path; without it, approving a draft means a full re-plan.)

   
   > 🚦 **Gate (plan · plan-alm:1.approve-draft):** An existing **Draft** plan was found — approve it in place (no re-plan), re-plan from scratch, or cancel. Approving here writes the status via `set-plan-status.js` and exits without re-running discovery; no deployment is triggered.

   Ask via `AskUserQuestion`:
   > "This site already has an ALM plan saved as **Draft** (`docs/alm-plan.html`). What would you like to do?"

   | Question | Header | Options |
   |---|---|---|
   | What would you like to do? | Existing draft plan | Approve this draft now — no re-plan (Recommended), Re-plan from scratch, Cancel |

   - **Approve this draft now (Recommended)** → capture the approver using the **Phase 4 approver-capture procedure** (the always-interactive prompt with git/OS-name prefill), then write the status atomically with the helper:

     ```bash
     node "${PLUGIN_ROOT}/scripts/lib/set-plan-status.js" --projectRoot "." --status Approved --approver "{APPROVER}" --render
     ```

     Commit (`git add docs/alm-plan.html docs/.alm-plan-data.json && git commit -m "Approve ALM plan for {siteName}"`), run skill tracking (Phase 4 finalize), print the Phase 4 next-steps guidance, and **exit**. Do **not** continue to step 1 — there is nothing to re-plan.
   - **Re-plan from scratch** → proceed to step 1 (the rest of Phase 1 regenerates the plan; Phase 4 saves the new version).
   - **Cancel** → exit cleanly (leave the Draft as-is).

   If `exists === false`, or `planStatus` is anything other than `"Draft"` (`Approved` / `In Execution` / `Completed` / null), skip this step silently and proceed to step 1.

1. **Resolve the site identity from the local project.** `.powerpages-site/website.yml` is the source of truth for `websiteRecordId` and `siteName`, and it is present for **both** Power Pages site types:
   - **Code / SPA sites** — scaffolded by `/power-pages:create-site` and downloaded with `pac pages download-code-site`. These also have a `powerpages.config.json` and SPA source (`src/`, build output in `dist/`/`build/`).
   - **Data-model sites (standard and enhanced data model / "EDM")** — downloaded with `pac pages download --modelVersion 1|2`. These have **no** `powerpages.config.json`; instead `.powerpages-site/` holds the config tree (`web-pages/`, `web-templates/`, `content-snippets/`, …) plus a `.powerpages-site/.portalconfig/` manifest pair. There is no local build output.

   **Resolution order** (first match wins):
   1. **`.powerpages-site/website.yml`** (preferred, present for every downloaded/deployed site) — read with the `Read` tool and extract:
      - `id` field → `websiteRecordId`
      - `name` field → `siteName` (the file uses short keys; it is `name:`, not `adx_name:`)
   2. **`powerpages.config.json`** (fallback — code/SPA sites only; used during plugin development from this repo root or for sites scaffolded but not yet deployed) — read `siteName` and `websiteRecordId`.

   **Determine `SITE_TYPE`** (recorded in planData as `siteType` and used to skip SPA-only assumptions below; it is a data field in `docs/.alm-plan-data.json`, not rendered in the HTML):
   - `declarative` when `.powerpages-site/.portalconfig/` exists, **or** `.powerpages-site/website.yml` resolved while no `powerpages.config.json` is present. (This value was formerly `data-model`; plans written before the rename may still carry `data-model`, which is equivalent.)
   - `code` when `powerpages.config.json` is present.

   If neither marker is found, stop with:
   > "No Power Pages site found in the current directory. Run this skill from your site project root — that's where `.powerpages-site/` lives after `pac pages download-code-site` (code/SPA site) or `pac pages download --modelVersion 2` (enhanced data-model site). If you haven't created the site yet, run `/power-pages:create-site` first."

   `environmentUrl` is always re-confirmed from `pac env who` in step 4 — it does not need to come from either source.

2. Check for `.solution-manifest.json` in the project root:
   - Store `SOLUTION_DONE = true` if found, `false` otherwise
   - If found, read `solution.uniqueName` and store as `SOLUTION_UNIQUE_NAME`

3. Check for `docs/alm/last-pipeline.json` in the project root:
   - Store `PIPELINE_DONE = true` if found, `false` otherwise
   - If found, read `pipelineName` and `stages[]` for later use

4. Run silently:
   ```bash
   pac env who
   ```
   Capture the environment URL and display name. Store as `DEV_ENV_URL` and `DEV_ENV_NAME`. **The URL label varies by PAC version**: current PAC (2.8.x) prints it under `Org URL:`; older builds used `Environment URL:` — read whichever is present (there is no `Environment URL:` line on 2.8.x, so do not look only for that label). The display name is the `Friendly Name:` / `Connected to...` value. If you can't parse it reliably, leave `DEV_ENV_URL` empty — Step 6's `verify-alm-prerequisites.js` resolves the authoritative URL from `pac env who` via the shared `getEnvironmentUrl()` helper (which matches both labels) and returns it as `.envUrl`.

5. Run silently:
   ```bash
   node "${PLUGIN_ROOT}/scripts/lib/list-environments.js"
   ```
   Store the JSON array as `ENV_LIST` for pre-filling environment URLs in Phase 2. (This helper parses `pac env list`; the old `pac env list --output json` is invalid on current PAC CLI — `pac env list` only accepts `--filter` — so the helper exists to produce the JSON the table form doesn't. It prints `[]` and exits 0 if PAC is unauthenticated, so pre-fill simply degrades to manual entry.) Each entry is `{ displayName, environmentId, environmentUrl, uniqueName, active }`.

6. Acquire dev environment token (silently):
   ```bash
   node "${PLUGIN_ROOT}/scripts/lib/verify-alm-prerequisites.js" --envUrl "{DEV_ENV_URL}"
   ```
   Store `.token` as `DEV_TOKEN` and `.userId` as `userId`.

   **Track plan quality.** Initialize a `PLAN_QUALITY` accumulator to `"complete"` at the start of Phase 1. If this token acquisition fails (auth error), set `DEV_TOKEN = null`, set `PLAN_QUALITY = "degraded"`, and record the cause (e.g. *"dev-environment auth failed — contents/size/host discovery skipped"*) — then continue. Contents discovery is skipped gracefully, but the resulting plan is built on partial inputs; Phase 3 surfaces this as a prominent risk so the user reviews before executing. (There is no execute path to block here — `plan-alm` only plans — but a degraded plan must be visibly flagged.)

6b. **Environment-match guard** — confirm `pac env who` points at the project's environment *before* running discovery. `DEV_ENV_URL` comes from whatever environment PAC happens to be connected to, which is **not** guaranteed to be the project's. If it isn't, every query in Steps 7–12 runs against the wrong environment and silently produces a degraded plan (zero or wrong site settings, wrong size, wrong host) that *looks* valid. Cross-check both signals available:

    1. **Recorded-URL comparison** (no token needed): collect any environment URL the project already records — `powerpages.config.json` → top-level `environmentUrl` (code/SPA sites; absent for declarative/EDM sites) and `.solution-manifest.json` → top-level `environmentUrl` if present. Normalize by **origin** (lowercase host, drop trailing slash + path/query). If any recorded URL exists and its origin **differs** from `DEV_ENV_URL`'s origin → **mismatch**.
    2. **Site-existence probe** (covers declarative/EDM sites that record no URL; only when `DEV_TOKEN` is available): verify the site's `websiteRecordId` actually exists in the connected env:
       ```
       GET {DEV_ENV_URL}/api/data/v9.2/powerpagesites({websiteRecordId})?$select=powerpagesiteid
       Authorization: Bearer {DEV_TOKEN}
       ```
       A `404` (or empty result) means the connected environment does not contain this site → **mismatch**. (Skip this probe when `DEV_TOKEN = null` — Step 6 already degraded the plan; don't double-prompt.)

    If **neither** signal indicates a mismatch, continue silently to Step 7 — do not prompt. Only prompt on a detected mismatch:

    
    > 🚦 **Gate (progress · plan-alm:1.env-match):** PAC CLI is connected to an environment that does not match the project's. Switch and re-run, or continue against the connected env (degraded plan).

    Ask via `AskUserQuestion`:

    | Question | Header | Options |
    |---|---|---|
    | PAC CLI is connected to **{DEV_ENV_NAME}** (`{DEV_ENV_URL}`), which does not match this project's configured environment ({recorded URL, or "this site was not found there"}). Discovery will run against the connected environment. How do you want to proceed? | Env Mismatch | Switch PAC env & re-run (Recommended), Continue against {DEV_ENV_NAME} anyway |

    Exactly two outcomes (both halt-or-proceed; no separate "cancel" — "Switch & re-run" already stops the skill):
    - **Switch PAC env & re-run (Recommended)**: stop the skill. Tell the user to point PAC at the right environment (`pac auth select --name ` or `pac org select --environment `) and re-run `/power-pages:plan-alm`. Nothing has been written.
    - **Continue against {DEV_ENV_NAME} anyway**: proceed to Step 7 against `DEV_ENV_URL`, but set `PLAN_QUALITY = "degraded"` and record the cause (*"discovery ran against {DEV_ENV_NAME}, which may not be the project's environment — verify the plan's site settings / size / host before executing"*) so Phase 3 surfaces it as a prominent risk.

    > **Why this exists**: a real EDM-site run produced a valid-looking plan after PAC had silently stayed connected to a different env than the project targeted. The site-existence probe + recorded-URL comparison catch that at the earliest gate, before any discovery runs.

7. Discover and classify site settings (if `DEV_TOKEN` is available and `websiteRecordId` is known):

   Use Node.js `https` module to query. **Paginate via `@odata.nextLink`** — sites with > 500 settings would otherwise silently truncate, dropping tier classifications and underreporting `plannedEnvVarCount`. Send `Prefer: odata.maxpagesize=5000` so Dataverse emits the continuation link, then loop until exhausted:
   ```
   GET {DEV_ENV_URL}/api/data/v9.2/mspp_sitesettings?$filter=_mspp_websiteid_value eq '{websiteRecordId}'&$select=mspp_name,mspp_value&$top=5000
   Authorization: Bearer {DEV_TOKEN}
   Prefer: odata.maxpagesize=5000
   OData-MaxVersion: 4.0
   OData-Version: 4.0
   Accept: application/json
   ```
   On each response, append `value[]` to the running array. If `@odata.nextLink` is present, GET that URL with the same headers (no need to re-add the filter — the nextLink already encodes the query). Stop when the response has no `@odata.nextLink`. Cap at 100 iterations for safety.

   Classify the returned settings using `${PLUGIN_ROOT}/scripts/lib/classify-site-settings.js` — the single source of truth for the credential regex and tier mapping shared with `setup-solution` Phase 5. Either pipe the JSON array of `{name, value}` rows into the script's stdin (CLI mode) or `require()` it inline:

   ```bash
   echo '' \
     | node "${PLUGIN_ROOT}/scripts/lib/classify-site-settings.js"
   ```

   Output (the four-bucket shap

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [microsoft](https://github.com/microsoft)
- **Source:** [microsoft/power-platform-skills](https://github.com/microsoft/power-platform-skills)
- **License:** MIT
- **Homepage:** https://aka.ms/ppskills

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** yes
- **Shell / process execution:** yes
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: flagged — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-microsoft-power-platform-skills-plan-alm
- Seller: https://agentstack.voostack.com/s/microsoft
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
