Skip to content

Latest commit

 

History

History
449 lines (341 loc) · 106 KB

File metadata and controls

449 lines (341 loc) · 106 KB

Power Pages Plugin

A plugin for creating, deploying, and managing Power Pages code sites. Supports static SPA frameworks (React, Vue, Angular, Astro) with Dataverse integration, Web API access, and browser-based previews via Playwright.

Server-rendered frameworks (Next.js, Nuxt, Remix, SvelteKit) are NOT supported.

Read PLUGIN_DEVELOPMENT_GUIDE.md for UX and reliability standards when creating new skills and agents.

Key Conventions

  • DRY — Never duplicate logic. Shared scripts live in scripts/ (e.g., generate-uuid.js, scripts/lib/validation-helpers.js, scripts/lib/discover-site-components.js). Shared reference docs live in references/. Always check for existing helpers before writing new code.
  • Validation scripts must import from scripts/lib/validation-helpers.js for boilerplate, path finders, auth helpers, and constants.
  • UUID generation must use the shared scripts/generate-uuid.js — never copy it into skill-specific directories.
  • Power Pages config loading must reuse scripts/lib/powerpages-config.js anywhere a script reads .powerpages-site table-permission or site-setting YAML. Keep that module focused on loading/parsing code-site config only; put validation or business rules in separate validator modules.
  • Script changes require tests — Whenever you add a new script or modify an existing script, add or update node:test coverage under scripts/tests/. Prefer one *.test.js file per script/module being tested, and keep the test command passing: node --test plugins/power-pages/scripts/tests/ (Node's built-in runner discovers *.test.js files under the given directory). Validator changes are not an exception; they must always ship with test coverage.
  • Secure process validation — After changing Power Pages child_process usage, run node scripts/validate-secure-process-execution.js from the repository root. Its fixture suite is node --test scripts/tests/validate-secure-process-execution.test.js.
  • Dataverse-backed validation must stay opt-in for local runs only. Do not require live Dataverse connectivity in CI workflows or default test runs; gate it behind explicit local flags such as --validate-dataverse-relationships.
  • Azure CLI --allow-no-subscriptions — this flag is only valid on az login. Other az subcommands (az account get-access-token, az account show, etc.) reject it as an unrecognized argument and exit 2, so do NOT add it to anything other than az login. When the user is not logged in to the Azure CLI, suggest plain az login first; only suggest az login --allow-no-subscriptions as a fallback if they don't have any associated Azure subscription, since that variant lets subscription-less accounts sign in and still mint AAD-scoped Dataverse/Power Platform tokens via subsequent az account get-access-token calls. Reuse the shared getAuthToken helper in scripts/lib/validation-helpers.js instead of shelling out to az directly.
  • Reference docs shared across skills live in references/ — reference via ${PLUGIN_ROOT}/references/ paths, don't duplicate.
  • Templates use __PLACEHOLDER__ tokens (e.g., __SITE_NAME__) replaced during scaffolding. The gitignore file is stored without the dot prefix and renamed to .gitignore during scaffolding.
  • Hooks are defined centrally in hooks/hooks.json, using PostToolUse with matcher Skill so validation runs when a tracked Power Pages skill completes.
  • ALM split-decision thresholds are intentionally tighter than the platform hard caps. scripts/lib/alm-thresholds.js recommends a split at 75 MB / 4000 components (vs platform caps of 95 MB / 6000), reserving ~20 MB / ~2000-component growth headroom in each split child. Override per-project via .alm-config.json if you have a justified reason to push closer to the caps.
  • OAuth credential-style site settings (ConsumerKey / ClientId / ClientSecret / etc.) are NOT excluded from solutions. setup-solution Phase 5 prompts per credential to choose between (a) Secret-typed env var (Key Vault per stage), (b) String-typed env var (plain text per stage), or (c) skip. The site-setting record is added to the solution and routed to an env var so secret values never ship in the solution zip. Plans generated before 2026-05-08 use the older excluded bucket — setup-solution's preloadedSettings handler treats those as credentialNeedsDecision for backward compatibility.
  • MCP Learn grounding for ALM skills — solution and pipeline skills (setup-solution, export-solution, import-solution, diagnose-deployment, setup-pipeline, deploy-pipeline, ensure-pipelines-host, force-link-environment) include a Phase 1.5 step that grounds the agent in current Microsoft Learn ALM docs before proceeding. The shared discovery pattern lives in references/alm-docs-grounding.md. Add the same Phase 1.5 + the two mcp__plugin_power-pages_microsoft-learn__microsoft_docs_search/fetch tools to allowed-tools when introducing a new ALM skill.
  • ALM artifacts live under docs/alm/ — every ALM-only state file (5 plan/decision JSONs and 9 last-*.json skill-run markers, including last-export.json written by export-solution Phase 7.1) writes to <projectRoot>/docs/alm/, not the project root. Always resolve paths through scripts/lib/alm-paths.js (almPath(root, 'lastDeploy'), almPath(root, 'planContext'), etc.) and call ensureAlmDir(root) once before the first write. Never inline a raw path string. Files that intentionally stay at the project root: .solution-manifest.json (referenced by non-ALM skills too), .datamodel-manifest.json (owned by setup-datamodel, not ALM), .alm-config.json (user-authored override), .alm-deferred (opt-out marker), deployment-settings.json (Microsoft-standard schema). When you add a new ALM artifact, add the key + filename to FILE_NAMES in alm-paths.js, then write through the helper.
  • New skills must be added to README.md — Whenever you add a new user-invocable skill under skills/, you must also document it in README.md under the appropriate section (Site scaffolding and deployment / Data modeling / Backend integration / Security and access / ALM and CI/CD / Polish / Support), update the skill count in the ## Skills intro, and — if the skill is part of the recommended end-to-end flow — update the Typical Workflow code block. The README is the user-facing source of truth for what the plugin can do; an undocumented skill is effectively invisible to users browsing the marketplace.

Skill Development Conventions

.plugin/plugin.json            ← Open Plugins metadata (name, version, keywords)
.mcp.json                      ← MCP server config (Playwright for browser automation)
agents/
  data-model-architect.md      ← Agent: proposes Dataverse data models (read-only)
  webapi-integration.md        ← Agent: implements Web API integration in frontend code
  table-permissions-architect.md ← Agent: proposes table permissions plan (read-only)
  webapi-settings-architect.md ← Agent: proposes Web API site settings with validated column names (read-only)
  ai-webapi-integration.md     ← Agent: implements generative-AI summarization service code + UI wiring
  ai-webapi-settings-architect.md ← Agent: proposes Summarization/* site settings (read-only)
scripts/
  generate-uuid.js             ← Shared UUID v4 generator (used by multiple skills)
  check-activation-status.js   ← Checks if site is already activated (used by deploy-site, activate-site)
  poll-async-operation.js      ← Polls Dataverse asyncoperations until terminal state (used by export-solution, import-solution)
  encode-solution-file.js      ← Base64-encodes a solution zip for OData request bodies (used by import-solution)
  parse-deployment-errors.js   ← Parses PAC CLI stderr + OData errors into structured findings (used by diagnose-deployment)
references/                    ← Shared reference docs used by multiple skills
  odata-common.md              ← Auth headers, token refresh, error handling, retry patterns
  dataverse-prerequisites.md   ← PAC CLI check, Azure CLI token, API access verification
  framework-conventions.md     ← Framework detection, paths, route discovery
  datamodel-manifest-schema.md ← .datamodel-manifest.json format spec
  solution-api-patterns.md     ← OData body templates for publisher/solution CRUD, export/import async actions, manifest format
  deployment-error-catalog.md  ← Known deployment failure patterns with root cause, severity, and fix procedures
  cicd-pipeline-patterns.md    ← PAC CLI SP auth syntax, ADO YAML stage structure, GitHub Actions env job structure
skills/
  create-site/
    SKILL.md                   ← Skill definition with frontmatter (model, allowed-tools)
    assets/{react,vue,angular,astro}/  ← Framework templates with __PLACEHOLDER__ tokens
    references/design-aesthetics.md  ← Design principles, font/color/motion guidance for inline design step
    scripts/validate-site.js   ← Node script validating generated sites
  deploy-site/
    SKILL.md                   ← Deployment skill definition
  setup-datamodel/
    SKILL.md                   ← Dataverse data model creation skill definition
    references/odata-api-patterns.md  ← OData API body templates for table/column/relationship creation
    scripts/validate-datamodel.js ← Node script validating Dataverse data model creation
  add-sample-data/
    SKILL.md                   ← Sample data insertion skill definition
    references/odata-record-patterns.md  ← OData API patterns for record creation and lookups
  add-seo/
    SKILL.md                   ← SEO essentials skill definition (robots.txt, sitemap.xml, meta tags)
    scripts/validate-seo.js    ← Node script validating SEO assets (robots.txt, sitemap.xml, meta tags)
  activate-site/
    SKILL.md                   ← Site activation/provisioning skill definition
    scripts/activate-site.js   ← Activates a site via PP API + polls status
    scripts/generate-subdomain.js  ← Random subdomain suggestion generator
    scripts/validate-activation.js ← Validates site was provisioned via PP API
  create-webroles/
    SKILL.md                   ← Web roles creation skill definition
    scripts/validate-webroles.js ← Node script validating web role YAML files were created
  integrate-webapi/
    SKILL.md                   ← Web API integration skill definition
    scripts/validate-webapi-integration.js ← Node script validating Web API integration code
  add-ai-webapi/
    SKILL.md                   ← Generative-AI summarization integration skill (Layer 3; preview)
    references/ai-api-reference.md ← Canonical Search/Data Summarization API shapes, headers, error codes
    references/explore-prompt.md   ← Phase 2 Explore-agent prompt body + manifest shape
    references/scope-classification.md ← Phase 3 list-trigger / scope-confirmation question mapping
    references/agent-invocation-prompt.md ← Phase 5 ai-webapi-integration prompt template
    references/framework-equivalents.md ← Vue/Angular/Astro safe-markdown + citation rendering snippets
    scripts/validate-ai-webapi.js ← Node script validating summarization code, headers, and Summarization/* settings
  setup-auth/
    SKILL.md                   ← Authentication & authorization skill definition
    references/authentication-reference.md ← Login/logout flow, auth service, framework patterns
    references/authorization-reference.md  ← Role-based access control, guards, directives
    scripts/validate-auth.js   ← Node script validating auth service and authorization code
  setup-solution/
    SKILL.md                   ← Solution creation skill definition
    scripts/validate-solution.js ← Validates .solution-manifest.json and queries Dataverse to confirm solution exists
  export-solution/
    SKILL.md                   ← Solution export skill definition
    scripts/validate-export.js ← Validates solution zip exists, non-empty, contains Solution.xml
  import-solution/
    SKILL.md                   ← Solution import skill definition
    scripts/validate-import.js ← Validates docs/alm/last-import.json marker and checks for component failures
  diagnose-deployment/
    SKILL.md                   ← Deployment diagnostics skill definition (no validator — no artifacts created)
  setup-pipeline/
    SKILL.md                   ← CI/CD pipeline setup skill (Power Platform Pipelines — full implementation; GitHub/ADO coming soon)
    scripts/validate-pipeline.js ← Validates docs/alm/last-pipeline.json marker (PP Pipelines) or pipeline YAML (GitHub/ADO)
  deploy-pipeline/
    SKILL.md                   ← Deployment run skill — creates stage runs, validates package, deploys via PP Pipelines API
    scripts/validate-deploy-pipeline.js ← Validates docs/alm/last-deploy.json marker for required fields; blocks on Failed status
  plan-alm/
    SKILL.md                   ← ALM orchestrator skill definition (8-phase: detect, gather, plan, approve, execute skills in sequence)
    assets/alm-plan-template.html ← HTML template with __PLACEHOLDER__ tokens for the ALM plan document
    scripts/render-alm-plan.js ← Renders alm-plan-template.html from planData JSON (stages diagram, checklist, risks)
    scripts/validate-plan-alm.js ← Validates docs/alm-plan.html exists and is > 500 bytes; gracefully exits 0 if not a plan-alm session

ALM intent routing — plan-alm is the front door

When the user expresses an ALM intent in natural language — promote this site to {env}, ship to staging, deploy to production, set up CI/CD, move to next environment, push out a release, run the pipeline, export and import to staging — invoke /power-pages:plan-alm first, before any individual ALM skill. plan-alm is a planner: it detects the project state, runs the pre-plan completeness check, asks about promotion strategy, and writes a rendered HTML plan (whose steps[] array is the recommended execution sequence). It does not deploy anything. After the user approves the plan, the user runs the individual skills (setup-solution, setup-pipeline, deploy-pipeline, or export-solution/import-solution, plus activate-site/test-site) in the plan's order. Each detects the approved plan via its Phase 0 gate, proceeds without re-nagging, refreshes the plan on completion, and points the user at the next step — but never auto-chains. This separation keeps plan-alm safe to run unattended (no single answer can trigger an irreversible deployment).

Do not jump straight to /power-pages:setup-pipeline, /power-pages:deploy-pipeline, /power-pages:export-solution, or /power-pages:import-solution in response to an ALM intent. Those are individual building blocks; running them without a plan first misses the planner's analysis (completeness check, host resolution, deployment-strategy selection, size/split decisions, rendered HTML plan).

Skip plan-alm only when the user is explicit about the individual skill. Phrases like "just run setup-pipeline", "skip planning, just deploy", "I only need to export the solution zip" are direct invocations — honor them. Anything ambiguous about deployment intent → plan-alm first.

Every ALM execution skill enforces this with a Phase 0 ALM-plan gate. If a user invokes one directly without a plan, the skill recommends running plan-alm first (option 1, recommended) with a "continue without a plan" escape hatch; choosing to plan runs plan-alm (which only plans) and then the skill proceeds. The Phase 0 gate is meant to fail closed — don't bypass it on the user's behalf.

Plugin Components

Agents

Auto-triggered by the main conversation when relevant:

  • data-model-architect: Read-only agent that analyzes site requirements, discovers existing Dataverse tables via OData API, and proposes a data model (new/modified/reused tables + Mermaid ER diagram). Uses pac env who + Azure CLI auth to query Dataverse. Renders the ER diagram visually in the browser via Playwright (writes a temp HTML file with Mermaid.js CDN, navigates to it, takes a screenshot) before entering plan mode. Does NOT create, modify, or delete any tables — purely advisory. The main conversation uses its output to create tables.
  • webapi-integration: Implementation agent that creates production-ready Web API integration code for a single Dataverse table in a Power Pages code site. Detects the frontend framework (React/Vue/Angular/Astro), creates a shared powerPagesApi.ts client (token management, retry logic, OData URL builder) if one doesn't exist, then generates TypeScript entity types, a domain mapper, and a CRUD service layer for the target table. Also creates framework-specific hooks (React), composables (Vue), or injectable services (Angular). Follows Power Pages Web API best practices: /_api/ endpoints, dual token headers, @odata.bind for lookups, explicit $select (never *), formatted value annotations, exponential backoff retry, and 8-minute token TTL. Handles one table per invocation — invoke separately for multiple tables.
  • table-permissions-architect: Read-only agent that analyzes site code, discovers existing web roles and table permissions, and proposes a table-permissions plan (web roles → table permissions with CRUD flags and scopes) rendered as a Mermaid flowchart. Checks for .powerpages-site folder to verify site deployment. Presents the plan via plan mode; after approval, creates web role and table-permission YAML files using deterministic scripts. Supports an AI-only read posture (invoked transitively by /add-ai-webapi via /integrate-webapi) that proposes read: true only, with Parent scope + appendTo for $expand targets. Invoked by /integrate-webapi and /audit-permissions.
  • webapi-settings-architect: Read-only agent that queries Dataverse for exact column LogicalNames (case-sensitive) and proposes Webapi/<table>/enabled and Webapi/<table>/fields site settings. Never uses * for field settings except for aggregate OData queries — always lists specific columns. Presents the plan via plan mode; after approval, creates site-setting YAML files using create-site-setting.js. Supports the AI-only read posture (minimal fields list: no primary key, only _<col>_value lookup read forms). Invoked by /integrate-webapi.
  • ai-webapi-integration: Implementation agent that creates production-ready generative-AI summarization service code for a Power Pages SPA site — Search Summary (/_api/search/v1.0/summary) and Data Summarization (/_api/summarization/data/v1.0/...). Uses raw fetch (never the OData wrapper), attaches the __RequestVerificationToken CSRF header, groups all functions in a single aiSummaryService.* file, emits a framework-idiomatic wrapper (React hook / Vue composable / Angular service / Astro util), and wires real UI call sites with loading/error/content/empty branches, citation rendering, and a safe-markdown renderer. Invoked sequentially per target by /add-ai-webapi (every target shares the one service file, so parallel runs would conflict).
  • ai-webapi-settings-architect: Read-only agent that proposes the three Layer-3 summarization settings — Summarization/Data/Enable, per-prompt Summarization/prompt/<identifier>, and Summarization/Data/ContentSizeLimit (mandatory 200000 for list summaries). Cross-checks that Layer 1/2 prerequisites (Webapi/<table>/*, table permissions) exist for every summarised table and $expand target. Presents the plan via plan mode; after approval, creates site-setting YAMLs (script path, or hand-written block-literal YAML for long/complex prompts). Invoked by /add-ai-webapi Phase 6.

Skills

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

  • create-site: 6-step workflow — gather requirements (including design direction), plan (with explicit scaffold prerequisites), scaffold from template, build pages/components/routing with design applied from the start using skills/create-site/references/design-aesthetics.md and live Playwright preview, review, deploy
  • deploy-site: 6-step workflow — verify PAC CLI, authenticate, confirm environment, upload via pac pages upload-code-site, verify deployment (confirm .powerpages-site folder, commit, offer activation), handle blocked JS attachments
  • setup-datamodel: 7-step workflow — verify prerequisites, invoke data-model-architect agent, review proposal, pre-creation checks, create tables & columns via OData API, create relationships, publish & verify. Writes .datamodel-manifest.json for hook validation.
  • add-sample-data: 6-step workflow — verify prerequisites, discover tables (from .datamodel-manifest.json or OData API), select tables & configure record count, generate & review sample data plan, insert records via OData API with relationship handling, verify & summarize.
  • activate-site: 5-step workflow — verify prerequisites (PAC CLI auth + Azure CLI token + cloud-aware API URL resolution + activation status check via shared script), gather parameters (site name, subdomain, website record ID), confirm with user, activate & poll via skills/activate-site/scripts/activate-site.js, present summary with site URL.
  • add-seo: 7-step workflow — verify site exists, gather SEO config (production URL, exclusions, meta description), plan & approve, create robots.txt, generate sitemap.xml from discovered routes, add meta tags (title, description, viewport, Open Graph, Twitter Card, favicon) to index.html, verify via Playwright & commit.
  • create-webroles: 6-step workflow — verify .powerpages-site/web-roles/ exists (redirect to deploy-site if missing), discover existing roles, determine new roles needed, create web role YAML files with UUIDs from shared scripts/generate-uuid.js, verify web roles (validate files, UUIDs, uniqueness constraints), review & prompt deployment via deploy-site skill.
  • integrate-webapi: 7-step workflow — verify site exists, use Explore agent to analyze code and identify tables needing Web API integration, review plan with user, invoke webapi-integration agent per table to create API client/types/services/hooks, verify integrations (validate all files exist, project builds), invoke table-permissions-architect and webapi-settings-architect agents (in parallel) to configure table permissions and site settings, review & deploy via deploy-site skill. Supports an [AI-READ-ONLY] sentinel that hardens the flow to read-only when invoked by /add-ai-webapi.
  • add-ai-webapi: 8-phase workflow — verify site/deployment, Explore-agent scan for search/data summarization candidates, review plan with user, delegate Layer 1/2 (Web API site settings + table permissions) to /integrate-webapi in AI-only read mode and to /create-webroles, invoke ai-webapi-integration agent sequentially per target to create the summarization service + framework wrapper + UI wiring, invoke ai-webapi-settings-architect for Layer 3 (Summarization/* settings), verify (header-contract grep, $select grep, build, validator), review & deploy. This skill owns Layer 3 only and delegates everything else. Validator: skills/add-ai-webapi/scripts/validate-ai-webapi.js. AI summarization APIs are a preview feature gated by a three-level admin hierarchy.
  • setup-auth: 8-step workflow — verify prerequisites (site deployed + web roles), gather auth requirements and plan, create auth service with Entra ID login/logout (anti-forgery token + form POST), create authorization utilities (role checking), create auth UI (AuthButton component), apply role-based access control to components, verify auth setup (validate files, build, auth UI renders), create ProfileRedirectEnabled site setting and deploy.
  • setup-solution: 7-step workflow — verify prerequisites, gather publisher/solution configuration (publisher prefix is irreversible — requires explicit confirmation), check existing publishers/solutions to avoid duplicates, create publisher + solution via OData API, add Power Pages website and web role components via AddSolutionComponent, verify components and write .solution-manifest.json, present summary. Reuses references/solution-api-patterns.md.
  • export-solution: 7-step workflow — verify prerequisites, identify solution (from .solution-manifest.json or user input), confirm managed vs unmanaged export (irreversible choice), trigger ExportSolutionAsync, poll via scripts/poll-async-operation.js, download and decode solution zip via DownloadSolutionExportData, verify zip contains Solution.xml. Reuses scripts/poll-async-operation.js and references/solution-api-patterns.md.
  • import-solution: 7-step workflow — verify prerequisites and confirm target environment, locate and validate solution zip, configure import (staged vs direct, overwrite options), optionally stage via StageSolution to check missing dependencies, import via ImportSolutionAsync and poll, verify solution exists in target and write docs/alm/last-import.json marker, present component results. Reuses scripts/poll-async-operation.js, scripts/encode-solution-file.js, and references/solution-api-patterns.md.
  • diagnose-deployment: 7-step workflow — verify prerequisites and locate project, collect artifacts (config, manifests, build output), surface upload errors by re-running pac pages upload-code-site in capture mode and parsing via scripts/parse-deployment-errors.js, query recent Dataverse async operation failures, pattern-match against references/deployment-error-catalog.md, offer auto-fixes for fixable errors with explicit per-fix user confirmation, present findings table (severity/type/status). Never auto-applies any fix without user permission.
  • setup-pipeline: 7-phase workflow — detect project context (powerpages.config.json, .solution-manifest.json, pac env who, pac env list, RetrieveSetting('DefaultCustomPipelinesHostEnvForTenant') on dev env to auto-discover host environment), select platform (Power Platform Pipelines = full; GitHub/ADO = coming soon), confirm pipeline configuration with auto-filled values (pipeline name, host env URL, target environments), run preflight checks (Pipelines installed, solution exists, no name conflict), create deploymentenvironments records for source + each target (poll validationstatus until Succeeded), create deploymentpipelines record + $ref associate source env (relative path + @odata.context) + create deploymentstages per target, verify and write docs/alm/last-pipeline.json + docs/pipeline-setup.md + commit. Uses references/cicd-pipeline-patterns.md for all HAR-confirmed API patterns.
  • deploy-pipeline: 8-phase workflow — verify prerequisites (docs/alm/last-pipeline.json, az login, host env token), select target stage (from stages in docs/alm/last-pipeline.json; warn if last deploy failed), pre-flight check on the target env's blockedattachments setting via fix-blocked-attachments.js --dry-run (Phase 2.5, Power Pages projects only — prompts the user to unblock .js/.css proactively when they're on the env's blocklist, saving the ~50-75 min wasted import for sites with thousands of bundle chunks; complementary to the reactive Phase 7.6 handler), resolve pipeline info via RetrieveDeploymentPipelineInfo (v9.1) to get SourceDeploymentEnvironmentId and available artifacts, create deploymentstageruns record + call ValidatePackageAsync (204) + poll operation field until not 200000201 (surface validationresults issues), optionally PATCH deploymentsettingsjson for env var / connection reference overrides, final deploy consent gate at Phase 6.0 (explicit Deploy now / Cancel AskUserQuestion before either DeployPackageAsync or the pac pipeline deploy fallback — closes a gap where Phase 5 → Phase 6.1 could fire without a final confirmation when validation passes cleanly), call DeployPackageAsync + poll stagerunstatus until terminal (handle approval gates with user pause), write docs/alm/last-deploy.json + present deployment summary.
  • force-link-environment: 6-phase workflow — verify prerequisites (Azure CLI token for the target host, PAC CLI auth) and ground in Microsoft Learn (custom-host-pipelines#using-force-link…), identify host env URL (from docs/alm/last-host-check.json, docs/alm/last-pipeline.json, or user input) and source dev env's BAP env GUID, resolve or create the deploymentenvironments record on the new host (re-querying by environmentid to recover the record ID when create-deployment-environment.js throws on the "already associated" validation failure), require explicit AskUserQuestion consent for the destructive cross-host stamp move (makers in the previous host lose pipeline access for this env; previous host's record is left with stale validationstatus; reversible by re-running from the previous host), call scripts/lib/force-link-environment.js to POST ManageEnvironmentStamp + re-poll validationstatus until Succeeded, write docs/alm/last-force-link.json marker. Auto-fix entry point for Pattern 15 in references/deployment-error-catalog.md.
  • plan-alm: 4-phase planner workflow — detect project state (powerpages.config.json, existing manifests, pac env who), gather ALM strategy via branched question flow (PP Pipelines or Manual export/import path), generate HTML ALM plan (docs/alm-plan.html with pipeline diagram and a recommended-execution checklist), then save it (Approved or Draft) and commit. It does not execute any deployment. The user runs the individual ALM skills afterward — setup-solution, setup-pipeline/export-solution, deploy-pipeline/import-solution, activate-site, test-site — each of which detects the plan (Phase 0 gate), proceeds, and refreshes the plan on completion (via refresh-alm-plan-data.js, which also reports the next recommended step). This keeps plan-alm safe under autopilot: it never triggers an irreversible action.

For small mid-cycle changes (one file, one snippet, one site setting) that previously used a separate hotfix solution: instead, run setup-solution in sync mode to adopt the modified components into the existing base solution, bump the solution version, and use deploy-pipeline to ship. This keeps a single solution lineage (cleaner audit trail, simpler dependency management) and avoids solution sprawl. Power Platform Pipelines computes incremental imports internally, so re-deploying the base after a small fix is fast.

Skills are defined in SKILL.md files with YAML frontmatter (name, description, allowed-tools, model). Skill-specific hooks: blocks are not used — hook registration is centralized.

Hooks

Hook registration is centralized in hooks/hooks.json — a single PostToolUse hook (matcher Skill) runs hooks/run-skill-posttool-validation.js after every Skill tool call. The runner derives tracked skills directly from skills/*/SKILL.md via scripts/lib/powerpages-hook-utils.js, looks up an optional skills/<skill>/scripts/validate*.js validator for the skill that just completed, and invokes it with the current cwd.

ALM plan reconcile backstop (auto-heal). After any ALM plan skill completes (powerpages-hook-utils.js → ALM_PLAN_SKILLS / isAlmPlanSkill) and a docs/.alm-plan-data.json exists in the cwd, the runner also spawnSyncs refresh-alm-plan-data.js --reconcile --render. The refresh-alm-plan-data.js calls in each SKILL.md are advisory — silently dropped on session fragmentation, manual execution, or oversight — so the reconcile performs any refresh whose marker (docs/alm/last-*.json) is newer than the plan. This is best-effort and non-blocking: it never changes the hook's exit code, honors .alm-deferred, and is idempotent. Because it fires on any ALM skill (not just the marker's writer), a skip in skill A is healed when the next ALM skill (B) completes. Skills keep their explicit per-phase refresh calls as defense-in-depth + immediate render; the hook is the backstop.

To wire a new skill into validation:

  1. Write the validator at skills/<skill>/scripts/validate-<skill>.js using the runValidation((cwd) => { ... }) pattern from scripts/lib/validation-helpers.js.
  2. No manual tracked-skill registration is needed. Any folder with skills/<skill>/SKILL.md is automatically tracked for telemetry and hook detection.
  3. Add or update test coverage in scripts/tests/powerpages-hook-utils.test.js if you introduce a new validator naming pattern.

All skill folders are tracked. Skills without a scripts/validate*.js file are tracked for telemetry/detection but skip validation.

Anti-patterns (see PLUGIN_DEVELOPMENT_GUIDE.md for the rationale): do not add hooks: Stop: blocks to individual SKILL.md frontmatter — they duplicate the centralized PostToolUse hook and fire too often. Do not use type: prompt Stop hooks for skill-completion checks — they create runaway forced-continuation loops.

Shared Scripts

Shared utility scripts live at scripts/ and are referenced by multiple skills and agents via ${PLUGIN_ROOT}/scripts/.

  • generate-uuid.js: Generates a random UUID v4. Self-contained, no dependencies. Used by create-webroles and the main agent when creating table permission / site setting files from the webapi-permissions agent plan.
  • update-skill-tracking.js: Updates skill usage tracking site settings. Takes --projectRoot, --skillName, and --authoringTool args. The agent passes its own name as --authoringTool (e.g., ClaudeCode, GitHubCopilot). Creates/increments a per-skill counter (Site-AI-<SkillName>.sitesetting.yml) and records the authoring tool (Site-AI-AuthoringTool.sitesetting.yml). Exits silently if .powerpages-site/site-settings/ does not exist. Used by every user-invocable skill (each skill calls it in its final phase per the skill-tracking convention).
  • check-activation-status.js: Checks whether a Power Pages site is already activated (provisioned) in the environment. Takes --projectRoot arg. Site-identity resolution (resolveSiteIdentity(), exported + injectable for tests): (1) powerpages.config.json (code/SPA sites) → siteName + optional websiteRecordId; (2) else .powerpages-site/website.yml (declarative/data-model sites) → namesiteName, idwebsiteRecordId; (3) pac pages list ONLY when the GUID is still unknown — declarative sites (and code sites whose config included the GUID) skip the pac pages list exec entirely. Then queries the Power Platform GET websites API and matches by both websiteRecordId and name. Outputs JSON: { activated: true/false, siteName, websiteRecordId, websiteUrl } or { error }. The CLI flow is guarded by require.main === module; module.exports = { resolveSiteIdentity, getWebsites }. Used by deploy-site and activate-site.
  • poll-async-operation.js: Polls a Dataverse asyncoperations record until it reaches a terminal state (Succeeded/Failed/Canceled) or times out. Args: --asyncJobId, --envUrl, --token (optional, refreshed via Azure CLI if omitted), --intervalMs (default 5000), --maxAttempts (default 60). Outputs JSON status. Used by export-solution and import-solution.
  • encode-solution-file.js: Base64-encodes a solution zip file for use in Dataverse OData request bodies (ImportSolutionAsync, StageSolution). Args: --zipPath. Outputs { encoded, fileSizeBytes, fileName }. Used by import-solution.
  • parse-deployment-errors.js: Parses PAC CLI stderr output or OData error JSON into structured findings array. Each finding has { patternId, type, severity, message, rawMatch, autoFixAvailable, suggestedFix }. Reads from --input, --file, or stdin. Used by diagnose-deployment.

Shared lib modules live at scripts/lib/ and are imported by other scripts via require('./validation-helpers') or sibling requires. Never inline their logic in skill scripts — always require from scripts/lib/.

ALM Prerequisites & Context

  • scripts/lib/verify-alm-prerequisites.js: Verifies all prerequisites for ALM skills — PAC CLI installed + authenticated (pac env who), Azure CLI installed + logged in, Dataverse API reachable (WhoAmI). Args: --envUrl (opt, overrides env from PAC CLI), --require-manifest (fails if .solution-manifest.json not found), --expectedEnvUrl (opt — env-drift guard: assert the resolved env matches this origin and HARD-STOP on mismatch). Output: { envUrl, token, userId, organizationId, tenantId }. Exit 0 on success, exit 1 on any failure. --expectedEnvUrl is the recommended guard for any ALM skill that runs against the project's source/dev env: since getEnvironmentUrl() now parses PAC 2.8.x's Org URL: successfully, a drifted PAC context resolves silently instead of failing loudly (the old parse-miss had been an accidental safety net), so an ALM op could target the wrong environment (e.g. PROD). Skills pass the project's env URL (from .solution-manifest.json top-level environmentUrl / powerpages.config.json environmentUrl / the approved plan's source env) so a mismatch stops the run before any token/write. Prefer this over pinning --envUrl, which only redirects the Dataverse-API calls while later PAC-CLI ops (pac pipeline deploy, pac env select) still follow the ambient context. Used by setup-solution, export-solution, import-solution, setup-pipeline, deploy-pipeline, plan-alm.

  • scripts/lib/detect-project-context.js: Reads Power Pages project context from the project root. The siteType discriminator is the build axis — code/SPA vs declarative (design-studio) site — NOT the Dataverse data-model axis (a declarative site can be on the standard OR enhanced data model; both download to a .powerpages-site/ tree). siteType: "declarative" is the declarative bucket (it was historically labeled "data-model"; that value is now the legacy alias — nothing branches on the literal, so older plan-data carrying "data-model" stays equivalent). Resolves identity in order: (1) powerpages.config.jsonsiteType: "code" (code/SPA sites); (2) .powerpages-site/siteType: "declarative" (declarative design-studio sites — standard or enhanced data model — which have no powerpages.config.json). The authoritative declarative marker is the .powerpages-site/.portalconfig/ directory (only declarative sites have it); website.yml is the identity source (idwebsiteRecordId, namesiteName) but is NOT a reliable declarative signal alone because both site types carry it. environmentUrl: null for declarative sites (no env URL in the local files — callers re-confirm via pac env who). Also reads .solution-manifest.json and .datamodel-manifest.json. Args: --projectRoot (opt). Output: { projectRoot, siteType, siteName, websiteRecordId, environmentUrl, solutionManifest, datamodelManifest }. Exit 0 on success, exit 1 only if neither powerpages.config.json nor a .powerpages-site/ (.portalconfig//website.yml) marker is found. Note: findProjectRoot (in validation-helpers.js) likewise treats a .powerpages-site/ directory as a project-root marker.

  • scripts/lib/alm-paths.js: Single source of truth for ALM artifact paths. Exports ALM_DIR (always docs/alm), FILE_NAMES (frozen object mapping logical key → filename for all 14 ALM artifacts), almDir(projectRoot) → path, almPath(projectRoot, key) → path, ensureAlmDir(projectRoot) → path (mkdir -p idempotent). Every ALM-only state file (5 plan/decision JSONs + 9 last-*.json skill-run markers including last-export.json) writes under <projectRoot>/docs/alm/. Always resolve through this helper — never inline a raw docs/alm/... path in a script. Files intentionally NOT moved here (and not in FILE_NAMES): .solution-manifest.json, .datamodel-manifest.json, .alm-config.json, .alm-deferred, deployment-settings.json. Adding a new ALM marker means adding its key + filename to FILE_NAMES first; almPath throws on unknown keys to catch typos at call-site.

  • scripts/lib/check-alm-plan.js: Phase 0 gate helper used by every ALM skill to detect (a) whether an ALM plan exists for this project, (b) whether the user has explicitly deferred ALM via the .alm-deferred marker, and (c) whether an existing plan is stale (the source solution was modified after the plan was generated). Args: --projectRoot, --envUrl (opt — required for staleness check), --token (opt), --solutionId (opt — required for staleness check). Output: { exists, deferred, deferral, planPath, htmlPath, stale, staleness: { reason, detail }, generatedAt, planStatus, solution: {...} }. Without env/solution context the helper does an existence-only check; with them it queries Dataverse for solutions(solutionId)?$select=modifiedon and compares against planData.generatedAt. Used by setup-solution, setup-pipeline, deploy-pipeline, export-solution, import-solution, configure-env-variables, ensure-pipelines-host, force-link-environment Phase 0 gates — the "fail closed when no plan" pattern. PLAN_STATUS lifecycle — promotes ApprovedIn Execution: plan-alm is plan-only and leaves the plan Approved; this helper performs the ApprovedIn Execution transition (and writes the first heartbeat) the first time an execution skill's Phase 0 runs — it is the only thing that sets In Execution, so without it the heartbeat/active-chain machinery (multi-hour-deploy stale-heartbeat reclassification) never engages. Gated on heartbeat-write: read-only callers pass --no-heartbeat (plan-alm's own deferral check, audits, tests) and are never promoted. The terminal In ExecutionCompleted transition is owned by refresh-alm-plan-data.js (completion evaluator).

  • scripts/lib/set-plan-status.js: The single deterministic owner of the creation-time Draft / Approved write — the one PLAN_STATUS transition that used to be done by hand-authored Edits in plan-alm Phase 4 (to the HTML spans and the JSON), with no helper. Because the badge + approved-by / approval-date spans are re-derived from docs/.alm-plan-data.json on every render, the old manual HTML Edit was non-durable (reverted on the next refresh) and a partial write left the plan "approver recorded but PLAN_STATUS=Draft" — stuck forever, since check-alm-plan.js only promotes from Approved. This helper writes PLAN_STATUS + PLAN_MODE + APPROVED_BY + APPROVAL_DATE together (atomic temp+rename) and optionally re-renders (reuses refresh-alm-plan-data.js → findRendererPath/invokeRenderer). Enforced invariants: only Draft / Approved are settable here (In Execution is owned by check-alm-plan.js, Completed by refresh-alm-plan-data.js); Approved requires a non-empty --approver; Draft clears the approver fields; a plan already In Execution / Completed is not re-drafted without --force. Args: --projectRoot, --status Draft|Approved, --approver, --approvalDate (opt — defaults to now), --force, --render, --rendererPath (opt). Output: { ok, previousStatus, status, mode, approver, approvalDate, rendered }. Called by plan-alm Phase 4 (both save options) and the Phase 1 step-0b in-place Draft→Approved fast-path. The validate-plan-alm.js consistency guard blocks the two half-written states (Draft+approver, Approved+no-approver) for plans created the old way or hand-edited.

  • scripts/lib/resolve-target-solution.js: Resolves "which solution should this new Dataverse record land in?" Implements the strict 3-step order from the ALM-aware-by-default principle: (1) explicit --solutionUniqueName (or equivalent caller arg) wins; (2) .solution-manifest.json in the project root; (3) neither → throw NoSolutionConfiguredError. The module NEVER auto-picks from Dataverse — interactive prompt UX is the caller's responsibility (catch the error, present an AskUserQuestion list, re-invoke with explicit populated). Callers that need to confirm the solution still exists in Dataverse can pass verifyExists: true; the module then enriches the result with { solutionId, version, ismanaged }. Component-creation scripts must require this helper and pass through --solutionUniqueName so records land in the user's solution instead of Default.

Solution Splitting Decision Tree (v1.3.0+)

  • scripts/lib/alm-thresholds.js: Central default threshold constants for the split decision tree. Loads optional .alm-config.json from project root and merges over defaults. Exports DEFAULTS, DEFAULT_CONFIG, loadConfig(projectRoot), classifyTier(value, greenUpperExclusive, yellowUpperExclusive), deepMerge(target, source). Used by estimate-solution-size.js and compute-split-plan.js.
  • scripts/lib/estimate-solution-size.js: Estimates solution size + component counts by querying Dataverse. Args: --envUrl, --websiteRecordId, --token (opt), --publisherPrefix (opt), --siteName (opt), --solutionId (opt — scopes env var count to the target solution; without it falls back to a publisher-prefix tenant-wide query that overcounts when prefix is shared), --datamodelManifest (opt), --projectRoot (opt — enables disk cross-check: walks the local build-output directory (dist/, public-output/, build/, .output/) and surfaces the byte total). Output: { totalSizeMB, componentCountSiteTotal, componentCountSiteActionable, componentCountInSolution, orphansOnSite, tableCount, tableCountScope, schemaAttrCount, webFilesAggregateMB, webFilesIndividual[], webFileCount, webFileSampleSize, webFilesDiskMeasuredMB, webFilesDiskMeasuredPath, webFilesDiskFileCount, cloudFlowCount, botCount, envVarCount, envVarCountScope, envVarCountTenantWide, mediaRatio, siteType, tables[], tableRelationships[], breakdown, estimationMethod, estimationAccuracyPct, truncationSuspected, truncationWarnings, ppcGroundTruthCount }. Table discovery is site-referenced, NOT publisher-prefix: tableCount/tables[] are scoped to the custom tables the site actually references — its .powerpages-site/table-permissions/ (+ datamodel manifest) intersected with the env's custom-unmanaged tables (via resolve-site-tables.js + query-metadata.js). tableCountScope"site-referenced" | "manifest-only" | "unavailable" (the last → 0 tables, never an env-wide prefix dump). --publisherPrefix now scopes ONLY the env var count, not tables. tableRelationships[] are [a,b] dependency edges (lookups + N:N, via query-table-relationships.js) among the scoped tables, consumed by compute-split-plan.js to cluster related tables into the same solution. Metadata-based estimation with ±15% caveat. Web-file size is measured via stratified sample (first 50 + middle 50 + last 50, cap 150) scaled to full count. Disk fields are null unless --projectRoot was passed AND a build-output directory was found. Truncation canaries fire when Dataverse pagination disagrees with @odata.count, when ppcs land on a page-size boundary, when sampled average bytes/file < 1 KB at scale, or when the disk total exceeds the Dataverse total by >2× — any signal flips truncationSuspected: true with a per-cause truncationWarnings[] entry. Used by plan-alm Phase 1 Step 10.
  • scripts/lib/compute-split-plan.js: Runs the split decision tree against a size-estimate blob. Args: --estimate <path>, --projectRoot (opt — for .alm-config.json overrides), --siteName (opt), --publisherPrefix (opt). Output: { sizeAnalysis, assetAdvisory, splitStrategy, appliedStrategies, compositeSubPartitioned, proposedSolutions[], recommendations[], truncationSuspected, truncationWarnings }. Evaluates strategies in priority order: Strategy 3 (Schema Segmentation) → Strategy 1 (Layer Split) → Strategy 2 (Change-Frequency) → Strategy 4 (Config Isolation). Schema Segmentation is dependency-aware + capacity-bounded: it builds connected-component clusters from estimate.tableRelationships (union-find), then bin-packs whole clusters (never splitting a relationship) into the fewest solutions that keep each under maxTableCount/maxSchemaAttrs where possible — capped at maxSchemaSplitSolutions (default 8). This replaced the old one-solution-per-table-name-stem heuristic that produced ~one solution per table. Two cases CAN exceed a per-solution cap, and BOTH raise an recommendations[] warning rather than failing silently: (a) an indivisible dependency cluster larger than maxTableCount stays whole (oversized-cluster table-count warning); (b) when MORE than maxSchemaSplitSolutions independent attr-heavy clusters must share the capped solution count, the FFD least-loaded fallback co-locates clusters and a solution's summed columns exceed maxSchemaAttrs (oversized-schema attr-cap warning). The split trigger + thresholds are unchanged — only the packing. Strategy 4 stacks additively. Strategy 1 also runs a composite sub-partition pass: when Core still exceeds the size OR component-count cap after Web Assets are peeled off, Core is replaced with change-frequency-shaped sub-children (_Foundation/_Config/_Content, plus _Integration whenever the parent had any flows or bots — coverage takes priority over the changeFreqMinFlows heuristic, which governs only the TOP-LEVEL strategy choice). When additive Strategy 4 is firing concurrently (a top-level _EnvVars solution), _Config drops Environment Variable from its componentTypes to avoid double-claim; when it isn't, _Config absorbs env vars so they have an owner. Sub-partitioning sets compositeSubPartitioned: true and appends composite-sub-partition to appliedStrategies. validateSplits checks BOTH the size AND component-count cap per split (skipping isFutureBuffer solutions). Supports .alm-config.json overrides including strategyOverride to bypass the tree. See solution-splitting-logic.md spec in design docs for full logic.
  • scripts/lib/resolve-site-tables.js: Single source of truth for "which custom tables does this site actually use." collectReferencedEntityNames({ projectRoot, datamodelManifestPath }) reads .powerpages-site/table-permissions/*.tablepermission.yml (entitylogicalname, via powerpages-config.js → loadTablePermissions) + the datamodel manifest → { names:Set, available, sources }. scopeCustomTables(referencedNames, customUnmanagedTables) intersects that set with the env's custom-unmanaged tables. SME-confirmed: table permissions are the complete signal ("if a table is used in the site there will be permissions for it"), so forms/lists are NOT scanned. Used by estimate-solution-size.js and discover-site-components.js to replace the publisher-prefix table dump.
  • scripts/lib/query-metadata.js: queryCustomUnmanagedTables(envUrl, token, makeRequest?)[{ logicalName, metadataId, schemaName, displayName }] (the single EntityDefinitions?$filter=IsCustomEntity query, IsManaged===false filtered). Consolidates the formerly-triplicated custom-table query (estimator, discover-site-components, setup-solution). Reuses odataGetAll from validation-helpers.js.
  • scripts/lib/query-table-relationships.js: fetchTableRelationships(envUrl, table, token, makeRequest?){ oneToMany[], manyToMany[] }. Extracted from skills/audit-permissions/scripts/query-table-relationships.js (now a thin CLI wrapper over this lib) and extended with ManyToMany. OneToMany errors propagate; ManyToMany is best-effort. Used by the estimator to build tableRelationships[] and by audit-permissions for relationship-scope validation.
  • scripts/lib/validation-helpers.js also exports odataGet(url, token, makeRequest?) + odataGetAll(url, token, makeRequest?, maxPages?) — the shared, injectable OData GET + @odata.nextLink pagination used by the new metadata/relationship helpers (avoids each lib rolling its own paginator).

Solution Management

  • scripts/lib/verify-solution-exists.js: Checks whether a Dataverse solution exists by unique name via OData solutions?$filter=uniquename eq '...'. Args: --envUrl, --uniqueName, --token (opt). Output: found → { found: true, solutionId, uniqueName, version, isManaged }, not found → { found: false, uniqueName }. Exit 0 regardless of found/not-found; exit 1 on API error.
  • scripts/lib/create-solution.js: Creates a Dataverse solution via OData POST to /solutions. Handles 409 (already exists) by re-querying and returning the existing record's ID. Args: --envUrl, --token, --uniqueName, --friendlyName, --version, --publisherId, --description (opt). Output: { solutionId, uniqueName, created } where created: false means it already existed.
  • scripts/lib/bump-solution-version.js: Bumps the patch segment (4th segment) of a Dataverse solution's version and PATCHes it back. Single source of truth for the bump rule — pads missing trailing segments with 0 (so 1.01.0.0.1), uses integer arithmetic (1.0.0.91.0.0.10, not lexical), rejects non-numeric or negative segments, rejects more than 4 segments. Used by setup-solution Phase 4 sync-mode bump AND export-solution Phase 4 Step 4.0 (always-on pre-export bump so every produced zip carries a strictly-increasing version label for the manual export/import path — eliminates the "exported zip carries same version as previous export" failure for managed-solution upgrades). Args: --envUrl, one of (--uniqueName OR --solutionId), --token (opt — refreshed via getAuthToken if omitted), --dryRun (opt — computes the next version without PATCHing). Output: { solutionId, uniqueName, previous, next, bumped }. Exit 0 on success, exit 1 on missing args / solution not found / PATCH rejected. Also exports compareVersions(a, b) → -1|0|1 and parseVersionToSegments(v) → number[4] as programmatic helpers — compareVersions is the canonical way for any caller to compare two Dataverse version strings (import-solution Phase 3.0 uses it for the version-skew advisory). Same segment-wise integer rules — compareVersions('1.0.0.9', '1.0.0.10') correctly returns -1, where raw string > would say 1.0.0.9 > 1.0.0.10 is true and label the 10th deploy of the day as a downgrade. Never compare version strings with raw >/</=== in SKILL.md prose — always shell out to node -e "console.log(require('.../bump-solution-version').compareVersions(...))". Do not inline the bump or comparison rule in any SKILL.md — both setup-solution and export-solution must call this helper so divergent semantics cannot happen.
  • scripts/lib/create-solutions-batch.js: Parallel bulk creation of Dataverse solutions sharing one publisher. Used by setup-solution Phase 4 Step 2 in MULTI_SOLUTION_MODE when the split plan recommends N solutions. Fans out via Promise.allSettled so independent failures don't poison the batch — typical 5-6 solution splits complete in ~2s vs ~10s for a serial agent loop. Args: --envUrl, --publisherId, --solutionsFile <path> (JSON array of { uniqueName, friendlyName, version, description, isFutureBuffer? }), --token (opt; refreshed once at batch start via getAuthToken if omitted). Skips entries with isFutureBuffer: true (reserved 0/0 slots that exist as data only). Output: { total, success, skipped, failed, results: [{ uniqueName, solutionId, created } | { uniqueName, skipped: true, reason: "futureBuffer" } | { uniqueName, error }] }. Exit 0 always (caller inspects failed + per-entry error); exit 1 only on fatal setup errors (missing required args, unparseable JSON).
  • scripts/lib/discover-component-types.js: Resolves Dataverse solution component type integers at runtime by querying solutioncomponents for known object IDs — never hardcodes component types. Args: --envUrl, --token, --websiteRecordId, --powerpageComponentId (opt), --siteLanguageId (opt), --objectIds (opt, comma-separated for generic lookup). Output: { websiteComponentType, subComponentType, siteLanguageComponentType, resolved[] }.
  • scripts/lib/add-components-to-solution.js: Bulk-adds solution components via AddSolutionComponent OData action. Refreshes the Azure CLI token every --batchSize calls (default 20). Treats "already in solution" as success (idempotent). Args: --envUrl, --componentsFile (path to JSON array of { componentId, componentType, addRequired?, description? }), --solutionUniqueName, --batchSize (opt), --token (opt). Input shape validated upfront — keys must be camelCase; PascalCase entries (ComponentId/ComponentType) are rejected with a targeted error before any Dataverse call (closes a silent-failure mode where the destructure returned undefined and produced a stream of HTTP 400 "missing parameters" responses). Per-entry validation surfaces the first malformed row with its array index. Output: { total, success, skipped, failed, failures[] }. Progress goes to stderr; exits 0 always (caller inspects failures); exits 1 on fatal setup errors (missing required args, malformed input).
  • scripts/lib/classify-site-settings.js: Single source of truth for the credential regex + tier classification used by plan-alm Phase 1 Step 7 and setup-solution Phase 5. Exports classify, bulkClassify, autoClassifyCredential, plus the four named regexes (CREDENTIAL_REGEX, AUTH_PREFIX_REGEX, CREDENTIAL_SECRET_REGEX, CREDENTIAL_STRING_REGEX). CLI mode reads JSON array from stdin, emits the four-bucket { keepAsIs, authNoValue, promoteToEnvVar, credentialNeedsDecision } shape. Do not inline the regex in any SKILL.md — both skills must require this module so a regex change propagates to plan time AND execution time.
  • scripts/lib/generate-env-var-schema-name.js: Single 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). Inlining the rule risks divergent schema names across skills — call the helper. Args: --publisherPrefix, --settingName. Output: { schemaName, sanitized }.
  • scripts/lib/create-env-var-definition.js: Creates an environmentvariabledefinition record in Dataverse. Handles 409 (duplicate) by returning the existing definition's ID. Args: --envUrl, --token, --schemaName, --displayName, --type (opt — canonical Dataverse option-set codes: 100000000=String, 100000001=Number, 100000002=Boolean, 100000003=JSON, 100000004=DataSource, 100000005=Secret), --defaultValue (opt). Output: { definitionId, schemaName, created }. Note: earlier revisions of this helper and discover-env-var-definitions.js had Secret/JSON swapped (Secret=100000003); both are now correct per the canonical mapping verified against live tenant data.
  • scripts/lib/link-site-setting-to-env-var.js: Links an mspp_sitesetting record to an environmentvariabledefinition via OData PATCH on the v9.0 API (not v9.2). HAR-confirmed: navigation property is EnvironmentValue@odata.bind; headers if-match: * and clienthost: Browser are required (omitting causes 400). Args: --envUrl, --token, --siteSettingId, --definitionId, --schemaName. Output: { ok, verified, siteSettingId, definitionId }.
  • scripts/lib/install-pipelines-app.js: Installs the Power Platform Pipelines application package on an existing Dataverse env (replaces ensure-pipelines-host Phase 4.B's manual PPAC click-through). Resolution: BAP applicationPackages LIST + /install POST → 200 sync / 202 + Location poll, with PAC CLI fallback (pac application install --environment-id ... --application-list msdyn_AppDeploymentAnchor) on 401/403/5xx. 409 on install POST treated as idempotent (already-installed). Args: --bapToken, --envId, --instanceApiUrl (opt — for verification probe), --hostToken (opt), --no-pac-fallback (opt), --correlationId, --timeoutSec, --apiVersion, --bapBase. Output: { status, alreadyInstalled, installPath: 'bap'\|'pac'\|'cached', packageUniqueName, pipelinesSolutionVersion, durationSec, correlationId, pollAttempts, locationHeader, pacFallbackReason }.
  • scripts/lib/discover-env-var-definitions.js: Enumerates env var definitions matching a publisher prefix and joins each with its bound mspp_sitesetting (if any). Used by plan-alm Phase 1 Step 10b to populate planData.envVars[] with row-level metadata so the rendered plan's Env Variables tab shows schema name, type, default value, and bound site setting per definition (instead of just a count). Args: --envUrl, --publisherPrefix, --websiteRecordId, --token (opt). Output: { envVars: [{ schemaName, type, defaultValue, siteSetting }], count }. Degrades gracefully (empty array, exit 0) on auth failure or query errors so the renderer's count-summary fallback can take over.
  • scripts/lib/refresh-alm-plan-data.js: Updates docs/.alm-plan-data.json with post-run state from the marker files written by setup-pipeline / deploy-pipeline / ensure-pipelines-host / test-site / import-solution / activate-site / configure-env-variables / setup-solution / export-solution, then optionally re-renders docs/alm-plan.html. Driven by the execution skills' final-phase refresh (and the PostToolUse --reconcile backstop) — NOT by plan-alm, which is now a plan-only 4-phase planner that only renders the initial plan in Phase 3 — so the rendered Pipelines tab, Validation tab, hostResolution card, env var values matrix, checklist, and risks list reflect actual run state instead of frozen pre-run intent. Args: --projectRoot, --phase (setup-solution/setup-pipeline/configure-env-variables/deploy-pipeline/export-solution/import-solution/activate-site/test-site/ensure-pipelines-host/finalize) OR --reconcile (mutually exclusive with --phase), --render (also invoke renderer), --stageName (required for test-site; preferred for import-solution/activate-site though both can resolve via marker URL match). Output: { ok, phase, dataPath, htmlPath, rendered }. Returns ok:false (soft no-op) when docs/.alm-plan-data.json is missing — caller should preserve that file across phases for the helper to work. Plan-alm Phase 3 must NOT delete the file after the initial render — it's read by check-alm-plan.js for downstream Phase 0 ALM-plan gates and by this helper for post-run refreshes. Cross-cutting behaviors: (a) setStepStatus flips the matching entry in planData.steps[] to completed (or failed when the phase's marker indicates failure) — case-insensitive keyword match + stage filter, respects skip: true, never regresses completed→pending; (b) deploy-pipeline AND configure-env-variables both backfill planData.envVars[i].values{} from the project root's deployment-settings.json so the rendered plan's "Values by Environment" matrix auto-populates (accepts both top-level-stage and nested-stages shapes; SchemaName/Value and camelCase variants; never overwrites a populated cell — manual override wins); (c) configure-env-variables and setup-solution both re-ingest docs/alm/last-env-vars.json (when present) so freshly-created definitions appear in planData.envVars[] and plannedEnvVarCount zeros out; (d) export-solution ingests docs/alm/last-export.json into planData.manualMeta.lastExport (all 10 marker fields: solutionUniqueName/solutionId/previousVersion/version/managed/sourceEnvironmentUrl/zipPath/fileSizeBytes/asyncOperationId/exportedAt) so the Manual-path tab can show the most recent export. Marker absence is a silent step-sync-only no-op (no manualMeta.lastExport: null row in the rendered plan); (e) deploy-pipeline ingests the batchValidation block from last-deploy.json into planData.pipelineMeta.lastDeploy.batchValidation (totalSolutions/succeeded/failed/pendingApproval/timedOut/elapsedSeconds/perSolutionStageRunIds) so the rendered plan can show the Phase 3.6 parallel-validation outcome distinct from the serial deploy outcome. Explicitly set to null for single-solution / legacy v2 deploys so renderers can branch on it; legacy elapsedSecondsApprox field name is accepted and normalized to elapsedSeconds on ingest. ensure-pipelines-host phase: host-only update of planData.hostResolution from last-host-check.json (drops NoHost risks) WITHOUT touching pipelineMeta or the Setup pipeline step — for when the host was resolved but the pipeline doesn't exist yet. --reconcile mode: the enforcement backstop — scans the last-*.json markers and, for each one newer than docs/.alm-plan-data.json (a skipped refresh), applies the mapped phase (MARKER_TO_PHASE; lastPipeline→setup-pipeline supersedes the host-only phase; lastEnvVars→configure-env-variables if deployment-settings.json exists else setup-solution) against a single loaded planData, writes once, renders once. Honors .alm-deferred, soft no-op when no plan, idempotent. Output { ok, reconciled:[phases healed], failed:[{phase,error}], rendered } — a phase whose refresh throws (e.g. a marker schema it can't parse) is captured in failed (and written to stderr) instead of being silently swallowed, while the remaining phases still heal. Invoked by the PostToolUse hook after every ALM skill (see Hooks). Completion evaluator (In ExecutionCompleted): after every phase's step-sync (both refresh() and reconcile()), evaluatePlanCompletion flips PLAN_STATUS to Completed + stamps COMPLETED_AT once every non-skip step is completed and none is failed. This is what makes the LAST execution skill terminate the plan automatically — no skill calls --phase finalize (the explicit finalize phase / refreshFinalize exists but nothing invoked it, so the lifecycle previously never reached Completed). Only advances from In Execution (the normal post-promotion state — see check-alm-plan.js) or Approved (defensive fallback); never regresses a Draft or already-Completed plan, and a failed step blocks completion so a failed deploy can't look "done".

PP Pipelines

  • scripts/lib/list-environments.js: Enumerates the Dataverse environments the signed-in PAC user can access, as JSON, for ENV_LIST pre-fill (plan-alm Phase 1 Step 5, setup-pipeline, ensure-pipelines-host "Other (paste URL)" prompts). Why it exists: the skills used to run pac env list --output json, which is INVALID on current PAC CLI (verified 2.8.1 — pac env list accepts only --filter and errors on --output), so the JSON pre-fill silently never worked. This helper runs the plain pac env list and parses its table (anchored on the env GUID + https URL + unique-name tokens, so display names with spaces survive). pac admin list --json was rejected as the source — it's admin-only and tenant-wide, the wrong scope for a per-user pre-fill. Exports parseEnvList(stdout) (pure, tested) + listEnvironments(). CLI prints a JSON array of { displayName, environmentId, environmentUrl, uniqueName, active }; prints [] and exits 0 on any failure (unauthenticated PAC, parse miss) so callers degrade to manual entry. Match envs by environmentUrl origin.
  • scripts/lib/discover-pipelines-host.js: Discovers the tenant-level default Power Platform Pipelines host environment URL by calling RetrieveSetting('DefaultCustomPipelinesHostEnvForTenant') on the dev/source environment. Args: --envUrl, --token, --userId. Output: { found, hostEnvUrl }. Exit 0 (including when not found); exit 1 on error.
  • scripts/lib/create-deployment-environment.js: Creates a deploymentenvironments record in the Pipelines host environment using the unprefixed field schema (name, environmentid, environmenttype), then polls validationstatus until Succeeded (200000001) or Failed (200000002). Args: --hostEnvUrl, --token, --name, --bapEnvId, --environmentType (200000000 Dev / 200000001 Target), --environmentUrl (opt, only echoed in output marker). Idempotent: if a record already exists for the same environmentid, returns it with reused: true. Output: { deploymentEnvironmentId, name, bapEnvId, environmentUrl, environmentType, validationStatus, reused }.
  • scripts/lib/create-deployment-pipeline.js: Creates a deploymentpipelines record, associates the source environment via $ref (relative path + @odata.context), and creates deploymentstages records for each target environment. Args: --hostEnvUrl, --token, --pipelineName, --description, --sourceDeploymentEnvironmentId, --stagesJson (JSON array of { name, targetDeploymentEnvironmentId, order }). Output: { pipelineId, pipelineName, stages[] }.
  • scripts/lib/create-stage-run.js: Creates a deploymentstageruns record to initiate a pipeline deployment stage. Args: --hostEnvUrl, --token, --pipelineId (opt), --stageId, --sourceDeploymentEnvironmentId, --solutionId (GUID), --artifactName (unique name). Output: { stageRunId }.
  • scripts/lib/poll-validation-status.js: Polls stagerunstatus on a deploymentstageruns record until Validation Succeeded (200000007) or Failed (200000003). Args: --hostEnvUrl, --token, --stageRunId, --intervalMs (opt, default 5000), --maxAttempts (opt, default 36). Output: { stageRunId, validationResults, stageRunStatus }.
  • scripts/lib/validate-stage-runs-batch.js: Parallel batch validation of N stage runs against the same stage. Used by deploy-pipeline Phase 3.6 in MULTI_RUN_MODE (multi-solution v3 manifest) to compress validation from N × ~120s to roughly the slowest single validation. For each solution, runs create-stage-run + POST ValidatePackageAsync + poll-validation-status concurrently via Promise.all — wrap-errors-into-result-object pattern (helper never rejects per-solution; errors land on the result object's status + error fields). Deploy (DeployPackageAsync) is NOT parallelized — Dataverse takes an env-level import lock so parallel deploys queue on the host anyway. Args: --hostEnvUrl, --stageId, --sourceDeploymentEnvironmentId, --solutionsFile <path> (JSON array of { solutionUniqueName, solutionId }), --token (opt), --pipelineId (opt), --intervalMs / --maxAttempts (opt, forwarded to poll). Output: { total, succeeded, failed, pendingApproval, timedOut, allPassed, elapsedSeconds, results: [{ solutionUniqueName, solutionId, stageRunId, status: "Succeeded"|"Failed"|"PendingApproval"|"Timeout"|"Error", validationResults?, error? }] }. PendingApproval is detected via post-timeout probe (the underlying poll helper doesn't know about 200000005, so a poll timeout triggers a single ?$select=stagerunstatus re-query to distinguish "still validating" from "awaiting approval"). elapsedSeconds is wall-clock measured around the fan-out (excludes token-acquire prelude) so deploy-pipeline Phase 3.6.6 can persist it into last-deploy.json's batchValidation block without out-of-band timing. Also supports --rePoll mode: solutionsFile entries must include stageRunId (carried from the original batch's results); the helper skips create-stage-run + ValidatePackageAsync and only runs the poll-and-probe pattern. Used by deploy-pipeline Phase 3.6.4 after the user approves PendingApproval validations in PPAC — re-poll the existing stage runs without re-creating them. In rePoll mode --stageId / --sourceDeploymentEnvironmentId are not required. Exit 0 always (caller inspects allPassed); exit 1 only on fatal setup errors.
  • scripts/lib/poll-deployment-status.js: Polls stagerunstatus on a deploymentstageruns record until a terminal state. Returns { status: 'Awaiting' } (exit 0, non-throwing) for approval gates (200000005 PendingApproval, 200000008 AwaitingPreDeployApproval) — caller must pause for user. Args: --hostEnvUrl, --token, --stageRunId, --intervalMs (opt, default 8000), --maxAttempts (opt, default 75). Output: { stageRunId, status, errorDetails }.
  • scripts/lib/ensure-pipelines-host-detect.js: Detection-only wrapper around the ensure-pipelines-host workflow. Runs Phases 1.0 (cache fast-path) + 2 (resolution order: org-setting → BAP env GET → tenant default custom → tenant-wide enumeration) + 5 (verify if host found). Never enters Phase 3 (decision tree) or Phase 4 (provisioning) — always exits with actionTaken: "none". Used by plan-alm Phase 1 step 12 and other orchestrators that want to inspect host state without inviting user prompts. Resolution order mirrors ProjectHostProvider.tsx from the AppDeploymentConfiguration UI. Args: --devEnvUrl, --token (opt), --projectRoot (opt, for cache file). Output: { resolutionStatus, finalHostEnvUrl, finalHostEnvId, finalHostEnvName, hostType, pipelinesSolutionVersion, candidates: { existingCustomHosts[], ... }, actionTaken: "none" }.
  • scripts/lib/provision-platform-host.js: Provisions a Power Platform Pipelines Platform Host (PE) via the BAP getOrCreate endpoint. Idempotent: a tenant that already has a PE gets the existing one back (200 + provisioningState=Succeeded); a tenant without one gets it provisioned (202 + lifecycle op poll). Same call make.powerapps.com → Pipelines → Get started makes. Args: --bapToken, --tenantId, --correlationId (opt — defaults to a fresh UUID), --bapBase (opt — defaults to https://api.bap.microsoft.com), --timeoutSec (opt). Output: { status, alreadyExisted, envId, envUrl, envName, region, provisioningState, lifecycleOpId, durationSec, correlationId }. Used by ensure-pipelines-host Phase 4.0.
  • scripts/lib/provision-custom-host.js: Provisions a new Power Platform Pipelines Custom Host via the BAP env-create API with the D365_ProjectHost organization template (template pre-installs the Pipelines app so the env is immediately host-capable). Same template PPAC's New custom host button uses. Args: --bapToken, --tenantId, --displayName, --region (opt), --sku (opt — Sandbox / Trial / Production), --correlationId, --bapBase, --timeoutSec. On 409 capacity errors the helper surfaces errorCode so the caller can offer a SKU fallback (e.g. Sandbox → Trial → Production). Output: { status, envId, envUrl, envName, sku, lifecycleOpId, durationSec, correlationId }. Used by ensure-pipelines-host Phase 4.A.
  • scripts/lib/force-link-environment.js: Force-links an existing deploymentenvironments record (in a Pipelines host env) to take over the source environment's host association. API behind PPAC's "Force Link" button — the documented remediation when creating an environment record fails with "this environment is already associated with another pipelines host". Args: --hostEnvUrl, --token (host-scoped), --deploymentEnvironmentId (the record on the new host). POSTs to /api/data/v9.0/ManageEnvironmentStamp with the GUID in upper-case-in-braces format (HAR-confirmed against supplierportalpipelineshostch.crm17, 2026-05-11). Idempotent: re-running on an already-stamped env is a 204 no-op. Output: { ok, deploymentEnvironmentId, hostEnvUrl, validationStatus, errorCode? }. Used by force-link-environment skill (Pattern 15 auto-fix in deployment-error-catalog.md).
  • scripts/lib/pac-bap-shim.js: PAC-CLI shim for BAP env-list / env-GET. Provides the same data shape that resolve-env-by-id.js and list-tenant-envs.js consume from BAP, but sourced from pac admin list --json instead. Why this exists: the BAP API at api.bap.microsoft.com rejects Az-CLI-acquired tokens in some tenants (verified 2026-04-28: D365DemoTSCE53051106 returns 401 InvalidAuthenticationToken even though the token claims show the right user/tenant/audience). PAC CLI succeeds because it uses a different first-party client ID with implicit BAP grants. The shim is the read-side fallback — enables detection scripts to work in tenants where Az→BAP fails. Exports listTenantEnvs(), resolveEnvById(envId) with the same return shape as the BAP-backed callers.
  • scripts/lib/verify-env-var-values.js: Verifies that environmentvariablevalues records actually landed on a target environment after deploy / import / configure. Read-only — no Dataverse writes. Why this exists: deploy-pipeline Phase 5.2 PATCHes deploymentsettingsjson onto the stage run; the Pipelines handler writes value records as part of the import, BUT it does NOT always write values for every definition — definitions not bound to an mspp_sitesetting (or another consumer the platform recognizes) can land as zero-value on the target even when the stage run reports success. This helper closes the gap. Args: --envUrl, one of (--schemaNames comma-separated OR --settingsFile <path> to derive from deployment-settings.json), --stageLabel (required when reading the settings file), --token (opt). Output: { summary: { landed, missing, mismatched, error }, results: [{ schemaName, status: "landed"|"missing-value-record"|"missing-definition"|"value-mismatch"|"query-error", expected?, value? }] }. Used by deploy-pipeline Phase 7.6.5, import-solution Phase 6b.verify, configure-env-variables Phase 7.
  • scripts/lib/validate-deployment-settings.js: Pre-deploy validator for deployment-settings.json. Classifies each EnvironmentVariables[] entry by valueFormat (kv-uri / kv-resource-id / kv-placeholder / empty / plain-text / invalid-uri) and status (valid / invalid / unknown-type / skipped). When --envUrl is provided, Secret-type entries are validated against canonical Azure Key Vault reference formats. Why this exists: the Power Platform Pipelines handler validates the PATCH at import time, AFTER the stage run has been queued — a bad Secret reference fails the import with "ImportAsHolding failed: The value provided as a secret reference does not match a valid secret reference format" after a potentially-hours-long queue wait. This helper catches the bad reference in sub-second time. Args: --settingsFile, --envUrl (opt — enables Dataverse type lookups), --stageLabel (opt — narrows to a single stage), --token (opt). Output: { summary: { valid, invalid, "unknown-type", skipped }, findings: [{ schemaName, valueFormat, status, value, message, type? }] }. Used by deploy-pipeline Phase 5.1b (pre-PATCH gate). The catalog of canonical Secret formats lives in this helper — do NOT duplicate the regex elsewhere.

Solution Export

  • scripts/lib/export-solution-async.js: Triggers async Dataverse solution export via ExportSolutionAsync and polls asyncoperations until complete (statecode 3 = Succeeded). Args: --envUrl, --solutionName, --managed (true/false), --token (opt). Output: { asyncOperationId, solutionName, managed }.
  • scripts/lib/download-export-data.js: Downloads the solution zip after a successful async export via DownloadSolutionExportData. Decodes the base64 response and writes the zip file to disk. Args: --envUrl, --asyncOperationId, --outputPath, --token (opt). Output: { zipPath, fileSizeBytes }.

Shared References

Shared reference documents live at references/ and are referenced by multiple skills via relative paths (e.g., ../../references/odata-common.md). This avoids duplicating common patterns across skill-specific reference docs and SKILL.md files.

  • odata-common.md: Auth headers, PowerShell token helper, token refresh cadence, HTTP status codes, Dataverse error codes, retry pattern. Used by setup-datamodel and add-sample-data.
  • dataverse-prerequisites.md: PAC CLI auth check (pac env who), Azure CLI token acquisition, API access verification (WhoAmI). Used by setup-datamodel, add-sample-data, setup-solution, export-solution, and import-solution.
  • framework-conventions.md: Supported frameworks, framework → build tool / router / build output / public dir / index HTML mapping, framework detection via package.json, route discovery patterns. Used by create-site and add-seo.
  • datamodel-manifest-schema.md: Schema spec for .datamodel-manifest.json (fields, types, usage). Written by setup-datamodel, read by add-sample-data, validated by validate-datamodel.js.
  • skill-tracking-reference.md: Skill usage tracking instructions — script invocation syntax, skill name mapping table, and YAML format. Referenced by all skills to record usage via update-skill-tracking.js.
  • solution-api-patterns.md: OData body templates for publisher POST, solution POST, AddSolutionComponent, ExportSolutionAsync, DownloadSolutionExportData, ImportSolutionAsync, StageSolution. Also documents .solution-manifest.json format. Used by setup-solution, export-solution, and import-solution.
  • deployment-error-catalog.md: Catalog of 10 known deployment failure patterns (stale manifest, blocked JS, missing websiteRecordId, auth expiry, empty build output, solution missing dependencies, solution timeout, PAC CLI not installed, environment mismatch, duplicate component). Each entry includes root cause, severity, auto-fix availability, and fix procedure. Used by diagnose-deployment.
  • cicd-pipeline-patterns.md: PAC CLI service principal auth syntax; complete ADO azure-pipelines.yml template; complete GitHub Actions deploy.yml template; commented solution export/import blocks; secrets/variables setup tables; manual steps that cannot be automated; Power Platform Pipelines API patterns (HAR-confirmed): host env discovery via RetrieveSetting, deploymentenvironments create + validationstatus poll, deploymentpipelines create, $ref associate source (relative path format), deploymentstages create, RetrieveDeploymentPipelineInfo, stage run create + ValidatePackageAsync (204) + operation poll, deploymentsettingsjson PATCH, DeployPackageAsync, stagerunstatus terminal values, docs/alm/last-pipeline.json and docs/alm/last-deploy.json formats. Used by setup-pipeline and deploy-pipeline.
  • approval-gates.md: Canonical terminology, marker syntax, and catalog of every user-confirmation point ("Approval Gate") across the entire power-pages skill set (12 ALM + 12 non-ALM). Defines six categories (intent / plan / progress / consent / final / pause), an explicit-pairing marker (<!-- gate: skill:phase | category=X | cancel-leaves=Y --> + human > 🚦 Gate (...) block), the cancel-leaves vocabulary, and the seven gate-related lint rules enforced by scripts/lint-skills-alm.js at hard-fail severity: GATE-must-have-marker, GATE-id-must-be-unique, GATE-must-be-in-catalog, GATE-intent-must-call-helper, GATE-cancel-leaves-known-vocab, GATE-prose-block-required (marker must be followed by a 🚦 prose block within 10 lines, outside any code fence), and CATALOG-row-must-have-marker (reverse of GATE-must-be-in-catalog — every kind: gate catalog row must have a SKILL.md marker). §6.1–§6.12 catalogue the ALM skills; §6.13–§6.24 catalogue the 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). report-issue is excluded because its workflow lives in the cross-plugin shared file. New skills must extend §6 in the same PR they introduce an AskUserQuestion — lint will block the PR otherwise.

Skill-specific reference docs (e.g., skills/setup-datamodel/references/odata-api-patterns.md) contain only patterns unique to that skill and point to the shared docs via ${PLUGIN_ROOT}/references/ paths for common content.

MCP Integration

Playwright MCP server for browser automation and live site previews during development.

Template System

Framework templates use __PLACEHOLDER__ tokens (e.g., __SITE_NAME__, __PRIMARY_COLOR__, __BG_COLOR__) that get replaced during site scaffolding. The gitignore file is stored without the dot prefix to avoid git interference in the plugin repo — it gets renamed to .gitignore during scaffolding.

Validation Scripts

create-site/scripts/validate-site.js

Checks generated sites for: required files (package.json, .gitignore, powerpages.config.json), config schema fields ($schema, compiledPath, siteName, defaultLandingPage), build/dev scripts in package.json, unreplaced __PLACEHOLDER__ tokens, git initialization, and src/ directory existence.

setup-datamodel/scripts/validate-datamodel.js

Checks created Dataverse data models by reading .datamodel-manifest.json (written by the setup-datamodel skill during table creation). Queries the Dataverse OData API to verify each table and column in the manifest actually exists in the environment. Gracefully exits 0 on auth errors (doesn't block if token expired) or when no manifest is found (not a data model session).

add-seo/scripts/validate-seo.js

Checks SEO assets added to Power Pages sites: verifies robots.txt exists in public/ with proper User-agent and Sitemap directives, sitemap.xml exists with <urlset> and <loc> entries (no unreplaced placeholders), and index.html has meta description and viewport tags. Only runs validation when at least one SEO file (robots.txt or sitemap.xml) is detected — gracefully exits 0 otherwise to avoid blocking non-SEO sessions.

create-webroles/scripts/validate-webroles.js

Checks that web role YAML files were created in .powerpages-site/web-roles/. Validates each file has required id and name fields and that the id field contains a valid UUID v4 format. Gracefully exits 0 when no .powerpages-site/web-roles/ directory is found (not a web roles session).

integrate-webapi/scripts/validate-webapi-integration.js

Checks that Web API integration code was created for a Power Pages code site: verifies the shared API client (src/shared/powerPagesApi.ts or equivalent) exists, at least one service file exists in src/shared/services/ or src/services/ with /_api/ endpoint references, and corresponding type definition files exist in src/types/. Gracefully exits 0 when no integration files are detected (not an integration session).

setup-auth/scripts/validate-auth.js

Checks that authentication and authorization code was created: verifies auth service (src/services/authService.ts or equivalent) exists with login/logout/getCurrentUser functions and anti-forgery token handling, Power Pages type declarations (src/types/powerPages.d.ts) exist, authorization utilities (src/utils/authorization.ts) exist, and an auth UI component (AuthButton or equivalent) exists. Gracefully exits 0 when no auth files are detected (not an auth session).

setup-solution/scripts/validate-solution.js

Checks that .solution-manifest.json was written with required fields (solution.uniqueName, solution.solutionId, publisher.publisherId, at least one component of type 61). Queries Dataverse OData to confirm the solution actually exists in the environment. Gracefully exits 0 on auth errors or when no manifest is found.

export-solution/scripts/validate-export.js

Checks that a solution zip file was written (*_managed.zip or *_unmanaged.zip pattern). Verifies file size > 1000 bytes and that Solution.xml is present inside the zip (via unzip -l). Gracefully exits 0 when no solution zip is found.

import-solution/scripts/validate-import.js

Checks docs/alm/last-import.json marker for required fields (solutionName, targetEnvironment, importedAt). Blocks if all components failed to import (0 success + N failures). Gracefully exits 0 when no import marker is found.

setup-pipeline/scripts/validate-pipeline.js

Checks for docs/alm/last-pipeline.json (Power Platform Pipelines path) — validates required fields: pipelineId, hostEnvUrl, sourceDeploymentEnvironmentId, non-empty stages[], and each stage has stageId + targetDeploymentEnvironmentId. Also confirms docs/pipeline-setup.md was created. Falls back to checking azure-pipelines.yml or .github/workflows/deploy.yml for YAML keys and docs/ci-cd-setup.md (GitHub/ADO future path). Gracefully exits 0 when no pipeline artifacts are found.

deploy-pipeline/scripts/validate-deploy-pipeline.js

Checks docs/alm/last-deploy.json marker for required fields (pipelineId, stageRunId, solutionName, status, deployedAt). Blocks if status === "Failed" — a failed deployment requires investigation before retrying. Gracefully exits 0 when no deploy marker is found (not a deploy-pipeline session).

Skill Development Guide

All skills in this plugin follow a consistent set of patterns. When creating a new skill, follow every convention below to maintain consistency across the plugin.

Phase-Wise Workflow

Every skill is a sequence of phases (typically 5-8): Prerequisites, Discover/Gather, Plan/Review, Implement, Verify (mandatory standalone phase), Deploy/Summarize. Never skip or reorder phases.

Task Tracking

Create all tasks upfront at Phase 1 start using TaskCreate (one per phase). Each task needs subject (imperative), activeForm (present continuous for spinner), and description. Mark in_progress when starting, completed when done. Include a progress tracking table at the end of the SKILL.md.

SKILL.md Frontmatter

---
name: <skill-name>
description: >-
  <when to use this skill>
user-invocable: true
argument-hint: <optional>
allowed-tools: Read, Write, Edit, Bash, Glob, Grep, Task, TaskCreate, TaskUpdate, TaskList, AskUserQuestion
model: opus
---

Note: allowed-tools must be a comma-separated list, not JSON array or YAML list syntax. Do not add hooks to skill frontmatter; Power Pages skills register lifecycle hooks centrally.

Plugin Version Check

Every SKILL.md must include the following line immediately after the closing --- of the frontmatter (before the # title):

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

This runs a lightweight check comparing the local plugin version against origin/main and shows an update notice if a newer version is available.

Key Patterns

  • Approval Gates — Every load-bearing AskUserQuestion is an Approval Gate. Pause at minimum after gathering requirements, after presenting a plan, after implementation, and before deployment (Three-Point Approval Pattern). Every skill in this plugin (ALM and non-ALM alike) must (a) catalogue each gate in references/approval-gates.md §6 with a stable gate-id, and (b) mark it in SKILL.md with the explicit-pairing comment <!-- gate: skill:phase | category=<intent|plan|progress|consent|final|pause> | cancel-leaves=<vocab> --> followed by a human-readable > 🚦 **Gate (...)** block. Pure data-gathering prompts (free-text fallbacks, configuration sub-prompts) take a <!-- not-a-gate: <reason> --> comment instead. scripts/lint-skills-alm.js enforces this at hard-fail severity across the whole plugin — there is no warn-only carve-out for any skill class. When you add a new skill that introduces an AskUserQuestion, you must extend references/approval-gates.md §6 with the new gate-id(s) in the same PR; CI will block the PR otherwise. Do not coin alternative terms ("review gate", "approval checkpoint", "manual step" etc.) — the canonical term is Approval Gate.
  • Deployment prompt — Skills that modify site artifacts should end by asking "Ready to deploy?" and invoke /deploy-site if yes.
  • Lifecycle hooks — Hook registration is centralized in hooks/hooks.json; scripts/lib/powerpages-hook-utils.js derives tracked skills from skills/*/SKILL.md and discovers optional scripts/validate*.js validators. Do not define hook registration in individual SKILL.md files.
  • Graceful failure — Track API call results, never auto-rollback, report failures clearly, continue with remaining items.
  • Token refresh — Refresh Azure CLI token every ~20 records / 3-4 tables / ~60 seconds.
  • Git commits — Commit after every significant milestone (each page/component, design foundations, phase completion).
  • Agent spawning — Process sequentially (not parallel), wait for completion, present output for approval.
  • Skill tracking — Every skill must record usage in its final phase via > Reference: ${PLUGIN_ROOT}/references/skill-tracking-reference.md (pointer pattern, not hardcoded command). When adding a new skill, also add its entry to the skill name mapping table in references/skill-tracking-reference.md.
  • Shell-agnostic docs — SKILL.md, agent, and reference files must not embed shell-specific syntax inside shell commands or code blocks. Use ```bash fences (or plain ```) only for cross-platform commands like pac, az, dotnet, and node. Do not use PowerShell cmdlets (Get-ChildItem, Test-Path, New-Item, Get-Content, Remove-Item, ConvertFrom-Json, Invoke-RestMethod, etc.) or PowerShell-only variable syntax inside shell commands/code blocks (e.g., $var = command, $env:...) — prefer <placeholder> angle-bracket style there (e.g., <envUrl>). Repo runtime placeholders used in prose/templates (such as **Initial request:** $ARGUMENTS) are allowed. For filesystem and JSON operations the agent already has first-class tools (Glob, Read, Write, Edit) — describe the intent in prose rather than prescribing a shell command.
  • Dataverse API calls — Use deterministic Node.js scripts (in the skill's scripts/ directory) for Dataverse API queries. Scripts should import getAuthToken and makeRequest from scripts/lib/validation-helpers.js. Never use inline PowerShell Invoke-RestMethod for API calls — scripts are more reliable, testable, and cross-platform.
  • ALM-aware by default — Any skill that creates, modifies, or depends on Dataverse records that belong in a Power Pages site's solution (site components, env var definitions, web roles, site settings, server logic, cloud flow bindings, bot consumers, custom tables/columns, etc.) MUST ensure those records land in the user's solution when .solution-manifest.json exists. Concrete rules:
    • Solution selection — strict resolution order. When a skill or script needs "which solution?" for an AddSolutionComponent call, resolve in this order and stop at the first match:
      1. Explicit --solutionUniqueName CLI arg (or solutionName=… skill argument). Always wins. Used by advanced flows and CI.
      2. .solution-manifest.json in the project root — read solution.uniqueName. This is the default path for nearly every invocation.
      3. No manifest AND no explicit arg:
        • Interactive skill: query Dataverse for unmanaged solutions whose publisher prefix matches the site publisher, present them via AskUserQuestion alongside the option "Run /power-pages:setup-solution first (recommended)" and "Leave in Default (not recommended)". Proceed only after explicit selection.
        • Non-interactive script: exit with a clear error — --solutionUniqueName not provided and no .solution-manifest.json found. Run /power-pages:setup-solution first, or pass --solutionUniqueName. Never silently fall back to Default. Skills must never auto-pick "the first solution that looks relevant" — auto-selection masks misconfigurations (wrong env, wrong branch, wrong project).
    • Component-creation scripts must accept a --solutionUniqueName argument and, when provided, add the created record to that solution via AddSolutionComponent. Test that solutionUniqueName flows through end to end.
    • Skill workflows must read .solution-manifest.json during prerequisite checks and pass the solution's uniqueName to any component-creation script they call. When no manifest is present, the skill should surface that gap to the user (per the resolution order above) rather than silently creating records in Default.
    • Skills that can leave Dataverse artifacts uncovered (e.g. setup-auth writing OAuth secrets as env vars) must end by prompting the user to run /power-pages:setup-solution in sync mode so the discovery pass picks up any newly-created records.
    • New component types added to Power Pages must be reflected in scripts/lib/discover-site-components.js (the single source of truth for site inventory) and, if applicable, in the PPC_TYPE_LABELS enum. Discovery should never silently skip a type.

Planned Skills (Not Yet Implemented)

The following skills are planned but require POC validation before implementation:

Sprint 2 — Needs POC First

  • setup-environments: Blocked by BAP API auth scope (https://service.powerapps.com/) differing from Dataverse token scope — needs POC in personal tenant. Managed env flag + admin assignment also need validation.
  • setup-git-versioning: Blocked pending determination of whether pac pages has a git-config subcommand, or if git integration is portal-only. If no CLI surface exists, this reduces to a guidance doc.
  • configure-secrets: Blocked pending mapping of full API path for Key Vault-backed environment variables (environmentvariablevalues with keyVaultReference JSON) and validation of az keyvault set-policy assignment in same session.

Sprint 3 — Future / Complex

  • setup-approvals: Blocked by the fact that ADO environment approval gates have no create/trigger API — the approval workflow setup requires human interaction in the ADO UI. Power Platform Pipelines approval status (UpdateApprovalStatus) schema is undocumented.
  • setup-pipeline GitHub/ADO paths: Currently "coming soon" stubs. Full implementation spec is at C:\Users\nityagi\OneDrive - Microsoft\Design Documents\Plans\ALM skills for plugin\ado-cicd-skills-guide.md.

Common Review Pitfalls

These patterns have caused repeated PR review feedback. Check for them before submitting changes to skills, validators, or hooks.

  • Phase cross-references break silently — When renumbering or reordering phases in a SKILL.md, also update: references/ docs that mention phase numbers, the Key Decision Points section, and any other files that cross-reference this skill's phases. After any phase reorder, grep for the old phase number across the skill directory and its references.
  • Validators must match the exact constraint — If the rule is "no exports at all", block all module.exports/exports — don't just check if exported names are in an allowlist. If the rule is "try/catch required", verify both try AND catch exist. Re-read the exact constraint from SKILL.md and test the boundary cases.
  • Hook scripts run on every Skill tool use — The PostToolUse hook fires for all tracked skills, so unconditional process.stderr.write creates noise. Gate debug logging behind process.env.DEBUG. Only errors should go to stderr unconditionally.
  • Template placeholders are context-encoded — bare string placeholders render as HTML text, while structured values render as script-safe JSON. Use __JSON_KEY__ for every JavaScript or application/json value, __ATTR_KEY__ for attributes, and reserve __RAW_KEY__ for code-owned trusted markup.
  • Guidance must be consistent within a skill — If one section says "always use raw fetch", a framework-specific table in the same file must not recommend a different HTTP client without qualification. Reviewers will flag contradictions.

Secure Coding Requirements

These requirements apply to scripts, hooks, skills, templates, reports, and documentation.

  • Process execution: Non-constant data from users, files, environment variables, APIs, or CLI output MUST NEVER reach exec/execSync command strings or a child process launched with shell: true. New and changed calls MUST use execFile, execFileSync, spawn, or spawnSync with a fixed executable, an argv array, and shell: false; follow the argv patterns in scripts/lib/pac-bap-shim.js and scripts/lib/telemetry/lib/pac-auth.js instead of adding quoting helpers.
  • Authenticated URL boundary: Introduce one authenticated-URL validator in scripts/lib/validation-helpers.js, then require every caller to reuse it before getAuthToken, an Authorization header, or any authenticated request is created. The validator MUST require HTTPS, reject credentials and unexpected ports, and match the parsed hostname against the Microsoft cloud mappings in CLOUD_TO_API and the approved Dataverse host suffixes; validate redirects and @odata.nextLink values again, and NEVER use substring host checks or local one-off regexes.
  • Untrusted report data: Keep report data as data. Prefer DOM textContent; otherwise use the encoder for the exact HTML text, HTML attribute, JavaScript string, or URL component context, and NEVER put raw values into innerHTML, event-handler attributes, script source, or navigation URLs. Reuse or extend the encoding boundary in scripts/lib/render-template.js and scripts/lib/templates/security-review-report.html rather than adding per-report escaping.
  • Script embedding: Every value embedded in <script>, including strings, MUST pass through JSON.stringify; escape characters that can terminate or alter the script context after serialization. The non-string branch in scripts/lib/render-template.js shows part of this pattern but is not a complete encoder for string placeholders. Quoted __PLACEHOLDER__ substitution is not JSON-safe. Treat filenames, labels, findings, and scanner output as untrusted even when the plugin produced the file.
  • Plugin code resolution: Resolve plugin-owned code from __dirname or the host-provided ${PLUGIN_ROOT} / ${CLAUDE_PLUGIN_ROOT} and fail closed when neither is available. process.cwd() is a project-input location only and MUST NEVER be a fallback for locating plugin scripts, hooks, templates, configuration, or dependencies.
  • Runtime dependencies: Runtime packages MUST use an exact version and a committed lock or equivalent integrity record at the owning package boundary. NEVER fetch and execute @latest, an unpinned package, or installer output during a plugin run; this includes MCP launchers such as scripts/launch-playwright-mcp.js.
  • Archives: Treat archive entry names and metadata as hostile before listing or extraction. Upgrade existing solution-archive entry points, including skills/export-solution/scripts/validate-export.js, to use a shell-free library or fixed executable plus argv, inspect entries first, enforce file/count/size limits, reject absolute paths, traversal, links, and special files, and extract only into a new private temporary directory.
  • Approval guardrails: references/approval-gates.md and scripts/lint-skills-alm.js are the source of truth for plugin Approval Gates, and host tool approvals MUST remain enabled. Documentation and skills MAY recommend narrow command allowlists, but MUST NEVER recommend global permission bypasses or options such as --dangerously-skip-permissions.
  • Telemetry and privacy: Follow the ## Telemetry contract below and minimize every event to its allowlisted operational fields; NEVER collect prompts, report contents, secrets, tokens, full paths, URLs, hostnames, tenant data, or user content. Disclosures MUST accurately state the shipped default, local mirror behavior, and opt-out precedence; telemetry-capable CI jobs MUST set POWER_PLATFORM_SKILLS_TELEMETRY_POWER_PAGES_OPTOUT=1. Edit shared/telemetry/ first, refresh scripts/lib/telemetry/lib in the same change, and NEVER copy another plugin's ikey.json, resolver, instrumentation key, or event stream.
  • Secret temporary files: Prefer stdin and keep secrets out of argv, environment variables, logs, and errors. scripts/store-keyvault-secret.js demonstrates stdin handling, mode 0600, and finally cleanup; it does not yet provide private-directory or exclusive-creation guarantees. When a tool requires a file, it MUST also create a unique private temporary directory and open the secret file exclusively (wx / O_EXCL), then remove both in finally.
  • Security regression tests: Security fixes and security-sensitive code MUST add node:test coverage under scripts/tests/ for inert shell metacharacters, hostile HTML and script-closing text, traversal-style filenames and archive entries, and Windows and POSIX path/process behavior. Tests MUST prove that data remains data and that validation fails closed without placing a working exploit in documentation or fixtures.
  • Private vulnerability handling: Suspected exploitable reports MUST stay in a GitHub private security advisory or another approved private channel until remediation and disclosure are coordinated. Do not paste exploit details, secrets, customer data, or working payloads into public issues, PRs, logs, reports, or test output; public changes should describe the affected class and the guardrail.

Telemetry

This plugin ships 1DS telemetry for skill-run and script-run signals. The canonical shared library lives at the repo-root shared/telemetry/; scripts/lib/telemetry/lib is a physical copy bundled with this plugin so local checkouts and installed plugins do not depend on symlink handling. Zero npm dependencies — nothing to install.

  • scripts/lib/telemetry/lib is copied from the repo-root shared/telemetry/lib — edit shared/telemetry/lib/ first, then refresh this plugin's copy in the same change. The real files next to the copy are ikey.json (this plugin's config) and resolver.js (the resolver contract implementation). Posture: the committed ikey.json ships disabled: false — transmission is enabled for power-pages (the tenant-side Kusto stream + annotation for PagesAIPluginEvent are provisioned). Set disabled: true to hard-off (no POST, no local log) if you need to suppress all telemetry at the source.
  • Region routing (Artemis geo + cloud stamp) lives in scripts/lib/telemetry/region/ and is wired through scripts/lib/telemetry/resolver.js, which implements the resolver contract (resolve({ event, cfg, cloud, configDir }) / isProvisioned(cfg)). The shared dispatcher is routing-agnostic — it auto-discovers resolver.js by convention (sibling of ikey.json) and calls it; the region/ implementation is entirely plugin-owned and never referenced by the shared library.
  • Privacy posture: usage telemetry is default-on. When PAC is signed in, events include the Dataverse organization GUID (orgId) and Entra tenant GUID (tenantId). Power Pages also includes the signed-in user's Entra object ID (eventInfo.aadObjectId) when PAC exposes it; otherwise that field is omitted. The local diagnostic mirror retains the same fields. Events do not include file paths, prompts, tool inputs, site names, Dataverse URLs, credentials, usernames, or hostnames. There is no consent prompt in skills. Users opt out via /power-pages:telemetry off, which stores a per-plugin choice in ~/.power-platform-skills/config.json (telemetry["power-pages"] = "off"). Opting out stops transmission only; the local diagnostic mirror is still written. Re-enable with /power-pages:telemetry on. Automation/CI can disable transmission by setting POWER_PLATFORM_SKILLS_TELEMETRY_POWER_PAGES_OPTOUT=1 (or true); this opt-out has the highest precedence and overrides both a persisted /power-pages:telemetry choice and /power-pages:telemetry on.
  • Strict allowlist: shared/telemetry/lib/events.js enforces exactly the fields listed in the spec. Never add a field to a builder without first adding it to the allowlist and documenting it in the design doc.
  • Fail closed: telemetry code must never change a script's exit code or break a skill run. Emission is fire-and-forget via a detached dispatcher child, so the hook or script returns before the HTTPS POST completes.

See shared/telemetry/README.md for the integration guide.

Maintaining This File

Update when plugin structure or conventions change or you learn something which can be useful for new skills or agents.

Keep this file concise — detailed docs belong in PLUGIN_DEVELOPMENT_GUIDE.md or individual SKILL.md / agent files.