Skip to content

Power Pages ALM: 10 new skills + 45 helpers + 6 reference docs for site-to-pipeline lifecycle - #167

Merged
T-Nid (tyaginidhi) merged 41 commits into
mainfrom
featureBranch/PowerPagesALM
May 26, 2026
Merged

Power Pages ALM: 10 new skills + 45 helpers + 6 reference docs for site-to-pipeline lifecycle#167
T-Nid (tyaginidhi) merged 41 commits into
mainfrom
featureBranch/PowerPagesALM

Conversation

@tyaginidhi

@tyaginidhi T-Nid (tyaginidhi) commented May 25, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR promotes the Power Pages ALM workstream from featureBranch/PowerPagesALM into main. It adds 10 new ALM/CI-CD skills, 44 shared helpers, 54 new test files, and 6 supporting reference docs that the existing Power Pages plugin (create-site, deploy-site, setup-datamodel, etc.) needed to move from "site authoring" to a complete "site → solution → pipeline → deploy → activate → test" lifecycle.

Status: Draft. Opening for visibility while the maintainers schedule a review window. The work has been review-stable on featureBranch/PowerPagesALM for the past several PRs (#47, #68, #75, #77, #87, #107, #108, #119, #126, #134, #140, #141, #145, #147, #149, #154, #156, #161, #163, #164, #166 — see commit history; #162 reaches this PR via the merge commits that brought main into featureBranch).

Scope at a glance

  • 143 files changed, +44,037 / −15 lines
  • 39 commits: 21 sub-PRs (line-level reviewed individually) + intervening main-merge commits + one automated github-actions commit (e3b76ef, +16 lines, adds the standard "Plugin check" prologue to 8 ALM SKILL.md files that didn't have it — landed after PR ALM review follow-ups: B1-B4 + D1, heartbeat Phase 0, halt-on-deploy-failure, LAST_SYNC_AT #166 merged)
  • 920 tests pass on the branch (node --test plugins/power-pages/scripts/tests/)
  • 0 lint errors from lint-skills-alm.js (49 unchanged warnings, all in non-ALM skills outside the lint's scope)
  • 6 approval-gate categories (intent / plan / progress / consent / final / pause), ~111 catalogued gate calls across 12 ALM skills, lint-enforced markers

New skills (10)

User-invocable via /power-pages:<skill-name>:

Skill Purpose
setup-solution Creates publisher + solution, adds Power Pages site components. Sync mode adopts orphans from sibling skills, bumps version, writes .solution-manifest.json.
export-solution Triggers async export, polls, downloads, verifies zip. Always-on version bump (since PR #164) so every zip carries a strictly-increasing version.
import-solution Imports zip to target env, supports staged-import dependency check, env var values, cloud flow registration, site activation. Version-skew advisory if zip ≤ installed (since #164).
setup-pipeline Creates PP Pipelines deploymentenvironments + deploymentpipelines + deploymentstages. Auto-discovers host env, runs preflight checks, writes last-pipeline.json + pipeline-setup.md.
deploy-pipeline Triggers stage runs, validates package (parallel batch in multi-solution mode since #164), PATCHes env var overrides, deploys, polls. Strip-and-retry recovery for invalid Secret refs (Phase 7.6.4, since #166).
configure-env-variables Creates environmentvariabledefinition records, links to mspp_sitesetting, writes deployment-settings.json with per-stage values. Pre-write validation hard-stops on invalid Secret refs (Phase 6.1, since #166).
ensure-pipelines-host Detects + provisions Pipelines host (Platform or Custom). Decision tree across NoHost / AvailableUsingPlatformHost / AvailableUsingCustomHost / PlatformHostExistsUnbound / etc.
force-link-environment Wraps ManageEnvironmentStamp for Pattern 15 auto-fix (cross-host environment-stamp move).
diagnose-deployment 7-step diagnostic flow against references/deployment-error-catalog.md (10 known failure patterns, severity-classified, with auto-fix offers).
plan-alm 8-phase orchestrator: detects project state, gathers strategy (PP Pipelines vs Manual), renders HTML plan (docs/alm-plan.html), executes downstream skills, finalizes.

New shared helpers (44 in scripts/lib/)

Categorised in AGENTS.md — full list there. Representative coverage:

  • ALM Prerequisites & Contextverify-alm-prerequisites, detect-project-context, alm-paths (single source of truth for docs/alm/ paths), check-alm-plan (Phase 0 gate helper with heartbeat-based skip rule), resolve-target-solution (strict 3-step resolution order).
  • Solution Splitting Decision Treealm-thresholds, estimate-solution-size, compute-split-plan (Strategy 1-4 + composite sub-partition + Future Growth buffer).
  • Solution Managementverify-solution-exists, create-solution, create-solutions-batch (parallel), bump-solution-version (single source of truth for patch-bump rule, with --projectRoot manifest sync), add-components-to-solution, discover-component-types, discover-site-components (single source of truth for site inventory), classify-site-settings, create-env-var-definition, link-site-setting-to-env-var, discover-env-var-definitions, refresh-alm-plan-data (cross-phase planData refresher), strip-invalid-secret-values (Phase 7.6.4 backstop).
  • PP Pipelinesdiscover-pipelines-host, create-deployment-environment, create-deployment-pipeline, create-stage-run, poll-validation-status, validate-stage-runs-batch (parallel validation with --rePoll mode), poll-deployment-status, ensure-pipelines-host-detect, provision-platform-host, provision-custom-host, force-link-environment, pac-bap-shim, verify-env-var-values, validate-deployment-settings, install-pipelines-app, fix-blocked-attachments, list-tenant-envs, resolve-env-by-id, check-env-host-binding, verify-host-readiness.
  • Solution Exportexport-solution-async, download-export-data.

Every helper has a dedicated *.test.js file under scripts/tests/.

New reference docs (6 in references/)

  • approval-gates.md — canonical Approval-Gate terminology, marker syntax, 6 categories (intent / plan / progress / consent / final / pause), cancel-leaves vocabulary, lint rules, and a full catalog of every gate across the 12 ALM skills.
  • alm-docs-grounding.md — Phase 1.5 pattern for grounding ALM skills in current Microsoft Learn docs.
  • alm-prompts.md — canonical prompts for cross-skill agent reuse.
  • cicd-pipeline-patterns.md — PAC CLI service-principal auth + ADO/GitHub Actions templates + all HAR-confirmed PP Pipelines API patterns.
  • deployment-error-catalog.md — 10 known deployment failure patterns with root cause, severity, auto-fix availability, and fix procedure. Driven by diagnose-deployment.
  • solution-api-patterns.md — OData body templates for solution lifecycle CRUD + async export/import + manifest format.

Architecture highlights

  • ALM-aware by default — every skill that creates Dataverse records that belong in a Power Pages solution resolves the target solution via the strict 3-step order (explicit arg → .solution-manifest.json → error). Component-creation scripts accept --solutionUniqueName and add records via AddSolutionComponent. New component types must update scripts/lib/discover-site-components.js (single inventory source).
  • Approval Gates — every load-bearing AskUserQuestion is marked with <!-- gate: skill:phase | category=X | cancel-leaves=Y --> and catalogued in references/approval-gates.md §6. Lint enforces marker presence, gate-id uniqueness, catalog membership, intent-gate helper invocation, and cancel-leaves vocab.
  • ALM artifacts under docs/alm/ — 5 plan/decision JSONs + 9 last-*.json skill-run markers all resolved through alm-paths.js (almPath(root, key) / ensureAlmDir(root)). Never inline a raw path.
  • plan-alm is the front door — when the user expresses an ALM intent, plan-alm runs first. Individual skills' Phase 0 fail-closed gates enforce this with a heartbeat-based "is plan-alm currently orchestrating?" check (since ALM review follow-ups: B1-B4 + D1, heartbeat Phase 0, halt-on-deploy-failure, LAST_SYNC_AT #166's G1 fix; 60-min window, auto-refreshed by every in-chain skill).
  • Single-source-of-truth helpersclassify-site-settings.js (credential regex + tier), generate-env-var-schema-name.js (canonical schema name), bump-solution-version.js (patch-bump rule + compareVersions), resolve-target-solution.js (which solution?). SKILL.md prose must call these helpers, not inline the logic.
  • Defense in depth for invalid Secret refs — three layers: (1) configure-env-variables Phase 6.1 pre-write hard-stop, (2) deploy-pipeline Phase 5.1b pre-PATCH gate, (3) deploy-pipeline Phase 7.6.4 post-failure strip-and-retry.

Validators + hooks

Hook registration centralized in hooks/hooks.json. Skills currently registered with command-backed validators: activate-site, add-cloud-flow, add-seo, add-server-logic, audit-permissions, configure-env-variables, create-site, create-webroles, deploy-pipeline, ensure-pipelines-host, export-solution, force-link-environment, import-solution, integrate-webapi, plan-alm, setup-auth, setup-datamodel, setup-pipeline, setup-solution. add-sample-data, diagnose-deployment, test-site tracked without command validators (no artifacts to verify).

Sub-PRs that compose this work

This PR is a roll-up. Detailed review threads + line-level history live on the individual PRs that were merged into featureBranch/PowerPagesALM:

Sub-PR Title
#47 power pages alm using solutions
#68 Add setup-pipeline and deploy-pipeline skills
#75 Deployment variables, CI/CD pipeline deploy, setup-solution improvements
#77 import-solution + deploy-pipeline: env var prompting + post-deploy activation check
#87 deploy-pipeline tabbed history; plan-alm per-stage activation
#107 Solution-splitting decision tree + manifest v2
#108 ALM-aware-by-default safeguards
#119 ALM validation run: 11 fixes + integration tests
#126 Per-skill telemetry + 5 plan-alm UX fixes
#134 ensure-pipelines-host skill + plan-alm UX overhaul + 3-entity site model fix
#140 ALM plan UX overhaul: env-first host menu, post-run refresh, SKU fallback
#141 plan-alm cap eligible-env list at 5
#145 ensure-pipelines-host Platform Host getOrCreate as Phase 4.0
#147 ALM skills release-readiness: plan-UI, OAuth, MCP grounding
#149 force-link-environment skill + Pattern 15 auto-fix
#154 Artifacts under docs/alm/, post-sync approval gates, estimator pagination
#156 ALM split + plan-state auto-sync + getOrCreate + pre-flight unblock
#161 Approval Gates: catalog + marker sweep + lint + loop-iteration tightening
#163 Env var lifecycle: pre-PATCH validate + post-deploy verify + canonical Secret formats
#164 Manual export/import reliability + parallel validation batch
#166 ALM review follow-ups: B1-B4 + D1, heartbeat Phase 0, halt-on-deploy-failure, LAST_SYNC_AT, 16 Citizens-portal fixes, Phase 7.6.4 strip-and-retry

Plus PR #162 (canvas-apps configure-canvas-mcp doc fix) reaches this PR transitively via the main-merge commits 4949d99 and bf95c03.

Test plan

Local sanity (CI-runnable):

  • node --test plugins/power-pages/scripts/tests/ → 920/920 pass.
  • node plugins/power-pages/scripts/lint-skills-alm.js → 0 errors.

End-to-end against a real tenant:

  • Fresh project: /power-pages:create-site/power-pages:deploy-site/power-pages:plan-alm → orchestrates setup-solution + setup-pipeline + deploy-pipeline to a Staging env. Verify each docs/alm/*.json marker is written and the rendered docs/alm-plan.html reflects current state.
  • Multi-solution split: project that triggers compute-split-plan.js Strategy 1 / 2 / 3 / 4. Verify setup-solution creates N solutions, setup-pipeline writes deploymentOrder[], deploy-pipeline Phase 3.6 fans out validation in parallel + serial deploy.
  • Manual export/import path: setup-solutionexport-solution (verify version bump) → import-solution to target (verify version-skew advisory if zip ≤ installed).
  • Env vars end-to-end: configure-env-variables with a mix of String + Secret + invalid @KeyVault(...) value. Confirm Phase 6.1 hard-stops, then with a canonical Key Vault URI confirm deployment-settings.json writes + values land on target post-deploy (verify-env-var-values.js).
  • Diagnose failure path: deliberately fail a deploy (e.g. blocked attachments, AttachmentBlocked, invalid Secret ref). Confirm diagnose-deployment and deploy-pipeline Phase 7.6's pattern-matched remediations fire correctly.

Not in this PR

  • Sprint 2 skills (setup-environments, setup-git-versioning, configure-secrets, setup-approvals) — all blocked on POC validation per AGENTS.md.
  • GitHub Actions / ADO Pipelines paths for setup-pipeline — currently "coming soon" stubs; PP Pipelines path is fully implemented.
  • Two cosmetic items deferred per ALM review follow-ups: B1-B4 + D1, heartbeat Phase 0, halt-on-deploy-failure, LAST_SYNC_AT #166's review (D2 prose mismatch in import-solution's "version delta" claim; R15 displayName-drift requires runtime agent intervention).

🤖 Generated with Claude Code

T-Nid (tyaginidhi) and others added 30 commits March 11, 2026 21:35
- AsyncJobId → AsyncOperationId (actual field name in ExportSolutionAsync response)
- Fix statecode table: Succeeded is statecode=3 not 2
- Fix grep case: solution.xml is lowercase in zip, use grep -i

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ImportJobId → ImportJobKey (actual field name in ImportSolutionAsync response)
- StageSolutionUploadId not accepted by ImportSolutionAsync — always use CustomizationFile
- Fix solution.xml grep case in import SKILL.md (grep -qi)
- Clarify StageSolution is pre-flight only, does not alter import call

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- deployment-error-catalog: add Pattern 11 for AttachmentBlocked (-2147188706)
  discovered during live testing against portal environment
- import SKILL.md: fix grep -q Solution.xml → grep -qi solution.xml

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When ImportSolutionAsync fails with AttachmentBlocked (-2147188706):
1. Identify which extensions in the solution zip are on the blocked list
2. Explain the issue to the user clearly
3. Ask explicit consent before modifying environment settings
4. Remove only the specific blocked extensions (preserve others)
5. Retry the import

Follows same consent pattern as deploy-site Phase 6 (blocked JS handling).
Discovered during live testing against the portal environment.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…t text

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace prose description with explicit AskUserQuestion invocation.
User answers live before export proceeds — not via chat text.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Setup-solution skill verified for 100 components site

* Fix export-solution bugs found during live testing against IdeaSphere

- AsyncJobId → AsyncOperationId (actual field name in ExportSolutionAsync response)
- Fix statecode table: Succeeded is statecode=3 not 2
- Fix grep case: solution.xml is lowercase in zip, use grep -i

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix import-solution bugs found during live testing against IdeaSphere

- ImportJobId → ImportJobKey (actual field name in ImportSolutionAsync response)
- StageSolutionUploadId not accepted by ImportSolutionAsync — always use CustomizationFile
- Fix solution.xml grep case in import SKILL.md (grep -qi)
- Clarify StageSolution is pre-flight only, does not alter import call

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Add AttachmentBlocked import error pattern; fix import SKILL.md case bug

- deployment-error-catalog: add Pattern 11 for AttachmentBlocked (-2147188706)
  discovered during live testing against portal environment
- import SKILL.md: fix grep -q Solution.xml → grep -qi solution.xml

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Add Phase 5b: resolve AttachmentBlocked errors in import-solution

When ImportSolutionAsync fails with AttachmentBlocked (-2147188706):
1. Identify which extensions in the solution zip are on the blocked list
2. Explain the issue to the user clearly
3. Ask explicit consent before modifying environment settings
4. Remove only the specific blocked extensions (preserve others)
5. Retry the import

Follows same consent pattern as deploy-site Phase 6 (blocked JS handling).
Discovered during live testing against the portal environment.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Clarify Phase 5b consent: invoke AskUserQuestion directly, not as chat text

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* export-solution: ask managed vs unmanaged via AskUserQuestion directly

Replace prose description with explicit AskUserQuestion invocation.
User answers live before export proceeds — not via chat text.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Remove er-diagram-ideasphere.png — not part of skill code

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Add hotfix-solution skill for incremental Power Pages deployments

7-phase skill: discover components modified in a time window, package
into a timestamped hotfix solution, export (managed/unmanaged), and
import to a target environment with full AttachmentBlocked remediation.

- skills/hotfix-solution/SKILL.md — full skill definition
- skills/hotfix-solution/scripts/validate-hotfix.js — validates .last-hotfix.json
- AGENTS.md — updated with skill entry, hooks, validate script, shared refs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Add hotfix-solution skill for incremental Power Pages deployments

7-phase skill: discover components modified in a time window, package
into a timestamped hotfix solution, export (managed/unmanaged), and
import to a target environment with full AttachmentBlocked remediation.

- skills/hotfix-solution/SKILL.md — full skill definition
- skills/hotfix-solution/scripts/validate-hotfix.js — validates .last-hotfix.json
- AGENTS.md — updated with skill entry, hooks, validate script, shared refs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…ne (#68)

- Replace generate-pipeline (YAML-only stub) with setup-pipeline: full PP Pipelines implementation via OData API, plus GitHub/ADO coming-soon stubs
- Add deploy-pipeline: creates stage runs, validates packages, polls stagerunstatus, handles approval gates, retries, and writes .last-deploy.json
- cicd-pipeline-patterns.md: major expansion with HAR-confirmed API patterns, source code findings (RetryFailedDeploymentAsync, cancel, scheduled deploy, redeployment, platform host BAP API), and live-test bug fixes (18 total)
- References in setup-solution, export-solution, hotfix-solution, solution-api-patterns updated from generate-pipeline → setup-pipeline
- AGENTS.md updated with new skill descriptions, hooks, and architecture tree

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Resolves conflicts:
- plugin.json: take version 1.1.2 from main
- AGENTS.md: keep ALM skill documentation from branch, drop main's single-line stub

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…vements (#75)

* Add configure-env-variables skill and update deploy-pipeline for env var support

- New skill: configure-env-variables — creates environmentvariabledefinition records,
  adds them to solution (componenttype 380), generates deployment-settings.json with
  per-stage override values; guides UI step for mspp_sitesetting source linking
- deploy-pipeline: reads deployment-settings.json and injects deploymentsettingsjson
  in stage run PATCH so env var values are overridden per target stage at deploy time
- setup-solution: adds configure-env-variables to suggested next steps

Validated end-to-end against IdeaSphere: ids_LocalLoginEnabled=true in dev,
deployed to staging as false via deploymentsettingsjson injection (stage run abdc2380).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix setup-solution: include tables, Web API settings, and table permissions

Analysis of BYOC Supplier Portal solution (BYOCSupplierPortal_1_0_0_1.zip) revealed
three gaps that cause deployed sites to not work in target environments:

1. Dataverse tables not in solution: added Step 5.3 to read .datamodel-manifest.json
   and add each custom table as ComponentType=1 (EntityDefinitions/MetadataId)

2. Site settings split: replaced all-or-nothing site settings prompt with category-based
   approach — Webapi/* and feature flags included by default, only OAuth secrets
   (ClientSecret, AppSecret, ConsumerSecret) excluded by default

3. Table permissions (powerpagecomponenttype=18) explicitly called out as always-included

Also added Patterns 12/13/14 to deployment-error-catalog.md covering these three failure
modes, and documented Dataverse table AddSolutionComponent pattern in solution-api-patterns.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* setup-solution: add site language, full manifest review, table schema note

- Site language (powerpagesitelanguage, componenttype ~10375) is NOT auto-included
  by AddRequiredComponents:true on the website — must be discovered and added
  explicitly. Confirmed against IdeaSphere: 10375 was missing from solution.

- New Step 5.4: present full manifest to user before writing anything — shows
  website, site language(s), all component categories with counts, site settings
  split (Web API / feature flags / auth config / OAuth secrets), and Dataverse
  tables. User can deselect categories or tables before proceeding.

- Table schema note: solutions carry entity definitions (columns, relationships,
  forms) but NOT data/records. Explicit callout added so users know data migration
  is a separate step.

- solution-api-patterns.md: added site language componenttype (~10375) to the
  known values table + discovery query pattern.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* setup-solution: multi-select OAuth secrets as env vars

Phase 5 now has a dedicated Step 5.4 asking which (if any) OAuth secret site
settings to convert to environment variables instead of just excluding them.
Uses AskUserQuestion with multiSelect:true listing each secret individually,
plus 'Convert all' and 'Exclude all' convenience options.

Converted secrets get environmentvariabledefinition records (type=Secret/100000003)
created and added to the solution as componenttype 380. Phase 7 summary includes
a UI linking checklist with direct Power Pages Management URLs for each converted
secret (same UI-only limitation as configure-env-variables skill).

Step numbering updated: 5.4=OAuth secrets, 5.5=full manifest confirm, 5.6=add components.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* setup-solution: always discover tables from env, include-all as first option

- Step 5.2D: always query EntityDefinitions directly (IsCustomEntity=true,
  IsManaged=false) rather than relying solely on .datamodel-manifest.json.
  Groups by publisher prefix, presents all custom unmanaged tables found.
  .datamodel-manifest.json is still checked but not the only source.

- Step 5.5 table multi-select: 'Include all N tables (Recommended)' is now
  the first option (pre-selected default), followed by individual table options,
  with 'Exclude all tables' last.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* setup-solution: automate OAuth secret linking + add deployment path choice

Phase 5.4: link site settings to env vars via OData PATCH (EnvironmentValue nav
property + if-match/clienthost headers) — no UI step needed (HAR-verified Fix 19).
Phase 7: AskUserQuestion for PP Pipelines (recommended) vs export/import vs later.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…activation check (#77)

* deploy-pipeline: proactively prompt for env var values before deploy
import-solution: set env var values in target after import (Phase 6b)

deploy-pipeline Phase 5 now queries solution for env var definitions,
diffs against deployment-settings.json, and prompts for any unconfigured
values before calling DeployPackageAsync — not just when validation flags them.

import-solution Phase 6b queries target for env var definitions that landed
without values and prompts the user to set them, including Secret type vars
that need target-specific values (e.g. different OAuth credentials per env).

* Add post-deployment site activation check to import-solution and deploy-pipeline

After a successful import or pipeline deploy, both skills now check whether
the Power Pages site is already activated in the target environment. If not,
the user is asked for consent and /power-pages:activate-site is invoked to
handle subdomain selection, confirmation, and provisioning.

- import-solution: Phase 6c queries for componentType 10374, runs
  check-activation-status.js (PAC already on target), and invokes
  activate-site if not provisioned.
- deploy-pipeline: Phase 7.5 does the same but temporarily switches PAC CLI
  to the target env via `pac env select`, then switches back to source.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
… per-stage activation (#87)

* Add ALM deployment plan skill, 16 shared lib scripts, and solution packaging improvements

- New `plan-alm` skill: 8-phase orchestrator that generates an HTML ALM plan
  (docs/alm-plan.html) with branched question flow for PP Pipelines vs Manual path,
  then executes the plan by invoking existing skills in sequence
- 16 new shared lib scripts in scripts/lib/ covering ALM prerequisites, solution
  management, PP Pipelines API operations, and solution export/download
- 129 unit tests (up from 69) covering all new scripts and the plan-alm validator
- Script wiring: setup-solution, setup-pipeline, deploy-pipeline, export-solution,
  hotfix-solution, configure-env-variables, import-solution all reference shared
  scripts instead of inline API prose
- Cloud flow detect-and-guide in import-solution (Phase 6c) and deploy-pipeline
  (Phase 7.8): detects type-29 workflow components and guides manual registration
- Post-deploy warnings in deploy-pipeline for connection reference binding and
  bot republishing when solution manifest includes cloudFlows or botComponents
- import-solution Phase 2 pre-import zip inspection: warns about connection
  references, bots, and hardcoded org URLs before import proceeds
- solution-api-patterns.md Section 8: documents BYOC vs traditional portal
  packaging differences for cloud flows and bots
- cicd-pipeline-patterns.md: Coming Soon markers on ADO and GitHub Actions sections
- Plugin version bumped to 1.2.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* plan-alm: reorder Phase 2 questions — solution always before strategy

Q1 is now always solution setup (confirm existing or include setup-solution).
Q2 is promotion strategy (PP Pipelines vs Manual). Path-specific questions
renumbered Q3–Q8 for both paths. Host env discovery uses discover-pipelines-host.js.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Surface plan-alm as natural next step across site lifecycle skills

- setup-solution: Phase 7 now offers plan-alm as the recommended next step
  (replaces direct route to setup-pipeline/export-solution, which bypassed the orchestrator)
- deploy-site: plan-alm added as first suggested next step after deployment
- activate-site: plan-alm added to next steps after site provisioning
- create-site: plan-alm added to later-stage next steps alongside setup-datamodel/add-seo

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix plan-alm surfacing: activate-site only, revert setup-solution

- setup-solution: reverted Phase 7 to original (PP Pipelines / Manual / Decide later)
  setup-solution is invoked by plan-alm so must not route back to it
- deploy-site: removed plan-alm from next steps (wrong lifecycle stage)
- create-site: removed plan-alm from next steps (wrong lifecycle stage)
- activate-site: plan-alm suggestion retained — correct handoff point after site goes live

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* deploy-pipeline: tabbed deploy history with solution contents; plan-alm: per-stage activate prompt

## deploy-pipeline
- Rewrote deploy-history-template.html with vertical sidebar tabs (Overview / Solution / Config & Notes) matching the ALM plan layout
- Overview tab: status banner, summary bar, deployment details table, site activation
- Solution tab: solution metadata (version change, type, publisher, component count), validation result badge, Dataverse table chips + bot components from .solution-manifest.json
- Config & Notes tab: env var overrides, AI deployment notes, post-deploy warnings
- Added __SOLUTION_META_ROWS__, __VALIDATION_SECTION__, __SOLUTION_CONTENTS_SECTION__, __PREV_ARTIFACT_VERSION__ placeholders to Phase 7.4
- validate-deploy-pipeline.js: now blocks if deployHistoryFile referenced in .last-deploy.json but not written to disk
- validate-deploy-pipeline.test.js: new test file (7 cases) covering all validator paths

## plan-alm
- Per-stage activation: changed Phase 3 task creation from one "Deploy via pipeline" task to one Deploy + one Activate task pair per target stage
- Phase 7 PP path now loops per stage: deploy → immediately prompt activation → next stage
- AskUserQuestion activation prompt fires right after each successful stage deployment, not deferred to end of plan
- planData steps use per-stage deploy+activate pairs (Deploy to Staging / Activate site in Staging / etc.)
- render-alm-plan.js + alm-plan-template.html: authNoValue rendering fixes, stageClass driven by deployStatus field

## setup-solution
- Phase 1 Step 5: pre-load site settings from .alm-plan-context.json if present
- Three-tier site settings classification (keepAsIs / promoteToEnvVar / authNoValue / excluded)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add solution-splitting decision tree and manifest v2

Introduces an advanced solution-recommendation layer for plan-alm +
setup-solution + setup-pipeline + deploy-pipeline, based on the spec
in design docs (solution-splitting-logic.md).

New shared libs (under plugins/power-pages/scripts/lib/):
- alm-thresholds.js: default thresholds + .alm-config.json merge
- estimate-solution-size.js: queries Dataverse metadata, estimates
  size/count/schema attrs/web file aggregates/env vars
- compute-split-plan.js: decision tree with priority Schema > Layer >
  Change-Frequency > Config Isolation (additive). Includes asset
  advisory generator (Azure Blob primary, CDN secondary).

Tests (23 passing): alm-thresholds.test.js, compute-split-plan.test.js
cover all five scenarios from the spec (Typical, Feedback Portal,
Prabhat 34x950, Brad 1400 envvars, component-heavy) plus advisory
edge cases.

Template + renderer:
- alm-plan-template.html: tabbed UX (Overview, Size, Advisory, EnvVars,
  Solutions, Pipelines, Execution). Matches reference design doc.
- render-alm-plan.js: rewritten to accept v2 planData fields
  (sizeAnalysis, assetAdvisory, proposedSolutions, breakdown) and
  render all new tabs.

Skill wiring (changes only — no behavior regressions for single-sol sites):
- plan-alm SKILL.md: Phase 1 Step 10 adds estimate + compute-split-plan
  calls. Phase 2 Q1b asks user to accept/override/advisory-first/migrate.
  Phase 3 planData extended with v2 fields.
- setup-solution SKILL.md: Phase 1 Step 6 detects .alm-split-plan.json
  and enters MULTI_SOLUTION_MODE. Phase 6 writes manifest v2 when
  multiple solutions are present.
- setup-pipeline SKILL.md: reads schemaVersion from manifest; creates
  one pipeline per solution in v2 mode. Writes .last-pipeline.json v2
  with pipelines[] array.
- deploy-pipeline SKILL.md: MULTI_PIPELINE_MODE loops through pipelines
  in order for the selected stage.

Sample renders (plugins/power-pages/skills/plan-alm/samples/):
- ideasphere-estimate.json: realistic IdeaSphere profile with 128 MB,
  5 large media files, 7 env vars.
- ideasphere-alm-plan.html: end-to-end sample generated via
  estimate -> compute-split-plan -> render pipeline. Decision tree
  correctly selects Strategy 1 (Layer Split) -> 2 solutions. All
  tabs populated with real classifier output.

Plugin version bumped 1.2.0 -> 1.3.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Apply PR #107 review fixes

Addresses code-review findings on the solution-splitting PR:

- AGENTS.md: remove duplicate "Solution Splitting Decision Tree" section
- alm-thresholds.js: rename classifyTier args to make strict-upper-exclusive
  bounds explicit (greenUpperExclusive, yellowUpperExclusive) + explain
- compute-split-plan.js:
  * isComponentHeavy no longer silently drops sites above hardFlagComponentCount;
    they now route to Strategy 2 with an error-type recommendation calling out
    the hard flag.
  * partitionByChangeFrequency derives sizeMB from count shares so size and
    componentCount stay self-consistent (was using independent 25/20/10/45%
    fractions that didn't match the count allocation).
  * partitionBySchema uses estimate.breakdown.tables when available instead of
    the hard-coded 50/50 heuristic; falls back with a rationale suffix.
- estimate-solution-size.js: exit 1 on auth failure (was exit 0 with empty
  estimate, which silently clobbered prior good output when callers redirected
  stdout to a file). Dedupe repeated `token || resolved` pattern.
- plan-alm SKILL.md Step 10: use tmp-file write pattern so a failed estimator
  preserves the previous good .alm-size-estimate.json / .alm-split-plan.json.
- deploy-pipeline SKILL.md: document partial-deploy risk for MULTI_PIPELINE_MODE
  (no automatic rollback; record per-pipeline status in .last-deploy.json).
- render-alm-plan.js: comment explaining the single-occurrence plan-status regex.
- .gitignore: add ALM split-plan intermediates (.alm-size-estimate.json(.tmp),
  .alm-split-plan.json(.tmp)).
- Tests:
  * New estimate-solution-size.test.js (7 tests) covering classifyPPCs buckets,
    empty input, estimateTotalSize math stability + web file additivity, and
    BYTES_PER sanity.
  * compute-split-plan.test.js adds hard-flag routing + error recommendation,
    Change-Frequency size/count consistency (sum ~ totalSizeMB ±0.5), and
    partitionBySchema breakdown-driven sizing.
- Sample: trailing newline on ideasphere-plan-data.json.

Three validation items deferred to a live Dataverse run on IdeaSphere dev:
OData GUID filter quoting in the estimator, end-to-end multi-solution flow,
and hand-authored sample vs. real estimator output. Tracked in auto-memory
(project_pr107_deferred_validation.md).

Tests: 34/34 ALM unit tests pass (23 pre-existing + 4 new compute-split-plan
+ 7 new estimate-solution-size). render-alm-plan.test.js failures are
pre-existing (the renderer was rewritten in this PR; its tests were not
updated) — tracked separately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…gesALM

# Conflicts:
#	plugins/power-pages/.claude-plugin/plugin.json
#	plugins/power-pages/AGENTS.md
#	plugins/power-pages/references/skill-tracking-reference.md
* Restore missing ALM skill files from PowerPagesALM merge

The merge commit 49c93ad registered the merge but failed to bring in
any file additions from users/nityagi/PowerPagesALM. This restores all
missing power-pages files: setup-pipeline, deploy-pipeline, export-solution,
import-solution, configure-env-variables, diagnose-deployment, hotfix-solution,
setup-solution SKILL.md files and their validation scripts, plus shared
reference docs and lib scripts added in the ALM branch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Add ALM-aware-by-default safeguards for Power Pages skills

Fixes the recurring bug where Dataverse records created by skills land in the
Default solution and never travel across environments — e.g. invoice-checker
server logic and crd50_auth_openauth_microsoft_clientsecret env var.

New shared modules:
- scripts/lib/discover-site-components.js — authoritative site inventory +
  diff against a solution (14 tests). Covers all 31 documented
  powerpagecomponenttype values.
- scripts/lib/resolve-target-solution.js — strict 3-step solution resolution
  (explicit arg > .solution-manifest.json > throw); never silently falls back
  to Default (13 tests).
- scripts/lint-skills-alm.js — CI gate: flags SKILL.md that POSTs to Dataverse
  without reading the manifest, scripts that write without importing the
  resolver, and new ppctype values missing from PPC_TYPE_LABELS (10 tests).

Skill updates:
- setup-solution: Phase 1 auto-detects .solution-manifest.json and branches to
  sync mode; Step 5.4b adopts orphan env vars with the publisher prefix.
- export-solution: new Phase 2.5 pre-export completeness check.
- deploy-pipeline: new Phase 3.5 pre-deploy completeness check.
- create-environment-variable.js: now uses the resolver + adds the created
  definition via AddSolutionComponent (fixes the crd50_auth_* orphan case).

Docs:
- AGENTS.md + CLAUDE.md: ALM-aware-by-default principle with the strict
  solution resolution order.
- PLUGIN_DEVELOPMENT_GUIDE.md: new "ALM Checklist for New Skills" section.

All 159 plugin tests pass; alm-lint reports 0 findings on the current tree.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Extend alm-lint coverage: CI workflow, PATCH/PUT verbs, allowlist, user docs

- .github/workflows/power-pages-alm-lint.yml: new CI job gates PRs that touch
  plugins/power-pages/** on `node lint-skills-alm.js` passing.
- lint-skills-alm.js: PROSE_WRITE_PATTERN now covers POST/PATCH/PUT so skills
  that mutate existing publishers/solutions are also gated. DELETE is
  intentionally excluded (resolver does not apply). Helper-name list extended
  (apiPatch, patchRecord, updateRecord).
- lint-skills-alm.js: adds .almlintignore allowlist support. Each entry is
  `<path-or-glob> <rule-name> <reason-text>` — unknown rules and missing
  reasons throw at parse time. Inline alm-lint-ignore comments still work
  for per-file exceptions; the file handles systemic patterns.
- plugins/power-pages/references/alm-prompts.md: new user-facing walkthrough
  of the sync-mode, orphan-adoption, and pre-export completeness prompts.
  Linked from README.md.
- Tests: +11 (verbs, allowlist parsing, glob matching, filtering) —
  175/175 pass, alm-lint: 0 findings on the current tree.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Address review feedback on ALM safeguards

Reject invalid solution unique names and publisher prefixes up front
instead of silently stripping non-alphanumeric characters — a typo like
"my-sol" no longer becomes a confusing "not found" result. Also guards
OData filters from injection earlier in the call path.

Replace regex-parsing of PPC_TYPE_LABELS in lint-skills-alm with a
direct `require()` of the sibling discover-site-components module, so
formatting changes (comments, multi-line entries) can't silently shrink
the known-set used by the non-waivable DISCOVER-coverage rule.

Normalize rule-name casing in both inline `alm-lint-ignore:` tags and
.almlintignore file entries — previously the inline matcher was
case-insensitive but the file matcher was case-sensitive, so the same
rule name could suppress one path but not the other.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…to-end (#119)

* plan-alm: 3 ALM planning refinements

1. Solutions tab surfaces an Asset Advisory callout. When compute-split-plan
   recommends 'externalize-media' (bulk media/web-file asset move), the
   Solutions tab now shows a top-of-section callout summarizing the candidate
   count + MB and linking to the Asset Advisory tab via the existing nav
   switch. Without this, reviewers approved splits that a CDN/Blob move would
   have made unnecessary.
   - render-alm-plan.js: new buildAssetAdvisoryCallout()
   - +3 tests covering visible, hidden (no recommendation), and disabled
     (feature off) cases.

2. Single Power Platform Pipeline with multiple stage runs instead of one
   pipeline per solution. A PP Pipeline can already deploy different solutions
   via stage runs that specify different artifactname/solutionid against the
   same stage — creating N pipeline records was wasteful and cluttered the
   Pipelines UI.
   - .last-pipeline.json bumps to schemaVersion: 3 with a single pipelineId +
     stages[] + deploymentOrder[{ solutionUniqueName, solutionId, order }].
   - setup-pipeline Phase 6b: creates ONE pipeline + one stage per target env,
     records deploymentOrder. v2 (legacy pipelines[]) path documented with a
     migration prompt on re-run.
   - deploy-pipeline: new MULTI_RUN_MODE loops over deploymentOrder, creating
     one stage run per solution against the same stageId. Legacy
     MULTI_PIPELINE_MODE kept for v2 manifests.
   - render-alm-plan.js Pipelines tab: "Deployment Pipeline" (singular),
     shows one pipeline header + a Deployment order block listing per-solution
     runs. Future buffer surfaces as "Skipped (empty)".
   - +2 tests covering multi-solution and single-solution paths.

3. Split plans now include a {Prefix}_Future solution reserved for new
   component additions. Without this, every new server-logic / flow / env var
   tended to end up crammed into the wrong layer solution and forced a
   re-plan.
   - compute-split-plan.js: new appendFutureBuffer() called after partition
     when the plan has ≥2 solutions. Single-solution plans skip the buffer
     (no partition to protect). Tagged isFutureBuffer: true, sized at 0 MB /
     0 components, componentTypes: ['Any'].
   - setup-pipeline skips stage-run creation for buffer solutions
     (status: 'skipped-empty'); deploy-pipeline short-circuits them in the
     loop so an empty Future does not fail the deploy.
   - +4 tests (single-solution skip, buffer shape, last-position + naming
     invariant across strategies, appendFutureBuffer unit-level).
   - 4 existing tests updated for the new length/ordering.

Totals: 326/331 tests pass across the plugin; 5 pre-existing failures from
PR #107 (render-alm-plan.test.js scenarios) are unrelated to this change.
alm-lint reports 0 findings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* plan-alm + estimator: pre-plan completeness check, OData filter verified

Item 28 — plan-alm: pre-plan completeness check.
  New Phase 1 Step 11 runs discover-site-components.js against the source env
  and diffs against the current solution. If gaps exist, prompts the user to
  either run /power-pages:setup-solution in sync mode before planning, plan
  anyway (records the gap as KNOWN_GAPS so Phase 3 surfaces it in the Risks
  section), or cancel. Matches the same check already enforced at export
  (export-solution Phase 2.5) and deploy (deploy-pipeline Phase 3.5) — catches
  the drift a plan-turn earlier so reviewers do not invest time in stale plans.

Items 1–3 — OData GUID filter in estimate-solution-size.js verified.
  Tested both quoted (`_powerpagesiteid_value eq '<GUID>'`) and unquoted forms
  against org1e98cc97 (v9.2 endpoint). Both return HTTP 200 with identical
  result sets. Current code uses the quoted form and continues to work; added
  a verification comment inline and updated
  memory/project_pr107_deferred_validation.md with the 2026-04-21 result.

Tests: 326/331 pass, alm-lint: 0 findings. The 5 remaining failures are
pre-existing PR #107 regressions in render-alm-plan.test.js, unrelated to
these changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* estimate-solution-size: fix EntityDefinitions \$top=500 (400 error)

EntityDefinitions endpoint returns HTTP 400 "query parameter \$top is not
supported" on v9.2. Verified 2026-04-22 against org1e98cc97 — the estimator
could never successfully discover custom tables via this path, which made
plan-alm unusable on any site that relied on metadata-based table discovery.

Replaced \$top=500 with \$filter=IsCustomEntity eq true. Filter is
supported on EntityDefinitions even when \$top is not, and it narrows the
payload to custom tables without a post-fetch client filter.

Inline comment added citing the 2026-04-22 live verification so the next
agent doesn't re-add \$top.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* estimator + plan-alm + integration tests: siteTotal vs inSolution + Phase 7.7 trigger

Three fixes from today's ALM validation run on SupplierInvoicePortal dev →
staging:

1. Estimator now distinguishes site-total from in-solution counts.
   --solutionId arg queries solutioncomponents for the specific solution and
   returns componentCountSiteTotal + componentCountInSolution + orphansOnSite.
   Fixes the 908-on-site/361-in-solution ambiguity that misled plan reviewers.
   Live verified on SupplierInvoicePortal (6bcbd1a2-…) in org1e98cc97:
   siteTotal=908, inSolution=361, orphansOnSite=547.

2. plan-alm Solutions tab renders a Solution membership banner when the
   estimator has both counts. Calls out orphan count and links to sync-mode.

3. deploy-pipeline Phase 7.7 activation trigger broadened.
   Old trigger required the current solution to contain a website
   componentType=10374. That missed the real-world case where the site
   exists pre-existing on the target and the solution being shipped is
   tables+flows+bots only. New trigger: run whenever powerpages.config.json
   has a websiteRecordId (i.e. this is a Power Pages project). Informational
   secondary check on the solution contents remains.

4. New integration tests at scripts/tests/integration/ use a real HTTP mock
   server (http.createServer) so network code paths, Authorization header
   propagation, and @odata.nextLink pagination are exercised end-to-end.
   Covers discover-site-components.js with 4 scenarios. 330/335 plugin tests
   pass; alm-lint: 0 findings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* estimate-solution-size: stop double-counting table attributes as components

Solution componentCount was inflated 2-3x on schema-heavy sites because the
formula added schemaAttrCount (every table column). In Dataverse, a table
with N columns is ONE solutioncomponents row of type 1 — attributes ride
along, they're NOT individual components.

Live evidence on SupplierInvoicePortal (org1e98cc97):
  Before: componentCount = 393 + 11 + 503 + 1 = 908
  After:  componentCount = 393 + 11 + 1 + 2   = 407
  Actual in solution (solutioncomponents query): 361
  Orphan delta = 46 (was falsely 547)

New formula: ppcs + tables + envVars + cloudFlows (workflow entities, not
the type-33 ppc binding which is already in ppcs). Bots and bot components
remain a known undercount bound; they're captured by the solutioncomponents
query (`inSolution.byComponentType`) when --solutionId is passed.

No test changes — formula previously had no unit-test coverage asserting
the number (only the fields existed). Added an inline comment mapping each
term to its componenttype so the next contributor won't re-add attributes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* estimate-solution-size: add site-scoped bot + botcomponent queries

Before: siteTotal formula skipped bots (type 10137) and bot components
(type 10193) because they aren't reachable from /powerpagecomponents. Live
data on SIP had 4 bots + 30 components in solution — none reflected in
siteTotal — making orphansOnSite look artificially small (46 instead of 78).

Root cause dig: type-27 "Bot Consumer" ppcs store the bot schema reference
in the `content` JSON (`{"botschemaname": "..."}`), NOT in the `name` field
(which is literally the string "Bot Consumer"). The first pass used `name`
and silently returned zero bots.

Now: fetch type-27 ppcs with content column, JSON.parse each, extract
botschemaname, then query bots by schemaname to stay site-scoped (avoids
folding env-wide bots from other projects into this site's total). For each
discovered bot, query botcomponents filtered by parent bot id.

Output gets two new fields: botCountScoped + botComponentCountScoped.

Live re-verification on SupplierInvoicePortal (org1e98cc97):
  siteTotal: 407 → 439 (+2 bots + 30 bot components)
  inSolution: 361 (unchanged — from solutioncomponents query)
  orphansOnSite: 46 → 78 (more accurate — math: 82 ppc orphans + 1 env var
    − 2 bot consumer dupes in solution − 2 solution-only bots − 1 website)

Real orphans on this site: 81 type-3 web files + 1 type-35 server logic +
1 env var = 83 solutionizable records. The 78 "net delta" reflects
accounting offsets for rows that exist in solution but can't be represented
in a site-scoped scan (bot consumer duplicate rows, website itself).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* estimate-solution-size: exclude Vite/Rollup bundle chunks + fix swapped picklist constants

Two bugs fixed:

1. classifyPPCs used WEB_FILE=2 and WEB_PAGE=4, but the authoritative picklist
   (MS Learn + PPC_TYPE_LABELS in discover-site-components.js) says type 2 is
   Web Page and type 3 is Web File. On SIP, webFileCount was reporting 12
   (actually Web Pages) while the site had 208 actual web files. Fixed to
   match the canonical enum; added the full ppc type lookup for completeness.

2. Bundle chunk noise excluded from both siteTotal and inSolution.
   `pac pages upload-code-site` leaves behind hash-suffixed .js/.css chunks
   every rebuild (Vite: Home-BPuZZDcA.js, index-DyzztwOp.js). They stay in
   Dataverse as powerpagecomponent rows linked to the site but aren't
   referenced by the current build's index.html. User feedback: "we do not
   care about random webfiles entries not actually associated with a pages
   site." Added isProbablyBundleChunk() heuristic (regex on hash-like
   suffix + .js/.mjs/.cjs/.css/.map extension); split classified.webFiles
   into webFiles (real content) + bundleChunks (noise).

   siteTotal now subtracts bundleChunks. inSolution subtracts the subset of
   bundleChunks that are in the solution via objectid intersection, so the
   comparison stays apples-to-apples.

Live on SIP (org1e98cc97, SupplierInvoicePortal solution):
                        before today   after #1   after #2 (real)
  siteTotal             908            439        231
  inSolution            361            361        233
  orphans               547 (bogus)    78         0 (real)
  bundleChunkCount      -              -          208 (on site)
  chunksInSolution      -              -          128

Real orphans dropped to 0 because this solution is in fact in sync with the
site's meaningful content; the earlier 82-orphan number was dominated by
81 Vite chunks that don't represent real missing content.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* estimate-solution-size tests: update to authoritative picklist + bundle-chunk heuristic

Two test updates for today's picklist + bundle-chunk fixes:

1. classifyPPCs test now uses the authoritative powerpagecomponenttype
   picklist values (type 3 = Web File, type 2 = Web Page, type 8 = Web
   Template, type 11 = Web Role). Prior test asserted against swapped
   constants, which masked the bug that caused SIP's web file count to
   show 12 when the site actually held 208.

2. New test for the isProbablyBundleChunk heuristic. Covers 7 chunk name
   patterns (Vite hash suffixes, Webpack dot-separated hashes, sourcemaps
   with `.js.map`) + 7 real static asset names (hero.jpg, logo.svg,
   favicon.ico, Inter-Regular.woff2, app.js) to prove no false positives.

Regex updated from `\.(?:js|mjs|cjs|css|map)$` to
`\.(?:js|mjs|cjs|css)(?:\.map)?$` to correctly match `.js.map`
sourcemaps whose penultimate segment is already an extension.

All 8 estimate-solution-size tests pass. alm-lint: 0 findings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* countSolutionMembership: add cross-site safety check

Defensive check for solutions that contain ppcs from a site other than the
one we're planning. Rare in practice (usually people build one solution per
site) but possible when someone manually stitches components across sites.
Silent miscounting is worse than a 1-query cost, so wire it up.

When estimator is called with both --solutionId and --websiteRecordId, pass
the set of ppc ids from the site to countSolutionMembership. The helper
filters solutioncomponents type-10373 rows by objectid, flagging any whose
id isn't in the site set. Returns crossSitePpcs[] + the output object gets
a human-readable crossSitePpcWarning or null.

Live check on SupplierInvoicePortal (org1e98cc97):
  solutioncomponents type 10373: 311 rows
  Of those, in site 2bea4a71-...'s ppc set: 311
  Cross-site miss: 0
  warning: null ✓ (healthy)

Separately confirmed that the 82 orphan ppcs on this site (81 Vite bundle
chunks + 1 server logic "invoice-checker") all live in the well-known
Active solution (fd140aae-4df4-11dd-bd17-0019b9312238) — i.e. they're in
Dataverse's default working-copy bucket, not a different user solution.

Test coverage: 2 new integration tests against the http-mock server cover
(a) flagging a cross-site row and (b) skipping the check when
sitePpcIdSet is null. 6/6 integration tests pass, 331/336 full suite
(5 pre-existing render-alm-plan failures + 1 env-dependent auth test,
unchanged).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* setup-solution: add Step 5.4c orphan ppc adoption (live-validated)

Today's per-type breakdown on SupplierInvoicePortal exposed a gap not
covered by the existing Step 5.4b: ppc rows (server logic, real pages,
etc.) that live on the site but aren't in the user solution.

invoice-checker server logic was the canonical case — created by
/add-server-logic in an earlier session, never registered into the user
solution, sat in Active. Adopted live today (AddSolutionComponent
componenttype 10373); solution bumped 1.0.0.1 → 1.0.0.2.

Step 5.4c mirrors 5.4b but for powerpagecomponent rows:

  - Calls discover-site-components.js for missing.powerpagecomponents
  - Partitions orphans: real-content vs. bundle-chunk (Vite/Rollup hash)
  - Bundle chunks are reported but NOT prompted — they're stale build
    artifacts, not real content
  - Real-content orphans deduped by (type, name) — older stale dupes
    (e.g. a second index.html from a superseded build) excluded from
    the adoption prompt
  - AskUserQuestion multiSelect for real orphans
  - AddSolutionComponent per selection with componentType: 10373 and
    AddRequiredComponents: false

Documented API gotcha: DoNotIncludeSubcomponents: true returns HTTP 400
0x80040216 for non-Entity components. Leave it unset for type-10373.

Also updated:
  - Sync-mode description to list 5.4c in the step sequence
  - Manifest tag list to include `[ADOPTED ppc — was in Active only]`
  - Phase 5.6 bulk-add list to include adopted orphan ppcs as entry #8

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* estimate-solution-size: address PR #119 review nits

Three nits from the review agent, all addressed:

1. **Cloud-flow counting comment was misleading** — reviewer's concern was
   that `+ classified.cloudFlowLinks.length` appeared to double-count the
   type-33 ppc bindings already in ppcs.length. The code is actually
   correct (cloudFlowLinks.length serves as the workflow count since each
   type-33 binding pairs 1:1 with a type-29 workflow), but the comment
   didn't make that clear. Rewrote the block comment so future readers
   don't stumble on the same concern.

2. **Dropped `componentCount` alias, kept only `componentCountSiteTotal`** —
   per user: "we do not need backward compatibility since ALM skills are
   under active development on a feature branch." compute-split-plan.js
   now reads `estimate.componentCountSiteTotal` (11 call sites updated);
   compute-split-plan.test.js fixtures renamed too (10 call sites).
   render-alm-plan.js was already on the new name — no change needed.

3. **Bot pagination WARN on ceiling** — discoverBotsAndComponents emits a
   stderr WARN when bots.length hits BOTS_MAX_PAGES * BOTS_TOP (1,500) or
   botComponents.length hits COMPS_MAX_PAGES * COMPS_TOP (25,000). These
   caps are unlikely in any real tenant, but silent truncation is worse
   than a visible warning. Constants extracted so the ceiling is easy to
   bump in place.

333/338 tests pass; alm-lint: 0 findings. Live re-check against
SupplierInvoicePortal org1e98cc97:
  siteTotal: 231
  inSolution: 234 (+1 from earlier 233 — invoice-checker adopted)
  orphans: 0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* estimate-solution-size: stop subtracting bundle chunks from inSolution.total

Live validation against SupplierInvoicePortal in dev surfaced that we were
under-counting in-solution components by 128 (the bundle-chunk ppcs). Bundle
chunks that were added to the solution via AddSolutionComponent ARE legitimate
members — they ship with the managed export — so stripping them from the
in-solution count produced numbers that didn't match the Maker UI.

Power Platform UI shows SIP's solution at 362 (= 311 site components + 11
tables + 1 site record + 1 site language + 4 connection references + 2 cloud
flows + 2 agents + 30 agent components). Estimator now matches exactly.

Changes:
- inSolution.total: raw row count, no bundle-chunk subtraction.
- componentCountSiteTotal: raw site inventory (bundle chunks included).
- componentCountSiteActionable: new sub-count that strips bundle chunks for
  the "actionable content" view.
- orphansOnSite: computed from the actionable count so stale upload artifacts
  don't get flagged as content gaps.
- Component-type comment block rewritten with the authoritative
  solutioncomponents enum (10137=Connection Reference, 10192=Bot,
  10193=Bot Component). Earlier comments had these swapped.
- render-alm-plan: headline count prefers componentCountInSolution when
  available (that's what the pipeline ships); falls back to siteTotal.
- Membership banner wording clarified — raw rows vs. owned components,
  "actionable orphan" terminology.

All 40 targeted tests pass. The 5 pre-existing render-alm-plan test
failures (approval-gate / env-var / solutionContents scenarios) are unchanged
— unrelated to this commit and present on the baseline too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* estimate-solution-size: note that solutioncomponent types are dynamic per tenant

Two nits from review:

1) The componenttype block I rewrote yesterday presented 1/29/380/10137/10192/
   10193/10373/10374 as if they were stable constants. They are not — the
   Dataverse solutioncomponent.componenttype picklist is dynamic per tenant
   and AddSolutionComponent callers are expected to resolve values at runtime
   via scripts/lib/discover-component-types.js. (This is already documented
   in MEMORY.md but my rewrite didn't reflect it.)

   countSolutionMembership() in this file was already resolver-free and does
   the right thing (tallies whatever values Dataverse returns). Only the
   comment block needed to stop implying the integers are universal.

2) Added a concrete derivation of how siteTotalComponents evaluates to 439
   for the live SIP reference site, plus the note that connection references
   (4) and the website record itself (1) are not included — that's why the
   diff against inSolution is ~77 rather than exactly matching the 82 orphans
   originally reported.

Also fixed the bot-discovery doc block: same "dynamic, not static" caveat
plus pointer to discover-component-types.js for mutation paths.

40/40 targeted tests still pass.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bring in latest main through b19e385:
- canvas-mcp project-level MCP config fix (#122)
- code-apps versioning requirements removal (#121)
- mcp-apps plugin added with generate-mcp-app-ui skill
- shared report-issue skill added across plugins
- astro ^6.1.6 (XSS fix in define:vars) (#116)
- create-site preview logo update (#117)
- canvas-apps wizard input experience (#110)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s from first review (#126)

* skill-tracking: add to 6 ALM skills + extend reference table

Every Power Pages skill must record usage via update-skill-tracking.js in
its final phase (per plugins/power-pages/CLAUDE.md convention). Six ALM
skills were missing the tracking call:

- setup-solution (SetupSolution)
- export-solution (ExportSolution)
- import-solution (ImportSolution)
- hotfix-solution (HotfixSolution)
- diagnose-deployment (DiagnoseDeployment)
- configure-env-variables (ConfigureEnvVariables)

Each now has a "Record Skill Usage" sub-step at the end of its final
phase using the pointer pattern (> Reference: ...skill-tracking-reference.md).

references/skill-tracking-reference.md: added the 8 missing PascalCase
mappings (the 6 above plus setup-pipeline / deploy-pipeline, which
already had the invocation inline but were absent from the table).

Coverage: 23 / 24 power-pages SKILL.md files now record usage; only the
shared report-issue wrapper is intentionally excluded (cross-plugin
shared skill that does not create site artifacts).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* plan-alm: 5 UX fixes from first end-to-end review

1. Site identity: prefer .powerpages-site/website.yml (source of truth
   when run from a site root) over powerpages.config.json. The config
   file is kept only as a fallback for plugin-dev scenarios. Reads
   `id` → websiteRecordId and `adx_name` → siteName from the YAML.

2. Plan presentation: render docs/alm-plan.html AND open it in the
   default browser BEFORE prompting for approval. Previously the HTML
   was rendered but users had to approve based on the inline CLI
   summary alone; they couldn't see the full plan (size gauge, signal
   cards, per-solution breakdown) until after approving. Cross-platform
   launch via Node.js — silent + non-blocking.

3. Size gauge: drop the inline fill label when fillPct < 15%. For small
   sites (e.g. 4.9 MB / 95 MB ≈ 4% fill), the "4.9 MB" label inside the
   narrow pill overflowed and rendered as a floating chip that read
   like a different number. The headline size-gauge-value already
   shows the exact MB on the right, so no information is lost.

4. Remove the "Scale data — what we've tested" block from the Size tab.
   Static reference numbers (1k-2k typical, 6k tested, 10+ hour extreme
   case) weren't actionable for reviewers assessing THEIR plan; the
   tier classification on signal cards already conveys where they are.

5. Approved-by capture: Phase 4 options 1 and 2 now capture the approver
   via `git config user.name` (fallback to USER/USERNAME env var, then
   AskUserQuestion), and stamp both `<span id="approved-by">` and
   `<span id="approval-date">` in the HTML. Previously Approved-by was
   left blank in the audit trail.

Test suite: 14 pass / 5 fail — same counts as before these edits
(failures pre-existed and are unrelated to this change).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* plan-alm: delete samples/ folder + harden browser-open to PowerShell

Samples folder removal:
The plugins/power-pages/skills/plan-alm/samples/ folder shipped synthetic
IdeaSphere fixtures and a pre-rendered demo HTML (~75 KB total). Nothing
in the repo referenced it — grep across scripts/tests/ came back empty.
It was design scaffolding, not regression-protected, and actively
misleading after today's template changes (the pre-rendered HTML still
contained the old "Scale data" block + the broken size-gauge label).
Real demos should use live projects (e.g. C:\Projects\supplier-invoice-portal)
where docs/alm-plan.html is always current.

Browser-open command hardening (Gap 2 follow-up):
First pass used `cmd /c start "" <path>` via Node spawn with detached:true.
That command exits 0 on sandboxed terminal hosts without actually
launching a browser — the GUI spawn gets suppressed silently. Switched
to platform-specific launchers that respect file associations and are
resilient:
- Windows: `powershell.exe Start-Process` (reliable regardless of parent shell)
- macOS:   `open <path>`
- Linux:   `xdg-open <path>`

Also print a file:// URL alongside the spawn so if the auto-open fails
(headless environment, SSH session, restricted terminal), the user has
a direct clickable link. Updated the user-facing message to include the
URL as a fallback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…del fix (#134)

* ensure-pipelines-host: Slice A — detection scripts + plan SKILL.md

5 new helpers under scripts/lib + 57 unit tests + plan-only SKILL.md:
- check-env-host-binding.js — POST GetOrgDbOrgSetting('ProjectHostEnvironmentId')
- resolve-env-by-id.js — BAP env GET with $expand; 404-ambiguous handling
- list-tenant-envs.js — tenant-wide enum + per-env Pipelines probe (Production
  sku default, name-hint+admin-perms ranking, --firstHitWins, --includeName)
- verify-host-readiness.js — WhoAmI + solutions filter (covers both presence
  and version capture in one query)
- ensure-pipelines-host-detect.js — orchestrator wrapper that runs cache
  fast-path + Phase 2 resolution order + Phase 5 verify; emits
  .last-host-check.json schema with actionTaken: "none"

Resolution order mirrors ProjectHostProvider.tsx; produces 8 status values
including AvailableUsingPlatformHost / AvailableUnboundCustomHost /
MultipleUnboundCustomHosts / PlatformHostExistsUnbound / CannotRedirect.

Live-validated against SIP (org1e98cc97 → pascalepipelineshost):
  resolutionStatus: AvailableUsingCustomHost
  pipelinesSolutionVersion: 9.1.2026034.260325188
  ready: true (7.8s first run, 5.4s on cache hit)

Recon findings recorded in SKILL.md Open Items 1, 3, 10:
- Pipelines solution uniquename = msdyn_AppDeploymentAnchor (live-confirmed)
- BAP env-list never returns linkedEnvironmentMetadata.templates (0/1000 envs)
- deploymentpipelines?$top=0 returns 400 — switched to solutions filter

Slice B (provision-custom-host + SKILL.md execution wiring) and Slice C
(setup-pipeline delegation) follow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ensure-pipelines-host: Slice B (provisioning + validator) + Slice C (setup-pipeline delegation)

Slice B — Custom Host fast-path provisioning:
- provision-custom-host.js — POSTs BAP env-create with the D365_ProjectHost
  template (eng.ms-confirmed: same template PPAC's New custom host button uses
  internally). Polls the lifecycle op URL with Retry-After-aware backoff;
  handles multiple response shapes (properties.provisioningState / state /
  status.code); 5xx-transient retries; 401/403 with explicit guidance.
  Falls back to a direct env GET when the lifecycle op response lacks
  linkedEnvironmentMetadata. Test suite covers 14 scenarios including timeout,
  Failed terminal state, transient 5xx, and synthetic-clock timeout.

- skills/ensure-pipelines-host/scripts/validate-ensure-host.js —
  Stop-hook validator. Accepts schemaVersion 1 or 2; treats CannotRedirect /
  OrgSettingStale / PermissionDenied as documented terminal-error states
  (skill ran successfully even though host isn't usable). Blocks on missing
  required fields, unknown resolutionStatus, ready=false on non-terminal
  status, or ready=true with finalHostEnvUrl missing. 15 tests.

- powerpages-hook-utils.js — registers ensure-pipelines-host in TRACKED_SKILLS
  so the central PostToolUse hook routes the validator on skill stop.

- ensure-pipelines-host/SKILL.md — drops PLAN-ONLY banner; scope note now
  reads as the executable contract.

Slice C — setup-pipeline delegates to ensure-pipelines-host:
- skills/setup-pipeline/SKILL.md Phase 1 step 4 rewritten. The old
  discover-pipelines-host-only call (which only checked tenant-default-custom-
  host) is replaced with ensure-pipelines-host-detect.js — the orchestrator
  wrapper that runs the full resolution order. Branches on resolutionStatus:
    AvailableUsing*      → use directly, continue
    *Unbound* / NoHost   → delegate to /power-pages:ensure-pipelines-host
    CannotRedirect /     → stop with admin-resolution guidance
    OrgSettingStale /
    PermissionDenied
  Phase 3 host-confirmation prompt updated — no more "NOT DETECTED — please
  provide" fallback; HOST_ENV_URL is always populated by Phase 1 (or the
  skill stops before reaching Phase 3).

Cumulative test count: 132 (5 Slice A test files + 2 Slice B test files +
existing regression suites: discover-pipelines-host, validate-deploy-pipeline,
powerpages-hook-utils, lint-skills-alm).

Live-validated against SIP (org1e98cc97):
  ensure-pipelines-host-detect.js → marker → validate-ensure-host.js → exit 0
  Missing marker → validate-ensure-host.js → exit 0 (graceful)

NOT YET LIVE-TESTED: provision-custom-host.js end-to-end (would create a real
Custom Host env in tenant — needs explicit user authorization before that
acceptance test). Unit tests cover all branch paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* plan-alm host integration + ensure-pipelines-host SKILL polish

plan-alm — wires the new ensure-pipelines-host detection into the
orchestrator so the generated docs/alm-plan.html reflects host state
before the user approves the plan.

- SKILL.md Phase 1 step 12 (NEW): runs ensure-pipelines-host-detect.js
  after the completeness check; acquires BAP_TOKEN; tmp-file-then-mv
  pattern; skips when PIPELINE_DONE=true; on detect failure sets
  HOST_RESOLUTION = { status: 'DetectionFailed' } and continues.
  Maps wrapper output (resolutionStatus / finalHostEnvUrl /
  isPlatformHost) into the plan-alm HOST_RESOLUTION shape with
  hostType = isPlatformHost ? 'platform' : (finalHostEnvUrl ? 'custom'
  : null).

- SKILL.md Phase 2 Q4 (REWRITTEN): 7-row branch table on
  HOST_RESOLUTION.status. AvailableUsing*/Unbound*/PlatformHost*
  populate HOST_ENV_URL and set WILL_ENSURE_HOST / WILL_PROVISION_CUSTOM
  flags as appropriate. CannotRedirect blocks the skill. OrgSettingStale
  / PermissionDenied / DetectionFailed fall back to manual entry with
  pac env list pre-fill.

- SKILL.md Phase 3 planData: new hostResolution block with the 9 spec
  fields. Documented field-by-field mapping inline so the orchestrator
  can populate it deterministically.

- SKILL.md Phase 3 risks: 5 new entries (NoHost,
  AvailableUnboundCustomHost, MultipleUnboundCustomHosts,
  PlatformHostExistsUnbound info-level; CannotRedirect warning).

- SKILL.md Phase 4 inline summary: adds "Pipeline host: {url} ({status})"
  line, with "Will be ensured during setup-pipeline" variant when
  willEnsureDuringExecution: true.

- render-alm-plan.js: buildHostCardHtml emits three modes
  (host-card-ok green for AvailableUsing*, host-card-pending yellow
  for willEnsureDuringExecution, host-card-blocked red for
  CannotRedirect). buildHostChecklistSubBullet emits an indented
  substep under the Setup pipeline checklist item only when the host
  will be ensured at execution time. Both wired into the replacements
  map.

- alm-plan-template.html: __PIPELINES_HOST_CARD__ placeholder above
  the existing pipeline stages diagram; __HOST_CHECKLIST_SUBSTEP__
  placeholder after __CHECKLIST_HTML__. CSS for .host-card-ok /
  -pending / -blocked plus .checklist-substep-list / .checklist-substep
  / .substep-note.

- render-alm-plan.test.js: +7 tests covering all three card modes,
  MultipleUnboundCustomHosts candidates count, no-render when
  hostResolution is absent, substep render path, substep omission
  when host already established. 26 tests / 21 pass / 5 fail —
  same 5 baseline failures as HEAD (pre-existing, unrelated to
  this work; verified via git stash baseline run).

Spec ambiguities resolved (worth review):
- DetectionFailed not in spec's 9-status list but spec says use it
  on non-zero exit. Added to status enum.
- Renderer emits checklist items as <div class="checklist-item">,
  not <li> — substep wrapped in <ul class="checklist-substep-list">
  so it's valid HTML when slotted directly into the template.
- Used --high-border for pending color (--warning-border doesn't exist
  in the palette).

ensure-pipelines-host SKILL.md polish:

- Drops "Scripts to be Implemented" stale header → "Scripts" with all
  6 helpers as shipped (table now reflects --includeName, --firstHitWins,
  --maxEnvsToProbe defaults, ensure-pipelines-host-detect.js wrapper row).

- Integration section: setup-pipeline and plan-alm both marked ✅ wired
  with concrete invocation snippets and behavior summaries.

- Phase 1.0 cache fast-path probe corrected from
  deploymentpipelines?$top=0 → solutions?$filter=uniquename eq
  'msdyn_AppDeploymentAnchor'&$top=1 (cheap-probe correction validated
  against pascalepipelineshost 2026-04-28; deploymentpipelines?$top=0
  returns 400 even on a working host). Threat-model and progress-table
  rows updated to match.

- Validation script section: registration mechanism corrected from
  hooks/hooks.json to TRACKED_SKILLS in
  scripts/lib/powerpages-hook-utils.js (the actual mechanism after
  Slice B added the skill there).

Live-validated end-to-end:
  Smoke render with synthetic AvailableUnboundCustomHost data →
  host-card-pending div present (4 hits incl CSS), checklist-substep
  present (4 hits incl CSS), HTML well-formed.

Cumulative test count: 428 (423 pass + 5 pre-existing failures, all
in render-alm-plan.test.js, none introduced by this work).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ensure-pipelines-host: PAC-CLI shim for BAP — works in tenants where Az→BAP fails

Adds --source pac|bap|auto (default auto) to the detection scripts so they
work in tenants where BAP rejects Az-CLI-acquired tokens with 401
InvalidAuthenticationToken (verified 2026-04-28 against
D365DemoTSCE53051106 demo tenant — token claims show correct user/tenant/
audience but BAP still rejects; PAC CLI works for the same user because it
uses a different first-party client ID with implicit BAP grants).

New file:
- scripts/lib/pac-bap-shim.js — wraps `pac admin list --json` into
  BAP-shaped env list. Maps PAC fields to BAP fields (EnvironmentId →
  name, Type → environmentSku, EnvironmentUrl → instanceUrl, etc.).
  Derives instanceApiUrl by inserting ".api" before crmN.dynamics.com.
  Fields not provided by PAC (tenantId, lastModifiedTime, permissions,
  isManaged) returned as null — none are critical for host detection.
  PAC doesn't list Platform-sku envs, so PE detection still requires
  source=bap with a working BAP token.

Updated scripts:
- list-tenant-envs.js — adds --source flag. auto (default): try BAP first,
  fall back to PAC on 401/403. pac: skip BAP entirely. bap: existing
  behavior. Output adds sourceUsed + fallbackReason.
- resolve-env-by-id.js — same flag pattern. PAC mode filters the full
  list by EnvironmentId. On PAC-not-listed returns
  { found: false, reason: "not-in-pac-list" }.
- ensure-pipelines-host-detect.js — propagates --source through to
  the helpers. --bapToken now optional in auto/pac modes.

Tests:
- pac-bap-shim.test.js (NEW) — 20 tests covering field mapping, URL
  derivation, JSON parsing edge cases (header prose, malformed JSON,
  exec errors).
- Updated existing tests for the new auto-mode semantics: tests that
  previously expected "throws when bapToken missing" now use
  --source bap explicitly.
- New tests for source=pac, source=auto with 401 fallback to PAC,
  surfaces original error when both fail.

Live-validated against new tenant (admin@D365DemoTSCE53051106):
- --source pac: detected Stage-1 + Stage-2 as eligibleForAppInstall in
  14s, 0 BAP calls.
- --source auto with rejected BAP token: BAP returns 401, falls back to
  PAC, returns same result in 7.4s.

Cumulative test count: 186 (181 pass, 5 pre-existing render-alm-plan
failures unrelated to this work). +28 net new tests in this commit.

Auth strategy section added to SKILL.md documenting the PAC-first-with-
BAP-fallback approach. Provisioning (provision-custom-host.js for
D365_ProjectHost env-create) still requires BAP — that's the one path
where Az tokens are needed; documented explicitly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* PP Pipelines helpers: fix unprefixed schema (msdyn_ format never shipped)

The two PP Pipelines API helpers used msdyn_-prefixed property names in
their POST bodies (e.g., msdyn_name, msdyn_url, msdyn_type, msdyn_pipelineid
@odata.bind, msdyn_targetenvironmentid@odata.bind). Live testing on
2026-04-28 against the shipped Pipelines version (msdyn_AppDeploymentAnchor
v9.1.2026034.260325188 in the D365DemoTSCE53051106 demo tenant) revealed
that the actual deploymentenvironment / deploymentpipeline / deploymentstage
entities use **unprefixed** field names — the msdyn_ prefix is rejected
("Invalid property 'msdyn_name'").

Per references/power-pipeline-skill-reference.md, the unprefixed schema is
the canonical format. The helpers had diverged from the spec — likely
authored from early-preview HARs that used msdyn_ before ship.

Fixed:
- create-deployment-environment.js: msdyn_name/msdyn_url/msdyn_type →
  name/environmentid/environmenttype. The semantics also changed:
  msdyn_url was the URL string; environmentid is the BAP env GUID. Helper
  now requires --bapEnvId (BAP env GUID); --environmentUrl is optional and
  used only for marker output. New required arg --environmentType
  (200000000 Dev / 200000001 Target). Idempotency added: probe by
  environmentid first, return existing record with reused: true.

- create-deployment-pipeline.js: msdyn_name → name, msdyn_description →
  description. Source binding changed from PUT msdyn_sourceenvironment/$ref
  to POST deploymentpipeline_deploymentenvironment/$ref (M2M relationship,
  not single-valued nav). Stage @odata.bind props:
    msdyn_pipelineid@odata.bind     → deploymentpipelineid@odata.bind
    msdyn_targetenvironmentid@od    → targetdeploymentenvironmentid@od
  Dropped msdyn_order (unprefixed schema doesn't use it; no equivalent).
  API version v9.2 → v9.1. Idempotency added: reuse existing pipeline by
  name, reuse existing stage by name+pipelineId, skip $ref if already
  associated.

Other 3 PP Pipelines helpers were already on unprefixed schema:
  create-stage-run.js, poll-validation-status.js, poll-deployment-status.js.

Tests rewritten to match new schema. 11 tests in the two affected suites
(was 9). Existing tests asserted on body fields with msdyn_ names — those
assertions inverted (assert msdyn_X is undefined). 458 total tests pass
across the plugin (5 pre-existing render-alm-plan baseline failures
unchanged, unrelated to this work).

Live-validated against the byoc-demo-site project on Stage-1
(host: org5fbe4359.crm5.dynamics.com): both helpers reused the records we
created via inline cjs scripts during the E2E run, sub-second response,
correct field shape. The temporary _setup-solution.cjs / _create-deployment-
envs.cjs / _create-pipeline.cjs / _deploy-pipeline.cjs scripts in the
byoc-demo-site project root are no longer needed and were already cleaned up
post-E2E.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix-blocked-attachments: helper + setup-pipeline Phase 4 preflight

Adds fix-blocked-attachments.js helper and integrates it into the
setup-pipeline skill's Phase 4 preflight check.

Root cause: fresh Dataverse environments block .js file attachments by
default (the blockedattachments org setting). Power Pages code sites emit
.js in their compiled output (Vite → dist/assets/index-XXX.js). This
causes two failure modes discovered during E2E on 2026-04-29:
  1. pac pages upload-code-site → AttachmentBlocked on the SOURCE env
  2. deploy-pipeline → ImportSolutionAsync → AttachmentBlocked on TARGET env

Previously fixed manually (pac env update-settings) per-env per-session.
Now automated and integrated into the pre-pipeline preflight.

scripts/lib/fix-blocked-attachments.js (NEW):
  - Reads blockedattachments via pac env list-settings
  - Removes specified extensions (default: js) from the semicolon list
  - Applies via pac env update-settings
  - Accepts --envUrl for targeting specific env (default: current active)
  - Accepts --dry-run to report without applying
  - execImpl injection for tests (avoids module-cache mocking issues)
  - 7 tests covering: remove single/multi extension, no-op, dry-run,
    --environment arg passthrough, pac error, missing setting

skills/setup-pipeline/SKILL.md Phase 4.4 (NEW preflight check):
  - Runs fix-blocked-attachments.js --dry-run on source AND all target envs
  - Surfaces wasBlocked extensions to user with an AskUserQuestion consent gate
  - If approved, re-runs without --dry-run to apply
  - Non-blocking if user declines (recorded as a warning)

Validated live against D365DemoTSCE53051106 tenant:
  - Stage-1 (source) already clean (fixed in prior session) → no-op
  - Stage-2 (target) also clean → no-op
  - Helper correctly parsed pac output and reported changed: false

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* create-deployment-pipeline: detect existing pipelines by source+target wiring

When the user requests a pipeline with a different name but the same source
+ target env wiring as an existing pipeline, our helper previously created a
duplicate pipeline. Caught during E2E on 2026-04-29 in D365DemoTSCE53051106
tenant: previous session created "BYOC Demo Site Pipeline" (Stage-1 →
Stage-2); current session created "BYOC Supplier Portal Pipeline" with the
same wiring — duplicates that should have been deduplicated.

Adds `findExistingPipelineByWiring`:
- Lists all pipelines on the host
- For each, checks deploymentpipeline_deploymentenvironment M2M for source
  match against requestedSourceDeId
- For each, checks deploymentstages for stages whose
  _targetdeploymentenvironmentid_value matches every requestedTargetDeId
- Returns the first pipeline with matching source AND all targets covered

Wired into createDeploymentPipeline:
- Step 1 first tries name match (existing behavior)
- If no name match, tries wiring match (NEW)
- If wiring match found, the pipeline is reused (no POST), the source $ref
  binding is verified (idempotent — POST only if not already there), and
  stages are matched by targetDeploymentEnvironmentId rather than name (so
  "Deploy to Staging" requested matches existing "Old Stage Name" if both
  target the same env)

Output schema additions:
  reused: bool                       // true when matched by wiring
  reusedByWiring: { originalName, requestedName } | null
  stages[i].reusedFromWiringMatch: bool

When reused, pipelineName in output is the ORIGINAL name (so callers writing
.last-pipeline.json get the truth, not the requested name).

Tests: 2 new (reuses by wiring with different name; does not reuse when
target envs differ). 7/7 pass total in this suite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* plan-alm: integrate test-site into Phase 7 (PP path)

After each stage's deploy + activate, run /power-pages:test-site against
the activated URL and capture per-stage results in planData.siteTests.
Failures are non-blocking — they surface as a yellow/red badge in the
HTML plan but do not fail the run.

Changes:
- Phase 7 Step C: URL resolution rules (.last-deploy.json siteUrl ->
  activate-site URL -> skip), siteTests[stageName] capture shape, and
  3-value runOutcome mapping (passed / passed-with-warnings / failed)
- Phase 3: added "Test site in {stageName}" task per stage triplet
  + Progress Tracking row + status-warning checklist style
- render-alm-plan.js: buildSiteTestsSection() renders a per-stage table
  with pass/fail counts, console errors, and a colored Outcome badge.
  Empty state when siteTests absent or all-null.
- alm-plan-template.html: __SITE_TESTS_SECTION__ placeholder on the
  Pipeline tab + CSS for site-tests-card and the test-result-* badges
- 3 new tests: populated stage -> green badge, absent/all-null ->
  empty-state note, runOutcome failed -> red badge with failure counts.
  Tests now 29 (24 pass, 5 pre-existing baseline failures unchanged).

Manual path is intentionally untouched — auto-test runs only on the
PP Pipelines path, mirroring the existing optional-activate pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* plan-alm: highlight the active pipeline on the Pipelines tab

Adds a planData.pipelineMeta block (read from .last-pipeline.json +
.last-deploy.json) and renders it on the Pipelines tab so reviewers
can see at a glance which pipeline is actually moving configurations
for this project.

Renderer changes (buildPipelinesHtml):
- Use pipelineMeta.pipelineName as the displayed name when present
  (falls back to the synthesized {SITE_NAME}-Pipeline for fresh plans)
- ACTIVE chip next to the pipeline name when isActive is true
- "Last run:" footer with version, status (color-coded), stage name,
  timestamp, and component count when lastDeploy is set
- "Reused — matched on source+target wiring" annotation when the
  pipeline was deduped against an existing one (surfaces both the
  original name and the requested name so the rename is traceable)
- Single-solution path now also renders a header when pipelineMeta is
  present — previously it had no header at all

SKILL.md changes:
- Document pipelineMeta block shape + the inline node snippet that
  populates it from .last-pipeline.json and .last-deploy.json
- Phase 6 (setup-pipeline complete): refresh pipelineMeta and
  re-render so the ACTIVE chip appears as soon as the pipeline exists
- Phase 7 Step A (deploy complete): refresh pipelineMeta.lastDeploy
  and re-render so the last-run footer reflects the latest deploy

Tests: 3 new in render-alm-plan.test.js — pipelineMeta absent (no chip,
no footer), pipelineMeta with isActive (name + ACTIVE chip + last-run
footer), and reusedByWiring (original + requested names + rationale).
Suite is now 32 tests, 27 pass, 5 pre-existing baseline failures
unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* plan-alm: dedicated Validation tab with per-stage sub-tabs

Replaces the small siteTests summary card on the Pipelines tab with
a full Validation tab modeled on the design mock at OneDrive ALM Plan
mock document — sidebar nav entry, sub-tab per target stage, per-stage
summary grid, categorized test cards with severity + status badges,
expand/collapse details, and a Steps + Expected + Actual layout.

Data model: planData.siteTests (rolled-up counts) is replaced by
planData.validationRuns (full categorized findings, identical shape to
the new .last-test-site.json marker that test-site writes). Plan-alm
Phase 7 Step C ingests .last-test-site.json verbatim into
validationRuns[stageName] and re-renders, so the Validation tab updates
immediately after each stage's run.

Renderer (render-alm-plan.js):
- buildValidationTab(d) produces the full tab body (sub-tab bar +
  vstage-pane per stage). Stages with no run get a "Not yet tested"
  empty state so reviewers still see the planned scope.
- buildValidationStagePane / buildValidationCategory /
  buildValidationTestCard render the categorized cards with severity
  pills, automated/manual type badge, PASS/FAIL/SKIP status badge,
  description, steps list, expected block, and an actual block (red
  variant when the test failed).
- buildValidationNavBadge — sidebar nav badge: "OK" (green) when all
  runs are clean, otherwise the count of critical+high failures.

Template (alm-plan-template.html):
- New sidebar nav button with __VALIDATION_NAV_BADGE__ placeholder.
- New tab-validation section containing __VALIDATION_TAB__.
- Removed the obsolete __SITE_TESTS_SECTION__ from the Pipelines tab.
- ~50 lines of new CSS for .subtab-bar, .subtab-btn, .subtab-status-*,
  .vstage-pane, .vstage-header, .test-summary-grid, .test-summary-card,
  .test-category, .test-card (+ -passed/-failed/-skipped variants),
  .severity-badge (critical/high/medium/low), .test-type-badge
  (automated/manual), .test-status-badge (pass/fail/skip), .test-steps,
  .test-expected, .test-actual (+ is-failed variant). Carries the
  existing test-result-badge styles forward for the stage-outcome chip.
- New JS handler for sub-tab clicks, scoped to .site-validation-card so
  it doesn't collide with the top-level nav.

test-site (SKILL.md):
- New Phase 6.7a — write .last-test-site.json to project root with the
  same categorized shape consumed by plan-alm. Documents stable
  category IDs (site-load / authentication / page-crawl / web-api /
  auth-pages / auth-api / console), severity rules (5xx -> critical,
  4xx on public -> high, console errors -> medium, etc.), status rules
  (passed/failed/skipped), and runOutcome rollup.

Tests: replaced 3 siteTests tests with 6 validationRuns tests covering
empty state (no runs / all-null), populated single stage (summary grid +
categories), failed test card (red severity, FAIL badge, is-failed
actual line, nav badge with failure count), multi-stage with sub-tab
switching (active states + JS handler present), test card details
(steps + expected + manual badge + validates), and OK nav badge for
clean runs. Suite is now 35 tests, 30 pass, 5 pre-existing baseline
failures unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* discover-site-components: enumerate powerpagesitelanguages (componenttype 10428)

Power Pages has THREE sibling unified entities for a single site, not just
the powerpagecomponent table the discovery script was querying:

  - powerpagesite          (componenttype 10427)  site root, 1 per site
  - powerpagecomponent     (componenttype 10426)  sub-records (most types)
  - powerpagesitelanguage  (componenttype 10428)  site languages, >=1 per site

Root cause of the BYOC Stage-2 site failing to render: discover-site-
components.js only enumerated powerpagecomponents. The English language
record on the site (componenttype 10428) was therefore never added to the
user solution. When the solution was exported and imported into Stage-2,
the language record never travelled. powerpagesite.content.defaultlanguage
references the language GUID that exists in dev but not in staging -> the
Power Pages runtime can't resolve a default language and the site silently
fails post-auth. (The reference doc had this gap documented as
"~10375 - must be added explicitly", but no skill ever implemented it,
so user solutions for code sites have been undercounted by ~1 ever
since unified entities shipped.)

Changes:
- discover-site-components.js: new discoverSiteLanguages() function
  enumerates powerpagesitelanguages for the site. Result now includes
  top-level siteLanguages: [...] and missing.siteLanguages: [...] for
  the solution-diff path. Older Power Pages installs that don't expose
  the unified entity (404 from /powerpagesitelanguages) get an empty
  array back instead of an exception, so existing call sites are
  unaffected.
- solution-api-patterns.md: replace the "approximate" component-type
  table with observed values (10426 / 10427 / 10428) and add a callout
  explaining the 3-entity ALM impact.
- setup-solution SKILL.md: Step 5.4 (orphan adoption) now also surfaces
  missing.siteLanguages alongside missing.powerpagecomponents, with
  guidance that languages must be pre-selected (not optional like
  bundle-chunk orphans) and that the AddSolutionComponent call needs
  the siteLanguageComponentType (~10428), not the subComponentType.

Tests: 16 in discover-site-components.test.js (was 14). New tests cover
populated + empty siteLanguages, missing[] diff including languages,
and graceful fallback when /powerpagesitelanguages 404s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 3-entity Power Pages model: apply learnings to all relevant skills + estimator

Audit-pass follow-up to commit 8d42f9d. The fix to discover-site-components.js
covered the discovery surface, but several other consumers of Power Pages
component data still assumed the 1-entity (PPC-only) model. This sweep
fixes them.

Changes:

- estimate-solution-size.js: new discoverPowerPageSiteLanguages() (parallels
  the discover-site-components fix). siteTotalComponents now includes the
  website root (10427) and language records (10428), so the site total
  reconciles with componentCountInSolution. For BYOC: was off-by-2 (119 vs
  121); now 121 vs 121. Older Power Pages installs that 404 on the
  unified entity get an empty array — same safety pattern as the discovery
  helper.

- discover-component-types.js: header doc updated with observed values
  (10427 for website root, 10426 for sub-components, 10428 for site language;
  was citing approximate ~10374/~10373/~10375 from earlier docs). Added a
  callout explaining why three component types exist and where it's
  documented in references/solution-api-patterns.md.

- setup-solution SKILL.md: updated the stale "~10375" reference in Step 4.3
  (the discover-component-types call site) to point at the observed value
  10428 and reference the 3-entity model spec.

- hotfix-solution SKILL.md: Step 2.2 now also queries
  powerpagesitelanguages?$filter=modifiedon ge {cutoff} so language
  changes get picked up by hotfixes (previously they were silently
  skipped). Step 4.3 passes --siteLanguageId to the type resolver when
  any modified record came from the language query, and Step 4.4
  builds the components array with the right componenttype per row
  (10426 vs 10428).

- plan-alm SKILL.md: Phase 1 step 11 (pre-plan completeness check) now
  surfaces missing.siteLanguages in the user-facing summary alongside
  site components, cloud flows, env vars, and custom tables. Always
  rendered when non-zero with explicit callout that this gap is a
  silent-failure mode.

Suite: 478 tests, 473 pass, 5 pre-existing baseline failures unchanged.
estimate-solution-size: 8/8 pass (discoverPowerPageSiteLanguages
gracefully returns [] on 404). Integration tests: 6/6 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* plan-alm: enrich Execution checklist with env URLs + per-stage validation outcomes

Two visible-in-the-rendered-plan gaps surfaced during BYOC review:

1. Reviewers couldn't tell which env each Deploy/Activate/Test step
   targeted without leaving the Execution tab.
2. Test-site results landed in the Validation tab but didn't show up
   as a status indicator on the Execution tab — the only signal was
   the "completed" badge, which masked failures and warnings.

buildChecklistHtml now:
- Parses the trailing "<verb> in {stageName}" segment from each step,
  matches against data.stages[].label, and renders a "Target: <envUrl>"
  substep beneath every stage-bound Deploy/Activate step.
- For Test-site steps specifically: looks up data.validationRuns[stageName]
  and renders a substep with the PASSED/WARNINGS/FAILED badge, the
  tested URL, the pass/fail/skip summary line, and a "View details ->"
  jump link to the Validation tab.
- Promotes a "completed" Test step to "warning" status (yellow) when
  the corresponding validationRun outcome is "failed" — surfaces
  failures at a glance in the Execution checklist.

test-site SKILL.md adds a top-level "Validation Test Categories" table
listing the 7 stable category IDs (site-load / authentication /
page-crawl / web-api / auth-pages / auth-api / console) with the
source phase and what each covers — answers the question "what
categories of tests does test-site emit?" without forcing readers
into Phase 6.7a.

plan-alm SKILL.md Phase 7 Step C now documents the substep rendering
behavior so future runs of plan-alm against fresh sites produce the
same visual output without rediscovering the convention.

Tests: 2 new (substep rendering with pass count + view-details link;
failed run escalates the step status from completed to warning).
Suite: 37 tests, 32 pass, 5 pre-existing baseline failures unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* gitignore: scheduled_tasks.lock runtime artifact

* ensure-pipelines-host: move deferred PE-provisioning spec to design doc + telemetry registration

Two PR-prep cleanups against the ensure-pipelines-host SKILL:

1. Strip the verbose "Deferred — Platform Host provisioning" spec from
   the in-branch SKILL.md. The full follow-up spec (getOrCreate
   contract, polling rules, JIT step, decision-tree path, threat-model
   row, open items) is the source of truth in
   `PowerPipelines/ensure-pipelines-host-PLAN.md` and is duplicated
   there already. SKILL.md now carries only a brief note pointing at
   the design doc, plus the contextual "why deferred" sentence.
   Inline references throughout the SKILL.md (Scope note, non-goals
   bullet, Idempotency callout, deferred-script entry) updated to
   point at the plan-doc anchor instead of an internal section.

2. Telemetry registration. ensure-pipelines-host was already in
   TRACKED_SKILLS (powerpages-hook-utils.js) for the validator hook,
   but the user-facing skill-tracking-reference.md mapping table was
   missing it — callers had no way to look up the PascalCase name.
   Added "ensure-pipelines-host | EnsurePipelinesHost |
   Site/AI/Skills/EnsurePipelinesHost" to the table, and switched
   Phase 6's tracking call from the hardcoded bash block to the
   pointer pattern (matching test-site / plan-alm) per the AGENTS.md
   convention "Every skill must record usage in its final phase via
   the > Reference: pointer pattern, not hardcoded command".

Suite still 475/480, 5 pre-existing baseline failures unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…gesALM

# Conflicts:
#	plugins/power-pages/.claude-plugin/plugin.json
…back (#140)

* ensure-pipelines-host: env-first prompt in Phase 3.C (NoHost path)

Restructures the NoHost decision tree from "pick an install method" to
"pick the host environment". The user picks an environment first; the
install method follows from that choice.

Phase 3.C — new flow:

  Step 1. Build the eligible-env list from
    RESOLUTION.candidates.eligibleForAppInstall[]. Apply project-context
    role labels at presentation time:
      dev env / source env / staging env / production env
    URL match is origin-only (lowercase, trailing slash stripped, path
    and query ignored). Multiple labels join with " · ".

  Step 2. Empty-list collapse: if no env qualifies after filtering,
    drop option 1 from the prompt entirely. Picker becomes a 3-option
    variant: Create new / PPAC / Cancel. Avoids a "list of nothing"
    dead end in fresh / small tenants.

  Step 3. Present the 4-option prompt:
      1. Use an existing environment  (with the labeled list)
      2. Create a brand-new dedicated host env
      3. Open PPAC and create one manually
      4. Cancel

  Step 4. Route the answer:
      - Option 1, picked env URL origin-equal to devEnvUrl
          -> Phase 4.B with ACTION_TAKEN = "user-installed-app-on-dev"
      - Option 1, any other listed env
          -> Phase 4.B with ACTION_TAKEN = "user-installed-app"
      - Option 1, "Other (paste URL)"
          -> Phase 4.B with user-supplied URL
      - Option 1, env with environmentSku === "Sandbox"
          -> Step 5 confirmation gate, then 4.B if confirmed
      - Option 2 -> Phase 4.A (unchanged: D365_ProjectHost env-create)
      - Option 3 -> Phase 4.C (unchanged: PPAC manual)
      - Option 4 -> exit

  Step 5. Sandbox confirmation gate (3-option Yes / Pick another /
    Cancel) for Sandbox-sku envs. Doesn't hard-block — Pipelines
    documentation cites Production but Sandbox works in practice and
    some tenants only have Sandbox envs available.

Phase 4.B update:
  When entered from Phase 3.C option 1 with CHOSEN_ENV_URL set, skip
  the legacy "which env?" sub-prompt — the env was already picked
  upstream. Phase 4.B's PPAC URL printing + post-install verification
  is unchanged. Legacy entry path (caller invokes 4.B directly) still
  shows the sub-prompt for back-compat.

actionTaken enum:
  Added "user-installed-app-on-dev" as a sibling of "user-installed-app"
  so post-rollout telemetry can distinguish how often users co-locate
  Pipelines with their dev env vs dedicating a separate env. The
  validator does not enum-check actionTaken so no validator change
  needed — the new value flows through transparently.

Other phases (Phase 1 detection, Phase 2 resolution, Phase 3.A / pre /
pre' / pre'' / 3.D / 3.E branches, Phase 4.A env-create call, Phase 4.C
PPAC manual, Phase 5 JIT + verify, Phase 6 artifact write, setup-pipeline
integration, plan-alm integration) are unchanged.

Design walkthrough lives in
PowerPipelines/host-env-selection-flow.html (sibling to
ensure-pipelines-host-PLAN.md, not in the branch).

Tests: 498 / 493 pass, same 5 pre-existing render-alm-plan baseline
failures unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Remove hotfix-solution skill — replaced by setup-solution sync + deploy-pipeline

The hotfix-solution skill (timestamped {base}HotfixYYYYMMDDHHmm sub-solutions)
predates three changes that made it obsolete for typical Power Pages projects:

  - setup-solution sync mode (re-runs adopt new components into the existing
    base solution non-destructively).
  - 3-entity site-component discovery (powerpagecomponent + powerpagesite +
    powerpagesitelanguage all enumerated; no silent missing-component
    breakage).
  - Completeness checks in 4 ALM skills (plan-alm, setup-solution,
    export-solution, deploy-pipeline) — gaps surface upstream.
  - Power Platform Pipelines computes incremental imports internally, so
    re-deploying the base after a small fix is fast.

Recommended replacement workflow for small mid-cycle changes:

  1. Make the change in dev.
  2. Run /power-pages:setup-solution in sync mode to adopt the modified
     components into the existing base solution.
  3. Bump the solution version.
  4. Run /power-pages:deploy-pipeline.

Single solution lineage, cleaner audit trail, simpler dependency management,
no solution sprawl in target environments.

Removed:
  - plugins/power-pages/skills/hotfix-solution/SKILL.md
  - plugins/power-pages/skills/hotfix-solution/scripts/validate-hotfix.js

References stripped:
  - plugins/power-pages/CLAUDE.md      (skills tree, user-invocable list,
                                        hooks, shared scripts, validation
                                        scripts)
  - plugins/power-pages/AGENTS.md      (mirrors CLAUDE.md)
  - plugins/power-pages/references/skill-tracking-reference.md (mapping row)
  - User memory MEMORY.md (skills list, with replacement note)

Migration guidance left in place in CLAUDE.md and AGENTS.md so users who
previously ran /power-pages:hotfix-solution get the recommended replacement
flow at a glance.

Tests: 498 / 493 pass, same 5 pre-existing baseline failures unchanged.
No new test failures introduced.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ALM-plan-gate: plan-alm is the front door for ALM intents

Three layers, one commit. Fixes the symptom where the agent jumps to
/power-pages:setup-pipeline or /power-pages:deploy-pipeline directly when
the user says "promote to staging" or "deploy this site to next env",
skipping the orchestrator entirely.

Layer 1 — shared helper (scripts/lib/check-alm-plan.js):
  Reads docs/.alm-plan-data.json, returns {exists, stale, staleness, ...}.
  Freshness signal compares the solution's modifiedon (Dataverse OData)
  against planData.GENERATED_AT when env credentials + solutionId are
  passed. Gracefully degrades to existence-only check otherwise. Network
  failures are non-fatal (returns existing=true / stale=false rather
  than blocking the gate).

  8 new tests in scripts/tests/check-alm-plan.test.js — no plan present,
  malformed JSON, plan present without env creds, solution-modified
  flags stale, solution-not-modified does not, network failure
  swallowed, htmlPath populated when alm-plan.html sits beside the JSON,
  argparse rejects missing projectRoot.

Layer 2 — Phase 0 ALM-plan gate in setup-pipeline + deploy-pipeline:
  Both skills now run check-alm-plan.js before their existing Phase 1.
  Three branches:
    - exists:false       -> recommend running /power-pages:plan-alm first
                            with options: Yes (recommended) / Continue
                            without a plan / Cancel
    - stale:false        -> pass through silently to Phase 1
    - stale:true         -> recommend refreshing via plan-alm with the
                            same 3-option prompt
  Skip rule: when invoked by plan-alm itself (orchestrator pass-through),
  Phase 0 is bypassed so plan-alm doesn't loop on its own children.
  Detection via INVOKED_BY_PLAN_ALM flag or recent + In-Execution plan
  status.

Layer 3 — agent-level routing hint in CLAUDE.md:
  New section "ALM intent routing — plan-alm is the front door" tells
  the agent to invoke /power-pages:plan-alm first when the user
  expresses an ALM intent (promote, ship, deploy, set up CI/CD, etc.)
  rather than jumping to an individual skill. Calls out explicit
  bypass phrases ("just run setup-pipeline", "skip planning") that
  preserve direct-invocation for power users.

Why all three layers:
  - Layer 1 alone -> no behavior change.
  - Layer 2 alone -> gate fires AFTER the agent picked the wrong skill.
                     User still sees a confirmation prompt, but the
                     wrong path is one click away.
  - Layer 3 alone -> agent intent-routes correctly most of the time,
                     but a stubborn user (or stubborn agent reading)
                     can still bypass.
  Combined -> agent prefers plan-alm for ALM intents (Layer 3); if it
              picks an individual skill anyway (Layer 2), the gate
              catches it; the freshness signal (Layer 1) keeps the
              plan honest as the source solution evolves.

Scope: setup-pipeline + deploy-pipeline only in this iteration. The
rest of the ALM skills (setup-solution, export-solution, import-solution,
configure-env-variables) get Phase 0 in a follow-up after the gate is
proven in practice.

Tests: 506 / 501 pass, same 5 pre-existing baseline failures unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ALM intent routing: add CLAUDE.md/AGENTS.md guidance for Layer 3

Companion edit to faf81a0 — the Layer 3 routing hint that should have
landed in the ALM-plan-gate commit but missed git's index because
CLAUDE.md is a symlink to AGENTS.md (mode 120000) and only the symlink
target tracks content. The same auto-sync mechanism that mirrored the
hotfix-removal edits in f15124e left this file dirty after the gate
commit.

Content unchanged from what's described in faf81a0:
  - New 'ALM intent routing — plan-alm is the front door' section
  - Tells the agent to invoke /power-pages:plan-alm first when the user
    expresses an ALM intent (promote / ship / deploy / set up CI-CD /
    move to next env / push to prod / release this version) rather than
    jumping to setup-pipeline / deploy-pipeline / export-solution /
    import-solution directly
  - Calls out explicit-bypass phrases that preserve direct-invocation
    for power users
  - Notes that setup-pipeline and deploy-pipeline enforce the gate at
    Phase 0 with their own escape hatch

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ALM skill UX fixes — three issues from user's recent run

1) Hooks error spam during plan-alm Phase 1 user-input pause
   (alm-skill-hook-error-issue.txt)

   Each ALM skill's frontmatter had a 'type: prompt' Stop hook
   alongside its deterministic 'type: command' validator. Stop hooks
   fire when the agent stops generating — including when it stops
   to ask the user a question. With 8 ALM skills aggregating their
   Stop hooks across the session, every user-input pause produced
   ~10 false-positive 'insufficient evidence in transcript' errors
   from the LLM-evaluated prompt hooks for skills that hadn't even
   been invoked yet.

   Fix: removed the prompt-style Stop hook from all 8 ALM skills:
     plan-alm, setup-solution, setup-pipeline, deploy-pipeline,
     export-solution, import-solution, configure-env-variables,
     diagnose-deployment.
   The deterministic command validators (validate-*.js) cover the
   same ground, gracefully exit 0 when the corresponding marker
   file isn't present (i.e., 'not a foo session'), and don't
   produce noise on user-input pauses. diagnose-deployment had
   only the prompt hook (no artifacts -> no command validator)
   so its empty hooks block was removed entirely.

2) Solutions tab shows empty 'structure will be determined'
   placeholder when planData.proposedSolutions = []

   The renderer's empty-state branch fired because the caller passed
   an empty array thinking 'we're not splitting, no solutions to
   list'. But the Solutions tab is supposed to show the BASE
   solution even in single-solution mode.

   Fix (two-pronged):
   - Renderer (render-alm-plan.js): added buildSynthesizedSingleSolution()
     that composes a single base-solution entry from
     solutionContents.solution / data.SITE_NAME / componentCount /
     totalSizeMB. When proposedSolutions is empty, the synthesizer
     fills in a single entry so the Solutions tab always shows
     SOMETHING about the solution that's about to ship.
   - SKILL.md: added explicit 'proposedSolutions[] is never empty'
     guidance — compute-split-plan.js returns a single entry for
     splitStrategy === single, and callers must pass that through.
     The renderer's safety net is a fallback, not the right
     contract.

3) Plan HTML doesn't open in default browser

   The earlier launcher used a node -e -> spawn -> powershell chain
   which the agent's sandbox classifier blocks (textbook nested-
   subprocess pattern). The user had to manually open the file
   each run.

   Fix: replaced the Node-spawn chain in plan-alm SKILL.md with a
   three-step instruction:
   - Step 1: print the absolute file:// URL via a single Node
     call (no spawn). This is the user's reliable fallback —
     never blocked by the sandbox.
   - Step 2: launch the browser via the OS-native shell tool
     directly (PowerShell Start-Process on Windows, open on
     macOS, xdg-open on Linux). No Node wrapper.
   - Step 3: report the URL to the user as a clickable
     backup, regardless of whether the launch succeeded.

   If the launch silently fails (sandboxed terminal, SSH, headless),
   the printed URL is the contract — the user clicks or pastes it
   themselves. No retry, no block, no loop.

Tests: 506 / 501 pass, same 5 pre-existing baseline failures
unchanged. Empty-proposedSolutions smoke-test confirms the
synthesizer renders a real solution card instead of the empty
placeholder.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ALM deferral marker (.alm-deferred) + hook-design guidance

Companion fix to e670581 (which removed the runaway-loop-causing prompt
hooks). This commit adds a deferral-marker pattern so users who explicitly
opt out of ALM for a project never see the Phase 0 plan-alm gate or
artifact-completion validators nag them about missing artifacts that
aren't supposed to exist.

Marker convention:
  Drop .alm-deferred at the project root. Recognized formats:
    - Empty file (touch marker)
    - Plain text (one-line reason)
    - JSON: { deferredAt, deferredBy, reason, scope }

  All ALM validators silent-approve when present. The check-alm-plan
  helper used by setup-pipeline / deploy-pipeline Phase 0 reports
  deferred:true so those skills pass through to Phase 1 without
  recommending plan-alm.

Code changes:

  - scripts/lib/validation-helpers.js: new readDeferralMarker(projectRoot)
    returns the marker contents as { path, raw, info } or null. Exported
    alongside approve / block / runValidation.

  - 6 ALM validators patched to call readDeferralMarker as their first
    check, before any artifact validation:
      validate-plan-alm.js
      validate-pipeline.js          (setup-pipeline)
      validate-deploy-pipeline.js
      validate-solution.js          (setup-solution)
      validate-export.js
      validate-import.js
    (validate-env-variables.js uses a different/broken runValidation
    signature that silent-approves regardless — left for separate fix.)

  - scripts/lib/check-alm-plan.js: returns deferred:true / deferral:{...}
    when .alm-deferred is present. The Phase 0 gate in setup-pipeline and
    deploy-pipeline now branches on this and passes through silently.

  - setup-pipeline + deploy-pipeline SKILL.md Phase 0 routing tables
    updated with the deferred:true branch (silent pass-through).

  - 5 new tests in check-alm-plan.test.js covering: empty marker,
    plain-text marker, JSON marker, marker beats existing plan, normal
    results carry deferred:false.

Documentation:

  - PLUGIN_DEVELOPMENT_GUIDE.md: new "Hook design for skill validation"
    section. Anti-patterns: type:prompt Stop hooks, block() for soft
    completion checks, re-deriving completion from ephemeral artifacts.
    Recommended patterns: marker-file gates with silent-approve,
    deferral-marker honoring, PostToolUse over Stop, explicit status
    in marker files. Acceptance criterion for new skill validators.

  - User memory feedback_skill_validation_hooks.md captures the live
    BYOC supplier portal cost-loop incident as a reference, with the
    specific anti-patterns + recommended patterns and how to defer ALM
    via .alm-deferred.

Tests: 511 / 506 pass, same 5 pre-existing baseline failures unchanged.
5 new deferral tests all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* configure-env-variables: fix validator silent-approve bug

The validator called runValidation('configure-env-variables', () => {...}),
passing a string as the first argument. Since runValidation(callback) only
takes one argument, the string was treated as the callback, threw when
invoked, and the catch silent-approved every time — all validation logic
was dead code.

Change signature to runValidation((cwd) => {...}) and use the cwd argument
in place of process.cwd() so the validator picks up the working directory
that the hook runner passes in (matches the pattern used by every other
ALM validator in this plugin).

Add test coverage exercising the eight observable behaviours: graceful
exit on missing settings, valid-settings approve, invalid JSON block,
missing/empty stages block, missing EnvironmentVariables array block,
no env vars in any stage block, and the project-root walk-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* plan-alm: detect .alm-deferred marker at Phase 1 entry

The .alm-deferred marker (introduced in fb44b59) tells every ALM
skill validator to silent-approve so projects that opted out of ALM
stop seeing "missing artifacts" warnings on every run. plan-alm
itself didn't honor the marker — it would build a plan against a
project that had explicitly deferred ALM, leaving the user with a
plan they decided not to maintain plus a marker that other skills
were still respecting.

Add Phase 1 Step 0: invoke check-alm-plan.js (which already returns
deferred:true when the marker is present), and when deferred ask
the user via AskUserQuestion whether to continue and remove the
marker, continue and keep it (with a follow-up note in the Phase 1
step 9 report so the user remembers other skills still skip), or
cancel. Numbered as Step 0 to avoid renumbering the rest of Phase 1
— several places in this SKILL.md cross-reference Phase 1 step
numbers (4, 7, 10, 11, 12).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ALM-plan-gate Layer 2: extend Phase 0 to 4 more ALM skills

Phase 0 ALM-plan gate was only in setup-pipeline and deploy-pipeline.
Direct invocation of the other ALM skills still bypassed the
orchestrator's pre-plan completeness check, asset-size advisory,
and deployment-strategy selection.

Extend the gate to setup-solution, export-solution, import-solution,
and configure-env-variables. Same shape as setup-pipeline:

- Skip rule when invoked by plan-alm itself (no orchestrator loops)
- check-alm-plan.js helper drives the decision (deferred / no plan /
  current / stale)
- AskUserQuestion offers Recommended (run plan-alm), bypass with
  BYPASSED_PLAN_GATE flag, or cancel
- Stale branch with refresh option for solution-modified-after-plan

Each skill gets a tailored "Why this gate exists" paragraph
explaining the specific failure mode that direct invocation
introduces — multi-solution split mismatch (setup-solution),
incomplete export (export-solution), missing per-stage overrides
(import-solution), incoherent env var classification
(configure-env-variables).

configure-env-variables uses ## Phase headings; the other three
nest under ## Phases with ### Phase 0.

Lint passes; full suite still at 521/526 (5 pre-existing render
failures unrelated to this branch).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Drop redundant Stop hooks from SKILL.md frontmatter

The plugin already runs every tracked skill's validator via a
centralized PostToolUse hook (hooks/hooks.json → run-skill-posttool-
validation.js → TRACKED_SKILLS map → per-skill validator). Carrying
a duplicate Stop hook in each SKILL.md frontmatter ran the same
validator a second time on every assistant pause, which fired far
more often than the once-per-skill-completion that PostToolUse
already provides. PLUGIN_DEVELOPMENT_GUIDE.md flagged these as an
anti-pattern; this commit acts on that guidance.

Precondition fix: seven ALM skills were declared via Stop hooks but
were not yet registered in TRACKED_SKILLS — dropping their Stop
hooks without registering them first would have silently disabled
their validation. Add configure-env-variables, deploy-pipeline,
export-solution, import-solution, plan-alm, setup-pipeline, and
setup-solution to the registry with their validator-script paths,
then strip the hooks: blocks from all seven SKILL.md files.

Add a guardrail test (powerpages-hook-utils.test.js) that walks every
SKILL.md and asserts none re-introduce a hooks: frontmatter block,
plus assertions that each newly registered ALM skill resolves to
its validator. Update AGENTS.md (mirrored to CLAUDE.md via symlink)
to describe the centralized pattern and call out the two anti-
patterns explicitly. Tighten the past-tense statement in
PLUGIN_DEVELOPMENT_GUIDE.md to a present-tense rule now that no
SKILL.md still carries the redundant block.

Test counts: 524/529 pass (5 pre-existing render-alm-plan baseline
failures unchanged).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* plan-alm: reconcile env-var views when only count is known

The rendered ALM plan can show three contradictory views of env vars
when the size estimator finds definitions but the gathering phase
has not enumerated per-variable details into envVars[]:

- Overview stat card: "0 Env Variables" (driven by envVars.length)
- Size Analysis signal: "5 Env Vars" (driven by sizeAnalysis.envVarCount)
- Risks warning: "(5 detected) — prompted at deploy" (agent-injected
  from the same count)
- Env Variables tab: "No environment variable definitions detected"
  (driven by envVars.length === 0)

Reproduced on supplier-invoice-portal/docs/alm-plan.html where the
estimator counted 5 env var definitions but the agent did not
enumerate them with schema name + bound site setting + type.

Fix the renderer to fall back to sizeAnalysis.envVarCount.value when
envVars[] is empty:

- STAT_ENVVARS prefers envVars.length, then the size estimator count
- buildEnvVarsHtml() shows a count-aware info note (.note-box.info)
  pointing to setup-solution / configure-env-variables / deploy-pipeline
  when count > 0 but per-variable details are missing
- The neutral "no detections" copy still applies when both sources are 0

Add three regression tests covering the three states: count-only,
both-zero, and details-populated.

Tests: 527/532 (5 pre-existing render-alm-plan baseline failures
unchanged; 3 new tests for this fix all pass).

Note: this is a renderer-side reconciliation. The deeper question —
should plan-alm Phase 1 enumerate env var definitions with schema
names so the table actually renders rows — is a larger SKILL.md
change deferred for a future iteration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* plan-alm: enumerate env var definitions for Env Variables tab rows

The previous fix (8cbc39a) reconciled the Env Variables tab with the
size estimator's count when per-variable details were missing — the
tab now shows a count-aware info note instead of contradicting the
warning. But the table itself still couldn't render rows because
plan-alm Phase 1 didn't query env var definitions for schema names,
types, default values, or bound site settings.

Add scripts/lib/discover-env-var-definitions.js — a new helper that:

- Queries environmentvariabledefinitions filtered by the site's
  publisher prefix (same filter the size estimator uses, so the
  count and the row enumeration agree on which definitions belong
  to this site).
- Queries mspp_sitesettings for the website with mspp_source eq 1
  (env var-backed bindings) and indexes by env var GUID, so each
  definition row carries its bound site setting name.
- Maps option-set type codes to labels (String / Number / Boolean /
  Secret / DataSource / Json) — exposed via TYPE_LABELS for reuse.
- Degrades gracefully — returns { envVars: [], count: 0 } when the
  prefix is missing, the token is unavailable, or the query errors.
  The renderer's count-summary fallback takes over in those cases.

Wire it into plan-alm Phase 1 as a new Step 10b (between size
estimation and the pre-plan completeness check). Adds Step 10b's
output as ENV_VARS_DETAILS, then plumbs it into planData.envVars
in Phase 3 so render-alm-plan.js gets row-level data and the Env
Variables tab renders the per-variable table.

Tighten Q6 (HAS_ENV_VARS detection) to prefer the live discovery
result over the manifest scan, and update the warning recommendation
template to substitute the count from ENV_VARS_DETAILS.length when
positive (with sizeAnalysis.envVarCount.value as fallback).

Add 8 tests covering: arg validation, prefix-required guard, v9.2
endpoint + filter shape, binding join (bound + unbound + duplicate
binding), site setting query scoping (website + mspp_source eq 1),
graceful degradation on query errors, and missing-defaultvalue /
unknown-type-code handling.

Tests: 535/540 (5 pre-existing render-alm-plan baseline failures
unchanged; 8 new tests for discover-env-var-definitions all pass).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Fix custom-host flow: env-first menu in plan-alm Q4 NoHost branch

Reproduced on a trial-license tenant: plan-alm Q4 NoHost asked
"setup-pipeline will provision a new Custom Host. Continue?" — user
said yes (the only forward path). Then setup-pipeline →
ensure-pipelines-host → Phase 4.A (env-create) fired without
showing the Phase 3.C env-first menu. The agent skipped 4.A's
pre-call confirmation gate too, narrating "user already pre-
confirmed via plan-alm." env-create returned 409
NotEnoughCapacity_HasTrialLicense_ProvisionEnvironment. The fallback
to 4.B prompted the user to install the Pipelines app via PPAC, but
on the wrong env GUID (not the one the user had eligible). Net
effect: user confirmed once at planning time, hit a trial-license
error mid-execution, and ended up at a manual install for an env
they never picked — when "install Pipelines app on ni-dev" was a
clean path the whole time.

Three coordinated fixes:

1. plan-alm Phase 2 Q4 NoHost branch now presents the same
   env-first menu as ensure-pipelines-host Phase 3.C: each eligible
   env as "install Pipelines app on this env" + create-new +
   PPAC-manual + switch-to-manual + cancel. The user's choice sets
   CHOSEN_ENV_URL / WILL_PROVISION_CUSTOM / WILL_USE_PPAC and flows
   through planData.hostResolution. Same shape as 3.C so users see
   one menu, one decision, no ambiguity.

2. ensure-pipelines-host Phase 3.C gains an entry skip rule: when
   hostResolution.chosenEnvUrl is set upstream, route directly to
   Phase 4.B with that env. When willProvisionCustom is set without
   chosenEnvUrl, route to 4.A. When willUsePpac, route to 4.C.
   Otherwise run the menu as written. Skip rule prevents the user
   being asked the same question twice.

3. Phase 4.A's pre-call confirmation gate is now marked
   NON-SKIPPABLE in the SKILL.md prose. plan-alm Q4 and the admin
   attestation are about strategy; the pre-call gate echoes the
   exact API body and is the user's last chance to catch a
   wrong-tenant or wrong-region provisioning. The 2026-05-05 bug
   surfaced because the agent treated plan-alm pre-confirmation as
   covering this gate.

Bonus fixes around the failure-fallback path:

- 4.A's 409 trial-license response handling: explicitly direct the
  agent to discard any env GUID returned in the 409 body and re-
  enter Phase 3.C / 4.B with the original eligible-env list from
  Phase 2 inventory. Prevents the wrong-env PPAC URL we saw in the
  reproduction.
- 4.B sub-prompt: add a sanity-check rule for the env GUID before
  printing the PPAC URL — must come from Phase 2 inventory or a
  user-confirmed pasted URL, never from a 4.A failure body or other
  state.
- Renderer host card now reflects the user's choice: "Will install
  Pipelines app on existing env <chosenEnvUrl>" / "Will create new
  Custom Host via PPAC manual flow" / "Will provision new Custom
  Host with D365_ProjectHost template" depending on the flag set.
  Falls back to the create-new description when the planData
  predates the new flags.

Tests: 538/543 pass (5 pre-existing render-alm-plan baseline
failures unchanged; 3 new renderer tests for the chosenEnvUrl /
willUsePpac / fallback branches all pass).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Widen default SKU filter to remove "create new env" bias

User report from a follow-up test session: even after the
2026-05-05 env-first menu fix (commit 075c33f), the agent still
created a new Custom Host instead of installing the Pipelines app
on a user-selected existing env.

Root cause is structural: list-tenant-envs.js defaulted --skus to
['Production'], which excluded Sandbox and Trial envs from
eligibleForAppInstall[]. On a Sandbox-only or trial-license tenant
the eligible list came back empty, the env-first menu collapsed to
the 3-option variant (drop option 1 entirely — no existing-env
choice), and the user had no path other than create-new (which
then failed with NotEnoughCapacity_HasTrialLicense_ProvisionEnvironment
on trial tenants).

Doc-code mismatch was a giveaway — the SKILL.md prose said
"sku ∈ {Production, Sandbox}" but the code only passed Production.

Fixes:

1. list-tenant-envs.js DEFAULT_SKUS = ['Production', 'Sandbox'].
   Sandbox is a valid host for the install-on-existing path (4.B);
   the create-new fast-path (4.A) is still Production-only per
   eng.ms doc but that constraint is on env-create, not on the
   helper. Trial stays opt-in via --skus Production,Sandbox,Trial
   because Trial cannot use 4.A (env-create returns 409) — surfacing
   Trial in 4.A's eligibility would mislead.
2. ensure-pipelines-host-detect.js mirrors the same default.
3. plan-alm Phase 1 step 12 explicitly passes
   --skus Production,Sandbox,Trial to the detection wrapper.
   plan-alm always offers the install-on-existing path (4.B)
   regardless of which sku, so widening here is safe and gives
   trial-license users a viable existing-env option.
4. ensure-pipelines-host Phase 3.C step 2 (empty-list branch) now
   tries widening to Trial before collapsing the prompt, and
   surfaces the SKU filter explicitly when the widened list is
   still empty so the user knows they can override.
5. SKILL.md prose updated throughout to reflect the new defaults
   and the Trial-via-flag opt-in.

Two regression tests added covering the new default and the Trial
opt-in path. Existing test pinning 'Production' as the default
asserted skusFilter was updated to ['Production', 'Sandbox'] with
a code comment explaining the bias fix.

Tests: 540/545 (5 pre-existing render-alm-plan baseline failures
unchanged).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* deploy-pipeline: blocked-attachment diagnosis with explicit user gate

User reported having to run fix-blocked-attachments.js manually then
call RetryFailedDeploymentAsync after a stage run failed because
the target env's blockedattachments setting rejected .js files in
the solution zip — without ever being asked whether to modify the
tenant-level setting. The skill silently retried after the manual
intervention.

Add Phase 7.6.1 (failure diagnosis) and 7.6.2 (gated remediation):

- 7.6.1 queries the failed stage run for errordetails / validationresults
  and pattern-matches against AttachmentBlocked / -2147188706 /
  MissingDependency. Routes to the targeted remediation when the
  blocked-attachment pattern matches; falls through to the generic
  retry/exit prompt otherwise.
- 7.6.2 invokes fix-blocked-attachments.js with --dry-run to identify
  which extensions are blocked, then surfaces a mandatory
  AskUserQuestion gate before any setting change. Only on explicit
  Yes does the skill remove the block and call
  RetryFailedDeploymentAsync. On No / Cancel, the skill stops with a
  PPAC remediation pointer.

The flag is --dry-run (helper's existing API), not --dryRun, and the
output field is wasBlocked[] (not blockedTypesPresent). Prose updated
to match.

PAC CLI is switched to the target env before the helper runs (so it
queries the right env's settings) and switched back after — same
pattern as the activation check in 7.7.

Test plan: this is a SKILL.md change (no executable code), so no test
coverage. Validation by running deploy-pipeline against a target env
with .js in blockedattachments.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ensure-pipelines-host: SKU fallback for trial/capacity 409 errors

User reported that env-create returned 409
NotEnoughCapacity_HasTrialLicense_ProvisionEnvironment on a
trial-license tenant, then the skill fell straight to Path 4.B
(install on existing env) without offering the user the option to
retry env-create with a smaller SKU. The user noted that
Production is recommended but Sandbox/Developer/Trial work too —
the SKU gate is on env-create's license allocation, not on the
Pipelines app itself.

Add SKU support to provision-custom-host.js:

- New --environmentSku arg with allowed values
  Production / Sandbox / Developer / Trial.
- DEFAULT_SKU = 'Production' (preserves existing behaviour for
  callers that omit the flag).
- Validate the SKU before any HTTP call; surface a clear error if
  the caller passes a bogus value.
- Internal variable rename: the input arg `environmentSku` is now
  read-only (destructured const); a local `resolvedSku` carries
  what BAP reports back across the response and final-poll paths.

Add Phase 4.A SKU-fallback prompt to ensure-pipelines-host SKILL.md:

- Triggers on 409 with a capacity-related error code (HasTrialLicense,
  NotEnoughCapacity, OrganizationDisabled, EnvironmentCapacityExceeded).
- Builds the fallback SKU list dropping the SKU that just failed
  (so we never offer the same SKU twice).
- Each option carries its own caveat — Sandbox is non-prod-documented,
  Developer is single-user, Trial expires in 30 days.
- After two consecutive capacity failures, stop offering SKU
  fallbacks and route to Path 4.B.
- Fall-through to Path 4.B remains available as an option from the
  same prompt.

Always discard env GUIDs returned in the 409 response body — the
provisioning failed, and Path 4.B must use the Phase 2 eligible-env
inventory. (Same defensive rule as the trial-license fallback added
in commit 075c33f.)

Tests: 4 new tests in provision-custom-host.test.js covering
DEFAULT_SKU/ALLOWED_SKUS exports, invalid-SKU rejection, SKU flowing
into POST body, and Production-default backward compatibility.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* setup-solution: enumerate connection refs + tighten componenttype prose

Two follow-ups from the same user run:

1. Connection references missing from the solution. The user
   discovered them only after a deployment validation error, manually
   queried the connector ID, found that componenttype was 10160 in
   their env (not the documentation's 10137), and added them via
   AddSolutionComponent.
2. Site model componenttypes 10426/10427/10428 didn't match the
   user's env — actual values were 10429/10428/10430. The user
   discovered this only after the first AddSolutionComponent batch
   failed with HTTP 400; re-ran discover-component-types.js, got
   the right values, re-added 246 components.

Both are the same root cause: SKILL.md and helper-script comments
showed "typical" componenttype values prominently enough that the
agent quoted them directly instead of running the runtime resolution.

Fixes:

- setup-solution Step 5.2 gains Query G — enumerate connection
  references owned by this site's publisher, intersect with the
  cloud flows in Query E, resolve componenttype at runtime via
  discover-component-types.js --objectIds. Skipped when no cloud
  flows exist. Surfaces a soft warning when flows exist but no
  matching refs were found (caller should verify connector usage).
- setup-solution components-to-add list (Phase 5.6) gains entry 8
  for connection references, with `addRequired: false` and
  componentType pulled from the runtime-resolved value. Explicit
  callout that the value is env-specific and observed as 10137 or
  10160 across tenants.
- setup-solution prose throughout: replace tilde-notation
  ("~10427", "~10426", "~10428") with the concrete instruction to
  use values resolved by discover-component-types.js. The tilde
  notation read as authoritative-but-fuzzy; the agent treated it
  as ground truth.
- Adopted-orphan-ppc entry (was hardcoded `componentType: 10373`)
  now uses `subComponentType` resolved at Step 5.1.
- discover-site-components.js header comments rewritten — the three
  site-model componenttypes are env-specific; observed reference
  values are listed alongside the "typical" values.
- discover-component-types.js doc header expanded to call out the
  --objectIds path as the right way to resolve any non-standard
  componenttype (connection refs, env vars, etc.) and explains why
  the values vary across tenants (Dataverse assigns IDs at install
  time based on package install order).

No code-side test changes — the helpers' executable behaviour
didn't change, only the doc/SKILL.md prose. Verification will come
from a fresh setup-solution run on the user's env.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* plan-alm: post-run refresh helper so Pipelines/Risks/Validation tabs update

User reported that the rendered alm-plan.html on supplier-invoice-portal
showed:
- Pipelines tab still saying "Will be ensured during setup-pipeline" /
  "Will provision new Custom Host with D365_ProjectHost template" even
  though setup-pipeline + deploy-pipeline had completed successfully.
- Risks panel still surfacing the pre-run NoHost warning even though
  the host was resolved.
- No pipeline name, ID, ACTIVE chip, or last-run footer despite a
  completed deploy.

Three structural causes:

1. plan-alm Phase 3 explicitly told the agent to **delete** the
   docs/.alm-plan-data.json file after rendering ("Delete
   docs/.alm-plan-data.json after success."). Without that JSON,
   every downstream phase that wanted to refresh+re-render had no
   source data — and check-alm-plan.js (used by every Phase 0 ALM
   gate) returned "no plan exists" forever after.
2. Phase 6 (setup-pipeline) post-run prose said "refresh
   pipelineMeta and re-render" but never told the agent to also
   refresh hostResolution from .last-host-check.json — so the
   host card stayed at NoHost. It also didn't say to drop resolved
   warnings from risks[], so pre-run noise stayed forever.
3. Phase 7 Step C (test-site) re-render used a hand-rolled node -e
   snippet that worked but was opaque; if the planData JSON was
   missing (per #1), it failed silently and left validationRuns
   empty.

Fixes:

- Replace "Delete docs/.alm-plan-data.json after success" in
  Phase 3 with explicit guidance to keep the file. Document why:
  Phases 5/6/7/8 update fields in this file then re-render the
  HTML, and check-alm-plan.js reads it for downstream Phase 0
  gates. Deleting it broke both flows.

- New helper scripts/lib/refresh-alm-plan-data.js abstracts the
  "read planData → apply phase-specific updates → write back →
  optionally re-render" pattern. Five phases supported:
  - setup-solution: no-op (placeholder for future use)
  - setup-pipeline: rewrites hostResolution from .last-host-check.json
    (status flips NoHost → AvailableUsingCustomHost; willEnsure*
    / willProvision* flags clear), populates pipelineMeta from
    .last-pipeline.json (name, ID, host URL, stages; lastDeploy
    null for now), drops resolved entries from risks[] (NoHost,
    *Unbound*, Platform-Host warnings).
  - deploy-pipeline: writes pipelineMeta.lastDeploy from
    .last-deploy.json (status, stageName, deployedAt,
    artifactVersion, componentCount, activationStatus, siteUrl).
  - test-site: writes validationRuns[stage] from
    .last-test-site.json.
  - finalize: sets PLAN_STATUS = "Completed".
  Helper returns ok:false (exit 0, soft no-op) when planData JSON
  is missing — caller surfaces the reason. Renderer is invoked
  via execFileSync when --render is passed.

- SKILL.md Phase 6 / Phase 7 Step A / Phase 7 Step C / Phase 8
  updated to invoke the helper after each completion. Phase 8 no
  longer relies on Edit-tool footer-only updates — runs the
  finalize phase + render so all tabs reflect end-of-run state.

- 13 new tests cover the helper: arg validation, missing-planData
  soft-fail, hostResolution rewrite, lastDeploy population,
  validationRuns ingest, risks pruning (with various pre-run
  warning shapes), reusedByWiring preservation, and finalize.

Tests: 557/562 (5 pre-existing render-alm-plan baseline failures
unchanged).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* plan-alm: clickable checklist step names link to relevant tabs

The Execution Checklist tab listed each step as inert text. To see
what a step did, the user had to read the step name, then manually
click the matching tab in the sidebar (Solutions / Pipelines /
Validation / Overview) to find the actual content.

Wire each step name as an anchor that re-uses the existing
data-tab click handler installed by the template's footer script.
The mapping (step pattern → target tab):

- Test site*           → Validation tab
- Deploy*              → Pipelines tab
- Import*              → Solutions tab
- Activate*            → Pipelines tab
- Setup pipeline       → Pipelines tab
- Setup solution       → Solutions tab
- Export solution      → Solutions tab
- Ensure pipelines host → Pipelines tab
- Finalize             → Overview tab

Steps that don't match any pattern (custom user-added steps,
unmapped future skills) render as plain text — no broken link.

The anchor's onclick reuses the same selector the template's footer
script listens on (.nav-btn[data-tab="..."].click()), so sidebar
tabs and checklist links can never disagree. Falls back to
href="#tab-..." for middle-click / right-click / copy-link.

CSS: .checklist-link uses dashed underline + ↗ glyph so the link
affordance is clear without making the checklist look noisy.

9 new tests cover each pattern in the mapping plus the no-link
path and the onclick contract. The pre-existing test that asserts
status-warning escalation needed its lookback widened from 200 to
500 chars because the wrapped anchor adds ~100 chars before the
step name.

Tests: 566/571 (5 pre-existing render-alm-plan baseline failures
unchanged).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* plan-alm: stage cards + host card lead with env name, demote URL to nav link

User reported the Pipelines tab stage cards showed only env URLs.
URLs are useful for jumping to an env, but reviewers recognize envs
by their display names ("ni-dev", "Supplier Portal Staging"), not
by GUID-y instance URLs. The plan needs to lead with the friendly
identifier and keep the URL available as a one-click navigation
aid.

Changes:

1. render-alm-plan.js: extract a shared buildStageCardHtml() so
   the Overview pipeline diagram and the Pipelines tab body render
   stage cards identically. Card layout is now:
   - stage label (Dev / Staging / Production)  — large, bold
   - env display name (ni-dev / etc.)          — medium, bold
   - env URL (clickable, target="_blank")      — small, mono
   When envName is missing the slot is omitted entirely (no empty
   div), preserving the older URL-only render for fresh planData.
2. host card (Pipelines tab) gains the same treatment: when
   hostResolution.hostEnvName is present, the card-env-name div
   leads with the friendly name and the URL becomes a clickable
   navigation link beneath it. URL-only fallback preserved for
   planData generated before the field existed.
3. CSS: new .stage-env-name and .host-card .card-env-name /
   .card-env-url rules. Anchors get a dashed-underline-on-hover
   affordance so the link nature is clear without making the
   card look noisy.
4. ensure-pipelines-host-detect.js: capture finalHostEnvName from
   the BAP env displayName at every code path that sets
   finalHostEnvUrl (org-binding success, single-custom-host pick,
   platform-host fallback). Output JSON shape grows by one
   nullable field — backward compatible.
5. refresh-alm-plan-data.js: map finalHostEnvName →
   hostResolution.hostEnvName during post-run refresh.
6. plan-alm SKILL.md: Phase 2 PP_STAGES build instructions extended
   to populate envName from ENV_LIST (matched by URL origin).
   "Other (paste URL)" entries leave envName unset; renderer falls
   back gracefully.

5 new render tests cover: env-name on stage cards (Overview +
Pipelines), URL-only fallback when name missing, host card leading
with name + clickable URL, host card URL-only fallback. 1 new
refresh-helper test pins finalHostEnvName flow-through.

Tests: 571/576 (5 pre-existing render-alm-plan baseline failures
unchanged).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…irst menu (#141)

User asked how the env-first prompt presents choices when more than
5 envs qualify for hosting Pipelines. Honest answer: today the
SKILL.md prose says "Pick from your eligible envs: env[0], env[1], …"
with no cap — in tenants with many envs (Microsoft-internal test
tenants have 1000+) the agent would dump 30+ options into a single
AskUserQuestion call, which becomes unscannable past ~7-8 options.

Add a 5-env presentation cap to both the ensure-pipelines-host
Phase 3.C prompt and the mirrored plan-alm Q4 NoHost prompt. The
two are intentionally identical because plan-alm Q4 collects the
host choice once at planning time and ensure-pipelines-host trusts
it via the chosenEnvUrl skip rule (commit 075c33f).

Cap policy:

1. Always-visible role-labeled envs first. dev / source / staging /
   production envs from PP_STAGES surface at the top regardless of
   ranking — they're the project's own envs and nearly always the
   right pick.
2. Fill remaining slots up to 5 from the rest of the eligible list
   in list-tenant-envs.js's native order (name-hint pattern →
   admin-perms → recency).
3. Append "Other (paste URL)" as the last per-env entry inside
   option 1's nested list. Long-tail escape hatch.
4. When eligible.length > 5, suffix option 1's headline with
   "Showing top 5 of {N}; the remaining {N-5} eligible env(s) can
   be reached via the 'Other (paste URL)' entry." Pre-fill the
   URL input from ENV_LIST so the user can paste-or-pick from the
   pac env list output.

Cap policy is documented once in plan-alm Q4 ("Eligible-env
presentation cap" sub-section) and ensure-pipelines-host Phase 3.C
links back to it, so the two prompts can't drift.

Test scenarios listed inline in both prompts:
- 0 eligible → 3-option variant (existing empty-list branch)
- 1-5 eligible → list all inline, no suffix
- 6+ eligible with role-labeled envs → role-labeled first, then
  ranked fill, then suffix
- 6+ eligible with no role-labeled envs → top 5 by ranking, suffix

No code changes — pure SKILL.md prose. Lint passes (26/26).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
)

Promotes the BAP getOrCreate Platform-Host fast-path from "deferred /
future iteration" to a shipped first-position option in the NoHost menu.
For first-time tenants this is the lowest-friction path — no admin role
required, idempotent, ~3-5 min — replacing the prior workaround of
sending the user to make.powerapps.com to trigger PE provisioning.

Phase 3.C menu restructured:
  1. Provision a Platform Host (recommended) — NEW, calls getOrCreate
  2. Set up a Custom Host — sub-prompt: existing env / create-new / Back
  3. Open PPAC manually
  4. Cancel

New helper provision-platform-host.js mirrors provision-custom-host.js
but targets /getOrCreate with a Platform/D365_1stPartyAdminApps body.
Returns alreadyExisted=true on the 200 idempotent path (existing PE
returned) or alreadyExisted=false on the 202 + Location-poll path.

Wrong-tenant safety: Phase 1.4 tenant-identity gate now resolves and
echoes the tenant display name (via Microsoft Graph) alongside the
tenant GUID, and Phase 4.0 echoes them again in a non-skippable
pre-call confirmation gate before invoking getOrCreate. PE is
tenant-singleton + admin-non-deletable; idempotency + the two gates
are the principal mitigations.

plan-alm Q4 NoHost menu mirrors the new shape with a WILL_PROVISION_PLATFORM
aux flag threaded through hostResolution.willProvisionPlatform so
ensure-pipelines-host skips its 3.C re-prompt at execution time.

Also drops --allow-no-subscriptions from getAuthToken's az invocation
(get-access-token rejects it on recent CLI versions; the flag is only
valid on az login / az account show), updates AGENTS.md guidance, and
fixes the same flag misuse on az account show in activate-site/SKILL.md.

Plugin bumped 1.3.0 -> 1.4.0.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nding (#147)

* Unify confusing Canvas Edit vs Generate skills (#137)

* Initial work to unify canvas skills

* Attempt at review

* Version

* Updates

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Rename to /canvas-app

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* ALM skills release-readiness pass: plan-UI fixes, OAuth handling, MCP grounding

Final pre-release IronItOut pass on the Power Pages ALM skill suite. Addresses
seven user-reported glitches in generated ALM plans, expands OAuth credential
handling beyond exclude-only, and grounds solution + pipeline skills in current
Microsoft Learn ALM docs.

Plan UI fixes
- Overview stat card + Size Analysis env-vars signal now render dual count
  ("0 / +N planned") so a fresh project doesn't read as empty when N auth
  settings are queued for env-var promotion. New planData.plannedEnvVarCount
  field carries the count from plan-alm Phase 1 Step 7 to the renderer.
- Env Variables tab now renders both an "Existing" sub-table and a "Planned"
  summary when both are present, instead of a single empty-state message.
- Pipelines host card no longer leaks the BAP getOrCreate API name or "no
  admin role required" disclaimer. User-facing prose stays focused on the
  tenant-identity confirmation gate.
- Risks list rewritten: drops the contradictory "OAuth secrets excluded /
  configure manually" wording; clarifies that auth-setting promotion is a
  user-choice gate, not automatic; adds a sync-mode discoverability info
  entry pointing users at /power-pages:setup-solution sync mode for adding
  components later.

OAuth handling
- Tier 1 credential-style site settings (ConsumerKey / ClientId / ClientSecret
  / etc.) are no longer blanket-excluded. plan-alm Phase 1 Step 7 routes them
  to a new credentialNeedsDecision bucket; setup-solution Phase 5 Step 5.4.C
  prompts per credential to choose between (a) Secret-typed env var (Key Vault
  per stage), (b) String-typed env var (plain text per stage, for non-secret
  values that vary per env), or (c) skip and manage out-of-band. Site setting
  records are added to the solution and routed to env vars so secret values
  never ship in the solution zip. Backward compat: pre-IronItOut plans with
  preloadedSettings.excluded are treated as credentialNeedsDecision.

Solution growth
- Lowered alm-thresholds.js split-decision defaults from 95 MB / 6000 components
  to 75 MB / 4000 components (with warnComponentCount also dropping from 3000
  to 2500). This recommends a split earlier so each split child has ~20 MB /
  ~2000-component growth headroom before hitting the platform hard caps.
- Hardcoded thresholds in render-alm-plan.js Size Analysis card updated to
  match the new defaults (95→75, 6,000→4,000).
- AGENTS.md documents the rationale for the threshold tightening.

MCP Learn grounding
- New shared reference references/alm-docs-grounding.md captures the discovery
  pattern (search → fetch anchor → summarize → flag divergence). Cap'd at
  ~30 sec, runnable offline.
- All 7 ALM skills (setup-solution, export-solution, import-solution,
  diagnose-deployment, setup-pipeline, deploy-pipeline, ensure-pipelines-host)
  now declare microsoft_docs_search + microsoft_docs_fetch in allowed-tools
  and embed a Phase 1.5 grounding step with skill-specific query and anchor.
  Anchors: solution-concepts-alm for solution skills, pipelines for pipeline
  skills.

Skill-writing consistency
- setup-pipeline + deploy-pipeline now end with the standard
  > Reference: ${CLAUDE_PLUGIN_ROOT}/references/skill-tracking-reference.md
  pointer, matching the established pattern in setup-solution / create-site /
  setup-datamodel.

Plugin bumped 1.4.0 → 1.5.0.

Tests: 593/598 pass. The 5 failures are pre-existing render-alm-plan tests
that predate this branch (verified earlier via git stash). Deferred to a
follow-up triage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* plan-alm + setup-solution: stat-card fallbacks, threshold-from-constant, bulk OAuth prompt

Three follow-ups from regenerating supplier-invoice-portal/docs/alm-plan.html
against the IronItOut changes:

1. Overview tab showed "0 components" / "0 MB" while Solutions tab showed the
   correct 288 components. Same data-assembly run, divergent values — happens
   when estimate-solution-size.js's componentCount.value comes back 0 (auth
   scope or query failure) but compute-split-plan.js populates
   proposedSolutions[].componentCount from a different code path. Added a
   fallback chain so the Overview headline reads from proposedSolutions
   aggregate when the top-level fields are 0/missing. Same fallback for
   totalSizeMB.

2. Size Analysis still showed "< 95 MB" despite the threshold drop to 75 MB.
   render-alm-plan.js had a hardcoded const SIZE_LIMIT_MB = 95, plus prose
   referencing the old cap, plus signal-card thresholds in fixed strings.
   Pulled all of them from alm-thresholds.js DEFAULTS so a future bump in the
   central thresholds flows through here without additional code edits.

3. Per-credential prompt for 25 OAuth credentials is too much. Replaced with a
   bulk-with-override prompt: one AskUserQuestion covers all N credentials
   with five options:
     - Auto-classify by name (recommended) — Secret env var for *Secret /
       *Password / *ApiKey / *AppKey patterns; String env var for *Id /
       ConsumerKey patterns. One confirmation, all N handled.
     - All as Secret env vars (Key Vault per stage)
     - All as String env vars (plain text per stage)
     - Skip all (manage out-of-band per env)
     - Pick per credential (the original 25-prompt path, kept as escape hatch)

   plan-alm risks message updated to describe the bulk prompt accurately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ALM verify-pass: hardcoded values, ensure-pipelines-host scope copy, stale tests

Follow-up audit pass on the IronItOut work. Four real divergences:

1. render-alm-plan.js Asset Advisory pass-box hardcoded "(2 MB)" — pulled
   from ALM_THRESHOLDS.maxSingleFileMB so a future bump in alm-thresholds.js
   flows through automatically.

2. ensure-pipelines-host frontmatter argument-hint and Scope section both
   leaked the BAP `getOrCreate` API name into user-facing copy. The argument-
   hint is rendered in slash-command help; the Scope section is rendered when
   the skill loads. Replaced with outcome-focused prose: "Platform-Host fast-
   path (idempotent, ~3–5 min)". Implementation detail (endpoint name, body
   shape) stays in Phase 4.0 technical sections only.

3. plan-alm Phase 1 Step 11 hardcoded "componenttype 10428" in a user-facing
   risk message. Component type integers are environment-specific (the
   discover-site-components helper queries them at runtime). Replaced with
   the logical name "powerpagesitelanguage" — same UX, no leak of an
   environment-specific number.

4. Five "pre-existing" render-alm-plan test failures were stale, not pre-
   existing in the always-broken sense. Each asserted a renderer feature that
   was speced but never implemented:
     - "stage labels" — checked for an Approval-gate badge that the renderer
       doesn't emit (approvals live in plan-alm Phase 3 risks, not in stage
       cards). Surgical fix: dropped only the failing assertion; kept the
       three stage-label / URL assertions that pass.
     - "HAS_ENV_VARS warning note" — asserted a top-level HAS_ENV_VARS flag
       that the renderer never honors (env-var concerns flow through the
       risks list and the dual-count Overview stat). Removed.
     - "exits non-zero on missing keys" — asserted hard validation that the
       renderer is intentionally permissive about (plan-alm Phase 3 is the
       contract enforcer, not the renderer). Removed.
     - "solutionContents excluded note" — asserted a "Site Settings
       Breakdown" rendering section that doesn't exist. Reduced the test to
       the assertion that does pass: tables surface in the Solutions tab.
       Updated test data to use credentialNeedsDecision instead of the old
       excluded bucket.
     - "solutionContents authNoValue warning" — asserted the same missing
       breakdown section. Removed.

Suite is now 595/595 green. The deferred-failure note in the IronItOut PR
description can come out.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Extract classification + schema-name logic from SKILL.md prose into helpers

Audit pass identified two pieces of deterministic logic embedded as inline
prose in multiple SKILL.md files — risking transcription drift and divergent
behavior across skills. Pulled both into testable helpers in scripts/lib/
and updated the SKILL.md prose to reference them as the single source of
truth.

scripts/lib/classify-site-settings.js
  Source of truth for the credential regex (Tier 1 / credentialNeedsDecision)
  + AUTH_PREFIX regex (Tier 2 / promoteToEnvVar / authNoValue) + the bulk
  auto-classify Secret-vs-String regex pair (used by setup-solution Phase
  5.4.C.1). Exports:
    - classify({name, value}) → { tier }
    - bulkClassify(settings) → { keepAsIs, authNoValue, promoteToEnvVar, credentialNeedsDecision }
    - autoClassifyCredential(name) → { default: 'secret'|'string', reason }
    - CREDENTIAL_REGEX, AUTH_PREFIX_REGEX, CREDENTIAL_SECRET_REGEX, CREDENTIAL_STRING_REGEX
  CLI mode reads a JSON array from stdin and writes the four-bucket shape
  to stdout. Tests: 19 covering tier mapping, edge cases, regex patterns,
  precedence (Secret check runs first when both could match).

  Before: the credential regex appeared verbatim in plan-alm Phase 1 Step
  7 AND setup-solution Phase 5 — same pattern in two prose blocks. A
  regex change in one would silently diverge from the other.

scripts/lib/generate-env-var-schema-name.js
  Source of truth for the canonical env var schema name rule
  `{prefix}_{settingName.replace(/[^A-Za-z0-9]+/g,'_').toLowerCase()}`.
  Used by setup-solution (creates definitions) AND configure-env-variables
  (references them). Tests: 14 covering case insensitivity, special
  characters, run collapsing, prefix normalization, deterministic output.

  Before: setup-solution's prose said "lowercase" (e.g.
  ids_authentication_registration_localloginenabled) while
  configure-env-variables suggested CamelCase (e.g. ids_LocalLoginEnabled).
  These would NOT link — setup-solution would create an env var with
  one name and configure-env-variables would try to reference a different
  name. Fixed by aligning both prose blocks to the helper.

SKILL.md updates:
- plan-alm Phase 1 Step 7: drop inline regex, point at classify-site-settings.js
- setup-solution Phase 5 Step 5.3: drop inline regex from the tier table
- setup-solution Phase 5 Step 5.4.A: insert call to generate-env-var-schema-name.js
- setup-solution Phase 5 Step 5.4.C.1: drop inline auto-classify regex table
- setup-solution Phase 5 Step 5.4.C.3: insert call to generate-env-var-schema-name.js
- configure-env-variables Phase 2: replace CamelCase suggestion with helper call
- AGENTS.md "Shared lib modules" section: register both new helpers

Tests: 628/628 pass (33 new + 595 pre-existing). Suite is fully green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ensure-pipelines-host Phase 4.B: automate Pipelines app install (replaces manual PPAC click-through)

Phase 4.B (install Pipelines on existing env) was previously a manual
click-through to PPAC: the skill printed the URL and asked the user to
"Install app → Power Platform Pipelines → Next → accept terms → Install".
This commit replaces that flow with a fully automated path that calls the
same BAP applicationPackages/install endpoint PPAC's button uses.

scripts/lib/install-pipelines-app.js
  Discover + install the Power Platform Pipelines application package on an
  existing Dataverse env. Resolution order:
    1. BAP applicationPackages LIST — discover the package by uniqueName
       (msdyn_AppDeploymentAnchor) or displayName ("Power Platform Pipelines"
       substring fallback). If properties.state is already Installed, return
       alreadyInstalled=true (idempotent — same shape as
       provision-platform-host.js's getOrCreate idempotent path).
    2. BAP applicationPackages/install POST — 200 sync or 202 + Location
       poll. Same polling pattern as the two existing provision-* helpers.
    3. PAC CLI fallback when BAP returns 401/403/5xx — same scenario the
       pac-bap-shim handles for env enumeration. Tries modern PAC arg
       names first, falls through to legacy ones if rejected.
    4. Optional Dataverse verification probe — solutions?$filter=uniquename
       eq 'msdyn_AppDeploymentAnchor' confirms the install actually landed,
       not just that BAP reported success.
  Output flag installPath: 'bap' | 'pac' | 'cached' so callers can record
  telemetry on which route succeeded.

scripts/tests/install-pipelines-app.test.js
  20 tests covering discovery (idempotent), 200 sync, 202 + Location poll,
  409 conflict (idempotent), 403 → PAC fallback, 5xx → PAC fallback,
  transport-error → PAC fallback, --no-pac-fallback honored, both-fail
  surfaces combined error, polling timeout, Failed terminal state.

ensure-pipelines-host SKILL.md Phase 4.B rewrite
  Replaced the four-step manual instruction (Open URL → Click Install app →
  Click Next → wait for Installed) with:
    - A non-skippable pre-call confirmation gate (echoes tenant + env, same
      pattern as Phase 4.0 / 4.A).
    - A single helper call.
    - Branching on result.installPath ('cached' | 'bap' | 'pac').
    - Last-resort manual PPAC fallback only when both BAP and PAC fail
      (rare — surfaces the combined error).
  Added the new helper to the Scripts table + AGENTS.md helper inventory.

Tests: 648/648 pass (20 new + 628 pre-existing).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ALM skills: self-refresh the plan HTML after writing marker files

The rendered ALM plan (docs/alm-plan.html) was staying frozen at pre-run
state when users invoked /power-pages:deploy-pipeline (or setup-pipeline,
setup-solution, test-site) directly instead of via plan-alm. The refresh
helper was only described in plan-alm Phase 7 prose — none of the four
marker-writing skills self-triggered it. So a standalone deploy-pipeline
run wrote .last-deploy.json correctly but never updated the plan HTML.

Each of the four ALM skills that writes a marker file now ends its final
phase with a refresh-alm-plan-data.js call that reads the marker it just
wrote and re-renders the plan HTML. The helper soft-no-ops when
docs/.alm-plan-data.json is absent (standalone invocation, no plan in the
project), so the call is safe to run unconditionally.

  - deploy-pipeline Phase 7.5b → --phase deploy-pipeline
  - setup-pipeline  Phase 7.5b → --phase setup-pipeline
  - setup-solution  final phase → --phase setup-solution
  - test-site       Phase 6.7b → --phase test-site --stageName {label}

plan-alm Phase 7 still calls the helper as belt-and-suspenders; the helper
is idempotent, so running it twice with the same marker produces the same
output.

Tests: 648/648 still pass — change is SKILL.md prose only, no code or
test changes needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ALM Manual path: refresh plan HTML after export / import / activate

Manual-path skills (export-solution, import-solution, activate-site) had no
plan-refresh trigger today — refresh-alm-plan-data.js only knew about
setup-solution / setup-pipeline / deploy-pipeline / test-site / finalize.
Symptom: when a user runs plan-alm with STRATEGY=manual (or invokes any of
the three skills standalone), the rendered docs/alm-plan.html stays frozen
at pre-run state — the per-target Import / Activate steps never flip from
in_progress to completed in the UI.

scripts/lib/refresh-alm-plan-data.js
- Added export-solution, import-solution, activate-site to the PHASES set
  and the applyRefresh switch.
- Each handler is intentionally a passthrough (no marker file ingestion
  today — none of the three skills writes a structured marker the helper
  could reasonably consume per-target). The KEY work the refresh does for
  these phases is re-rendering the HTML so the agent's planData.steps[i]
  .status updates flow through. Per-target history (which target imported
  successfully, when) is a future enhancement noted in the handler docstrings.
- CLI doc comment + validation-error enumeration updated to list the new
  phases.

Self-refresh in skills (final phase, after the existing skill-tracking call):
  - export-solution: --phase export-solution
  - import-solution: --phase import-solution
  - activate-site:   --phase activate-site

plan-alm Phase 6 (Manual path) and Phase 7 (per-target import + activate
loop): added explicit refresh-helper calls after each step. Belt-and-
suspenders with the skill self-refresh; the helper is idempotent so running
it twice is harmless.

Tests: 652/652 pass (4 new in refresh-alm-plan-data.test.js for the new
phases + the existing 648).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ALM Manual path: per-target import history + render IMPORTED/FAILED substep

Builds on the previous Manual-path refresh commit. The passthrough handler
for import-solution now actually ingests .last-import.json and captures the
outcome per target stage, parallel to validationRuns[stageName] for tests.

scripts/lib/refresh-alm-plan-data.js
- refreshImportSolution accepts stageName (already plumbed through the
  switch from --stageName) and reads .last-import.json into
  planData.manualImports[stageName].
- Stage resolution: explicit --stageName wins; falls back to URL match
  against planData.stages[].envUrl when omitted; synthesizes
  "unresolved-{targetEnvironment}" key when neither resolves so the import
  isn't silently dropped.
- Captured fields: solutionName, targetEnvironment, importedAt, status,
  artifactVersion, componentCount, componentFailureCount (counted from
  componentResults[].status), importJobId.
- Multi-target imports preserve prior entries across calls — second
  import to Production doesn't clobber the Staging entry from the first.

skills/plan-alm/scripts/render-alm-plan.js
- buildChecklistHtml reads planData.manualImports and matches against
  "Import to {stageName}" steps. Renders an IMPORTED / FAILED badge with
  version + component count + failure count inline as a substep — same
  visual idiom as the test-site validation substep.
- Promotes step status from completed → warning when component failures
  are present, mirroring the test-site fail-promote behavior.

skills/import-solution/SKILL.md + skills/plan-alm/SKILL.md
- Both self-refresh + Phase 7 Manual path orchestrator pass --stageName
  so URL-matching is a fallback, not the primary path.

Tests: 657/657 pass (5 new for per-target history + 652 pre-existing).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* setup-solution: write .last-env-vars.json sidecar so refresh updates the Env Variables tab

The refreshSetupSolution handler reset plannedEnvVarCount to 0 but didn't
populate planData.envVars[] with the freshly-created definitions. Symptom:
after setup-solution runs (creating N env var definitions in Dataverse),
the rendered plan's Env Variables tab stays empty — Overview stat card
goes from "0 / +N planned" to just "0", which reads as a regression.

Root cause: refreshSetupSolution only knew how to reset the planned counter;
it had no marker file to read for the post-setup env var state. Discovery
(discover-env-var-definitions.js) needs Dataverse credentials, which the
refresh helper deliberately doesn't acquire.

Fix:
- setup-solution Phase 6 step 2b now runs discover-env-var-definitions.js
  with the post-setup state and writes the result to .last-env-vars.json.
  Tmp-file write pattern preserves a prior good sidecar on transient
  discovery failure (parallel to .alm-size-estimate.json in plan-alm).
- refreshSetupSolution reads .last-env-vars.json and replaces
  planData.envVars with the discovery result. Empty array is a valid
  post-state (user picked Skip-all in the bulk OAuth prompt) — the
  renderer's "no env vars" path applies.

Tests: 660/660 pass (3 new for setup-solution sidecar ingestion + 657
pre-existing).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ALM plan: per-target activations + test-site stageName fallback

Two reliability gaps the audit found in how Activate / Test plan steps
update after execution.

activate-site previously had zero structured data captured for the rendered
plan — only a bare step.status flip. No site URL, no per-target activation
timestamp, no failure detail. For PP path the .last-deploy.json carried
activationStatus + siteUrl (deploy-pipeline embeds activation), but the
Manual-path standalone activate-site invocation had nothing.

Fix:
- activate-site Phase 5.1b writes .last-activate.json with stageName,
  siteName, siteUrl, websiteRecordId, environmentUrl, activatedAt, status.
  Also writes the marker on the AlreadyActivated detection path (Phase 1.4)
  so re-runs reflect the live state in the plan.
- refreshActivateSite ingests the marker into planData.activations[stage]
  parallel to validationRuns. Stage resolution: explicit --stageName wins;
  falls back to marker.stageName; falls back to environmentUrl match against
  planData.stages[].envUrl; finally synthesizes "unresolved-{envUrl}" key.
- Renderer adds an ACTIVATED / ALREADY LIVE / FAILED substep on
  "Activate site in {stage}" checklist steps with the live site URL as a
  clickable link. Failed activations promote step from completed to warning
  (mirrors test-site fail-promote behavior).

test-site stageName fallback:
- refreshTestSite previously hard-failed when --stageName arg was missing.
  Standalone test-site invocations would not get the per-stage validationRun
  badge.
- Now falls back to (1) marker's stageName field (test-site Phase 6.7a now
  captures stageName when known); (2) the single target stage in
  planData.stages when only one exists. Multi-stage with no signal still
  returns no-op (intentional — never silently pick a stage).
- test-site SKILL.md updated to write stageName into the marker when known
  and document the fallback chain.

Tests: 667/667 pass (7 new for activate-site + test-site fallback paths
+ 660 pre-existing).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: McCall Saltzman <69215460+lesaltzm@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* force-link-environment skill + setup-pipeline Pattern 15 auto-fix; drop unused plan-alm Q7 git question

New skill /power-pages:force-link-environment wraps the Dataverse ManageEnvironmentStamp action (HAR-verified against AppDeploymentConfiguration UI) so users can take over an environment's Pipelines-host association when a previous host has claimed it. setup-pipeline Phase 5 now detects the "already associated with another pipelines host" error and offers the new skill as the documented auto-fix for Pattern 15.

The plan-alm Q7 source-control question and its GIT_STATUS planData field were dead weight (renderer didn't read the field, no automation was driven from it) — removed to keep the question flow focused. Will be re-introduced when the git-versioning skill ships.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* PR #149 review fixes: setup-pipeline Phase 5/5a CLI handoff + minors

Addresses the medium and minor findings from review:

- setup-pipeline Phase 5: correct create-deployment-environment.js invocation
  to use --bapEnvId (BAP env GUID, the actual required arg) + --environmentType
  instead of the stale --environmentUrl-only prose. Document where bapEnvId
  comes from (pac env list / pac env who) and that it must be retained for
  Phase 5a's force-link handoff. (medium #1)

- setup-pipeline Phase 5a: clarify bapEnvId source for the --dev-env arg passed
  into /power-pages:force-link-environment; tighten the post-success retry
  scope to "re-attempt just the failing environment" instead of restarting
  Phase 5 wholesale (the create script's findExistingByBapId short-circuit
  makes the targeted retry safe and cheaper). (medium #2)

- setup-pipeline Phase 5a: expand the case-insensitive substring list for
  Pattern 15 detection (six variants now) and add the stable Dataverse error
  code 0x80048d18 as a fallback signal so backend wording drift doesn't
  silently break detection. (minor #5)

- force-link-environment.js: document why post-validation poll uses v9.0
  (HAR-captured against AppDeploymentConfiguration UI) vs the v9.1 used by
  create-deployment-environment.js. Scope lastErrorMessage to the FAILED
  branch only, removing the dead per-iteration assignment. (minors #4, #6)

- validate-force-link.test.js: replace the destructure-with-eslint-disable
  pattern with a small withoutField() helper since the repo doesn't run
  ESLint and the disable comments were misleading. (minor #7)

- force-link-environment SKILL.md: spell out the previousHostEnvUrl extraction
  order (last-host-check.json → errormessage regex → null fallback) so the
  agent doesn't have to invent the rule. Note that this field is informational
  only and never blocks the marker write. (minor #8)

Tests: 690/690 still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* plan-alm Env Variables tab: expandable cards + per-env values matrix; surface displayName/description from discovery

Env Variables tab UX overhaul to match the design mock and fix the empty-tab regression seen on the supplier-invoice-portal plan (planData.envVars=[] even though 7 definitions existed on the live env).

discover-env-var-definitions.js — output now includes displayName and description. displayname was already in the OData $select but wasn't surfaced on the return object; description is newly added to $select. Both fields fall back gracefully (displayName → schemaName, description → empty string) when the underlying record has them unset, so old plans don't break.

render-alm-plan.js — buildEnvVarsHtml now emits one expandable card per env var (mirrors the mock's pattern) instead of the prior flat <table>. Cards expose displayName, schemaName, type, bound site setting, default value, and description in a fields grid; optional rationale renders as a styled callout when populated. A new buildEnvVarValuesMatrix renders a single env-var × stage comparison table below the cards, but only when at least one env var has values{} populated (after deploy-pipeline back-fills). The planned-count note wording was tightened from "up to N" (which read as "all N will be created") to "N candidate site settings one at a time" with the per-setting choice explained. A neutral refresh hint always closes the tab when either count is non-zero, telling users the data back-fills after setup-solution / deploy-pipeline.

alm-plan-template.html — new CSS classes (.envvar-card, .envvar-header, .envvar-body, .envvar-tag, .envvar-name, .envvar-type, .envvar-chevron, .envvar-fields, .envvar-values) modelled on the existing .split-solution-card pattern. Click-to-toggle JS hook wired alongside the existing solution-card handler.

Tests — 3 new (cards render with 2 blocks + headers/bodies, values matrix renders when populated, matrix omitted when no values{} maps). Discovery test extended to assert displayName/description threading + description in the $select clause. The prior "renders the per-variable table" test was retargeted at the new card structure.

Live plan refresh — manually re-ran discovery against the supplier-invoice-portal dev env (orgfa4e9100.crm17, publisherPrefix crd50) and ingested the 7 definitions into the project's planData; the regenerated alm-plan.html now shows 7 expandable cards instead of the prior "planned (25)" note.

Root cause confirmation: the empty envVars[] was an orchestration gap (Step 10b not invoked or sidecar not ingested), not a query gap — the size estimator's count-7 and the discovery's count-7 used the same prefix-filter against the same env. The renderer changes here defend against the case when the sidecar IS populated, and the next-up upstream fix should ensure setup-solution / refresh-alm-plan-data reliably write .last-env-vars.json so this populates automatically.

Tests: 693/693 pass (+3 vs main).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ator pagination + solution-scoped env vars (#154)

Four coordinated changes to the Power Pages ALM skills, all landing on this PR:

1. ALM artifacts now live under docs/alm/ instead of the project root.
   - New scripts/lib/alm-paths.js helper: single source of truth for 13 ALM-only
     state files (5 plan/decision JSONs + 8 last-*.json skill-run markers).
   - All writers, readers, validators, lib helpers, SKILL.md prose, reference
     docs, and tests updated to resolve paths via almPath() / ensureAlmDir().
   - .solution-manifest.json, .datamodel-manifest.json, .alm-config.json,
     .alm-deferred, deployment-settings.json intentionally stay at root.
   - Hard cutover (no legacy fallback) per feature-branch scope.

2. Post-sync approval gates in deploy-pipeline Phase 3.5 and
   export-solution Phase 2.5.
   - When the user picks "sync first" for a solution-completeness gap, the skill
     now captures PRE_SYNC_VERSION + PRE_SYNC_MISSING, runs setup-solution, then
     re-confirms with the user via AskUserQuestion showing the version bump and
     newly-adopted components before proceeding to deploy/export.
   - Pause/Cancel write no marker files and skip skill-tracking so the audit
     trail stays clean.
   - Plan-alm Phase 2 Q1b "keep as single solution anyway" override now
     re-surfaces tier signals (size, components, schema attrs, web files,
     env vars) and requires a free-text overrideReason before honoring it.

3. Estimator pagination correctness + truncation canary + env-var
   solution-scoping.
   - estimate-solution-size.js: odataGet now sends Prefer: odata.maxpagesize=5000;
     collectPaginated raised to a single PAGINATION_SAFETY_CAP (~500K rows).
     All $top=500 queries lifted to $top=5000. Removes the silent 500-row
     truncation that produced webFileCount=495 on stress-test sites with
     6000+ web files.
   - New truncation canary: countOData ground-truth probe + page-boundary
     detection. Output exposes truncationSuspected + truncationWarnings;
     compute-split-plan prepends them to recommendations[] as type:error
     entries so the rendered plan flags possibly-bad inputs inline.
   - envVarCount now solution-scoped via inSolution.byComponentType[380]
     when --solutionId is passed; falls back to publisher-prefix tenant-wide
     when no solution is set up yet. Output adds envVarCountScope and
     envVarCountTenantWide for diagnosability.
   - discover-env-var-definitions.js: paginates correctly (Prefer header +
     nextLink), accepts --solutionId, intersects against
     solutioncomponents.componenttype=380. Return shape adds scope field.
   - plan-alm Phase 1 Step 7 (mspp_sitesettings) updated to instruct
     pagination via Prefer + nextLink — was previously $top=500 plain.
   - rawDiscovery snapshot embedded verbatim in docs/.alm-plan-data.json
     so the rendered plan stays diagnosable post-hoc.

4. plan-alm Phase 7 Step B: explicit pac env select before activate-site
   on a multi-stage plan.
   - Previously prose said "Switch PAC CLI to the target environment" with
     no explicit command. On the Production iteration of a Staging+Production
     plan, PAC is back at dev after Staging's Step C, so activate-site could
     accidentally target Staging or dev. Now the SKILL.md shows the explicit
     pac env select --environment "{stage.targetEnvironmentUrl}" command,
     matching the proven pattern in deploy-pipeline Phase 7.7.

Test coverage:
  - 23 new tests across alm-paths, estimator (pagination + canary +
    solution-scoping), discover-env-var-definitions (pagination + scoping).
  - Updated existing test fixtures for docs/alm/ paths and new scope field.
  - All 715 tests passing (was 693 before this PR).

No code changes for SKILL.md prose edits to gates and Phase 7 Step B — they
are agent instructions, not executable code.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
T-Nid (tyaginidhi) and others added 10 commits May 14, 2026 21:23
…gesALM

# Conflicts:
#	plugins/canvas-apps/AGENTS.md
#	plugins/canvas-apps/agents/canvas-app-planner.md
…156)

Cluster of correctness and ergonomics fixes for the Power Pages ALM stack
(plan-alm, setup-solution, setup-pipeline, deploy-pipeline and the shared
refresh-alm-plan-data / compute-split-plan / estimate-solution-size /
create-solutions-batch / provision-platform-host helpers).

Behavior changes:

1. Solution split decision tree (compute-split-plan.js)
   - validateSplits enforces BOTH size AND component-count caps per split
     (skips isFutureBuffer)
   - Strategy 1 (Layer) runs a composite sub-partition pass when Core still
     exceeds either cap after Web Assets are peeled off; Core is replaced
     with _Foundation/_Config/_Content (+ _Integration whenever parent had
     any flows or bots — coverage takes priority over the changeFreqMinFlows
     heuristic in sub-partition context)
   - Sub-partition preserves full componentTypes coverage (Cloud Flow / Bot
     Component / Env Variable never silently dropped) and prevents env-var
     double-claim across additive Strategy 4

2. Size estimator (estimate-solution-size.js)
   - Stratified sample (first 50 + middle 50 + last 50, cap 150) replaces
     the biased slice(0, 80) — fixes field-reported undercounts on sites
     with large media biased to the tail of the ppc id range
   - New --projectRoot flag enables disk cross-check via the local build
     output (dist/public-output/build/.output) with symlink-loop protection
   - Two new undercount canaries: disk-vs-Dataverse divergence + suspiciously
     small average bytes/file at scale

3. Multi-solution parallel creation
   - NEW create-solutions-batch.js fans out N create-solution.js calls via
     Promise.allSettled with coordinated single 401-retry and idempotent
     409 handling; skips isFutureBuffer entries; ~2s for 5-6 solutions vs
     ~10s for the prior serial agent loop
   - setup-solution Phase 4 switches to the batch helper in MULTI_SOLUTION_MODE
   - setup-solution Phase 5 multi-solution component routing now documented
     (was a gap)

4. Renderer (render-alm-plan.js)
   - Strategy rationale text corrected: all multi-solution strategies now
     describe per-solution stage runs within one pipeline (matches Phase 6b
     architecture; the "each solution gets its own pipeline" copy was stale)
   - Composite-sub-partition rationale sentence renders when
     compositeSubPartitioned: true
   - Web Files signal card surfaces a disk-compare note + sample-extrapolation
     note when the relevant data is available

5. BAP getOrCreate URL (provision-platform-host.js)
   - Endpoint corrected from .../BusinessAppPlatform/getOrCreate (returns
     404 from BAP) to .../BusinessAppPlatform/environments/getOrCreate
   - Pre-existing regression test was asserting the wrong URL; flipped to
     the correct path and updated the test name/message

6. Pre-flight blocked-attachments check (deploy-pipeline Phase 2.5)
   - Prompts the user to unblock .js/.css on the target env BEFORE kicking
     off the long-running import, saving the ~50-75 min the reactive Phase
     7.6 path otherwise wastes; both paths coexist (pre-flight catches the
     env-setting case, reactive catches per-table file-column policies and
     concurrent admin edits)

7. Phase 5 heading + stale schema docs (setup-pipeline/SKILL.md)
   - Phase 5 renamed to "Register Environments with the Pipelines Host" —
     it's metadata-only registration of existing BAP envs, not provisioning
   - Stale msdyn_-prefixed field names and 192350001/192350002 validation
     status codes corrected to the shipped unprefixed schema and
     200000001/200000002

8. Plan-data auto-sync (refresh-alm-plan-data.js)
   - setStepStatus flips planData.steps[i].status per phase automatically;
     case-insensitive keyword + stage filter, respects skip:true, never
     regresses completed to non-failed (was previously the agent's
     responsibility via Edit, consistently missed in multi-phase
     orchestration)
   - deploy-pipeline phase additionally backfills planData.envVars[i].values{}
     from the project root's deployment-settings.json (accepts both
     top-level-stage and nested-stages shapes; SchemaName/Value and
     camelCase variants; never overwrites a populated cell — manual override
     wins). Populates the rendered plan's "Values by Environment" matrix.

Tests:
~62 new tests across 6 test files; full plugin suite 762 passing
(1 pre-existing PAC CLI environmental flake unrelated to this branch).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ng (#161)

* ALM follow-ups: v2 manifest validator, env-var type mapping, configure-env-vars phase, input validation, deploymentsettingsjson caveat

Five issues surfaced during live ALM runs after the prior PR landed. Each
had a clear root cause; this commit fixes them with focused changes plus
targeted test coverage.

1. validate-solution.js v2 manifest support
   - Pre-fix: validator only knew about v1's singular `solution.uniqueName`;
     a v2 multi-solution manifest (with `solutions[]` array) hit the
     "missing solution.uniqueName" block-path immediately and the
     post-toolUse hook failed the skill.
   - Fix: dispatch on `manifest.schemaVersion === 2` (or array-shape
     detection for hand-edited manifests without the version field).
     Walk `solutions[]`, validate each entry, require the website
     componentType 61 to be present in at least one solution. v1 path
     unchanged.
   - New tests: 12 scenarios in scripts/tests/validate-solution.test.js
     (didn't exist before) covering both v1 and v2 happy paths plus all
     the per-entry blocking conditions.

2. refresh-alm-plan-data.js: add configure-env-variables phase
   - Pre-fix: PHASES set didn't include configure-env-variables, so
     invoking --phase configure-env-variables threw the validation error.
   - Fix: new refreshConfigureEnvVariables handler — re-reads
     docs/alm/last-env-vars.json (definitions), backfills values{} from
     deployment-settings.json (per-stage matrix), zeros plannedEnvVarCount,
     drops resolved risks, step-syncs the matching checklist entry.
     Both "env variables" and "environment variables" step-name spellings
     match.

3. Env var TYPE_LABELS / ENV_VAR_TYPES Secret↔JSON swap
   - Pre-fix: discover-env-var-definitions.js and create-env-var-definition.js
     both had 100000003=Secret and 100000005=Json — swapped from the
     canonical Dataverse option-set values (Secret=100000005, JSON=100000003,
     DataSource=100000004). A Secret env var created via the Power Platform
     UI returned type 100000005 and was rendered as "Json" in the plan;
     calling create-env-var-definition.js with --type 100000003 to mean
     Secret silently created a JSON-typed record.
   - Fix: both helpers now use the canonical mapping. SKILL.md references
     in setup-solution and import-solution that hardcoded `--type 100000003`
     for Secret are updated to `--type 100000005`. Tests pin the full
     mapping as a regression guard.

4. add-components-to-solution.js input validation
   - Pre-fix: PascalCase input keys (ComponentId/ComponentType) silently
     destructured to undefined; the OData POST body carried
     `ComponentId: undefined` and Dataverse returned HTTP 400 "missing
     parameters" for every entry. The user saw a stream of 70+ failures
     with no upfront signal.
   - Fix: new validateComponentsShape() runs once on the parsed input,
     detects the PascalCase-everywhere pattern as a special case (clear
     error message naming the fix), then per-entry-validates each row's
     componentId (string) and componentType (number) with index-anchored
     errors and a PascalCase hint when applicable. Aborts before any
     Dataverse call.
   - New tests: 6 scenarios covering PascalCase detection, per-entry
     missing fields, malformed entries, and the validateComponentsShape
     helper directly.

5. deploymentsettingsjson unbound-env-var caveat (deploy-pipeline SKILL.md)
   - Live-observed: setting deploymentsettingsjson on a stage run with
     EnvironmentVariables[] does NOT always create environmentvariablevalues
     records on the target post-deploy. The Pipelines handler appears to
     write values only when the env var definition is bound to an
     mspp_sitesetting (or another consumer the platform recognizes).
     Definitions that aren't yet linked land as zero-value, and the user
     hits a broken site post-deploy with no clear signal in the deploy
     output.
   - Fix: caveat note added at Phase 5.2 explaining the behavior + workaround
     pointer to configure-env-variables / link-site-setting-to-env-var.js.
     New Phase 7.6.5 added — post-deploy verification step that queries
     environmentvariablevalues for each schemaName in ENV_VAR_OVERRIDES on
     the target env, logs structured envVarLandingWarnings[] into
     docs/alm/last-deploy.json, and surfaces a user-facing prompt when any
     value didn't land. Complementary to the existing Phase 7.6 reactive
     handler.

Tests:
+22 net new tests across 5 files (validate-solution.test.js new;
add-components-to-solution.test.js +6 input-validation; refresh-alm-plan-data.test.js
+3 configure-env-variables; discover-env-var-definitions.test.js and
create-env-var-definition.test.js pin the canonical type mapping).
Full plugin suite: 784/784 passing (clean run, no flakes).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* deploy-pipeline: add Phase 6.0 final-consent gate before DeployPackageAsync / pac pipeline deploy

The pre-existing flow had every individual decision gated by AskUserQuestion
(target stage, blocked-attachments unblock, completeness drift, validation
approval, env var values, mid-deploy approval) but no final "ready to ship?"
prompt immediately before the actual deploy. When validation passed cleanly
and no completeness drift was detected, Phase 5 → Phase 6.1 fired
DeployPackageAsync without a final confirmation — riskier for Production
targets where the cancellation cost (stage run already validated) is small
compared to a wrong-stage import.

Adds Phase 6.0 — explicit "Deploy now / Cancel" AskUserQuestion immediately
before either DeployPackageAsync or the pac pipeline deploy fallback. Names
the target env + new artifact version, calls out the ~3-60 min runtime, and
notes the non-transactional nature of the import (partial failure leaves
whatever already imported on the target). Cancel exits cleanly without
writing docs/alm/last-deploy.json.

Key Decision Points list updated to include the new gate at position 8.
AGENTS.md deploy-pipeline summary mentions the Phase 6.0 gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* .gitignore: drop stale .alm-size-estimate / .alm-split-plan entries

These ALM split-plan intermediates moved off the project root into
docs/alm/ in PR #154 (ALM artifacts under docs/alm/). The plan-alm
SKILL.md, setup-solution SKILL.md, and scripts/lib/alm-paths.js all
write/read these files under docs/alm/ now. The root-level paths the
.gitignore was guarding are no longer produced by any code path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* approval-gates: catalog + terminology + AGENTS.md pointer

Introduces references/approval-gates.md as the single source of truth
for user-confirmation points (Approval Gates) across the ALM skill
family. Doc only — no SKILL.md edits, no lint rule, no enforcement
yet. Those land in the follow-up commit per §10 (Landing plan).

What this commit defines:

- Terminology decision (§1). Adopt "Approval Gate" as the canonical
  term. CI/CD heritage (ADO release pipelines, Spinnaker, GitHub
  Environments), already the most common word in our existing
  SKILL.md prose. Drops "review gate" as a project-specific coinage.

- Definition + non-gate filter (§2). Cancel-leaves-state test
  catches both partial AND complete-but-wrong state.

- Six categories (§3). intent / plan / progress / consent / final /
  pause. Each has a defining attribute that distinguishes its blast
  radius — not just trigger timing. consent covers shared/irreversible
  state regardless of whether it fires proactively or reactively
  (so deploy-pipeline:2.5 pre-flight unblock and :7.6.2 reactive
  unblock are both consent).

- Marker syntax (§4). HTML comment as the structural lint anchor:
  <!-- gate: skill:phase | category=X | cancel-leaves=Y -->
  paired with a human-readable > 🚦 **Gate (...)** block. Replaces
  proximity-based matching with section-boundary pairing — robust
  to interleaved rationale prose. Normalized cancel-leaves vocabulary
  (§4.3) so lint can flag duplicates and known values.

- Five lint rules (§5). GATE-must-have-marker, GATE-id-must-be-unique,
  GATE-must-be-in-catalog, GATE-intent-must-call-helper,
  GATE-cancel-leaves-known-vocab. ALM hard-fail, non-ALM warn-only
  until §8 catalog extends.

- ALM-skill catalog (§6). ~90 gate rows + ~12 not-a-gate rows across
  12 ALM skills. Replaces v1's "sweep of every SKILL.md" overclaim —
  v2 is explicitly scoped to ALM with non-ALM (~70 additional calls)
  listed in §8 as deferred.

- Decisions pre-resolved (§9). Six decisions with recommendations
  for reviewers to confirm before the follow-up SKILL.md sweep lands.

AGENTS.md changes (CLAUDE.md propagates via symlink):

- Shared References list gets a bullet for approval-gates.md.
- Key Patterns "User confirmation" bullet upgraded to "Approval Gates"
  with the marker syntax inline and an explicit ban on coining
  alternative terms.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* approval-gates: enforce via markers + lint across 12 ALM skills

Lands the follow-up to the catalog v2 commit. Markers inserted in every
ALM SKILL.md, five lint rules added with hard-fail for ALM and warn-only
for non-ALM (the deferred §8 catalog scope).

Markers added (~90 across the 12 ALM SKILL.md files):

  plan-alm:               17 gates + 1 not-a-gate
  setup-solution:         11 gates + 0 not-a-gate
  setup-pipeline:          8 gates + 0 not-a-gate
  deploy-pipeline:        12 gates + 0 not-a-gate
  export-solution:         5 gates + 0 not-a-gate
  import-solution:         7 gates + 0 not-a-gate
  configure-env-variables: 3 gates + 0 not-a-gate
  ensure-pipelines-host:   3 gates + 1 not-a-gate
  force-link-environment:  1 gate  + 2 not-a-gate
  activate-site:           2 gates + 0 not-a-gate
  test-site:               4 gates + 1 not-a-gate
  diagnose-deployment:     1 gate  + 0 not-a-gate

Each gate marker uses the pairing format documented in
references/approval-gates.md §4:

  <!-- gate: skill:phase | category=X | cancel-leaves=Y -->
  > 🚦 **Gate (X · skill:phase):** One-line summary.

The HTML comment is the structural lint anchor; the visible block is for
humans. Section-boundary pairing replaces the v1 proximity rule — markers
just need to live anywhere earlier in the same `## Phase` or `### Phase`
section as the AskUserQuestion they cover.

Five lint rules added to scripts/lint-skills-alm.js (KNOWN_RULES set,
collectFindings logic, exports for tests):

  GATE-must-have-marker         — every prompt needs a preceding marker
  GATE-id-must-be-unique        — gate-ids globally unique across plugin
  GATE-must-be-in-catalog       — every gate-id has a catalog row
  GATE-intent-must-call-helper  — `intent` markers require a real helper
                                  invocation (check-alm-plan.js,
                                  verify-alm-prerequisites.js,
                                  check-activation-status.js)
  GATE-cancel-leaves-known-vocab — vocab from §4.3, or valid kebab-case

ALM_SKILLS set drives severity routing — ALM skills → 'error', non-ALM
→ 'warning'. main() now prints warnings to stdout and errors to stderr;
exit code is 1 only when at least one error fires.

Test coverage:

  - 26 existing tests still pass (no regressions)
  - 18 new tests covering all five rules, the parsing helpers
    (extractGateMarkers, extractNotAGateMarkers, findPromptLines,
    splitIntoSections), severity routing (ALM error vs non-ALM warning),
    catalog-absent graceful degradation, and the exported constants
  - 44 / 44 total tests pass

Catalog adjustments (in approval-gates.md §6 to match what's actually in
the SKILL.md files):

  - setup-solution: collapsed §6.2's four 5.5* rows into a single
    setup-solution:5.5.manifest-confirm covering all four multi-select
    sub-prompts. Single marker, single catalog row.
  - diagnose-deployment: collapsed §6.12's per-pattern :6.fix-001 / :6.fix-N
    template into a single :6.auto-fix loop gate. Pattern IDs themselves
    are stable; the gate identity is the loop, not the pattern.

Cross-references:

  - PLUGIN_DEVELOPMENT_GUIDE.md: Three-Point Approval Pattern section
    now points at references/approval-gates.md as the canonical catalog.
  - deployment-error-catalog.md Pattern 15: cross-references the two
    consent gates that detect and auto-fix the cross-host stamp problem
    (setup-pipeline:5a.pattern-15 and force-link-environment:4.destructive).

One pre-existing finding suppressed:

  - ensure-pipelines-host: SKILL-must-read-manifest fires because the
    skill creates Dataverse records (deploymentenvironments,
    deploymentpipelines) on the host. The rule's intent is about
    solution-component routing via .solution-manifest.json, which doesn't
    apply to host lifecycle. Suppressed via inline alm-lint-ignore with
    justification.

Final lint state: 0 errors, 49 warnings (all on non-ALM skills, per the
deferred §8 catalog scope). Non-ALM skills get warnings now and will get
hard-fail enforcement in a separate follow-up commit after their catalog
sections are populated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* .gitignore: drop stale .claude/scheduled_tasks.lock entry

scheduled_tasks.lock is a Claude Code runtime artifact, not produced by
anything in this repo. Grep across all .md / .js / .json files in the
plugin confirms zero references. Removing the entry; .claude/settings.local.json
stays (that one is real local config).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* approval-gates: tighten loop-iteration semantics across ALM skills

Hardens the SKILL.md prose against the runtime gate-skip failure mode
documented in approval-gates.md catalog §3.7. Driven by an observed
session where deploy-pipeline ran MULTI_RUN_MODE across three solutions
(Foundation / Config / Content) and the Phase 6.0 final-consent gate
fired zero times after the user answered "staging" once at the top of
the skill.

The marker was present. The lint passed. The agent simply did not call
`AskUserQuestion` again because the SKILL.md prose left the per-iteration
semantics implicit. This commit makes them explicit.

approval-gates.md §3.7 (new section):

  - Categorizes default loop behavior per gate category. `consent` and
    `final` are documented as PER-ITERATION (hard rule, no override).
  - Specifies required SKILL.md prose for any gate inside a loop:
    explicit "fires N times for N iterations" callout, named loop
    variable, explicit negation of the most common shortcut ("the
    upstream choice does NOT cover subsequent iterations").
  - Flags runtime telemetry as the future hardening (out of scope for v2).

SKILL.md tightening:

  deploy-pipeline:
    - Phase 6.0 marker: explicit per-iteration callout with example
      ("Three solutions in `deploymentOrder` → three Phase 6.0 prompts");
      negates the shortcut ("upstream Phase 2 stage selection — whether
      via interactive prompt or `--stage` argument — does NOT cover
      subsequent iterations").
    - Phase 6.0 prose: "the actual deployment" → "each deployment";
      "the production-promotion moment" → "every production-promotion
      moment".
    - MULTI_RUN_MODE loop description: explicit per-iteration sequence
      "Phases 3 → 3.5 → 4 → 5 → 6.0 → 6.1 → 6.2" with the gate name
      called out, and an explicit failure-mode warning ("If you find
      yourself proceeding from iteration 1's success directly to
      iteration 2's DeployPackageAsync without a fresh Phase 6.0
      prompt, you have skipped the gate").
    - Same rule mirrored for MULTI_PIPELINE_MODE.
    - Phase 4 pending-approval marker: per-iteration in MULTI_RUN_MODE.
    - Phase 5 env-vars marker: values supplied for iteration 1 do NOT
      carry to iteration 2.
    - Phase 6 pending-approval marker: per-iteration.

  setup-pipeline:
    - Phase 5a Pattern 15 marker: explicit per-env consent; if two
      target envs both turn out to be stamped to different hosts, the
      gate fires twice (once per env). Destructive blast radius is
      per-env (different previous hosts, different groups of affected
      makers).

  diagnose-deployment:
    - Phase 6 auto-fix marker: explicit per-finding loop. Yes from
      finding 1 does NOT cover finding 2. Each fix has its own blast
      radius. Skip-all short-circuits the loop; bare Yes does not.

  plan-alm:
    - Phase 7 activate-step-b marker: explicit per-stage; activate
      Staging does NOT cover Production. Different URLs, different
      audiences, different go-live timing.

Why prose, not code:

The lint rule (GATE-must-have-marker) catches structural absence of a
marker in SKILL.md. It cannot prove the agent actually fired the
`AskUserQuestion` call at runtime. Loop-semantics prose narrows the
LLM's interpretation space — "the gate fires N times for N iterations"
leaves no textual room for "once is enough."

Runtime telemetry (a `gate-fire-log.js` helper + post-run validator)
remains the durable fix for this class of failure, but is out of scope
for this commit. Tracked in catalog §11 as the open hardening item.

Lint: 0 errors, 49 warnings (unchanged — non-ALM deferred per §8).
Tests: 44 / 44 pass (no regressions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* approval-gates: tighten per-iteration semantics across remaining ALM skills

Follow-up to the earlier deploy-pipeline tightening. Sweeps the rest of
the ALM skill family for the same shortcut risk — gates that fire
multiple times within a single skill invocation (per-env loops, per-
target loops, per-failure occurrences) and gates that fire fresh per
skill invocation when an orchestrator loops over targets/stages.

The same runtime failure mode applies wherever a destructive or
irreversible action repeats inside or across skill invocations: the LLM
reads the first answer as global authorization and skips subsequent
prompts. Each gate marker now explicitly names its iteration unit
(per-env, per-target, per-failure, per-invocation), the loop variable
it iterates against, and the most common consent shortcut it negates.

setup-pipeline:4.4.blocked-attachments
  - Fires PER ENV that has blocks. Phase 4.4 checks source + every
    target env; M of N envs blocking `.js` = M consent prompts. Each
    env has its own security setting and its own affected makers.

deploy-pipeline:7.6.2.blocked-attachments
  - Fires PER FAILURE matching the AttachmentBlocked pattern. In
    MULTI_RUN_MODE this is rare (Phase 2.5 pre-flight usually catches
    it) but if iteration N still fails after a clean pre-flight, the
    gate re-fires for that failure. Each failure is its own consent
    decision (different solution, possibly different blocked extensions).

import-solution:5b.blocked-attachments
  - Fires fresh per skill invocation. plan-alm Manual path looping
    over targets calls import-solution once per target — if both
    targets block the same extensions, the gate fires once per target.
    Consent for Staging does NOT cover Production.

activate-site:3.confirm
  - Fires fresh on every skill invocation. plan-alm multi-stage
    activation calls activate-site once per stage; each invocation
    re-prompts with its own siteName / subdomain / target env.
    Each stage's site URL is a separate go-live decision.

force-link-environment:4.destructive
  - Fires fresh on every skill invocation. Each invocation force-links
    exactly one env to one host. Multi-env stamp migrations require
    one skill invocation per env, each with its own consent prompt
    echoing the env identity. No --yes flag, no batch mode, no
    consent carry-over.

plan-alm:2.q4-manual-target
  - Fires PER TARGET in the MANUAL_TARGET_COUNT loop. Two targets =
    two URL prompts. Do not collect all targets in a single multi-
    input prompt; each target is a distinct decision (different
    audiences, possibly different SKUs).

Lint: 0 errors, 49 warnings (unchanged — non-ALM deferred per §8).
Tests: 44 / 44 pass (no regressions).

Driven by a verified positive case: the earlier deploy-pipeline
tightening produced correct per-iteration prompting in the user's
next run. Carrying the same pattern through the rest of the family
before the same skip surfaces in the other skills.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…l Secret formats (#163)

* env-var values: shared verify helper + Secret URI path aligned with add-server-logic

Two parallel improvements, both motivated by the gap analysis between
add-server-logic Phase 7 (which has a working end-to-end Key Vault path)
and the other ALM skills (which previously assumed manual PPAC wiring
for Secret values).

1. New helper: scripts/lib/verify-env-var-values.js

Read-only post-write verification — given a target env URL and a list
of schema names (or a deployment-settings.json + stage label), checks
whether environmentvariablevalues records actually landed on the target.
Returns structured JSON per schema:

  status ∈ { landed | missing-value-record | missing-definition |
             value-mismatch | query-error }
  summary  { total, landed, missing, mismatched, error }

Three call patterns supported:
  - Post-deploy gap check (deploy-pipeline Phase 7.6.5) — reads schema
    names + per-stage expected values from deployment-settings.json
    filtered by --stageLabel.
  - Post-write verification (import-solution 6b, configure-env-variables
    7.2b) — bare --schemaNames list, presence-only check.
  - Per-stage value-match audit — --expectedValues JSON map for strict
    equality assertions.

OData escaping: single quotes in schema names get doubled per OData
spec. Defensive against user-coined slugs that survive validation but
contain unusual characters.

Auth: helper acquires its own token via Azure CLI scoped to the target
envUrl. Bypasses PAC CLI entirely — read-only means no PAC env switch
needed, and the caller doesn't have to remember to switch back. The
inline OData in the earlier Phase 7.6.5 prose required manual PAC
switching that the LLM could easily forget mid-run.

Exit codes deliberately conservative: 0 for "check ran cleanly" even
when entries failed to land (caller decides whether to block). 1 only
when the check itself was inconclusive (auth failed, env unreachable,
usage error). This prevents the helper from being misread as
"deploy failed" when the deploy itself succeeded.

Tests: 21 scenarios in scripts/tests/verify-env-var-values.test.js
covering all status branches, summary aggregation, settings-file
parsing for both top-level and Stages[] shapes, dedupe semantics,
case-insensitive stage matching, OData escape, and graceful
error-per-schema (one query-error doesn't abort the rest of the run).
Total suite: 823 / 823 pass across 3 consecutive runs.

2. SKILL.md edits — three skills wired to the helper

deploy-pipeline Phase 7.6.5: collapsed ~25 lines of hand-rolled OData
+ manual PAC-switching prose to a single helper invocation. Caller
still owns the side effects (envVarLandingWarnings[] write into
docs/alm/last-deploy.json + user-facing summary). Per-status branching
in the SKILL.md prose tells the caller what to surface for each of
missing-value-record / missing-definition / value-mismatch / query-error.

import-solution Phase 6b.verify: added a verify step after the per-
variable value-set prompts complete. If summary.missing > 0, surface a
single warning to the user — but do not block the import summary
(import itself succeeded; missing env-var values are recoverable post-
import via configure-env-variables).

configure-env-variables Phase 7.2b: same verify step on the dev env
after definition+value creation. If summary.landed < summary.total,
recommend re-running — most likely cause is a transient OData write
that left a definition without its paired value.

3. configure-env-variables Phase 3 — Secret URI branch

Replaced the single "create env var definition" path with three sub-
phases:

  3.A — String env vars: unchanged, definition-only flow via
        create-env-var-definition.js. Per-stage values come later from
        deployment-settings.json via deploymentsettingsjson PATCH at
        deploy time.

  3.B — Secret env vars WITH Key Vault Secret URI: uses the atomic
        deep-insert path via scripts/create-environment-variable.js
        --type secret --value <secretUri>. This is the same pattern
        add-server-logic Phase 7.2a Step 4 already uses end-to-end.
        Dataverse resolves the secret at runtime by dereferencing the
        URI in environmentvariablevalues.value. ALM-aware solution
        adoption is handled by the script's resolve-target-solution
        chain.

  3.C — Secret env vars WITHOUT a URI (legacy / deferred): definition-
        only with no value record, user wires manually via PPAC. This
        is the legacy fallback for cases where the user hasn't yet
        chosen Key Vault or hasn't stored the secret. A dedicated
        configure-secrets skill (catalog §10 "Coming") will eventually
        orchestrate 3.B for credential-style settings end-to-end.

Cross-reference added explicitly: configure-env-variables Phase 3.B
points at add-server-logic Phase 7.2a for the full vault-selection +
secret-storage flow. Stops the two skills from looking like parallel
implementations — they share scripts/create-environment-variable.js,
scripts/list-azure-keyvaults.js, scripts/create-azure-keyvault.js,
scripts/store-keyvault-secret.js. Helpers don't change; just more
callers learn to use them.

Why this matters

- Closes the runtime gap from the deploy-pipeline session where Secret
  env var values appeared to land at the source but didn't reach the
  target (the deploymentsettingsjson handler's site-setting-binding
  requirement). The verify helper now catches this case explicitly
  instead of relying on prose instructions to write hand-rolled OData.

- Aligns configure-env-variables with add-server-logic's proven
  Key Vault pattern. The catalog's "configure-secrets is blocked on
  keyVaultReference JSON" framing was outdated — URI-in-value works,
  add-server-logic has shipped it, and other skills can adopt the
  same pattern today without waiting for a POC.

- Centralizes verification semantics in one helper with one test suite.
  Future skills adding env-var writes (configure-secrets when it ships,
  any per-target value-set flow) get the same diagnostic shape and
  failure-mode classification by calling the same script.

Files: 5 changed, ~480 insertions, ~25 deletions.
  + scripts/lib/verify-env-var-values.js                 (new, ~310 lines)
  + scripts/tests/verify-env-var-values.test.js          (new, ~340 lines, 21 tests)
  M skills/deploy-pipeline/SKILL.md                      (Phase 7.6.5 prose collapse)
  M skills/import-solution/SKILL.md                      (Phase 6b.verify added)
  M skills/configure-env-variables/SKILL.md              (Phase 3.A/B/C split + 7.2b verify)

Lint: 0 errors, 49 warnings (unchanged — non-ALM deferred per §8).
Tests: 823 / 823 pass across 3 consecutive runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* deploy-pipeline: pre-PATCH Secret-reference validation (catches placeholder syntax upfront)

Driven by an observed runtime failure: a maker's deployment-settings.json
contained `@KeyVault(vaultName=lakeshore-staging-kv;secretName=api-secret)`
as the value for a Secret-type env var. That placeholder syntax is not
recognized by Dataverse or the Power Platform Pipelines handler. The PATCH
went through, the stage run queued behind a long Content import, and
4h41m later failed with:

  ImportAsHolding failed: The value provided as a secret reference does
  not match a valid secret reference format.

The verify-env-var-values.js helper from the prior commit catches the
*post-deploy* version of this (value didn't land on target). This commit
catches the *pre-deploy* version (PATCH payload is structurally bad) so
the deploy never queues in the first place.

New helper: scripts/lib/validate-deployment-settings.js

Read-only structural + format validator for deployment-settings.json.
Classifies each EnvironmentVariables[] entry by:

  valueFormat ∈ { kv-uri | kv-resource-id | kv-placeholder |
                  empty | plain-text | invalid-uri | non-secret }
  status      ∈ { valid | invalid | unknown-type | skipped }
  severity    ∈ { error | warning | info }

When --envUrl is provided, looks up each schema's env var type on dev
and applies type-specific rules. For Secret types:
  - kv-uri / kv-resource-id / empty → valid
  - kv-placeholder → invalid (error severity, with specific message
    about @KeyVault(...) NOT being a recognized syntax)
  - plain-text → invalid (error, with security warning — the file is
    committed to git, so a real secret value here is both a leak AND
    a deploy failure)
  - invalid-uri → invalid (error, with hint about likely typo —
    `.com` vs `.net`, missing /secrets/ segment, short version suffix)

Without --envUrl, falls back to structural-only validation but still
flags placeholder patterns since they're never valid for any type.

Canonical Key Vault Secret reference formats — codified in the helper's
regex and documented in configure-env-variables Phase 3.B:

  1. Key Vault Secret Identifier URI
     https://<vault>.vault.azure.net/secrets/<name>[/<32-hex-version>]
     (vault name 3–24 chars, lowercase alphanumeric + hyphens)

  2. Azure resource ID
     /subscriptions/<sub>/resource[Gg]roups/<rg>/providers/
       Microsoft.KeyVault/vaults/<vault>/secrets/<name>

  3. Empty string ""
     (use definition's default for this stage)

Tests: 24 scenarios in scripts/tests/validate-deployment-settings.test.js:
  - All format-regex hits (KV-URI with/without version, resource ID
    both casings, vault-name length validation)
  - All status branches per type (Secret strict, String permissive,
    unknown-type fallthrough)
  - File parsing for both top-level and Stages[] shapes
  - Stage-label filtering (case-insensitive)
  - Missing SchemaName flagged
  - Missing file / invalid JSON exit 1
  - Multi-entry aggregation with mixed severities

deploy-pipeline Phase 5 — new step 5.1b inserted BEFORE 5.2 PATCH:

Invokes the validator on deployment-settings.json (when present) with
the source env URL and the selected stage label. Branches on
summary.invalid:
  - summary.invalid === 0 → proceed
  - summary.invalid > 0 → STOP with structured findings + remediation
    pointer to configure-env-variables Phase 3.B / add-server-logic
    Phase 7.2a. No "proceed anyway" prompt — there's no partial-validity
    case where forcing the bad PATCH leads to a successful import.

The rationale is documented inline: catching this at Phase 5 turns a
~4h queue wait + fail into a sub-second hard stop. The cost difference
is 4 orders of magnitude.

configure-env-variables Phase 3.B — new "Acceptable Secret reference
formats" subsection:

Documents the three accepted formats (KV-URI, resource ID, empty
string) and the rejected patterns (@KeyVault placeholder, angle-bracket
TODOs, ${ENV_VAR} expansions, plain-text values, near-miss URIs).
References the specific live failure as "real-world failure case" so
future SKILL.md authors don't reinvent the broken syntax. Cross-
references add-server-logic Phase 7.2a for the end-to-end vault flow.

Validation:

  Lint: 0 errors, 49 warnings (unchanged — non-ALM deferred per §8).
  Tests: 847 / 847 pass across 3 consecutive runs.
    +24 new tests for validate-deployment-settings
    +21 pre-existing for verify-env-var-values (last commit)
    802 pre-existing baseline (unchanged).

What this commit doesn't change:

  - add-server-logic SKILL.md, scripts, or workflow — unchanged.
  - scripts/create-environment-variable.js — unchanged; the deep-insert
    path is the canonical writer, but the validator only reads.
  - Existing helpers (create-env-var-definition.js,
    link-site-setting-to-env-var.js, discover-env-var-definitions.js,
    verify-env-var-values.js) — all unchanged. The new validator slots
    in alongside them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Manual export/import reliability: always-bump source version + version-skew advisory

Closes the manual ALM path's "stale-version zip" failure mode. Adds a shared
bump-solution-version.js helper used by both setup-solution (sync mode) and
export-solution (always-on, pre-export), so every exported zip carries a
strictly-greater version label regardless of whether new components were
adopted. import-solution gains a Phase 3.0 advisory that compares the zip's
version against the target's installed version and offers re-export when
they're equal or the zip is lower.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* deploy-pipeline: parallel validation batch (Phase 3.6) + serial-deploy rationale

Splits the multi-solution deploy phase into a parallel validation batch
followed by the existing serial deploy loop. Adds validate-stage-runs-batch.js
which fans out create-stage-run + ValidatePackageAsync + poll concurrently for
N solutions; for a typical 5-solution split this compresses validation from
roughly N*120s (~10 min serial) to roughly the slowest single validation
(~3 min). The deploy phase stays strictly serial because Dataverse takes an
env-level import lock; a new design-rationale callout documents the four
constraints (import lock, inter-split dependencies, per-iteration consent
gate, clean failure handling) so future contributors don't wrap the deploy
loop in Promise.all.

Two new approval gates catalogued: deploy-pipeline:3.6.batch-pending-approval
(pause) and deploy-pipeline:3.6.batch-validation-failed (plan, default abort).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Review fixes: compareVersions, --rePoll mode, elapsedSeconds, last-export ingest

Second-pass review surfaced two real bugs and several documentation gaps:
- import-solution Phase 3.0 used raw `>`/`<` for version comparison, which
  flips on 1.0.0.9 vs 1.0.0.10 (lexical comparison treats .9 as greater).
  Adds compareVersions(a, b) to bump-solution-version.js with integer
  segment-wise comparison and updates Phase 3.0 to call it via node -e.
- deploy-pipeline Phase 3.6.4's "re-poll pending approvals" path called
  poll-validation-status.js, which can't detect stagerunstatus=200000005
  (PendingApproval) and times out instead. Adds --rePoll mode to
  validate-stage-runs-batch.js that skips create-stage-run + Validate-
  PackageAsync and reuses the helper's PendingApproval probe.
- Helper now emits elapsedSeconds (wall-clock measured around the fan-out)
  so Phase 3.6.6 can persist it into last-deploy.json's batchValidation
  without out-of-band timing. refresh-alm-plan-data.js D4 ingest normalizes
  legacy elapsedSecondsApprox to the new name.
- import-solution Phase 3.0 now guards the null-INSTALLED case explicitly
  before calling the helper, with throw-on-malformed-version error handling.
- AGENTS.md entries for bump-solution-version.js, validate-stage-runs-batch.js,
  and refresh-alm-plan-data.js updated to reflect the new exports/modes.

Tests: 866 -> 879 (+3 compareVersions, +4 --rePoll, +5 refresh ingest, +1
elapsedSeconds, +1 legacy-name normalization). Lint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…gesALM

Brings in PR #162 (configure-canvas-mcp docs fix) ahead of upcoming ALM
work merges. featureBranch/PowerPagesALM now contains all main commits
through 3b2009f.
…failure, LAST_SYNC_AT (#166)

* ALM review fixups: B1-B4 + D1 (consistency + docs)

Five issues surfaced by the cross-skill consistency review:

B1 — setup-solution multi-solution invocation missing required args
  Phase 5.6 multi-solution mode (line 729) called add-components-to-solution.js
  with only --solutionUniqueName, omitting --envUrl and --componentsFile.
  Helper validates all three as required and exits 1, blocking the skill on
  every multi-solution split deploy. Replaced the inline-prose invocation
  with a proper bash code block listing all three flags.

B2 — last-import.json missing `status` field in writer template
  refresh-alm-plan-data.js reads `marker.status` to step-sync the rendered
  ALM plan's per-stage checklist (completed vs failed). The Phase 6 step 3
  template never listed it, so every import showed `completed` regardless of
  outcome. Added the field with three documented values (Succeeded / Partial /
  Failed) and the rules for selecting between them.

B3 — three catalogued gates had no SKILL.md markers
  references/approval-gates.md lists `setup-solution:5.4b.orphan-envvars`,
  `setup-solution:5.4c.orphan-ppcs`, and `import-solution:2.multiple-zips`
  but the SKILL.md prompts that fire them lacked `<!-- gate: ... -->` markers.
  Lint's GATE-must-have-marker rule only fires at phase-section granularity
  and missed sub-sections under a marked parent. Added the missing markers.

B4 — catalog header counts wrong for two sections
  §6.2 setup-solution claimed (16 calls) but the table has 13 rows. §6.8
  ensure-pipelines-host claimed (8 calls) but the table has 10. Other sections
  follow the convention "header N = total row count"; reconciled the two
  outliers and clarified the §6.8 footnote about conditional 4.B.guid-confirm
  prompts.

D1 — AGENTS.md missing entries for 10 helpers (8 with zero mentions, 2 with
  only inline prose mentions). Added formal bullet entries under the right
  topical subsection:
    Under ALM Prerequisites & Context: alm-paths.js, check-alm-plan.js,
      resolve-target-solution.js
    Under PP Pipelines: ensure-pipelines-host-detect.js, provision-platform-host.js,
      provision-custom-host.js, force-link-environment.js, pac-bap-shim.js,
      verify-env-var-values.js, validate-deployment-settings.js
  Several of these are referenced from multiple SKILL.md files (verify-env-var-values
  by 3 skills, validate-deployment-settings by deploy-pipeline Phase 5.1b) and
  embody the "single source of truth" rule for their respective domains.

Tests: 879 pass (no test changes — these are SKILL.md / docs only).
Lint: 0 errors, 49 unchanged warnings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* G1: heartbeat-based Phase 0 skip rule for plan-alm orchestrations

Pre-existing gap surfaced by the cross-skill review: every ALM skill's
Phase 0 documented a skip rule with two detection mechanisms — (a) an
INVOKED_BY_PLAN_ALM env var/arg flag the orchestrator was supposed to set,
or (b) a `PLAN_STATUS === "In Execution"` plus `< 5min timestamp` check on
docs/.alm-plan-data.json.

Option (a) was never wired — no skill or helper set or read such a flag.
Option (b) broke on long deploys: deploy-pipeline alone can take 60 min
per stage, and the 5-min window aged out mid-orchestration. Once aged,
every downstream skill's Phase 0 would incorrectly fire its "no plan"
gate and prompt the user mid-chain, even though plan-alm was actively
running the orchestration.

This commit wires a heartbeat through check-alm-plan.js:

1. The helper output gains a new `inExecution` block:
   { status: "active" | "stale-heartbeat" | "not-running" | "no-plan",
     reason, windowMin }
   Replaces the brittle ad-hoc "< 5min" check from SKILL.md prose.

2. On every invocation that finds PLAN_STATUS === "In Execution",
   check-alm-plan.js writes LAST_INVOCATION_AT: <now> back to the plan
   file (atomic tmp+rename). This means each in-chain skill's Phase 0
   call refreshes the heartbeat for the NEXT skill, keeping the chain
   "active" as long as something is making forward progress.

3. Window is 60 minutes — comfortably larger than the longest single
   skill runtime (deploy-pipeline ~60 min per stage). A stalled or
   abandoned chain reclassifies as "stale-heartbeat" so an old plan
   doesn't silently bypass user confirmation.

4. The helper exports computeInExecution(planStatus, lastInvocationAt, now)
   and HEARTBEAT_WINDOW_MIN for programmatic use + testability.

5. A `--no-heartbeat` flag disables the write for read-only audits / tests.

6. plan-alm Phase 5 (option 1: "Approve and execute") now updates
   PLAN_STATUS to "In Execution" AND writes LAST_INVOCATION_AT in the
   JSON, not just the HTML span. Previously the JSON's PLAN_STATUS stayed
   "Draft" between Phase 5 and Phase 8's "Completed" — the design intent
   never matched the actual writes.

7. The six downstream ALM SKILL.md files (setup-solution, setup-pipeline,
   deploy-pipeline, export-solution, import-solution, configure-env-variables)
   now check `inExecution.status === "active"` as the skip predicate
   instead of reconstructing it from the JSON in prose. Identical prose
   block replaced via a small Node migration script.

Tests: 879 → 890 (+11 for heartbeat semantics). Lint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* G2: plan-alm Step A.1 halt-on-deploy-failure gate

Pre-existing pattern bug found by the cross-skill review: plan-alm Phase 7
Step A unconditionally marks the deploy task `completed` and proceeds to
Step B (Activate) regardless of what `deploy-pipeline` actually did. With
the new Phase 3.6 batch-validation gates (PR #164), the failure surface
widens — batch-validation-failed, batch-pending-approval cancelled,
blocked-attachments cancelled, mid-deploy AttachmentBlocked, Phase 6.0
consent cancelled — each writes `docs/alm/last-deploy.json` with a
non-Succeeded status, but plan-alm previously ignored that.

This adds Step A.1 right after deploy-pipeline returns:

1. Read last-deploy.json and parse `status`.
2. Branch:
   - "Succeeded" — mark deploy completed, proceed to refresh-plan + Step B.
   - "Partial" / `knownGaps` present — mark `completed-with-gaps`, show
     gaps, confirm before Step B.
   - "Failed" / "ValidationFailed" / "Canceled" / "NoMarker" — mark
     deploy `failed`, run refresh-plan (so the rendered plan shows the
     failure), fire the new `plan-alm:7.deploy-failure` gate.
3. The new gate asks: Retry this stage / Skip to next stage / Exit
   orchestration. Retry re-invokes deploy-pipeline and re-runs the check
   (Dataverse import idempotency handles already-succeeded solutions in a
   multi-solution loop). Skip continues the outer PP_STAGES loop without
   running Step B / Step C for this stage. Exit stops the skill cleanly.

Step B's header tightened to "only when Step A succeeded or
completed-with-gaps" to make the precondition explicit.

Catalog entry added in §6.1; plan-alm header count 18 → 19.

Tests: 890 pass (no test changes — pure prose). Lint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* G3: post-sync freshness — LAST_SYNC_AT shifts the reference forward

Cosmetic gap from the cross-skill review: when export-solution Phase 2.5
or deploy-pipeline Phase 3.5 invoked setup-solution in sync mode on a
completeness gap, setup-solution bumped solutions.modifiedon (via the
version PATCH + AddSolutionComponent calls). Subsequent check-alm-plan.js
calls would then correctly observe sol.modifiedon > GENERATED_AT and
incorrectly classify the plan as stale — even though the modification
was caused by the just-completed sync, not by drift the user should know
about.

This adds a LAST_SYNC_AT marker to the plan:

1. check-alm-plan.js's freshness check now compares sol.modifiedon
   against max(GENERATED_AT, LAST_SYNC_AT). Backward compatible: plans
   without LAST_SYNC_AT use GENERATED_AT alone. The detail message tells
   the user which reference point flagged the staleness
   ("last sync at ..." or "plan generated at ...").

2. refresh-alm-plan-data.js's refreshSetupSolution writes LAST_SYNC_AT
   = <now> on every invocation. Called by setup-solution Phase 7's
   self-refresh, so any successful sync (including fresh setup) marks
   the plan as "accepts modifications up to this point".

3. Unparseable LAST_SYNC_AT falls back to GENERATED_AT defensively —
   a corrupted marker must not silently mask real drift.

4. export-solution Phase 2.5 Option 1 + deploy-pipeline Phase 3.5
   Option 1 prose now mentions the LAST_SYNC_AT mechanism explicitly so
   the user understands why the plan isn't flagged stale after sync.

Tests: 890 → 895 (+5 for LAST_SYNC_AT semantics: shift-forward,
modifiedon-after-sync still stale, backward-compat no-LAST_SYNC_AT,
unparseable LAST_SYNC_AT defensive, refreshSetupSolution writes marker).
Lint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Second-pass review fixes: G2 status handling + G3 stamp coverage

Three real gaps found in the second critical review:

1. G2 status-handling gap — plan-alm Phase 7 Step A.1 branched on a small
   whitelist of deploy-pipeline status values (Succeeded / Partial / Failed
   / ValidationFailed / Canceled / NoMarker) and missed two real values
   the marker can carry:
     - "PendingApproval": deploy-pipeline writes this when the user cancels
       the approval-pause gate in Phase 6 (200000005).
     - "Unknown": writes when the poll timed out (10 min).
   The non-matching status fell through both branches, leaving Step A.1
   without an action. Rewritten as: knownGaps-first, then Succeeded, then
   a catch-all that covers PendingApproval / Unknown / any future value.

2. G2 knownGaps detection — Phase 3.6.5's "deploy succeeded subset" path
   writes status:"Succeeded" alongside a populated knownGaps[]. The
   original Step A.1 branched on status first ("Succeeded" → completed)
   and never reached the Partial branch, losing the gap signal.
   Reordered: check knownGaps presence FIRST; status-Succeeded is only
   the clean-completion path.

   The bash extractor now returns knownGaps as a real array (default [])
   plus knownGapsCount so the agent branches on a number not a null check.

3. G3 stamp coverage — refresh-alm-plan-data.js's refreshSetupSolution
   wrote LAST_SYNC_AT, but refreshExportSolution (Phase 4.0 bump-source-
   version modifies modifiedon) and refreshConfigureEnvVariables (env var
   creation + AddSolutionComponent modifies modifiedon) did not. After
   either of those phases, the next Phase 0 check would see
   sol.modifiedon > LAST_SYNC_AT and falsely flag stale.
   Extracted stampLastSyncAt(planData) helper and applied to all three
   phases that touch the source solution. The helper's JSDoc explicitly
   lists which phases need it and which don't (host-env vs source-env
   distinction).

Tests: 895 → 897 (+2 for export-solution / configure-env-variables
LAST_SYNC_AT stamps). Lint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Real-site validation fixes: 16 bugs across 8 skills + helpers

A Citizens portal validation run surfaced a long list of consistency and
state-sync bugs. This commit addresses every one.

P0 — would break the next deploy:
  - configure-env-variables Phase 6.1 (NEW): pre-write validation of
    deployment-settings.json. The Pipelines handler rejects @KeyVault(...)
    short-form, raw secrets, and malformed URIs at import time after the
    stage run has queued for hours. Block them at write time instead.

P1 — silently wrong output:
  - test-site SKILL.md Phase 6.7a: explicit aggregation pseudo-code that
    counts each test's severity into the right summary bucket, plus a
    real-world reproduction case so future agents don't shortcut to "all
    tests are low severity".
  - render-alm-plan.js severity grid: split the combined "Medium / Low"
    card into separate Medium and Low cards. Previously, a stage shipping
    4 medium-severity issues looked identical to one shipping 4 low-severity
    issues.
  - render-alm-plan.js Overview narrative: derive solution count from
    proposedSolutions.length instead of hardcoding "Four solutions ordered
    by change frequency". Mention Future Growth buffer when present.
  - render-alm-plan.js env-var values matrix: canonicalize stage keys
    before assembling the header so deployment-settings.json keys ("Staging",
    "Deploy to Staging", "CitizenServicesStaging") all collapse to one
    column instead of three duplicates.
  - render-alm-plan.js checklist substep DOM: 5 instances of bare <li>
    inside <div class="checklist-substep-list"> changed to <ul>. Valid HTML
    + matches CSS class intent.
  - configure-env-variables Phase 7.2c (NEW): re-run discovery to write
    last-env-vars.json with the freshly-created definitions. Without this,
    newly-created env vars never appeared in the rendered plan's Env
    Variables tab until plan-alm ran again.
  - refresh-alm-plan-data mirrorEnvVarsSnapshot (NEW): copy last-env-vars.json
    to alm-env-vars.json from refreshSetupSolution + refreshConfigureEnvVariables.
    Closes the audit gap where alm-env-vars.json sat at {envVars:[],count:0}
    after env vars existed.
  - refresh-alm-plan-data refreshConfigureEnvVariables + refreshSetupSolution:
    refresh sizeAnalysis.envVarCount.value from the post-phase env var count.
    Previously the Overview stat card stayed at 0 even after 2 env vars
    were created.
  - refresh-alm-plan-data refreshDeployPipeline: refresh hostResolution
    (and rawDiscovery.hostResolution) from last-host-check.json so the
    rendered host card and the raw envelope agree after a successful
    setup-pipeline / ensure-pipelines-host run.
  - refresh-alm-plan-data mirrorHostResolutionSnapshot (NEW): rewrite
    alm-host-resolution.json with current state from
    refreshSetupPipeline + refreshDeployPipeline. Closes the case where
    the audit snapshot persisted "NoHost" even after the host was bound.
  - bump-solution-version.js --projectRoot + updateManifestVersion (NEW):
    helper now updates .solution-manifest.json's solution.version (single)
    or solutions[].version (multi-solution by solutionId) atomically as
    part of every bump operation. setup-solution sync mode, export-solution
    Phase 4.0, and configure-env-variables Phase 7.2b.bump (NEW) all pass
    --projectRoot ".". Without this, the manifest drifted behind Dataverse
    every bump.
  - setup-pipeline Phase 7.3 SKILL.md: explicit sync-mode re-render
    instruction. The pipeline-setup.md file must regenerate from current
    Dataverse state on every setup-pipeline invocation, including those
    that follow configure-env-variables / setup-solution sync (which add
    components and change the component count surfaced in the markdown).

P1 — host resolution + endpoint:
  - ensure-pipelines-host provision-platform-host.js endpoint was reported
    as wrong but is actually CORRECT (line 165:
    /providers/Microsoft.BusinessAppPlatform/environments/getOrCreate
    matches the documented and verified endpoint). The real issue was
    stale alm-host-resolution.json — fixed via the mirror helper above.

P2 — schema + cosmetic:
  - status casing: setup-pipeline now writes "SkippedEmpty" (Pascal) instead
    of "skipped-empty" (kebab) so it aligns with deploy-pipeline's
    runs[].status casing across the two markers.
  - alm-size-estimate.json publisherPrefix: refreshSetupSolution
    patches publisherPrefix + siteName from .solution-manifest.json after
    setup runs, so the size-estimate file no longer carries plan-time
    defaults (e.g. "cr5fe") forever.
  - deploy-pipeline Phase 7.3 schema doc: explicit guidance that retries
    go as peer entries in runs[], not nested under runs[i].lastAttempt.
  - configure-env-variables --displayName guidance: pick a human-readable
    label, not the schema name. Prevents the "displayName == schemaName"
    drift that made the env vars tab read like raw tokens.
  - test-site notes hardcode: explicit instruction NOT to embed component
    counts, version numbers, or other run-specific data from prior
    deploys in a top-level notes string.

New gate catalogued:
  - configure-env-variables:6.1.invalid-secret-values (consent, hard-stop)

Tests: 897 → 906 (+9 for bump-solution-version manifest update + 3 phase
LAST_SYNC_AT coverage). Lint clean: 0 errors, 49 unchanged warnings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* deploy-pipeline Phase 7.6.4: strip-and-retry for invalid Secret references

Defensive write-back for the case the brief flagged as P0 sub-item (b):
"when deploy-pipeline retries-and-succeeds by stripping a value, write
that remediation back into deployment-settings.json so the broken value
doesn't re-ship on the next run."

The pre-write validator in configure-env-variables Phase 6.1 and the
pre-PATCH gate in this skill's Phase 5.1b catch most invalid Secret values
upstream. But three cases still slip through:
  - User hand-edits deployment-settings.json after configure-env-variables.
  - A legacy file from before Phase 6.1 existed was committed.
  - The pre-PATCH gate returned status:"unknown-type" for an entry whose
    Dataverse type lookup transiently failed.

New flow:
1. Phase 7.6.1 diagnostic table gains a third pattern row matching
   `secret reference does not match a valid` / `ImportAsHolding failed.*
   secret reference` / `KeyVault.*format` in errordetails or
   validationresults — routes to Phase 7.6.4.
2. Phase 7.6.4 invokes validate-deployment-settings.js to confirm the
   local file actually has invalid entries (defends against false-positive
   pattern matches), surfaces a consent gate with the schema names, and
   on Yes:
   - Calls scripts/lib/strip-invalid-secret-values.js to set Value:""
     on each invalid entry (which Dataverse interprets as "use definition
     default"). Atomic tmp+rename so a concurrent reader never sees a
     half-written file.
   - Re-PATCHes the stage run with the corrected deploymentsettingsjson.
   - Calls RetryFailedDeploymentAsync and resumes polling from Phase 6.2.
   - Records the action in last-deploy.json secretRemediation[] for audit.
   On No: surfaces the canonical Secret-reference formats + points at
   /power-pages:configure-env-variables for guided remediation.

New helper scripts/lib/strip-invalid-secret-values.js (181 lines, 14 tests):
  - Reads deployment-settings.json (accepts both top-level-stage and
    nested-`stages` shapes; both SchemaName/Value and schemaName/value
    casings).
  - Strips entries matching --schemaNames CSV. Empty-string value =
    "use definition default" per the canonical-Secret-format note.
  - --stageLabel narrows the strip to one stage. Without it, strips
    across all stages.
  - Reserved root keys ($schema, description, stages, EnvironmentVariables,
    ConnectionReferences) are filtered out of the stage scan — regression
    guard for the early implementation that tried to scan $schema.
  - Idempotent: no file write when no entries matched (file mtime unchanged).
  - Returns {stripped, notFound, totalStagesScanned} for audit.

New gate deploy-pipeline:7.6.4.strip-secret-values (consent) catalogued
in §6.4. Header count 17→18 gates, 20→21 calls. New cancel-leaves vocab
entry `invalid-secret-in-file` added to §4.3 table.

Tests: 906 → 920 (+14 for the new helper). Lint clean (the new
cancel-leaves slug is kebab-case so passes lint without explicit vocab
addition, but it's documented in the catalog table for discoverability).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The ALM workstream (PR #167: featureBranch/PowerPagesALM → main) is a
major capability addition — 10 new ALM/CI-CD skills, 44 shared helpers,
6 reference docs, 920 tests. The plugin description already reflects the
v2 scope (plan-alm orchestration, multi-solution manifest v2, pipelines,
force-link remediation). The major-version bump signals to consumers that
this release reshapes the plugin's surface beyond an incremental bug-fix
release.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@tyaginidhi
T-Nid (tyaginidhi) marked this pull request as ready for review May 25, 2026 17:40
@tyaginidhi
T-Nid (tyaginidhi) requested a review from a team as a code owner May 25, 2026 17:40
Copilot AI review requested due to automatic review settings May 25, 2026 17:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

- Line 23: add `force-link-environment` to the MCP Learn–grounded ALM
  skills list (it has Phase 1.5 + the two mcp__plugin_power-pages_
  microsoft-learn__microsoft_docs_* tools wired in `allowed-tools`, but
  was missing from the documented set).
- Line 163: `diagnose-deployment` was claimed to be "tracked without
  command validators" but it has no entry in TRACKED_SKILLS at all.
  Reworded to surface that distinction — it's intentionally not tracked
  (read-only, no artifacts to verify) rather than tracked-but-no-validator
  (which is what `add-sample-data` and `test-site` are).
- Line 172: `update-skill-tracking.js` description said "Used by all 9
  skills" — stale, plugin has more skills now. Reworded to
  "every user-invocable skill" with the per-skill-final-phase pattern.

CLAUDE.md picks these up automatically (symlink → AGENTS.md).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread .github/workflows/power-pages-alm-lint.yml
@tyaginidhi
T-Nid (tyaginidhi) merged commit 2c82e39 into main May 26, 2026
7 checks passed
@tyaginidhi
T-Nid (tyaginidhi) deleted the featureBranch/PowerPagesALM branch May 26, 2026 06:12
Hjalmar Otto Fjøsne (Hjaf) added a commit to equinor/power-platform-skills that referenced this pull request May 27, 2026
…23950)

Sync 3 upstream commits from microsoft/power-platform-skills:
- Power Pages ALM: 10 new skills, 45 lib helpers, 6 reference docs (microsoft#167)
- Power Pages security: manage-firewall, manage-headers, scan-site, security-review (microsoft#151)
- Docs: update Power Pages skill list (microsoft#174)

163 files added, 16 modified. Equinor guardrails preserved:
- SECURITY.md, marketplace.json, code-apps/, .devcontainer/ unchanged
- Review record updated (power-pages v1.3.0 → v2.0.0)
- All review records pass schema validation
Rishabh Jain (MrRishabhJain) pushed a commit that referenced this pull request Jun 4, 2026
Live E2E testing surfaced five defects in the git-connect/commit/pull
skills + helpers, and two orchestration gaps. All fixes verified against
a real Preprod env (https://org2811163f.crm10.dynamics.com).

Bug #1 — validation-helpers getEnvironmentUrl() regex matched
"Environment URL:" but pac env who actually prints "Org URL:", so the
helper returned null on real output. Every Stop-hook validator that
relies on it silently approved without checking anything. Regex now
matches both labels.

Bug #2 — All six check-/validate-* helpers queried
sourcecontrolconfigurations expecting branchname/repositoryurl/_solutionid_value;
those fields don't exist on that entity. Per-solution data lives on
sourcecontrolbranchconfigurations (keyed by partitionid = solutionId,
with _sourcecontrolconfigurationid_value FK). Added a shared
scripts/lib/source-control.js that queries the correct entities,
joins to solutions for uniquename and to sourcecontrolconfigurations for
org/project/repo, derives the repositoryUrl from gitprovider, and
filters out the env-level all-zeros-partitionid row. All six helpers
rewritten on top of it; --solutionName scoping added for the check-*
helpers.

Bug #3 — git-connect/SKILL.md Phase 3.4 documented the gitbranches POST
body as {"name":"...","gitrepositoryid":"..."}; that schema is rejected
by the virtual entity. Updated to the working five-field body
(branchname, organizationname, projectname, repositoryname,
upstreambranchname).

Bug #4 — git-connect/SKILL.md Phase 3.5 defaulted --folder to "/"; the
server rejects "/" with "The folder name '/' is invalid". Default is now
the solution unique name, and the doc explicitly warns that "/" is
invalid.

Bug #5 — create-solution.js only read OData-EntityId from the response
header, throwing when Dataverse returned 201 + body without the header
(common under Prefer: return=representation). Now reads
JSON.parse(res.body).solutionid first, falls back to the header.

Finding #8 — Added the same Phase-0 plan-alm gate that #167 introduced
in setup-solution to all three git skills (git-connect/commit/pull), so
direct invocation without a plan prompts for /plan-alm first. Each
skill's "Why this gate exists" paragraph is tailored to its own
mutation surface (binding permanence for connect, snapshot scope for
commit, overlay risk for pull).

New gate (git-connect Phase 2.3) — when the user picks a solution that
contains no Power Pages site components, the skill now stops before the
ConnectToGit call and surfaces a 4-option AskUserQuestion
(/setup-solution / different solution / continue anyway / cancel).
Prevents the silent "empty binding" failure mode where connect succeeds
but every commit/pull is a no-op. Prerequisites bullet and Progress
Tracking summary updated to match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
T-Nid (tyaginidhi) added a commit that referenced this pull request Jun 12, 2026
…#170)

* Apply Approval Gate pattern to non-ALM power-pages skills

Extend the catalog in references/approval-gates.md (§6.13-§6.24)
to cover the 12 non-ALM skills: create-site, deploy-site,
add-server-logic, add-cloud-flow, setup-auth, integrate-webapi,
setup-datamodel, add-sample-data, add-seo, create-webroles,
audit-permissions, integrate-backend. 45 gates + 9 not-a-gates
inserted as HTML comment markers + human 🚦 blocks above each
AskUserQuestion call.

Flip lint to hard-fail across the whole plugin -- the ALM-only
warn-only branch in scripts/lint-skills-alm.js has been removed.
severityForSkill() now returns 'error' for every skill. Update
the corresponding test in scripts/tests/lint-skills-alm.test.js.

AGENTS.md Key Patterns generalized: the Approval Gate convention
applies plugin-wide. New skills must extend the catalog in the
same PR that introduces an AskUserQuestion, or CI blocks the PR.

Workflow files get header comments explaining why power-pages-
alm-lint and power-pages-script-tests are deliberately separate
(policy enforcement vs functional tests; single-OS fast-fail vs
cross-OS matrix).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Catalog the 4 security skills picked up during rebase

Rebase onto origin/main brought in PR #151 which added 4 new
power-pages skills: manage-firewall, manage-headers, scan-site,
security-review. With the warn-only lint branch removed in the
previous commit, those skills now had unmarked AskUserQuestion
prompts and broke CI.

These skills use a different prompt shape — most calls happen
inside a runtime "recommend then ask" loop described in prose,
not at statically-locatable call sites. The catalog now has a
new §6.24a section explaining the convention for runtime-loop
skills: meta-mention sections get not-a-gate markers; concrete
call sites get full gate markers.

Catalog rows added:
- §6.25 manage-firewall — 1 not-a-gate (option-rules-meta)
- §6.26 manage-headers — 1 gate (per-finding loop, plan)
- §6.27 scan-site — 1 not-a-gate (option-rules-meta)
- §6.28 security-review — 2 gates (2.1 goal, 5.3 next-action)

Total: 3 gates + 2 not-a-gates. Lint passes (0 findings).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Apply review fixes: doc, list-rendering, lint cleanups, ALM orphan markers

Addresses the 12 findings from the pre-PR code review. Fixes ordered by
severity:

MUST FIX:
- PLUGIN_DEVELOPMENT_GUIDE.md: drop the stale "non-ALM warn-only" text
  on line 273 and mirror the v3 hard-fail language. AGENTS.md tells new
  authors to read this file first, so the stale doc would have misled
  them.

MARKDOWN RENDERING:
- audit-permissions, create-site (4 places), integrate-webapi: move
  gate marker blocks ABOVE the ordered list intro sentence so they
  don't sit between numbered items. CommonMark/GFM split a list at an
  unindented HTML comment + blockquote, restarting numbering. The
  rendered numbering on GitHub was 1,2,3,1,2 instead of 1..N.

PRE-EXISTING FILE BUGS:
- Stray ``` token at add-cloud-flow:346 and integrate-backend:271
  (both from PR #167) — removed. These broke fence parity for
  everything below them.

LINT CLEANUPS:
- severityForSkill() is now a constant (SKILL_SEVERITY); unused
  skillName parameter and void no-op removed; call sites simplified.
- Dead warnings-only exit branch in main() removed — no code path
  produces severity 'warning' post-v3.
- Tightened checkSectionPairing: m < promptLine (strictly before),
  not m <= promptLine, so a marker on the SAME line as the prompt no
  longer trivially satisfies the rule.
- CATALOG_GATE_ID_PATTERN now case-insensitive [A-Za-z] to align
  with GATE_MARKER_PATTERN — future CamelCase / underscored skill
  names won't break GATE-must-be-in-catalog.

ALM CATALOG ORPHANS:
- 16 catalog gate rows previously lacked SKILL.md markers (introduced
  pre-v3). Resolved in two ways:
  - Added markers where the prompt call site is concrete:
    test-site:5.5.form-submit, setup-pipeline:4.3.name-conflict +
    6b.v2-migration, export-solution:2.identify + 3.overwrite,
    force-link-environment:2.host-url + 2.dev-env, ensure-pipelines-
    host:3.C.host-type + 3.C.env-pick + 4.0.pre-call + 4.A.pre-call
    + 4.sandbox-confirm + 4.C.ppac-done, plan-alm:2.q4-host (renamed
    from q4-stage-env to match the actual host-selection prompt).
  - Removed orphan rows that described gates not implemented as
    separate AskUserQuestion calls: deploy-pipeline:6.1.pac-fallback-
    consent (covered by 6.0.final-consent prose), configure-env-
    variables:6.confirm-matrix (covered by 2.selection's per-stage
    matrix), force-link-environment:2.host-fallback + 2.dev-fallback
    (redundant — the gate IDs above cover the fallback path).

PROSE ANCHORS:
- setup-auth Phase 1.4 prompt prose now includes the literal
  backticked AskUserQuestion + colon anchor required by
  PROMPT_LINE_PATTERN. Removing the gate marker would now actually
  trigger GATE-must-have-marker.

VERIFIED: lint reports 0 findings; 984/984 plugin tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Address PR review feedback: lint upgrades + sweeping refinements

Reverts an accidental scope expansion, adds two new lint rules, closes
the runtime-loop coverage hole, renames the awkward 'Blast radius if
skipped' field, and removes dead module-level code. All 9 feedback
items from the pre-PR review are addressed.

LINT:
- CATALOG_GATE_ID_PATTERN: revert underscore that was silently added —
  was [A-Za-z0-9_-]*, now [A-Za-z0-9-]* matching the original kebab-only
  intent. The case-insensitivity change to [A-Za-z] (from [a-z]) is
  kept, with a comment explaining the asymmetric grammar with
  GATE_MARKER_PATTERN.
- New rule: CATALOG-row-must-have-marker — reverse check (catalog row →
  SKILL.md marker). Catches the orphan-row class of bug v3 closed by
  hand. Parses §6 catalog tables for rows tagged 'gate' and verifies
  each ID has a matching <!-- gate: ID --> marker in some SKILL.md.
- New rule: GATE-prose-block-required — every gate marker must be
  followed within 10 lines by a line carrying the 🚦 sentinel. Catches
  prose-block deletion without forcing structural rewrite of 80+ legacy
  v2 single-line markers.
- Tightened m < promptLine (strict precede) with a new test asserting
  marker-on-prompt-line fails the rule.
- Removed dead ALM_SKILLS export + its membership test — no downstream
  consumer existed.
- Simplified SKILL_SEVERITY comment (the wishful 'future per-skill
  policy' framing).

CATALOG:
- §3.2 plan-vs-consent: added a paragraph explaining why deploy-dispatch
  prompts ('Deploy now?') are tagged plan when the destructive consent
  lives inside the dispatched skill's own gate. Resolves the
  create-site:8.deploy (plan) vs deploy-site:3.confirm-env (consent)
  apparent inconsistency.
- §6.4 / §6.7 / §6.9: removed archaeological commentary about merged /
  removed rows. Git blame has it.
- §6.24a: rewritten to reflect that the runtime-loop coverage hole is
  now CLOSED — manage-firewall and scan-site got real call-site
  anchors.
- §10: added cross-plugin lint TODO for shared/skills/report-issue/;
  expanded the v3 PR changelog with the new lint rules + field rename.
- §11: added phase-number-drift convention (any SKILL.md phase renumber
  must grep the catalog), runtime-loop coverage caveat (now mostly
  resolved), and lint-prose-block weakness (only 🚦 enforced, not the
  3 structured labels).

RUNTIME-LOOP ANCHORS (F13 — closes the coverage hole):
- manage-firewall: new gate manage-firewall:3.action-choice (plan, at
  '### Default approach') + manage-firewall:3.execute-consent (consent,
  at '### Plan-validate-execute'). The destructive WAF mutations
  (enable/disable/add/update/delete rule) are now lint-anchored.
- scan-site: new gate scan-site:3.action-choice (plan, at '### Default
  approach'). The scan-trigger decision is now lint-anchored.
- Both prose blocks updated to include the literal 'AskUserQuestion:'
  string that PROMPT_LINE_PATTERN requires.

FIELD RENAME (F14):
- 'Blast radius if skipped:' → 'Why we ask:' across all 60 prose blocks
  + §4.1 catalog template. The old label suggested the gate's purpose
  was to describe Cancel state; the actual content explains why the
  gate prompts in the first place.

VERIFIED: lint reports 0 findings (with new reverse + prose-block
rules active); 984/984 plugin tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Address PR review feedback round 2: lint robustness + test coverage + doc consistency

Addresses 10 review-skill findings + 9 Copilot inline comments + the
Copilot Autofix on the rule-count wording.

## Lint script (scripts/lint-skills-alm.js)

LINT REGEX:
- CATALOG_GATE_ROW_PATTERN now tolerates up to 3 leading spaces before the
  pipe (GFM table indentation tolerance). Was `^\|`, now `^\s{0,3}\|`.
  Without this, a future markdown reformat that nests §6 tables under a
  parent list silently disables orphan detection for those rows.

GATE-prose-block-required:
- Window now INCLUDES the marker's own line (slice(startIdx, startIdx+10),
  was slice(startIdx+1, startIdx+11)). Single-line compact-style markers
  with marker + 🚦 on one line now pass.
- The 🚦 search now skips code-fence regions. Was a plain `.includes('🚦')`
  per line; now pre-computes per-line fence state across the full file
  and ignores 🚦 inside ```...``` blocks. Closes the "future contributor
  deletes the real Gate prose block but leaves an example 🚦 in a bash
  example within the window" silent-pass case.

CATALOG-row-must-have-marker:
- Docstring updated. Previously claimed "Waivable: yes — inline
  <!-- alm-lint-ignore: ... --> in the catalog row's section". The
  implementation never called extractIgnores on the catalog file, so
  inline-ignore never worked. New docstring says .almlintignore allowlist
  is the only suppression mechanism (the rule operates on the catalog as
  a whole, not per SKILL.md, so per-row inline ignore would be confusing
  to model). Matches actual behavior.

CLEANUPS:
- Removed dead `skillNameFromFile()` function. No callers since
  severityForSkill was deleted; not in module.exports.
- Removed `SKILL_SEVERITY` constant. Half-applied (2 of 9 severity:
  sites used it) — inlined `'error'` everywhere for consistency.
- Updated Usage header comment to match v3 exit-code behavior.
- main() now concatenates findings into one string + single
  process.stderr.write, then sets process.exitCode instead of calling
  process.exit. Prevents stderr-truncation on Windows when piping to a
  log file with large finding counts.

## Tests (scripts/tests/lint-skills-alm.test.js)

Added 7 new tests for the two rules introduced in commit 8d02547 that
shipped with zero coverage:

CATALOG-row-must-have-marker:
- fires when a catalog gate row has no SKILL.md marker
- passes when every gate row has a marker
- skips not-a-gate rows (no marker required for them)
- tolerates leading whitespace on table rows (GFM compliance)

GATE-prose-block-required:
- fires when marker has no 🚦 within 10 lines
- passes when 🚦 sentinel follows within window
- ignores 🚦 inside a fenced code block
- tolerates 🚦 on the same line as the marker (single-line style)

Test count: 45 → 52 (+7). Plugin-wide: 984 → 992.

## Docs

approval-gates.md / AGENTS.md / PLUGIN_DEVELOPMENT_GUIDE.md:
- Updated "five GATE rules" → "seven gate-related rules" (Copilot's
  finding). Lists all 7 enforced rules now. Includes the v3 additions
  CATALOG-row-must-have-marker and GATE-prose-block-required.

approval-gates.md §9 Decisions row 4:
- Marked the v2 "ALM: hard-fail. Non-ALM: warn-only" decision as
  superseded by §10. The §10 history correctly captures the v2→v3
  transition but §9 read as a current recommendation in isolation.

Verified: lint 0 findings; 992/992 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Fix readSettingsFile to handle the keyed-object stages shape

Discovered while validating PR #170 against the real Citizens portal site
(C:\Projects\Citizens portal). The site's deployment-settings.json uses
the Microsoft-standard 2024 schema shape:

  {
    "$schema": "https://schemas.microsoft.com/power-platform/deployment-settings/2024",
    "stages": {
      "Deploy to Staging": {
        "EnvironmentVariables": [...]
      }
    }
  }

— a keyed OBJECT of stages. readSettingsFile only handled two shapes:
1. Top-level `EnvironmentVariables: []` (single-stage)
2. `Stages: []` array with `{ Name, EnvironmentVariables }` entries

The keyed-object shape returned 0 entries, which meant
validate-deployment-settings.js silently passed even on known-broken
values like `@KeyVault(vaultName=...;secretName=...)`.

Real-world evidence: the Citizens portal's docs/alm/last-deploy.json
records a failed deploy attempt (2026-05-21) where deployment-settings.json
contained `@KeyVault(...)` for c311_api_secret. The pre-deploy validator
"validation passed (validation does not check Secret reference format)"
— because the parser couldn't read the file. The deploy then waited ~4h
in the host queue before failing with `ImportAsHolding failed: The value
provided as a secret reference does not match a valid secret reference
format`. This is exactly what the v3 deploy-pipeline:7.6.4.strip-secret-
values gate is supposed to prevent — but the gate's underlying validator
was broken.

Changes:

- readSettingsFile now handles all three shapes. Each returned entry
  now carries `stageLabel` (null for shape 1, the stage name for shapes
  2 and 3), so downstream consumers can attribute findings to the right
  stage without re-parsing the file.

- validate-deployment-settings.js had a duplicated read-settings parser
  (readEntriesPreservingStage) with the same bug. Deleted — it now
  uses readSettingsFile directly.

- Tests:
  - Existing readSettingsFile tests updated to expect the new stageLabel
    field on entries.
  - New test: keyed-object shape (Microsoft 2024 schema). Validates the
    real-world fix against a fixture matching the Citizens portal file.
  - validate-deployment-settings.test.js renamed readEntriesPreservingStage
    references to readSettingsFile.

Verified end-to-end against the real Citizens portal file:

  node validate-deployment-settings.js --settingsFile <citizens-portal>
  → summary { invalid: 1 }, one finding flagging the @KeyVault(...) value
    on c311_api_secret as 'kv-placeholder' / 'invalid'.

Plugin tests: 993/993 (was 992; +1 new shape-3 test).
Lint: 0 findings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Fix two readSettingsFile regressions surfaced by Copilot review

1. Dedupe loss in validateSettings → readSettingsFile path

   readSettingsFile dedupes by schemaName when no stageLabel filter is
   provided. This is correct for callers like verify-env-var-values
   that want a single "configured value of X" per schema. But it is
   WRONG for validate-deployment-settings, which must inspect every
   stage's value independently:

     - Staging: c311_api_secret = "https://kv.../secrets/api-secret"  (valid)
     - Production: c311_api_secret = "@KeyVault(vaultName=...)"       (invalid)

   Pre-fix, dedupe kept Staging's value and Production's broken value
   was silently skipped. validateSettings would report invalid: 0
   even though the deploy would fail at ImportAsHolding on Production.

   Fix: added a `preserveAllStages` option to readSettingsFile.
   validate-deployment-settings now calls it with that flag set so
   every per-stage entry is inspected.

2. Mixed-case `Stages` key when value is an object

   Comment said "either casing of stages/Stages key" but code only
   checked `parsed.stages || parsed.STAGES`. A hand-authored file
   using `Stages: { ... }` (capital-S + object form — falls through
   shape 2's array check because the value isn't an array) returned 0
   entries.

   Fix: added `parsed.Stages` to the fallback chain. All three casings
   (`stages` / `Stages` / `STAGES`) now resolve to the object-shape
   path when the value is a plain object.

3. Regression tests (+3, total 996/996)

   - Mixed-case `Stages` object is read correctly.
   - readSettingsFile dedupe (default) vs preserveAllStages behavior
     pinned with concrete expectations.
   - End-to-end: validateSettings catches a Production-only invalid
     value even when Staging is valid (the dedupe-loss scenario).

Verified against real Citizens portal site — still correctly catches
the @KeyVault(...) placeholder on c311_api_secret.

Lint: 0 findings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants