[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M69BV5CF31HJCE2JRKRDF2C2":3,"skill-same:01M69BV5CF31HJCE2JRKRDF2C2":155},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":26,"publishedAt":40,"linkOnly":41,"markdownContent":42,"markdownBody":43,"frontMatter":44,"refName":70,"commitSha":71,"skillDirectory":72,"forksCount":73,"repositoryHtmlUrl":74,"repositoryHomepageUrl":6,"ownerHtmlUrl":75,"ownerBlogUrl":76,"ownerDescription":77,"updatedAt":78,"install":79,"resources":92},"01M69BV5CF31HJCE2JRKRDF2C2","mcs-lab-auditor",null,"Audit Microsoft Copilot Studio labs end-to-end for any event or workshop. Resolves the mcs-labs repo location dynamically (clones\u002Fupdates it as needed), enumerates auditable scopes from the repo's `_events\u002F` and `_workshops\u002F` collections, drives Playwright through every numbered step of each lab in the chosen scope, compares the live UI to the written instructions with an LLM judge, and either files a GitHub issue (one per lab with findings) or appends a clean-pass entry to a local audit log. Use this skill when the user says \"audit the bootcamp\", \"audit a workshop\", \"run the lab auditor\", \"test the mcs-labs labs\", or invokes any `\u002Faudit-*` command from this plugin.","microsoft\u002FBootcampLabTestPlugin","microsoft","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F6154722?v=4","01M698VYDXZ6JW5WH3CRV650R8","Microsoft","01M69BV3S4XDND40KRTA6BGFVQ",1,"MIT","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FBootcampLabTestPlugin\u002Fblob\u002Fmain\u002Fskills\u002Fmcs-lab-auditor\u002FSKILL.md",[18,23],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000008","测试与质量","SCENARIO","使用场景",{"categoryNo":24,"categoryName":25,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发",[27,28,29,30,31,9,32,33,34,35,36,37,38,39],"mcs lab auditor","MCP","github","playwright","test","auth","status","copilot","plugin","copilot studio","microsoft copilot","run","console","2026-11-10 10:12:30",false,"---\nname: mcs-lab-auditor\ndescription: |\n  Audit Microsoft Copilot Studio labs end-to-end for any event or workshop. Resolves the mcs-labs repo location dynamically (clones\u002Fupdates it as needed), enumerates auditable scopes from the repo's `_events\u002F` and `_workshops\u002F` collections, drives Playwright through every numbered step of each lab in the chosen scope, compares the live UI to the written instructions with an LLM judge, and either files a GitHub issue (one per lab with findings) or appends a clean-pass entry to a local audit log. Use this skill when the user says \"audit the bootcamp\", \"audit a workshop\", \"run the lab auditor\", \"test the mcs-labs labs\", or invokes any `\u002Faudit-*` command from this plugin.\nallowed-tools:\n  - Read\n  - Glob\n  - Grep\n  - Write\n  - Edit\n  - Bash(gh issue list:*)\n  - Bash(gh auth status:*)\n  - Bash(gh repo view:*)\n  - PowerShell\n  - AskUserQuestion\n  # Claude Code browser tools (playwright@claude-plugins-official). Copilot CLI\n  # exposes the same @playwright\u002Fmcp actions under its bundled `playwright`\n  # server — see references\u002Fhost-tools.md. Copilot ignores unknown entries.\n  - mcp__plugin_playwright_playwright__browser_navigate\n  - mcp__plugin_playwright_playwright__browser_snapshot\n  - mcp__plugin_playwright_playwright__browser_take_screenshot\n  - mcp__plugin_playwright_playwright__browser_click\n  - mcp__plugin_playwright_playwright__browser_type\n  - mcp__plugin_playwright_playwright__browser_fill_form\n  - mcp__plugin_playwright_playwright__browser_select_option\n  - mcp__plugin_playwright_playwright__browser_press_key\n  - mcp__plugin_playwright_playwright__browser_wait_for\n  - mcp__plugin_playwright_playwright__browser_evaluate\n  - mcp__plugin_playwright_playwright__browser_console_messages\n  - mcp__plugin_playwright_playwright__browser_network_requests\n  - mcp__plugin_playwright_playwright__browser_close\n---\n\n# mcs-lab-auditor (orchestration skill)\n\nYou are auditing Microsoft Copilot Studio bootcamp labs. You run a real browser through each lab's instructions, decide whether what you see matches what the lab says, and produce either a GitHub issue (when something's wrong) or a local log entry (when everything passes).\n\n**Two write paths.** (a) the active instance's lab repo `{repo}` (e.g. `microsoft\u002Fmcs-labs`) — `gh issue create | comment` for lab findings AND `gh pr create` against the lab repo for per-lab fix PRs (one per lab with findings). (b) `microsoft\u002FBootcampLabTestPlugin` — `gh issue create | comment` for **plugin bugs** when the auditor itself is the problem (Playwright limitation, missing reference, unhandled UI pattern) AND `gh pr create` against the plugin repo for mechanical fixes. The auditor's goal is **100% lab coverage** — when a step can't be completed, the orchestrator runs the recovery patterns in `references\u002Fplugin-self-improvement.md` §2 before concluding it's stuck, then files BOTH a lab finding (if the lab is the problem) AND a plugin bug + fix PR (if the plugin is the problem). See `references\u002Fplugin-self-improvement.md` for the full procedure including the cascading-step (high-severity) classification.\n\nThis file is the orchestrator. It loads the reference files below as needed:\n\n- `references\u002Flab-parser-spec.md` — how to convert a lab's markdown into a step tree.\n- `references\u002Flab-resources-spec.md` — Lab Resources discovery + pre-flight scrape of per-event SharePoint config values. Used when a lab references `copilotstudiotraining.sharepoint.com\u002F...\u002FLab-Assets.aspx` (or similar) for connector credentials \u002F endpoint URLs.\n- `references\u002Fplugin-self-improvement.md` — never give up on a lab without recovery attempts; when stuck file bugs and PRs against BOTH the active instance's lab repo `{repo}` (lab side) and `microsoft\u002FBootcampLabTestPlugin` (plugin side) as appropriate; cascading-step failures are high-severity.\n- `references\u002Fplaywright-cookbook.md` — portal sign-in flow, scene-boundary auth probe, tool mapping per step kind, known quirks.\n- `references\u002Fworkshop-redemption.md` — Skillable-style workshop redemption flow.\n- `references\u002Fworkshop-redemption-chatbot.md` — chatbot Adaptive Card workshop redemption flow.\n- `references\u002Fllm-judge-prompts.md` — the per-step judge, the second-pass critique, the action classifier.\n- `references\u002Ffinding-schema.md` — finding record fields, outcome and severity definitions.\n- `references\u002Faudit-log-schema.md` — `audit-history.yml` entry shape.\n\nRead whichever you need before doing the corresponding step. Don't try to keep all of them in your head at once.\n\n## Top-level entry points (called from command files)\n\n| Command | What this skill does |\n|---|---|\n| `\u002Faudit-event [--event \u003Ckey>] [--resume \u003Cid>] [--labs csv] [--no-issue]` | Audit every lab in an event OR workshop. With `--event \u003Ckey>` the scope is pinned; without it, Phase 1.5 Q3a picks one. Generic over every scope in the repo's `_events\u002F` and `_workshops\u002F` collections. |\n| `\u002Faudit-bootcamp [--resume \u003Cid>] [--labs csv] [--no-issue]` | Shortcut for `\u002Faudit-event --event bootcamp`. Audits every bootcamp lab. Skips Q3 and Q3a in the interview. |\n| `\u002Faudit-lab [\u003Cslug>] [--no-issue] [--dry-run]` | Audit a single lab. With `\u003Cslug>`, the slug pins scope. Without, Phase 1.5 Q4 picks one from the **full all-labs catalog** (`lab_metadata.*`), not constrained to any event. |\n| `\u002Faudit-report [\u003Crun-id>]` | Summarize `audit-history.yml`. No browser activity. |\n| `\u002Faudit-account [show\\|redeem\\|clear]` | Manage the cached test account. |\n\n## Run lifecycle (for `\u002Faudit-bootcamp` and `\u002Faudit-lab`)\n\n### Phase 1 — Pre-flight (no browser yet)\n\n1. **Plugin self-version check (best-effort, runs FIRST).** Before anything else, confirm this plugin is current — auditing on a stale copy produces stale findings. Run:\n   ```\n   pwsh -NoProfile -File \"$env:CLAUDE_PLUGIN_ROOT\\scripts\\Test-PluginVersion.ps1\"\n   ```\n   It compares the local `.claude-plugin\u002Fplugin.json` version to the version published on `origin\u002Fmain` of `microsoft\u002FBootcampLabTestPlugin`. Behavior:\n   - `plugin up-to-date (vX.Y.Z)` → continue silently.\n   - `UPDATE AVAILABLE: local vA \u003C published vB …` (exit 10) → **surface the warning to the user** and recommend running `\u002Fplugin` to update before continuing, but do NOT hard-halt — let the user decide (they may be intentionally testing a local build).\n   - `version check skipped (…)` (offline \u002F `gh` unavailable) → note it and continue. The check is best-effort and never blocks a run.\n\n2. **Orchestrator-is-Opus assertion (MANDATORY).** The mcs-lab-auditor orchestrator REQUIRES Opus. The plugin halts at this step if the Claude Code session model is not Opus. Detection: the system env line `You are powered by the model named Opus 4.7` (or any future `Opus X.Y`). Lower-tier orchestration silently degrades the entire audit's reliability — Sonnet and Haiku struggle with recovery patterns, dialog disambiguation, and the long-form per-lab state tracking. Sub-agents (per-UC, per-step judge, critique, static fan-out, issue-filer, fix-PR filer, PR appender) CAN run on lower-tier models — that's the Q5 model-preset question. The orchestrator itself cannot.\n\n   On non-Opus orchestrator, halt with:\n   ```\n   ERROR: mcs-lab-auditor requires the orchestrator to run on Opus.\n   Current session model: \u003Cdetected model>\n   Switch to Opus (e.g. \u002Fmodel in Claude Code) and re-run the \u002Faudit-* command.\n   Lower-tier sub-agents are still supported — see Phase 1.5 Q5 model-preset.\n   ```\n   Do NOT proceed past Phase 1 step 2 on a non-Opus session.\n\n#### Resolve the active lab instance (FIRST, before any repo or portal access)\n\nRun once at the start of every audit\u002Fbuild run:\n\n```\npwsh -NoProfile -File \"$env:CLAUDE_PLUGIN_ROOT\u002Fscripts\u002FResolve-LabInstance.ps1\" -Mode Json -Instance \"\u003C--instance value or empty>\"\n```\n\nParse the JSON and hold these values for the whole run — every sub-skill uses them:\n\n- `{repo}`           = `.repo`           (e.g. `microsoft\u002Fmcs-labs`) — the target for ALL `gh issue` \u002F `gh pr` \u002F `gh repo` calls.\n- `{branch_prefix}`  = `.branch_prefix`  (e.g. `dewain`) — the prefix for ALL created branches. If `.branch_prefix_source` is `unresolved`, do NOT create any branch; halt and ask the user to set `branch_prefix` in their `lab-instances.yml` or authenticate `gh`.\n- `{clone_url}` \u002F candidates are consumed by `Resolve-LabRepo.ps1 -Instance` (next step).\n\nThen resolve the repo, passing the instance through:\n\n```\npwsh -NoProfile -File \"$env:CLAUDE_PLUGIN_ROOT\u002Fscripts\u002FResolve-LabRepo.ps1\" -Mode Status -Instance \"\u003C--instance value or empty>\"\n```\n\nReplace the old hardcoded `gh repo view microsoft\u002Fmcs-labs …` permission check with `gh repo view {repo} …`.\n\n#### Materialize the active portal\n\nWrite the resolved `.portal` object to `runtime\u002Faccount\u002Factive-portal.yml` (create the dir if needed). All redemption flows read THIS file, never `config\u002Fworkshop.yml` directly. For the default `mcs-labs` instance this file is a copy of `config\u002Fworkshop.yml`, so redemption behaves identically.\n\n3. **Resolve the plugin directory and the mcs-labs repo (NO hard-coded paths).**\n   - The plugin's own files are reached via the `${CLAUDE_PLUGIN_ROOT}` environment variable Claude Code sets for this plugin — never a hard-coded `C:\\Users\\...\\.claude\\plugins\\...` path. All script invocations below use `\"$env:CLAUDE_PLUGIN_ROOT\\scripts\\\u003Cname>.ps1\"`.\n   - The mcs-labs repo location is **resolved, not assumed**. Run:\n     ```\n     pwsh -NoProfile -File \"$env:CLAUDE_PLUGIN_ROOT\\scripts\\Resolve-LabRepo.ps1\" -Mode Path -Instance \"\u003C--instance value or empty>\"\n     ```\n     This finds the repo (via `$env:MCS_LABS_REPO`, the `judge-config.yml.build.registration.mcs_labs_repo_path_candidates`, or a built-in candidate list), **clones `microsoft\u002Fmcs-labs` into `%USERPROFILE%\\.mcs-lab-auditor\\mcs-labs` if no local copy exists**, and **pins the working tree to `origin\u002Fmain`** (it checks out `origin\u002Fmain` even if the clone was left on a stale feature branch — a clean tree only; a dirty tree is left untouched and surfaced loudly). It prints the resolved absolute path. Capture that path as `$REPO` and use it for every subsequent read of `_labs\u002F`, `_events\u002F`, `_workshops\u002F`, `_data\u002F`. On `--dry-run` or static-only runs you may pass `-NoPull` to skip the fetch. If resolution fails (clone error, no git), halt with the script's error message.\n   - **ASSERT the audited revision is `origin\u002Fmain` (MANDATORY unless `-NoPull`).** A lab audit is only meaningful against the *current* instructions. After capturing `$REPO`, confirm the working tree is exactly `origin\u002Fmain`:\n     ```\n     pwsh -NoProfile -Command \"git -C '$REPO' fetch --quiet origin; if ((git -C '$REPO' rev-parse HEAD) -ne (git -C '$REPO' rev-parse origin\u002Fmain)) { 'MISMATCH' } else { 'ok' }\"\n     ```\n     If this prints `MISMATCH` (the clone is on a stale branch, behind `origin\u002Fmain`, or has uncommitted changes blocking the pin), **halt** with a clear message that names the diverging-commit count and tells the user to commit\u002Fstash and re-run — do NOT proceed to parse labs against non-`origin\u002Fmain` content. Record the audited commit SHA (`git -C $REPO rev-parse --short origin\u002Fmain`) in `manifest.yml` so every finding is traceable to an exact instruction revision, and reference that SHA in issue\u002FPR bodies. (Auditing a stale branch silently produces findings that are already fixed on `main` — issue #41.)\n\n4. **Load configs**:\n   - `runtime\u002Faccount\u002Factive-portal.yml`\n   - `config\u002Fjudge-config.yml`\n\n5. **Check `gh` auth**:\n   ```\n   gh auth status\n   gh repo view {repo} --json viewerPermission\n   ```\n   If either fails, halt with a clear message before doing anything else.\n\n6. **Enumerate the auditable scopes (events AND workshops) AND the active scope's lab list.** The source of truth is the repo's two Jekyll collections, NOT the legacy `lab-config.yml.event_configs` table (which has drifted out of sync — it is missing newer workshops like `agent-in-a-day` and the academy series and still lists an obsolete `mcs-in-a-day-v2`). Use the legacy table only as a last-resort fallback when the collections are unreadable.\n   - Run:\n     ```\n     pwsh -NoProfile -File \"$env:CLAUDE_PLUGIN_ROOT\\scripts\\Get-EventCatalog.ps1\" -RepoRoot \"$REPO\" -Json\n     ```\n     This scans `$REPO\\_events\\*.md` (`type: event`) and `$REPO\\_workshops\\*.md` (`type: workshop`) and returns, per scope: `{ type, id, title, description, order, external, external_url, repository, auditable, labs[] }`. `labs[]` is the ordered slug list from each scope's front-matter `labs:` array. When a richer `$REPO\\_data\\agendas\\\u003Cid>.yml` exists, its `schedule[]` entries with `type: lab` override the order\u002Flabels — but the slug set is the same.\n     - **Events** are formal curated experiences (bootcamp, the buildathons). **Workshops** are less formal \u002F on-demand (azure-ai-workshop, mcs-in-a-day, agent-in-a-day, the agent-academy series).\n     - `external: true` scopes (e.g. the agent-academy workshops) host their labs in another repo (`repository`), so `auditable` is false — list them for awareness in the picker but never try to drive them; if one is chosen, explain it can't be audited locally and point at its `external_url`.\n   - Build the **all-labs catalog** for single-lab scope (Phase 1.5 Q4): `{slug → { title, … }}` from `lab_metadata.*` in `$REPO\\_data\\lab-config.yml`, with each `_labs\u002F\u003Cslug>.md` front-matter `title:` as a fallback. (`lab_metadata` is still maintained per-lab, so it remains the single-lab picker source.)\n   - Determine which scope drives the active run:\n     - `\u002Faudit-bootcamp` → scope = the `bootcamp` event (pinned).\n     - `\u002Faudit-event` → scope = the event\u002Fworkshop named by `--event \u003Ckey>` if passed; otherwise resolved by Phase 1.5 Q3a (scope picker). `--event` matches an `id` from the catalog regardless of whether it is an event or a workshop.\n     - `\u002Faudit-lab \u003Cslug>` → scope is a single slug; for display, use the first catalog entry whose `labs[]` contains the slug, or `none` if the slug belongs to no scope.\n   - For the active scope (whole event\u002Fworkshop, `--labs csv` subset, or single slug), confirm `$REPO\\_labs\\\u003Cslug>.md` exists for every chosen slug. If not, record `status: skipped, reason: lab_file_missing` and continue with the next slug — never abort the whole run because one lab is missing.\n\n7. **Run-start account prompt** (see Phase 1.5 below). On `--dry-run`, skip this — `--dry-run` only exercises the parser and writes `steps.json` per lab.\n\n8. **Create the run directory**:\n   ```\n   $run_id = (Get-Date -Format \"yyyy-MM-ddTHHmmZ\") + \"-\" + (-join ((1..4) | % { '{0:x}' -f (Get-Random -Maximum 16) }))\n   $run_dir = \"runtime\u002Fruns\u002F$run_id\"\n   ```\n   Initialize `manifest.yml` with the planned lab list, all `status: pending`, and the run start timestamp.\n\n#### Browser-MCP preflight (before any interactive step)\n\nThe interactive phase needs a Playwright MCP. Tool names differ per host — see\n[`references\u002Fhost-tools.md`](references\u002Fhost-tools.md). Before the first browser\naction:\n\n1. Check your available tools for the Playwright `browser_*` actions (Claude:\n   `mcp__plugin_playwright_playwright__*`; Copilot: the bundled `playwright`\n   server). \n2. **If present** → proceed with the interactive phase, calling each action by\n   its host-qualified name.\n3. **If absent** → do NOT call a browser tool. Fall back to `--static-only` for\n   this run and tell the user how to enable the browser for their host:\n   - Claude Code: enable the `playwright@claude-plugins-official` MCP plugin.\n   - Copilot CLI: the plugin bundles a `playwright` MCP (`.github\u002Fmcp.json`); it\n     needs `npx` + network on first use. Run `copilot mcp list` to confirm it\n     loaded.\n\n### Phase 1.5 — Run-start interview (MANDATORY)\n\nBefore any Playwright activity, run an interactive interview to confirm the\nscope of the run. The interview is a series of `AskUserQuestion` calls. Each\nquestion is **skipped only when a CLI flag has already provided the answer**\n— never silently default away a question the user hasn't answered, since\nsilent defaults have caused real audit runs to execute against the wrong\ntenant or to ship a doc-only sweep when the user expected a live audit.\n\nThe interview questions, in order (Q1, Q2, Q2a model preset, Q3 scope, Q3a event, Q4 lab):\n\n#### Q1. Account — which test account?\n\n**This question is asked every run unless `account_prompt_mode` says otherwise.** The plugin never silently chooses between \"cached\" and \"redeem new\" — the user picks.\n\nGoverned by `judge-config.yml.execution.account_prompt_mode`:\n\n- `always` (default): always ask, even if cache is valid. Forces the user to opt in.\n- `only_if_expired`: skip only if cached `expires_at` is in the future.\n- `only_if_missing`: skip only if no cached account exists at all. **Not recommended.**\n\nWhen asking, use `AskUserQuestion`. The options shown are **conditional on cache state**:\n\n| Cache state                                                          | Options shown                                                                                                                                                                  |\n| -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| No cache (`runtime\u002Faccount\u002Faccount.meta.json` missing)               | `Redeem a new workshop code` (will prompt for the code), `Abort the run`                                                                                                       |\n| Cache present but `workshop_code.enc` missing (legacy cache)         | `[Recommended] Use cached: \u003Cuser_id>`, `Redeem a new workshop code`, `Abort the run`                                                                                            |\n| Cache present AND `workshop_code.enc` present (full cache)           | `[Recommended] Use cached: \u003Cuser_id>`, `Redeem a new user from the cached workshop code` (no code prompt), `Redeem a new workshop code` (prompt for a different code), `Abort` |\n\nOption semantics:\n\n- **`Use cached: \u003Cuser_id>`** — Reuse the existing DPAPI-cached credentials. **No workshop code prompt** — the user already exists, there is no need to redeem again. Description: `Cached \u003Crelative-time> ago. Expires \u003Cexpires_at or \"unknown\">.`\n- **`Redeem a new user from the cached workshop code`** — Only shown when `workshop_code.enc` (DPAPI-encrypted full code) is present. Decrypts the cached code, prompts for nothing, runs the redemption flow with the cached code. Description: `Issues a fresh workshop user from the same code (hint: \u003Cworkshop_code_hint>...) without re-asking for it.`\n- **`Redeem a new workshop code`** — Discards the cached code (if any) and prompts the user for a fresh code via `AskUserQuestion`. Description: `Prompts for a new workshop code. Use this when the cached code is exhausted or you're switching events.`\n- **`Abort the run`** — Description: `Stop the run before any browser activity.`\n\nIf the user picks one of the two redemption options:\n\n1. Read `runtime\u002Faccount\u002Factive-portal.yml.portal_kind` (`chatbot | skillable | email`).\n2. **Determine where the workshop code comes from**:\n   - If the user picked `Redeem a new user from the cached workshop code`, decrypt `runtime\u002Faccount\u002Fworkshop_code.enc` via DPAPI. No `AskUserQuestion` prompt is needed.\n   - Otherwise, prompt the user via `AskUserQuestion`:\n     - Question: `What is the workshop code?`\n     - Options: `Cancel — use cached account` (if cache exists), `Abort the run`. The user types the actual code via the auto-provided \"Other\" free-text path.\n3. Dispatch the portal-specific redemption flow. Each flow consumes the workshop code (if required by `workshop_code_required: true`) and the deterministic config values from `runtime\u002Faccount\u002Factive-portal.yml.chatbot_account_request_form` and `runtime\u002Faccount\u002Factive-portal.yml.account_new_password_pattern` — **no further `AskUserQuestion` calls during redemption**:\n   - `chatbot` → follow `references\u002Fworkshop-redemption-chatbot.md` (Cards 1–5 + AAD sign-in + first-login password change).\n   - `skillable` (or missing) → follow `references\u002Fworkshop-redemption.md`.\n   - `email` → submit code on the portal, detect \"check your email\", then use `AskUserQuestion` to collect username\u002Fpassword (optional tenant), then continue.\n4. For all kinds, finish with the same AAD sign-in + caching path:\n   - DPAPI-encrypt the **new** password (after the first-login change) into `runtime\u002Faccount\u002Fcredential.enc`.\n   - DPAPI-encrypt the **full workshop code** into `runtime\u002Faccount\u002Fworkshop_code.enc` so the user can later choose `Redeem a new user from the cached workshop code` without retyping it.\n   - Write `runtime\u002Faccount\u002Faccount.meta.json` with `user_id`, `tenant_hint`, `workshop_code_hint` (first 4 chars only), `cached_at`, `expires_at`, `run_id`, `signed_in_at`, `password_changed_on_first_login: true`.\n\nThe redemption flow is responsible for first-run portal setup too: if\n`runtime\u002Faccount\u002Factive-portal.yml.workshop_portal_url` is still\n`REPLACE_ME_ON_FIRST_RUN`, it must prompt for `Workshop portal URL`, validate\n`^https?:\u002F\u002F`, persist the URL to the active instance's source — the user's `%USERPROFILE%\\.mcs-lab-auditor\\lab-instances.yml` inline `portal:` when the instance came from the user file, otherwise `config\u002Fworkshop.yml` — and re-materialize `runtime\u002Faccount\u002Factive-portal.yml`, then continue to the\nworkshop-code prompt.\n\nWhen skipping under a non-default `account_prompt_mode`, record the\ndecision in the run manifest as `account.skip_reason:\nexpires_at_in_future` (or similar) so future readers know the cached\naccount wasn't user-confirmed for this run.\n\n#### Q2. Phase mix — static, interactive, or both?\n\nSkip this question when ANY of:\n- `--static-only` CLI flag was passed (mix = static).\n- `--interactive-only` CLI flag was passed (mix = interactive).\n- `judge-config.yml.execution.require_interactive_phase: false` (mix = static).\n\nWhen asking, use `AskUserQuestion`:\n\n- Question: `Which phase(s) should this run execute?`\n- Options:\n  - `[Recommended] Both static and interactive` — description: `Full audit.\n    Static markdown checks fan out per lab, then interactive Playwright\n    drives the live UI for every lab in the plan. Highest cost, most\n    coverage.`\n  - `Static only` — description: `Markdown parser checks, image-ref\n    resolution, external link probes, prereq sanity. No browser, no\n    tenant state. Fast and cheap.`\n  - `Interactive only` — description: `Assumes a prior static pass\n    produced findings-static.json for each lab. Drives the live UI and\n    merges with the prior static findings at lab completion. Useful when\n    re-verifying a previously-audited lab in a new product release.`\n\nRecord the chosen mix as `execution.phase_mix: static | interactive | both`\nin the run manifest. If \"static only\" or \"interactive only\", also set\n`execution.skipped_interactive: true` or `execution.skipped_static: true`\nrespectively, with `reason: cli_flag` or `reason: interview`.\n\n#### Q2a. Model preset — Optimized per function, or All Opus?\n\nSkip this question when ANY of:\n- `--model-preset \u003Ckey>` CLI flag was passed (`optimized` | `opus` | `custom`).\n- `judge-config.yml.execution.model.preset` is set to a non-`prompt` value (i.e. the config has an explicit default, e.g. set by a previous run).\n- `--resume \u003Crun-id>` was passed (preset is inherited from the prior manifest).\n\nThe orchestrator is always Opus (asserted in Phase 1 step 2). This question only chooses the model for **sub-agents** spawned by the orchestrator: per-UC subagents, the per-step LLM judge, the critique pass, static fan-out subagents, the cross-lab consistency fan-in, the lab parser subagent, and the issue-filer \u002F fix-PR filer \u002F PR appender sub-skills.\n\nWhen asking, use `AskUserQuestion`:\n\n- Question: `Which model preset for sub-agents?`\n- Options:\n  - `[Recommended] Optimized per function` — description: `Each sub-agent uses the cheapest model that handles its job well. Per-UC subagents and the LLM judge on Sonnet 4.6; static fan-out and issue\u002FPR filers on Haiku 4.5. Approx $50 per bootcamp event run. ~85% completion rate.`\n  - `All Opus` — description: `Every sub-agent and judge call runs on Opus 4.7. Maximum reliability, ~$140 per bootcamp event run, ~90% completion rate. Use when finding quality matters more than spend.`\n  - `Custom (edit config)` — description: `Halt before redemption so you can edit config\u002Fjudge-config.yml.execution.model.* per-function overrides, then re-run \u002Faudit-*. No subagents spawn this run.`\n\nRecord the chosen preset as `execution.model.preset: optimized | opus | custom` in the run manifest. The resolved per-function model assignments (see `judge-config.yml.execution.model`) are also frozen into the manifest so a `--resume` produces the same assignments.\n\nOn `Custom`, the orchestrator writes the current resolved assignments back to `judge-config.yml.execution.model.*` as comments (so the user can see the defaults next to their overrides), prints a one-line summary of which keys are configurable, and halts.\n\n#### Q3. Scope — an event or a single lab?\n\nSkip this question when ANY of:\n- `--labs \u003Ccsv>` CLI flag was passed (scope = csv → the driving event\u002Fworkshop is the one whose `labs[]` is a superset of the csv, or `multi` if the csv crosses scopes).\n- The entry point is `\u002Faudit-lab \u003Cslug>` (slug already names the scope; scope = `one` immediately).\n- The entry point is `\u002Faudit-bootcamp` (scope is implicitly `event=bootcamp` and the question is skipped).\n- The entry point is `\u002Faudit-event --event \u003Ckey>` (scope is `event=\u003Ckey>` and the question is skipped — but Q3a still runs if no `--event` was passed).\n- `--resume \u003Crun-id>` was passed (scope is inherited from the prior manifest).\n\nWhen asking, use `AskUserQuestion`:\n\n- Question: `What should this run audit?`\n- Options:\n  - `[Recommended] A whole event or workshop (all its labs)` — description: `Audit every lab in an event or workshop in order, respecting lab_dependencies. Follow-up question picks which one.`\n  - `Just one lab` — description: `Pick a single lab from the full mcs-labs catalog. Follow-up question lists choices.`\n\n#### Q3a. Event\u002Fworkshop picker (only if Q3 = \"A whole event or workshop\")\n\nThe picker source is the **events + workshops catalog** built in Phase 1 step 6 from the repo's `_events\u002F` and `_workshops\u002F` collections — NOT the legacy `event_configs` table. Each entry already carries `type` (`event` | `workshop`), `id`, `title`, `description`, `auditable`, and `labs[]`.\n\n`AskUserQuestion` supports 2–4 options, and the catalog defines ~9 scopes (and growing). Show the most relevant 3 directly plus an `Other` escape hatch that accepts a free-text `id` validated against the catalog:\n\n1. The most-used scope in `runtime\u002Faudit-history.yml` over the last 30 days is the first option, marked `[Recommended]`.\n2. The two next-most-used (or, if no history, the highest-`labs`-count auditable scopes) fill the second and third slots.\n3. `Other event\u002Fworkshop (type the id)` is always option 4 — picks any remaining scope via \"Other\" free-text. Validate the typed id against the catalog `id`s; if invalid, re-ask Q3a with a `(invalid id: \u003Ctyped>)` prefix.\n\n**Label each option with its type** so the user can tell events from workshops, e.g. `[Event] Architecture Bootcamp (11 labs)` \u002F `[Workshop] Agent in a Day (4 labs)`. Render title and description directly from the catalog entry — never hardcode them, so adding a scope only requires a new `_events\u002F` or `_workshops\u002F` file in the repo.\n\nExample option set (regenerate dynamically — do not hardcode):\n\n- Question: `Which event or workshop?`\n- Options:\n  - `[Recommended] [Event] Architecture Bootcamp (11 labs)` — description: the catalog `description` for `bootcamp`.\n  - `[Workshop] Agent in a Day (4 labs)` — description: the catalog `description` for `agent-in-a-day`.\n  - `[Workshop] MCS in a Day (4 labs)` — description: the catalog `description` for `mcs-in-a-day`.\n  - `Other event\u002Fworkshop (type the id)` — description: `Free-text entry. Any id from the catalog, e.g. agent-buildathon-1day, agent-buildathon-1month, azure-ai-workshop.`\n\nIf the chosen scope has `auditable: false` (an `external` workshop such as the agent-academy series, whose labs live in another repo), do not start an audit: explain that it is hosted at the scope's `external_url` \u002F `repository` and cannot be driven locally, then re-ask Q3a for an auditable scope.\n\nOnce the scope is resolved, `scope_labs` is the `labs[]` slug list from its catalog entry (agenda-overridden order when `_data\u002Fagendas\u002F\u003Cid>.yml` exists).\n\n#### Q4. One-lab picker (only if Q3 = \"Just one lab\")\n\nThe picker source for a single-lab scope is **the full all-labs catalog** (`lab_metadata.*`), NOT one event\u002Fworkshop's `labs[]`. The user has every lab in the mcs-labs repo to choose from — including specialized, optional, and external labs that no single event\u002Fworkshop includes.\n\nBecause `AskUserQuestion` supports only 2–4 options per question and the catalog has 60+ labs, ask in two steps grouped by `lab_metadata.\u003Cid>.section`:\n\n**Q4a — Section**. Use `AskUserQuestion`:\n\n- Question: `Which section is the lab in?`\n- Options:\n  - `[Recommended] Core (essential foundations)` — description: `Build Intelligent Agents, Master Variables, Monitor Performance, etc. ~12 labs.`\n  - `Intermediate (practical applications)` — description: `Setup for Success, Ask Me Anything, BYOM, MCS Governance, etc. ~10 labs.`\n  - `Advanced & specialized (autonomous + dev)` — description: `Multi-Agent, Account News, Orchestration, MCP Connectors, Pipelines, etc. ~15 labs.`\n  - `Other (type the slug)` — description: `Free-text entry. Use this for optional\u002Fexternal labs or when you already know the slug.`\n\nMap sections from `lab_metadata.\u003Cid>.section`:\n- \"Core\" = labs where `section: core`.\n- \"Intermediate\" = labs where `section: intermediate`.\n- \"Advanced & specialized\" = labs where `section ∈ {advanced, specialized, optional, external}` (consolidated to keep the question to 4 options).\n\n**Q4b — Lab in the chosen section**. Render the section's labs as the `AskUserQuestion` options, with the lab's title (from `lab_metadata.\u003Cid>.title` or the front-matter as fallback) as the description.\n\nWhen a section has more than 4 labs (most do), paginate by listing the first 3 entries plus an explicit `More labs in \u003Csection> (type the slug)` option as the 4th. The \"Other\" free-text response is validated against `lab_metadata.*.id` — if the typed slug isn't in the catalog, re-ask Q4a with `(invalid lab slug: \u003Ctyped>)` prefix.\n\nThe 3-shown-per-section choice is intentional: most users navigate to a known section, then either see their target in the first 3 or type the slug. Re-asking with full pagination via numeric prefixes was rejected as more clicks than typing.\n\nFor \"Other (type the slug)\" at Q4a, skip Q4b entirely — the free-text response is the chosen slug. Validate it against `lab_metadata.*.id`; if invalid, re-ask Q4a with the validation prefix.\n\nRead the title from `lab_metadata.\u003Cid>.title` (the source of truth in `lab-config.yml`) with the front-matter `title:` of `_labs\u002F\u003Cslug>.md` as a fallback if `lab_metadata` is unavailable at interview time.\n\n#### Recording the interview outcome\n\nAppend all interview answers to the run manifest before Phase 1.7 starts:\n\n```yaml\ninterview:\n  account_choice: cached | redeemed | aborted\n  phase_mix: static | interactive | both\n  model_preset: optimized | opus | custom   # Q2a; see execution.model.resolved below for per-function assignments\n  scope: event | one             # \"event\" → audit a whole event\u002Fworkshop's lab list; \"one\" → audit a single slug from the all-labs catalog\n  scope_type: event | workshop | null  # which collection the chosen scope came from; null when scope == one\n  event: \u003Cscope-id|null>         # the chosen event\u002Fworkshop id; null when scope == one (single-lab runs have no driving scope)\n  scope_labs: [\u003Cslug>, ...]      # the final list driving Phase 1.7 (always 1+ entries)\n  skipped_questions:              # questions short-circuited by CLI flags or by the entry-point command\n    - { question: \"phase_mix\", reason: \"cli_flag: --static-only\" }\n    - { question: \"model_preset\", reason: \"cli_flag: --model-preset optimized\" }\n    - { question: \"event_picker\", reason: \"command: \u002Faudit-bootcamp\" }\n\nexecution:\n  model:\n    preset: optimized | opus | custom\n    resolved:                            # per-function frozen assignments for this run\n      uc_subagent:        sonnet | opus | haiku\n      judge:              sonnet | opus | haiku\n      critique:           sonnet | opus | haiku\n      static_subagent:    sonnet | opus | haiku\n      cross_lab:          sonnet | opus | haiku\n      lab_parser:         sonnet | opus | haiku\n      issue_filer:        sonnet | opus | haiku\n      fix_pr_filer:       sonnet | opus | haiku\n      pr_appender:        sonnet | opus | haiku\n```\n\nAfter the interview, the orchestrator has:\n- A valid signed-in browser context (or `phase_mix: static` so none is needed).\n- A confirmed `account_user_id`.\n- An explicit `phase_mix`.\n- An explicit `model_preset` plus the frozen per-function `execution.model.resolved` map. **Every Agent \u002F subagent spawn from here on MUST pass the resolved model via the tool's `model` parameter.**\n- An explicit lab list for Phase 1.7's plan.\n\nRecord the account itself under `account.user_id` and `account.source:\ncached | redeemed` as before.\n\n### Phase 1.6 — Lab Resources discovery and pre-flight scrape (CONDITIONAL)\n\nAfter Phase 1.5 (so the browser is signed in) but before Phase 1.7, check\nwhether any lab in the planned list references a per-event **Lab\nResources** SharePoint page. The full procedure is in\n`references\u002Flab-resources-spec.md`; the orchestrator-side summary:\n\n1. For each lab in the planned list, parse `_labs\u002F\u003Cslug>.md` for external\n   URLs matching the Lab Resources pattern (see\n   `lab-resources-spec.md` §1). If found, record\n   `lab_metadata.lab_resources_url` in the parsed step tree.\n2. If **any** lab has a Lab Resources URL, perform a single pre-flight\n   scrape: navigate, capture page text via `browser_evaluate`, parse the\n   label-value table per `lab-resources-spec.md` §3, and write the result\n   to `runs\u002F\u003Crun-id>\u002Flab-resources.yml`.\n3. **Skip Phase 1.6 entirely** when `--dry-run` is set OR the run is\n   static-phase-only (no browser session exists in either case).\n4. Take a debug screenshot at `runs\u002F\u003Crun-id>\u002Flab-resources.png` (local-only,\n   never uploaded anywhere).\n5. If the scrape fails (page 401\u002F403\u002F404, parser miss, network error),\n   log the failure, set `lab_resources.status: unavailable` in the run\n   manifest, and continue. Labs that don't depend on Lab Resources are\n   unaffected; labs that do will fall back to user-prompt or\n   `cannot_verify` per `lab-resources-spec.md` §6.\n\n**Security — non-negotiable.** Lab Resources values include real credentials\n(passwords, connection secrets, API keys). The orchestrator and every\ndownstream subagent MUST NOT include any scraped value verbatim in:\nissue bodies, PR descriptions, commit messages, `audit-history.yml`,\n`manifest.yml`, console output, judge prompts, transcripts. Reference\nvalues by key only — e.g. *\"the password from\nlab_resources.labs.setup-for-success.servicenow.password\"*, never the\nliteral. The cache file `runs\u002F\u003Crun-id>\u002Flab-resources.yml` lives in the\nrun directory and is excluded from any rendered finding artifact;\n`runtime\u002F` is gitignored at the repo root. See `lab-resources-spec.md` §4\nfor the full security contract.\n\nAfter Phase 1.6, the orchestrator has the lab-resources cache available\nunder `runs\u002F\u003Crun-id>\u002Flab-resources.yml` (or knows it's `unavailable`).\nPer-lab interactive subagents read from it when a step's text references\na Lab Resources value.\n\n### Phase 1.7 — Plan execution order (fan-out vs serial)\n\nAfter Phase 1.5 but before Phase 2, build the execution plan from\n`judge-config.yml.lab_dependencies` and the planned lab list.\n\n> **The static phase is not a substitute for the interactive phase.**\n> Static checks can spot typos, broken links, missing images, and structural\n> drift. They cannot spot UI drift in the live product — controls that have\n> been renamed, dialogs that have been added, settings that have moved.\n> The interactive phase is what catches the issues that actually break a\n> learner mid-lab. Doing only the static phase is the audit equivalent of\n> running `tsc --noEmit` and calling it tested.\n\nThe behavior is governed by\n`judge-config.yml.execution.require_interactive_phase` (default `true`).\nWhen `true`, the orchestrator MUST run Phase 2 against the signed-in\ntraining account. The only way to skip Phase 2 is to pass the\n`--static-only` CLI flag (or set the config to `false`), and either choice\nis recorded in the run manifest as `execution.skipped_interactive: true,\nreason: \u003Cflag|config>`.\n\n1. **Static phase** is always fully fanned out. Spawn one background\n   subagent per lab in the planned list (capped at\n   `execution.static_fanout_concurrency`). **Pass `model:\n   \u003Cexecution.model.resolved.static_subagent>`** on each `Agent` tool\n   call (typically `haiku` under the `optimized` preset). Each subagent\n   does the markdown-only checks: parser-spec validation, image-ref\n   resolution, external-URL link-check via `WebFetch`, TOC-anchor\n   sanity, prereq sanity, self-consistency between the Use Cases\n   Covered table and the real scene headings. Subagents write their\n   findings as `runs\u002F\u003Crun-id>\u002Flabs\u002F\u003Cslug>\u002Ffindings-static.json` AND a\n   sidecar `runs\u002F\u003Crun-id>\u002Flabs\u002F\u003Cslug>\u002Fscene-fingerprints.json` (shape\n   hash, identifier tokens, raw-excerpt line numbers — see\n   `references\u002Fcross-lab-consistency.md` for the schema). No browser,\n   no tenant state, no GitHub writes.\n\n   The lab-parser pass that produces `steps.json` for each lab spawns a\n   separate subagent with `model:\n   \u003Cexecution.model.resolved.lab_parser>` (typically `sonnet` — the\n   parser benefits from a stronger model than the static checks).\n\n1a. **Cross-lab consistency fan-in.** After every per-lab static\n    subagent in step 1 has returned, run a single fan-in pass with\n    `model: \u003Cexecution.model.resolved.cross_lab>` (typically `sonnet`)\n    that reads\n    every `scene-fingerprints.json` in scope, groups scenes by shape\n    hash, and emits drift findings for divergent identifier tokens (e.g.\n    `Address 1: State\u002FProvince` in one lab vs `Address1: State or\n    Providence` in a sibling). Full algorithm in\n    `references\u002Fcross-lab-consistency.md`. Output:\n    `runs\u002F\u003Crun-id>\u002Fcross-lab-consistency.json` (summary) plus\n    per-lab drift findings appended to each affected lab's\n    `findings-static.json` (flags: `parser_warning: true`,\n    `cross_lab_drift: true`, severity always `low`).\n\n    For a single-lab run (`scope_labs` size 1), the fan-in reads the\n    most recent prior-run `scene-fingerprints.json` for every other lab\n    in `lab_metadata.*.id` (the full all-labs catalog). Labs that have\n    never been audited contribute nothing — the issue body documents\n    this discovery limit.\n\n    Skip when `--dry-run` is set, OR when `--static-only` AND scope size\n    1 AND no prior runs on disk, OR when\n    `judge-config.yml.consistency.cross_lab_enabled: false`. Record\n    `consistency.cross_lab_status` in the run manifest either way.\n\n2. **Interactive phase** (REQUIRED by default — see the callout above)\n   topologically sorts the planned labs against `lab_dependencies`. Each\n   entry under `lab_dependencies` defines a `chain` (ordered list of slugs\n   that must run serially because each lab's tenant artifacts are read by\n   the next). Independent labs run in parallel — but only up to\n   `execution.fanout_concurrency` browser sessions per training account,\n   since concurrent UI activity in the same tenant can collide (e.g. two\n   labs both renaming the same default agent).\n\n   The default `fanout_concurrency: 1` preserves the legacy strict-serial\n   behavior. Raising it requires either (a) a workshop event whose labs\n   genuinely don't share tenant state, or (b) provisioning one training\n   account per concurrent slot.\n\n3. The orchestrator merges static and interactive findings for each lab\n   into a single `findings.json` at the end of the lab's interactive\n   pass. The static `findings-static.json` is also retained for\n   debugging.\n\n### Phase 2 — Per-lab loop (interactive UI execution via per-UC subagents)\n\nThis phase is the **core deliverable** of the audit. The orchestrator drives\na real browser through every step the lab tells the learner to perform,\nusing the account chosen in Phase 1.5, and compares what the live UI does\nto what the lab markdown says. Skip this phase and you have a documentation\naudit, not a lab audit.\n\n#### Why per-UC subagents (and not per-lab or per-step)\n\nThe interactive phase is the most context-heavy part of the run by far —\neach step generates 2 accessibility snapshots, 1 screenshot, console +\nnetwork logs, and an LLM-judge call. At ~50 steps × 11 labs the\norchestrator's conversation context would overflow long before the audit\ncompleted.\n\n**Granularity choice: per Use Case (the `### Use Case #N:` boundaries in\nthe lab markdown).** Each UC is 5–20 steps, self-contained (\"create a\nlearning assistant\" \u002F \"build a sales assistant\"), and runs as a\n**dedicated subagent**. The orchestrator only ever sees the UC's return\nsummary (counts, status, findings JSON path) — not the per-step\nsnapshots. Per-step granularity is too fine (subagent boot overhead\ndominates and intra-UC state — \"the agent I just created is in the\nleft-nav now\" — would have to be reconstructed each time). Per-lab\ngranularity is too coarse (subagent itself overflows on a 50-step lab).\n\n#### Browser handoff: shared MCP browser process\n\nThe orchestrator signs in **once** in Phase 1.5. The Playwright MCP\nserver keeps the browser process alive across subagent boundaries — a\nper-UC subagent that calls `mcp__plugin_playwright_playwright__*` (or your host's\nbrowser tool — see references\u002Fhost-tools.md) reuses\nthe same browser tab the orchestrator left it in. The subagent's first\nreal step is typically `_browser_navigate` to the URL the UC's first\nparser step expects, then a `_browser_snapshot` to confirm state.\n\nNo re-auth, no session export\u002Fimport, no second sign-in. The session\ncookies are tenant-side, not subagent-side.\n\n#### Per-UC state handoff: `uc-state.yml`\n\nUCs inside the same lab share tenant artifacts (UC#1 creates an agent\nnamed \"Copilot Teacher\"; UC#3 tests that agent; the judge for UC#3 needs\nto know \"Copilot Teacher\" was the chosen name). Each UC subagent writes\n`runs\u002F\u003Crun-id>\u002Flabs\u002F\u003Cslug>\u002Fuc-\u003CN>-state.yml`:\n\n```yaml\nuc_id: usecase-1\nfinished_at: \u003Ciso>\nstatus: complete | error | partial\nctx_vars:             # variables the next UC's judge can use as CTX_VARS\n  agent_name: \"Copilot Teacher\"\n  topic_names: [\"Welcome\", \"PolicyLookup\"]\n  knowledge_sources: [\"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fmicrosoft-365-copilot\u002F\"]\n  solution_name: \"Travel Tools\"     # any artifact name set in this UC\nbrowser_left_at:      # for resumes\n  url: \"https:\u002F\u002Fm365.cloud.microsoft\u002Fchat\u002F...\"\n  scene_completed: \"Share and test your agent\"\nfindings_count:\n  high: 0\n  medium: 0\n  low: 0\n```\n\nThe next UC's subagent reads all prior `uc-*-state.yml` files in the lab\ndir and merges their `ctx_vars` into its per-step judge `CTX_VARS` input\n(see `references\u002Fllm-judge-prompts.md` §A inputs). Resumable: a failed\nUC N can be retried in isolation as long as UC N-1's state file exists.\n\n#### Per-lab orchestration steps (executed by the orchestrator)\n\nFor each lab slug in the interactive-phase execution plan (respecting\ntopological order from Phase 1.7):\n\n1. **Mark the lab `running`** in `manifest.yml`.\n2. **Parse the lab** (`references\u002Flab-parser-spec.md`). Write the step\n   tree to `runs\u002F\u003Crun-id>\u002Flabs\u002F\u003Cslug>\u002Fsteps.json`.\n   - On `--dry-run`, stop here for this lab. Otherwise continue.\n   - If `phase_mix: static` for this run, this whole loop is skipped at\n     the Phase 1.7 plan level — the orchestrator never reaches Phase 2.\n3. **Slice steps.json by use case.** For each use case in\n   `steps.json.use_cases[]`, write\n   `runs\u002F\u003Crun-id>\u002Flabs\u002F\u003Cslug>\u002Fuc-\u003CN>-steps.json` containing only that\n   UC's scenes and steps. This is the subagent's input.\n4. **For each use case in order (serial)**:\n   - **Scene-boundary auth probe** at the start of UC N (cookbook\n     §scene-boundary-auth-probe). If expired, halt; mark lab `error,\n     reason: auth_expired`; tell user how to resume. Do NOT spawn the\n     subagent if auth failed.\n   - **Spawn a per-UC subagent** (background or foreground depending on\n     orchestrator preference — see \"Spawning\" below for the prompt\n     shape). Pass `model: \u003Cmanifest.interview.execution.model.resolved.uc_subagent>`\n     on the `Agent` tool call so the subagent runs on the model selected\n     by Phase 1.5 Q2a (e.g. `sonnet` under `optimized`, `opus` under\n     `opus`). Pass the subagent: the UC's `uc-\u003CN>-steps.json` path, the\n     paths of all prior `uc-*-state.yml` files in this lab dir, the\n     run-id, the lab slug, the lab-level transcript path, the\n     allowed-tools list scoped to `mcp__plugin_playwright_playwright__*` (or your host's\n     browser tool — see references\u002Fhost-tools.md)\n     + `Read` + `Write` + the per-step judge invocation, AND the\n     resolved `judge` + `critique` model values from the manifest so the\n     subagent's internal per-step judge call also runs on the right\n     model.\n   - **Wait for the subagent to return.** Its summary tells the\n     orchestrator: status (complete\u002Ferror\u002Fpartial), findings counts by\n     severity, the UC-state file path. The orchestrator does NOT need\n     the per-step snapshots — those are written to disk by the subagent\n     and only the judge reads them.\n   - **On error**: read the subagent's `uc-\u003CN>-state.yml.status`. If\n     `error, reason: auth_expired`, halt the whole lab. If\n     `error, reason: ui_blocker`, mark the UC failed and decide whether\n     to continue to UC N+1 (default: continue — a blocker in UC#1 may\n     not affect UC#2's independent flow) or halt the lab (when\n     `lab_dependencies` says the next UC depends on this UC's artifacts).\n   - **On partial**: the subagent retried `transient` failures and\n     captured findings, but couldn't finish the UC's scenes. Same\n     handling as `error` based on whether downstream UCs depend on the\n     unfinished state.\n5. **Merge UC findings into the lab's `findings.json`.** Concatenate\n   each `uc-\u003CN>-findings.json` into the single lab-level `findings.json`\n   the issue-filer reads. Merge with the static-phase\n   `findings-static.json` (if `phase_mix: both` or `interactive` with a\n   prior static run on disk).\n6. **End-of-lab disposition** — **always file an issue AND open a fix PR** when findings exist:\n   - If `findings.json` has any finding with `confidence >=\n     judge-config.yml.confidence.min_to_include_in_issue` AND the lab is\n     not in `non_deterministic_lab_slugs` (or `--force-issue` was\n     passed):\n     1. Invoke `mcs-lab-issue-filer` (sub-skill) with `model:\n        \u003Cexecution.model.resolved.issue_filer>` — files (or comments on)\n        the GitHub issue and returns the issue number.\n     2. **Then** invoke `mcs-lab-fix-pr-filer` (sub-skill) with that issue\n        number and `model: \u003Cexecution.model.resolved.fix_pr_filer>` —\n        applies the findings' `suggested_correction` diffs to the lab\n        markdown, copies any `proposed_screenshot_replacement` images\n        into `labs\u002F\u003Cslug>\u002Fimages\u002F`, and ships them as a PR. **PR dedup is\n        scoped to OPEN PRs only:**\n        - If an **open** fix-PR for the slug already exists (same author,\n          mergeable — resolved from Phase 1.4 `existing-state.yml` or a\n          head-prefix `gh pr list` query), the run's commit is **appended**\n          to that PR — no duplicate is opened.\n        - Otherwise (no open PR — including when prior PRs were merged or\n          closed), a **new** PR is opened on a run-unique branch\n          `{branch_prefix}\u002Ffix-\u003Cslug>-content-audit-\u003Crun-id>` from `main`, titled\n          `\u003Cslug>: fix audit findings from #\u003Cissue-number>` with body\n          `Closes #\u003Cissue-number>`. A merged\u002Fclosed prior PR never blocks a\n          new one — each run with fresh findings gets its own PR.\n     3. If screenshots were refreshed and an open fix-PR already exists\n        for the slug, invoke `mcs-lab-pr-appender` (sub-skill) with\n        `model: \u003Cexecution.model.resolved.pr_appender>` for the\n        screenshots-only refresh path.\n   - Otherwise: append a clean entry to `runs\u002F\u003Crun-id>\u002Fclean-labs.yml`.\n   - Either way: append the per-lab summary entry to\n     `runtime\u002Faudit-history.yml` (`references\u002Faudit-log-schema.md`).\n   - Mark lab `done`, `issue_filed`, or `issue_and_pr_filed` in\n     `manifest.yml`.\n   \n   > **Why both an issue AND a PR?** The issue documents the *why* (audit\n   > run, evidence, screenshots, links to run artifacts) so maintainers can\n   > triage confidently. The PR ships the *what* (concrete markdown edits +\n   > screenshot replacements) so fixes are immediately reviewable and\n   > mergeable without copy\u002Fpaste.\n7. **Pause** for `judge-config.yml.execution.min_seconds_between_labs`\n   before the next lab.\n\n#### Spawning: what the per-UC subagent's prompt looks like\n\nThe orchestrator hands each per-UC subagent a self-contained prompt with:\n\n- Lab slug, UC id, run id, account user_id (for context only — the\n  browser is already signed in).\n- Path to `uc-\u003CN>-steps.json` (input).\n- Paths to all `uc-\u003C1..N-1>-state.yml` for prior UCs in this lab\n  (read-only).\n- Path where the subagent must write `uc-\u003CN>-state.yml` and\n  `uc-\u003CN>-findings.json`.\n- The dispatch table from `references\u002Fplaywright-cookbook.md#tool-mapping`.\n- The judge prompt template from `references\u002Fllm-judge-prompts.md#A`.\n- Explicit rules:\n  - **The browser is already signed in and on a URL related to the prior\n    UC (or the lab's landing URL for UC#1).** Navigate as the first\n    step requires; never call sign-in flow.\n  - **Execute the lab by following the written steps and clicking the\n    actual UI controls — NEVER navigate by URL to shortcut a step, and\n    NEVER treat a URL seen in a screenshot as a navigation target\n    (issue #40).** Lab screenshots (`![...](images\u002F*.png)`) illustrate\n    the *expected result*; they are not instructions. The only legitimate\n    `_browser_navigate` calls are the ones the step text explicitly tells\n    the learner to make (e.g. \"go to make.powerapps.com\"). Reaching a\n    later page via a synthesized deep-link hides the very instruction\n    drift the audit exists to catch (a renamed\u002Fmoved\u002Fmissing control), so\n    if the named control can't be found, record a **finding**\n    (expected-vs-actual) instead of working around it with a URL.\n  - **Verify identity on every new first-party surface (issue #39).**\n    Before driving Copilot Studio, Power Apps, the Power Platform admin\n    center, Teams, or Outlook, confirm the page is signed in as the\n    *exact redeemed workshop user* (`account.user_id`), not merely \"signed\n    in\" — the audit browser can silently SSO into a different\n    OS-broker\u002FWindows account. If the surface shows a different user\n    (e.g. Power Apps defaulting to a prior run's `DEV - User XXXX`), halt\n    that UC with `error, reason: account_mismatch` and let the\n    orchestrator resolve it (full logout → re-login as the redeemed user)\n    rather than URL-hacking into the right environment.\n  - **Save snapshots to disk by default** —\n    `_browser_snapshot({filename: \"snapshots\u002F\u003Cstep-id>-before.yml\"})`\n    instead of returning inline. The judge reads from disk. This is the\n    single biggest context-saver for the subagent itself.\n  - **Save screenshots to\n    `runs\u002F\u003Crun-id>\u002Flabs\u002F\u003Cslug>\u002Fscreenshots\u002F\u003Cstep-id>.png`** as before.\n  - **Write `uc-\u003CN>-state.yml` at the end** with all ctx_vars set\n    during the UC.\n  - **Return to the orchestrator** only: status, findings counts,\n    `uc-\u003CN>-state.yml` path. Do NOT echo back snapshots or transcripts.\n  - **Connection-class failures** follow the network-retry policy and\n    if exhausted, halt with `error, reason: network_unstable` so the\n    orchestrator can `AskUserQuestion` the user.\n### Phase 3 — Wrap-up\n\n1. Close the browser (`mcp__plugin_playwright_playwright__browser_close` (or your host's browser tool — see references\u002Fhost-tools.md)).\n2. Print a summary to the user: total labs, count by status, list of filed issue URLs **and PR URLs**, run-id for future `\u002Faudit-report` lookups.\n3. Write `runs\u002F\u003Crun-id>\u002Fmanifest.yml` final state.\n\n## Resume flow (`--resume \u003Crun-id>`)\n\nWhen `--resume \u003Crun-id>` is passed:\n\n1. Read `runs\u002F\u003Crun-id>\u002Fmanifest.yml`.\n2. Determine which labs are `pending`, `running` (interrupted mid-run), or `error`.\n3. For `done`\u002F`issue_filed`\u002F`issue_and_pr_filed`\u002F`skipped` labs: keep as-is.\n4. For `running` or `error` (interrupted mid-lab): resume at the\n   **UC-level**. List `runs\u002F\u003Crun-id>\u002Flabs\u002F\u003Cslug>\u002Fuc-*-state.yml` files;\n   the first UC without a state file is where Phase 2 restarts for this\n   lab. Findings already on disk (`uc-\u003CN>-findings.json` for completed\n   UCs) are preserved; new UCs append. If a UC partially completed\n   (`uc-\u003CN>-state.yml.status: partial`), the orchestrator MAY re-run\n   that UC from scratch (since UC-level idempotency is best-effort —\n   the tenant may already have the agent the UC was supposed to create)\n   or skip ahead based on the partial state. Default behavior is to\n   re-run the partial UC and let its judge mark the \"create\" steps\n   `cannot_verify` if the artifact already exists.\n5. For `pending`: run as a fresh lab.\n6. Phase 1.5 interview under `--resume`: inherit `phase_mix` and\n   `scope_labs` from the prior manifest (skip Q2 and Q3). Re-ask the\n   account question (Q1) unless `account_prompt_mode` permits skipping\n   AND the cached `expires_at` is still in the future. A resume after\n   the cache expired requires fresh credentials, no exceptions. A\n   resume with `account_prompt_mode: always` still re-prompts — the\n   user may have meant to redeem a different account for the resumed run.\n\n## Important rules\n\n- **Run the interactive phase by default.** Static analysis alone is not a\n  complete audit. Skip Phase 2 only when the user has explicitly passed\n  `--static-only` or set `execution.require_interactive_phase: false`. Either\n  case must be recorded in the run manifest.\n- **Always prompt at Phase 1.5 unless explicitly told not to.** Default\n  `account_prompt_mode: always` means every fresh run asks \"use cached vs.\n  redeem new\", regardless of how recently the cache was written. This is\n  the user's safety net against running against the wrong tenant.\n- **Never modify the mcs-labs repo.** Only read its `_labs\u002F` and `_data\u002F` directories.\n- **Never commit, push, branch, or PR.** Issues only.\n- **Never run `copilot-studio-cleanup` or any agent-deletion command** as part of an audit run. Tenant hygiene is the user's responsibility (see [[feedback_no_cleanup_in_test_automation]]).\n- **Never log secrets.** The credential blob is DPAPI-encrypted; passwords never appear in transcripts, audit history, issue bodies, or `manifest.yml`. The first 4 chars of the workshop code may appear as `workshop_code_hint`.\n- **Halt loudly on auth_expired.** Don't try to silently re-auth mid-run — that's how data loss happens.\n- **Trust the critique pass.** If critique downgrades a finding to `pass` or `cannot_verify`, drop it from the issue.\n\n## What to do when stuck\n\n- **Parser couldn't classify a step**: emit a `parser_warning` finding (severity: low) and continue. Don't halt the whole lab.\n- **Playwright timeout on `_browser_wait_for`** (UI took too long but the page is reachable): capture diagnostics, mark the step `transient`, retry once. If still failing, record as a finding with `outcome: broken, severity: high, confidence: 0.6`. This is a UI-side problem, not a connection problem — keep going.\n- **Network \u002F connection error** (DNS failure, `net::ERR_*`, navigation timeout to a page that should resolve quickly, repeated `net::ERR_INTERNET_DISCONNECTED`, or any failed-network entry that recurs across consecutive steps): treat as a **connection class** failure, distinct from a UI timeout. The retry policy is:\n  1. Retry the failing operation up to `execution.network_retry_count` times (default `3`), pausing `execution.network_retry_backoff_seconds` between attempts (default `5`, `10`, `20` — exponential).\n  2. If all retries fail, **halt the lab** (mark `status: paused, reason: network_unstable`) and prompt the user via `AskUserQuestion`:\n     - Question: `Network looks unstable — what should we do?`\n     - Options:\n       - `[Recommended] Retry now — connection is back` — description: `Re-attempts the failing step and continues the lab from where we paused.`\n       - `Wait \u003CN> seconds and retry` — description: `Sleeps for execution.network_wait_seconds (default 120) before retrying. Use this if the outage is ongoing.`\n       - `Skip this lab` — description: `Marks the lab status: error, reason: network_unstable and continues with the next lab in the plan.`\n       - `Abort the run` — description: `Halts the entire run. Resume later with \u002Faudit-bootcamp --resume \u003Crun-id> when the connection is stable.`\n  3. Do NOT silently keep retrying past the cap, and do NOT record a connection-class failure as a lab finding — it's an environment issue, not a lab issue. Lab findings are reserved for things the lab author can fix.\n  4. Record every connection-class pause in `runs\u002F\u003Crun-id>\u002Ftranscript.md` with the timestamp, retry count, and the user's chosen response.\n- **Judge call fails**: retry once with a JSON-only reminder. If still failing, log the step as `error` in the transcript but continue with the next step — don't halt the lab over one bad judge call.\n- **Issue creation fails**: write a clear error to `runs\u002F\u003Crun-id>\u002Flabs\u002F\u003Cslug>\u002Ftranscript.md`, set `status: error, reason: gh_unavailable`, keep the rendered `issue-body.md` for the user to file manually.\n\n## What success looks like\n\nAfter a full bootcamp run:\n- `runtime\u002Faudit-history.yml` has 11 new entries (one per lab).\n- `runtime\u002Fruns\u002F\u003Crun-id>\u002F` contains parsed steps, findings, screenshots, transcripts, and rendered issue bodies for every lab.\n- 0–11 GitHub issues filed (typically 1–4 in practice; many labs pass cleanly).\n- The user has a one-line summary in chat: `Audited 11 labs in \u003Cduration>. 7 pass, 3 filed issues, 1 error (auth expired in \u003Cslug>, resume with \u002Faudit-bootcamp --resume \u003Crun-id>).`\n","# mcs-lab-auditor (orchestration skill)\n\nYou are auditing Microsoft Copilot Studio bootcamp labs. You run a real browser through each lab's instructions, decide whether what you see matches what the lab says, and produce either a GitHub issue (when something's wrong) or a local log entry (when everything passes).\n\n**Two write paths.** (a) the active instance's lab repo `{repo}` (e.g. `microsoft\u002Fmcs-labs`) — `gh issue create | comment` for lab findings AND `gh pr create` against the lab repo for per-lab fix PRs (one per lab with findings). (b) `microsoft\u002FBootcampLabTestPlugin` — `gh issue create | comment` for **plugin bugs** when the auditor itself is the problem (Playwright limitation, missing reference, unhandled UI pattern) AND `gh pr create` against the plugin repo for mechanical fixes. The auditor's goal is **100% lab coverage** — when a step can't be completed, the orchestrator runs the recovery patterns in `references\u002Fplugin-self-improvement.md` §2 before concluding it's stuck, then files BOTH a lab finding (if the lab is the problem) AND a plugin bug + fix PR (if the plugin is the problem). See `references\u002Fplugin-self-improvement.md` for the full procedure including the cascading-step (high-severity) classification.\n\nThis file is the orchestrator. It loads the reference files below as needed:\n\n- `references\u002Flab-parser-spec.md` — how to convert a lab's markdown into a step tree.\n- `references\u002Flab-resources-spec.md` — Lab Resources discovery + pre-flight scrape of per-event SharePoint config values. Used when a lab references `copilotstudiotraining.sharepoint.com\u002F...\u002FLab-Assets.aspx` (or similar) for connector credentials \u002F endpoint URLs.\n- `references\u002Fplugin-self-improvement.md` — never give up on a lab without recovery attempts; when stuck file bugs and PRs against BOTH the active instance's lab repo `{repo}` (lab side) and `microsoft\u002FBootcampLabTestPlugin` (plugin side) as appropriate; cascading-step failures are high-severity.\n- `references\u002Fplaywright-cookbook.md` — portal sign-in flow, scene-boundary auth probe, tool mapping per step kind, known quirks.\n- `references\u002Fworkshop-redemption.md` — Skillable-style workshop redemption flow.\n- `references\u002Fworkshop-redemption-chatbot.md` — chatbot Adaptive Card workshop redemption flow.\n- `references\u002Fllm-judge-prompts.md` — the per-step judge, the second-pass critique, the action classifier.\n- `references\u002Ffinding-schema.md` — finding record fields, outcome and severity definitions.\n- `references\u002Faudit-log-schema.md` — `audit-history.yml` entry shape.\n\nRead whichever you need before doing the corresponding step. Don't try to keep all of them in your head at once.\n\n## Top-level entry points (called from command files)\n\n| Command | What this skill does |\n|---|---|\n| `\u002Faudit-event [--event \u003Ckey>] [--resume \u003Cid>] [--labs csv] [--no-issue]` | Audit every lab in an event OR workshop. With `--event \u003Ckey>` the scope is pinned; without it, Phase 1.5 Q3a picks one. Generic over every scope in the repo's `_events\u002F` and `_workshops\u002F` collections. |\n| `\u002Faudit-bootcamp [--resume \u003Cid>] [--labs csv] [--no-issue]` | Shortcut for `\u002Faudit-event --event bootcamp`. Audits every bootcamp lab. Skips Q3 and Q3a in the interview. |\n| `\u002Faudit-lab [\u003Cslug>] [--no-issue] [--dry-run]` | Audit a single lab. With `\u003Cslug>`, the slug pins scope. Without, Phase 1.5 Q4 picks one from the **full all-labs catalog** (`lab_metadata.*`), not constrained to any event. |\n| `\u002Faudit-report [\u003Crun-id>]` | Summarize `audit-history.yml`. No browser activity. |\n| `\u002Faudit-account [show\\|redeem\\|clear]` | Manage the cached test account. |\n\n## Run lifecycle (for `\u002Faudit-bootcamp` and `\u002Faudit-lab`)\n\n### Phase 1 — Pre-flight (no browser yet)\n\n1. **Plugin self-version check (best-effort, runs FIRST).** Before anything else, confirm this plugin is current — auditing on a stale copy produces stale findings. Run:\n   ```\n   pwsh -NoProfile -File \"$env:CLAUDE_PLUGIN_ROOT\\scripts\\Test-PluginVersion.ps1\"\n   ```\n   It compares the local `.claude-plugin\u002Fplugin.json` version to the version published on `origin\u002Fmain` of `microsoft\u002FBootcampLabTestPlugin`. Behavior:\n   - `plugin up-to-date (vX.Y.Z)` → continue silently.\n   - `UPDATE AVAILABLE: local vA \u003C published vB …` (exit 10) → **surface the warning to the user** and recommend running `\u002Fplugin` to update before continuing, but do NOT hard-halt — let the user decide (they may be intentionally testing a local build).\n   - `version check skipped (…)` (offline \u002F `gh` unavailable) → note it and continue. The check is best-effort and never blocks a run.\n\n2. **Orchestrator-is-Opus assertion (MANDATORY).** The mcs-lab-auditor orchestrator REQUIRES Opus. The plugin halts at this step if the Claude Code session model is not Opus. Detection: the system env line `You are powered by the model named Opus 4.7` (or any future `Opus X.Y`). Lower-tier orchestration silently degrades the entire audit's reliability — Sonnet and Haiku struggle with recovery patterns, dialog disambiguation, and the long-form per-lab state tracking. Sub-agents (per-UC, per-step judge, critique, static fan-out, issue-filer, fix-PR filer, PR appender) CAN run on lower-tier models — that's the Q5 model-preset question. The orchestrator itself cannot.\n\n   On non-Opus orchestrator, halt with:\n   ```\n   ERROR: mcs-lab-auditor requires the orchestrator to run on Opus.\n   Current session model: \u003Cdetected model>\n   Switch to Opus (e.g. \u002Fmodel in Claude Code) and re-run the \u002Faudit-* command.\n   Lower-tier sub-agents are still supported — see Phase 1.5 Q5 model-preset.\n   ```\n   Do NOT proceed past Phase 1 step 2 on a non-Opus session.\n\n#### Resolve the active lab instance (FIRST, before any repo or portal access)\n\nRun once at the start of every audit\u002Fbuild run:\n\n```\npwsh -NoProfile -File \"$env:CLAUDE_PLUGIN_ROOT\u002Fscripts\u002FResolve-LabInstance.ps1\" -Mode Json -Instance \"\u003C--instance value or empty>\"\n```\n\nParse the JSON and hold these values for the whole run — every sub-skill uses them:\n\n- `{repo}`           = `.repo`           (e.g. `microsoft\u002Fmcs-labs`) — the target for ALL `gh issue` \u002F `gh pr` \u002F `gh repo` calls.\n- `{branch_prefix}`  = `.branch_prefix`  (e.g. `dewain`) — the prefix for ALL created branches. If `.branch_prefix_source` is `unresolved`, do NOT create any branch; halt and ask the user to set `branch_prefix` in their `lab-instances.yml` or authenticate `gh`.\n- `{clone_url}` \u002F candidates are consumed by `Resolve-LabRepo.ps1 -Instance` (next step).\n\nThen resolve the repo, passing the instance through:\n\n```\npwsh -NoProfile -File \"$env:CLAUDE_PLUGIN_ROOT\u002Fscripts\u002FResolve-LabRepo.ps1\" -Mode Status -Instance \"\u003C--instance value or empty>\"\n```\n\nReplace the old hardcoded `gh repo view microsoft\u002Fmcs-labs …` permission check with `gh repo view {repo} …`.\n\n#### Materialize the active portal\n\nWrite the resolved `.portal` object to `runtime\u002Faccount\u002Factive-portal.yml` (create the dir if needed). All redemption flows read THIS file, never `config\u002Fworkshop.yml` directly. For the default `mcs-labs` instance this file is a copy of `config\u002Fworkshop.yml`, so redemption behaves identically.\n\n3. **Resolve the plugin directory and the mcs-labs repo (NO hard-coded paths).**\n   - The plugin's own files are reached via the `${CLAUDE_PLUGIN_ROOT}` environment variable Claude Code sets for this plugin — never a hard-coded `C:\\Users\\...\\.claude\\plugins\\...` path. All script invocations below use `\"$env:CLAUDE_PLUGIN_ROOT\\scripts\\\u003Cname>.ps1\"`.\n   - The mcs-labs repo location is **resolved, not assumed**. Run:\n     ```\n     pwsh -NoProfile -File \"$env:CLAUDE_PLUGIN_ROOT\\scripts\\Resolve-LabRepo.ps1\" -Mode Path -Instance \"\u003C--instance value or empty>\"\n     ```\n     This finds the repo (via `$env:MCS_LABS_REPO`, the `judge-config.yml.build.registration.mcs_labs_repo_path_candidates`, or a built-in candidate list), **clones `microsoft\u002Fmcs-labs` into `%USERPROFILE%\\.mcs-lab-auditor\\mcs-labs` if no local copy exists**, and **pins the working tree to `origin\u002Fmain`** (it checks out `origin\u002Fmain` even if the clone was left on a stale feature branch — a clean tree only; a dirty tree is left untouched and surfaced loudly). It prints the resolved absolute path. Capture that path as `$REPO` and use it for every subsequent read of `_labs\u002F`, `_events\u002F`, `_workshops\u002F`, `_data\u002F`. On `--dry-run` or static-only runs you may pass `-NoPull` to skip the fetch. If resolution fails (clone error, no git), halt with the script's error message.\n   - **ASSERT the audited revision is `origin\u002Fmain` (MANDATORY unless `-NoPull`).** A lab audit is only meaningful against the *current* instructions. After capturing `$REPO`, confirm the working tree is exactly `origin\u002Fmain`:\n     ```\n     pwsh -NoProfile -Command \"git -C '$REPO' fetch --quiet origin; if ((git -C '$REPO' rev-parse HEAD) -ne (git -C '$REPO' rev-parse origin\u002Fmain)) { 'MISMATCH' } else { 'ok' }\"\n     ```\n     If this prints `MISMATCH` (the clone is on a stale branch, behind `origin\u002Fmain`, or has uncommitted changes blocking the pin), **halt** with a clear message that names the diverging-commit count and tells the user to commit\u002Fstash and re-run — do NOT proceed to parse labs against non-`origin\u002Fmain` content. Record the audited commit SHA (`git -C $REPO rev-parse --short origin\u002Fmain`) in `manifest.yml` so every finding is traceable to an exact instruction revision, and reference that SHA in issue\u002FPR bodies. (Auditing a stale branch silently produces findings that are already fixed on `main` — issue #41.)\n\n4. **Load configs**:\n   - `runtime\u002Faccount\u002Factive-portal.yml`\n   - `config\u002Fjudge-config.yml`\n\n5. **Check `gh` auth**:\n   ```\n   gh auth status\n   gh repo view {repo} --json viewerPermission\n   ```\n   If either fails, halt with a clear message before doing anything else.\n\n6. **Enumerate the auditable scopes (events AND workshops) AND the active scope's lab list.** The source of truth is the repo's two Jekyll collections, NOT the legacy `lab-config.yml.event_configs` table (which has drifted out of sync — it is missing newer workshops like `agent-in-a-day` and the academy series and still lists an obsolete `mcs-in-a-day-v2`). Use the legacy table only as a last-resort fallback when the collections are unreadable.\n   - Run:\n     ```\n     pwsh -NoProfile -File \"$env:CLAUDE_PLUGIN_ROOT\\scripts\\Get-EventCatalog.ps1\" -RepoRoot \"$REPO\" -Json\n     ```\n     This scans `$REPO\\_events\\*.md` (`type: event`) and `$REPO\\_workshops\\*.md` (`type: workshop`) and returns, per scope: `{ type, id, title, description, order, external, external_url, repository, auditable, labs[] }`. `labs[]` is the ordered slug list from each scope's front-matter `labs:` array. When a richer `$REPO\\_data\\agendas\\\u003Cid>.yml` exists, its `schedule[]` entries with `type: lab` override the order\u002Flabels — but the slug set is the same.\n     - **Events** are formal curated experiences (bootcamp, the buildathons). **Workshops** are less formal \u002F on-demand (azure-ai-workshop, mcs-in-a-day, agent-in-a-day, the agent-academy series).\n     - `external: true` scopes (e.g. the agent-academy workshops) host their labs in another repo (`repository`), so `auditable` is false — list them for awareness in the picker but never try to drive them; if one is chosen, explain it can't be audited locally and point at its `external_url`.\n   - Build the **all-labs catalog** for single-lab scope (Phase 1.5 Q4): `{slug → { title, … }}` from `lab_metadata.*` in `$REPO\\_data\\lab-config.yml`, with each `_labs\u002F\u003Cslug>.md` front-matter `title:` as a fallback. (`lab_metadata` is still maintained per-lab, so it remains the single-lab picker source.)\n   - Determine which scope drives the active run:\n     - `\u002Faudit-bootcamp` → scope = the `bootcamp` event (pinned).\n     - `\u002Faudit-event` → scope = the event\u002Fworkshop named by `--event \u003Ckey>` if passed; otherwise resolved by Phase 1.5 Q3a (scope picker). `--event` matches an `id` from the catalog regardless of whether it is an event or a workshop.\n     - `\u002Faudit-lab \u003Cslug>` → scope is a single slug; for display, use the first catalog entry whose `labs[]` contains the slug, or `none` if the slug belongs to no scope.\n   - For the active scope (whole event\u002Fworkshop, `--labs csv` subset, or single slug), confirm `$REPO\\_labs\\\u003Cslug>.md` exists for every chosen slug. If not, record `status: skipped, reason: lab_file_missing` and continue with the next slug — never abort the whole run because one lab is missing.\n\n7. **Run-start account prompt** (see Phase 1.5 below). On `--dry-run`, skip this — `--dry-run` only exercises the parser and writes `steps.json` per lab.\n\n8. **Create the run directory**:\n   ```\n   $run_id = (Get-Date -Format \"yyyy-MM-ddTHHmmZ\") + \"-\" + (-join ((1..4) | % { '{0:x}' -f (Get-Random -Maximum 16) }))\n   $run_dir = \"runtime\u002Fruns\u002F$run_id\"\n   ```\n   Initialize `manifest.yml` with the planned lab list, all `status: pending`, and the run start timestamp.\n\n#### Browser-MCP preflight (before any interactive step)\n\nThe interactive phase needs a Playwright MCP. Tool names differ per host — see\n[`references\u002Fhost-tools.md`](references\u002Fhost-tools.md). Before the first browser\naction:\n\n1. Check your available tools for the Playwright `browser_*` actions (Claude:\n   `mcp__plugin_playwright_playwright__*`; Copilot: the bundled `playwright`\n   server). \n2. **If present** → proceed with the interactive phase, calling each action by\n   its host-qualified name.\n3. **If absent** → do NOT call a browser tool. Fall back to `--static-only` for\n   this run and tell the user how to enable the browser for their host:\n   - Claude Code: enable the `playwright@claude-plugins-official` MCP plugin.\n   - Copilot CLI: the plugin bundles a `playwright` MCP (`.github\u002Fmcp.json`); it\n     needs `npx` + network on first use. Run `copilot mcp list` to confirm it\n     loaded.\n\n### Phase 1.5 — Run-start interview (MANDATORY)\n\nBefore any Playwright activity, run an interactive interview to confirm the\nscope of the run. The interview is a series of `AskUserQuestion` calls. Each\nquestion is **skipped only when a CLI flag has already provided the answer**\n— never silently default away a question the user hasn't answered, since\nsilent defaults have caused real audit runs to execute against the wrong\ntenant or to ship a doc-only sweep when the user expected a live audit.\n\nThe interview questions, in order (Q1, Q2, Q2a model preset, Q3 scope, Q3a event, Q4 lab):\n\n#### Q1. Account — which test account?\n\n**This question is asked every run unless `account_prompt_mode` says otherwise.** The plugin never silently chooses between \"cached\" and \"redeem new\" — the user picks.\n\nGoverned by `judge-config.yml.execution.account_prompt_mode`:\n\n- `always` (default): always ask, even if cache is valid. Forces the user to opt in.\n- `only_if_expired`: skip only if cached `expires_at` is in the future.\n- `only_if_missing`: skip only if no cached account exists at all. **Not recommended.**\n\nWhen asking, use `AskUserQuestion`. The options shown are **conditional on cache state**:\n\n| Cache state                                                          | Options shown                                                                                                                                                                  |\n| -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| No cache (`runtime\u002Faccount\u002Faccount.meta.json` missing)               | `Redeem a new workshop code` (will prompt for the code), `Abort the run`                                                                                                       |\n| Cache present but `workshop_code.enc` missing (legacy cache)         | `[Recommended] Use cached: \u003Cuser_id>`, `Redeem a new workshop code`, `Abort the run`                                                                                            |\n| Cache present AND `workshop_code.enc` present (full cache)           | `[Recommended] Use cached: \u003Cuser_id>`, `Redeem a new user from the cached workshop code` (no code prompt), `Redeem a new workshop code` (prompt for a different code), `Abort` |\n\nOption semantics:\n\n- **`Use cached: \u003Cuser_id>`** — Reuse the existing DPAPI-cached credentials. **No workshop code prompt** — the user already exists, there is no need to redeem again. Description: `Cached \u003Crelative-time> ago. Expires \u003Cexpires_at or \"unknown\">.`\n- **`Redeem a new user from the cached workshop code`** — Only shown when `workshop_code.enc` (DPAPI-encrypted full code) is present. Decrypts the cached code, prompts for nothing, runs the redemption flow with the cached code. Description: `Issues a fresh workshop user from the same code (hint: \u003Cworkshop_code_hint>...) without re-asking for it.`\n- **`Redeem a new workshop code`** — Discards the cached code (if any) and prompts the user for a fresh code via `AskUserQuestion`. Description: `Prompts for a new workshop code. Use this when the cached code is exhausted or you're switching events.`\n- **`Abort the run`** — Description: `Stop the run before any browser activity.`\n\nIf the user picks one of the two redemption options:\n\n1. Read `runtime\u002Faccount\u002Factive-portal.yml.portal_kind` (`chatbot | skillable | email`).\n2. **Determine where the workshop code comes from**:\n   - If the user picked `Redeem a new user from the cached workshop code`, decrypt `runtime\u002Faccount\u002Fworkshop_code.enc` via DPAPI. No `AskUserQuestion` prompt is needed.\n   - Otherwise, prompt the user via `AskUserQuestion`:\n     - Question: `What is the workshop code?`\n     - Options: `Cancel — use cached account` (if cache exists), `Abort the run`. The user types the actual code via the auto-provided \"Other\" free-text path.\n3. Dispatch the portal-specific redemption flow. Each flow consumes the workshop code (if required by `workshop_code_required: true`) and the deterministic config values from `runtime\u002Faccount\u002Factive-portal.yml.chatbot_account_request_form` and `runtime\u002Faccount\u002Factive-portal.yml.account_new_password_pattern` — **no further `AskUserQuestion` calls during redemption**:\n   - `chatbot` → follow `references\u002Fworkshop-redemption-chatbot.md` (Cards 1–5 + AAD sign-in + first-login password change).\n   - `skillable` (or missing) → follow `references\u002Fworkshop-redemption.md`.\n   - `email` → submit code on the portal, detect \"check your email\", then use `AskUserQuestion` to collect username\u002Fpassword (optional tenant), then continue.\n4. For all kinds, finish with the same AAD sign-in + caching path:\n   - DPAPI-encrypt the **new** password (after the first-login change) into `runtime\u002Faccount\u002Fcredential.enc`.\n   - DPAPI-encrypt the **full workshop code** into `runtime\u002Faccount\u002Fworkshop_code.enc` so the user can later choose `Redeem a new user from the cached workshop code` without retyping it.\n   - Write `runtime\u002Faccount\u002Faccount.meta.json` with `user_id`, `tenant_hint`, `workshop_code_hint` (first 4 chars only), `cached_at`, `expires_at`, `run_id`, `signed_in_at`, `password_changed_on_first_login: true`.\n\nThe redemption flow is responsible for first-run portal setup too: if\n`runtime\u002Faccount\u002Factive-portal.yml.workshop_portal_url` is still\n`REPLACE_ME_ON_FIRST_RUN`, it must prompt for `Workshop portal URL`, validate\n`^https?:\u002F\u002F`, persist the URL to the active instance's source — the user's `%USERPROFILE%\\.mcs-lab-auditor\\lab-instances.yml` inline `portal:` when the instance came from the user file, otherwise `config\u002Fworkshop.yml` — and re-materialize `runtime\u002Faccount\u002Factive-portal.yml`, then continue to the\nworkshop-code prompt.\n\nWhen skipping under a non-default `account_prompt_mode`, record the\ndecision in the run manifest as `account.skip_reason:\nexpires_at_in_future` (or similar) so future readers know the cached\naccount wasn't user-confirmed for this run.\n\n#### Q2. Phase mix — static, interactive, or both?\n\nSkip this question when ANY of:\n- `--static-only` CLI flag was passed (mix = static).\n- `--interactive-only` CLI flag was passed (mix = interactive).\n- `judge-config.yml.execution.require_interactive_phase: false` (mix = static).\n\nWhen asking, use `AskUserQuestion`:\n\n- Question: `Which phase(s) should this run execute?`\n- Options:\n  - `[Recommended] Both static and interactive` — description: `Full audit.\n    Static markdown checks fan out per lab, then interactive Playwright\n    drives the live UI for every lab in the plan. Highest cost, most\n    coverage.`\n  - `Static only` — description: `Markdown parser checks, image-ref\n    resolution, external link probes, prereq sanity. No browser, no\n    tenant state. Fast and cheap.`\n  - `Interactive only` — description: `Assumes a prior static pass\n    produced findings-static.json for each lab. Drives the live UI and\n    merges with the prior static findings at lab completion. Useful when\n    re-verifying a previously-audited lab in a new product release.`\n\nRecord the chosen mix as `execution.phase_mix: static | interactive | both`\nin the run manifest. If \"static only\" or \"interactive only\", also set\n`execution.skipped_interactive: true` or `execution.skipped_static: true`\nrespectively, with `reason: cli_flag` or `reason: interview`.\n\n#### Q2a. Model preset — Optimized per function, or All Opus?\n\nSkip this question when ANY of:\n- `--model-preset \u003Ckey>` CLI flag was passed (`optimized` | `opus` | `custom`).\n- `judge-config.yml.execution.model.preset` is set to a non-`prompt` value (i.e. the config has an explicit default, e.g. set by a previous run).\n- `--resume \u003Crun-id>` was passed (preset is inherited from the prior manifest).\n\nThe orchestrator is always Opus (asserted in Phase 1 step 2). This question only chooses the model for **sub-agents** spawned by the orchestrator: per-UC subagents, the per-step LLM judge, the critique pass, static fan-out subagents, the cross-lab consistency fan-in, the lab parser subagent, and the issue-filer \u002F fix-PR filer \u002F PR appender sub-skills.\n\nWhen asking, use `AskUserQuestion`:\n\n- Question: `Which model preset for sub-agents?`\n- Options:\n  - `[Recommended] Optimized per function` — description: `Each sub-agent uses the cheapest model that handles its job well. Per-UC subagents and the LLM judge on Sonnet 4.6; static fan-out and issue\u002FPR filers on Haiku 4.5. Approx $50 per bootcamp event run. ~85% completion rate.`\n  - `All Opus` — description: `Every sub-agent and judge call runs on Opus 4.7. Maximum reliability, ~$140 per bootcamp event run, ~90% completion rate. Use when finding quality matters more than spend.`\n  - `Custom (edit config)` — description: `Halt before redemption so you can edit config\u002Fjudge-config.yml.execution.model.* per-function overrides, then re-run \u002Faudit-*. No subagents spawn this run.`\n\nRecord the chosen preset as `execution.model.preset: optimized | opus | custom` in the run manifest. The resolved per-function model assignments (see `judge-config.yml.execution.model`) are also frozen into the manifest so a `--resume` produces the same assignments.\n\nOn `Custom`, the orchestrator writes the current resolved assignments back to `judge-config.yml.execution.model.*` as comments (so the user can see the defaults next to their overrides), prints a one-line summary of which keys are configurable, and halts.\n\n#### Q3. Scope — an event or a single lab?\n\nSkip this question when ANY of:\n- `--labs \u003Ccsv>` CLI flag was passed (scope = csv → the driving event\u002Fworkshop is the one whose `labs[]` is a superset of the csv, or `multi` if the csv crosses scopes).\n- The entry point is `\u002Faudit-lab \u003Cslug>` (slug already names the scope; scope = `one` immediately).\n- The entry point is `\u002Faudit-bootcamp` (scope is implicitly `event=bootcamp` and the question is skipped).\n- The entry point is `\u002Faudit-event --event \u003Ckey>` (scope is `event=\u003Ckey>` and the question is skipped — but Q3a still runs if no `--event` was passed).\n- `--resume \u003Crun-id>` was passed (scope is inherited from the prior manifest).\n\nWhen asking, use `AskUserQuestion`:\n\n- Question: `What should this run audit?`\n- Options:\n  - `[Recommended] A whole event or workshop (all its labs)` — description: `Audit every lab in an event or workshop in order, respecting lab_dependencies. Follow-up question picks which one.`\n  - `Just one lab` — description: `Pick a single lab from the full mcs-labs catalog. Follow-up question lists choices.`\n\n#### Q3a. Event\u002Fworkshop picker (only if Q3 = \"A whole event or workshop\")\n\nThe picker source is the **events + workshops catalog** built in Phase 1 step 6 from the repo's `_events\u002F` and `_workshops\u002F` collections — NOT the legacy `event_configs` table. Each entry already carries `type` (`event` | `workshop`), `id`, `title`, `description`, `auditable`, and `labs[]`.\n\n`AskUserQuestion` supports 2–4 options, and the catalog defines ~9 scopes (and growing). Show the most relevant 3 directly plus an `Other` escape hatch that accepts a free-text `id` validated against the catalog:\n\n1. The most-used scope in `runtime\u002Faudit-history.yml` over the last 30 days is the first option, marked `[Recommended]`.\n2. The two next-most-used (or, if no history, the highest-`labs`-count auditable scopes) fill the second and third slots.\n3. `Other event\u002Fworkshop (type the id)` is always option 4 — picks any remaining scope via \"Other\" free-text. Validate the typed id against the catalog `id`s; if invalid, re-ask Q3a with a `(invalid id: \u003Ctyped>)` prefix.\n\n**Label each option with its type** so the user can tell events from workshops, e.g. `[Event] Architecture Bootcamp (11 labs)` \u002F `[Workshop] Agent in a Day (4 labs)`. Render title and description directly from the catalog entry — never hardcode them, so adding a scope only requires a new `_events\u002F` or `_workshops\u002F` file in the repo.\n\nExample option set (regenerate dynamically — do not hardcode):\n\n- Question: `Which event or workshop?`\n- Options:\n  - `[Recommended] [Event] Architecture Bootcamp (11 labs)` — description: the catalog `description` for `bootcamp`.\n  - `[Workshop] Agent in a Day (4 labs)` — description: the catalog `description` for `agent-in-a-day`.\n  - `[Workshop] MCS in a Day (4 labs)` — description: the catalog `description` for `mcs-in-a-day`.\n  - `Other event\u002Fworkshop (type the id)` — description: `Free-text entry. Any id from the catalog, e.g. agent-buildathon-1day, agent-buildathon-1month, azure-ai-workshop.`\n\nIf the chosen scope has `auditable: false` (an `external` workshop such as the agent-academy series, whose labs live in another repo), do not start an audit: explain that it is hosted at the scope's `external_url` \u002F `repository` and cannot be driven locally, then re-ask Q3a for an auditable scope.\n\nOnce the scope is resolved, `scope_labs` is the `labs[]` slug list from its catalog entry (agenda-overridden order when `_data\u002Fagendas\u002F\u003Cid>.yml` exists).\n\n#### Q4. One-lab picker (only if Q3 = \"Just one lab\")\n\nThe picker source for a single-lab scope is **the full all-labs catalog** (`lab_metadata.*`), NOT one event\u002Fworkshop's `labs[]`. The user has every lab in the mcs-labs repo to choose from — including specialized, optional, and external labs that no single event\u002Fworkshop includes.\n\nBecause `AskUserQuestion` supports only 2–4 options per question and the catalog has 60+ labs, ask in two steps grouped by `lab_metadata.\u003Cid>.section`:\n\n**Q4a — Section**. Use `AskUserQuestion`:\n\n- Question: `Which section is the lab in?`\n- Options:\n  - `[Recommended] Core (essential foundations)` — description: `Build Intelligent Agents, Master Variables, Monitor Performance, etc. ~12 labs.`\n  - `Intermediate (practical applications)` — description: `Setup for Success, Ask Me Anything, BYOM, MCS Governance, etc. ~10 labs.`\n  - `Advanced & specialized (autonomous + dev)` — description: `Multi-Agent, Account News, Orchestration, MCP Connectors, Pipelines, etc. ~15 labs.`\n  - `Other (type the slug)` — description: `Free-text entry. Use this for optional\u002Fexternal labs or when you already know the slug.`\n\nMap sections from `lab_metadata.\u003Cid>.section`:\n- \"Core\" = labs where `section: core`.\n- \"Intermediate\" = labs where `section: intermediate`.\n- \"Advanced & specialized\" = labs where `section ∈ {advanced, specialized, optional, external}` (consolidated to keep the question to 4 options).\n\n**Q4b — Lab in the chosen section**. Render the section's labs as the `AskUserQuestion` options, with the lab's title (from `lab_metadata.\u003Cid>.title` or the front-matter as fallback) as the description.\n\nWhen a section has more than 4 labs (most do), paginate by listing the first 3 entries plus an explicit `More labs in \u003Csection> (type the slug)` option as the 4th. The \"Other\" free-text response is validated against `lab_metadata.*.id` — if the typed slug isn't in the catalog, re-ask Q4a with `(invalid lab slug: \u003Ctyped>)` prefix.\n\nThe 3-shown-per-section choice is intentional: most users navigate to a known section, then either see their target in the first 3 or type the slug. Re-asking with full pagination via numeric prefixes was rejected as more clicks than typing.\n\nFor \"Other (type the slug)\" at Q4a, skip Q4b entirely — the free-text response is the chosen slug. Validate it against `lab_metadata.*.id`; if invalid, re-ask Q4a with the validation prefix.\n\nRead the title from `lab_metadata.\u003Cid>.title` (the source of truth in `lab-config.yml`) with the front-matter `title:` of `_labs\u002F\u003Cslug>.md` as a fallback if `lab_metadata` is unavailable at interview time.\n\n#### Recording the interview outcome\n\nAppend all interview answers to the run manifest before Phase 1.7 starts:\n\n```yaml\ninterview:\n  account_choice: cached | redeemed | aborted\n  phase_mix: static | interactive | both\n  model_preset: optimized | opus | custom   # Q2a; see execution.model.resolved below for per-function assignments\n  scope: event | one             # \"event\" → audit a whole event\u002Fworkshop's lab list; \"one\" → audit a single slug from the all-labs catalog\n  scope_type: event | workshop | null  # which collection the chosen scope came from; null when scope == one\n  event: \u003Cscope-id|null>         # the chosen event\u002Fworkshop id; null when scope == one (single-lab runs have no driving scope)\n  scope_labs: [\u003Cslug>, ...]      # the final list driving Phase 1.7 (always 1+ entries)\n  skipped_questions:              # questions short-circuited by CLI flags or by the entry-point command\n    - { question: \"phase_mix\", reason: \"cli_flag: --static-only\" }\n    - { question: \"model_preset\", reason: \"cli_flag: --model-preset optimized\" }\n    - { question: \"event_picker\", reason: \"command: \u002Faudit-bootcamp\" }\n\nexecution:\n  model:\n    preset: optimized | opus | custom\n    resolved:                            # per-function frozen assignments for this run\n      uc_subagent:        sonnet | opus | haiku\n      judge:              sonnet | opus | haiku\n      critique:           sonnet | opus | haiku\n      static_subagent:    sonnet | opus | haiku\n      cross_lab:          sonnet | opus | haiku\n      lab_parser:         sonnet | opus | haiku\n      issue_filer:        sonnet | opus | haiku\n      fix_pr_filer:       sonnet | opus | haiku\n      pr_appender:        sonnet | opus | haiku\n```\n\nAfter the interview, the orchestrator has:\n- A valid signed-in browser context (or `phase_mix: static` so none is needed).\n- A confirmed `account_user_id`.\n- An explicit `phase_mix`.\n- An explicit `model_preset` plus the frozen per-function `execution.model.resolved` map. **Every Agent \u002F subagent spawn from here on MUST pass the resolved model via the tool's `model` parameter.**\n- An explicit lab list for Phase 1.7's plan.\n\nRecord the account itself under `account.user_id` and `account.source:\ncached | redeemed` as before.\n\n### Phase 1.6 — Lab Resources discovery and pre-flight scrape (CONDITIONAL)\n\nAfter Phase 1.5 (so the browser is signed in) but before Phase 1.7, check\nwhether any lab in the planned list references a per-event **Lab\nResources** SharePoint page. The full procedure is in\n`references\u002Flab-resources-spec.md`; the orchestrator-side summary:\n\n1. For each lab in the planned list, parse `_labs\u002F\u003Cslug>.md` for external\n   URLs matching the Lab Resources pattern (see\n   `lab-resources-spec.md` §1). If found, record\n   `lab_metadata.lab_resources_url` in the parsed step tree.\n2. If **any** lab has a Lab Resources URL, perform a single pre-flight\n   scrape: navigate, capture page text via `browser_evaluate`, parse the\n   label-value table per `lab-resources-spec.md` §3, and write the result\n   to `runs\u002F\u003Crun-id>\u002Flab-resources.yml`.\n3. **Skip Phase 1.6 entirely** when `--dry-run` is set OR the run is\n   static-phase-only (no browser session exists in either case).\n4. Take a debug screenshot at `runs\u002F\u003Crun-id>\u002Flab-resources.png` (local-only,\n   never uploaded anywhere).\n5. If the scrape fails (page 401\u002F403\u002F404, parser miss, network error),\n   log the failure, set `lab_resources.status: unavailable` in the run\n   manifest, and continue. Labs that don't depend on Lab Resources are\n   unaffected; labs that do will fall back to user-prompt or\n   `cannot_verify` per `lab-resources-spec.md` §6.\n\n**Security — non-negotiable.** Lab Resources values include real credentials\n(passwords, connection secrets, API keys). The orchestrator and every\ndownstream subagent MUST NOT include any scraped value verbatim in:\nissue bodies, PR descriptions, commit messages, `audit-history.yml`,\n`manifest.yml`, console output, judge prompts, transcripts. Reference\nvalues by key only — e.g. *\"the password from\nlab_resources.labs.setup-for-success.servicenow.password\"*, never the\nliteral. The cache file `runs\u002F\u003Crun-id>\u002Flab-resources.yml` lives in the\nrun directory and is excluded from any rendered finding artifact;\n`runtime\u002F` is gitignored at the repo root. See `lab-resources-spec.md` §4\nfor the full security contract.\n\nAfter Phase 1.6, the orchestrator has the lab-resources cache available\nunder `runs\u002F\u003Crun-id>\u002Flab-resources.yml` (or knows it's `unavailable`).\nPer-lab interactive subagents read from it when a step's text references\na Lab Resources value.\n\n### Phase 1.7 — Plan execution order (fan-out vs serial)\n\nAfter Phase 1.5 but before Phase 2, build the execution plan from\n`judge-config.yml.lab_dependencies` and the planned lab list.\n\n> **The static phase is not a substitute for the interactive phase.**\n> Static checks can spot typos, broken links, missing images, and structural\n> drift. They cannot spot UI drift in the live product — controls that have\n> been renamed, dialogs that have been added, settings that have moved.\n> The interactive phase is what catches the issues that actually break a\n> learner mid-lab. Doing only the static phase is the audit equivalent of\n> running `tsc --noEmit` and calling it tested.\n\nThe behavior is governed by\n`judge-config.yml.execution.require_interactive_phase` (default `true`).\nWhen `true`, the orchestrator MUST run Phase 2 against the signed-in\ntraining account. The only way to skip Phase 2 is to pass the\n`--static-only` CLI flag (or set the config to `false`), and either choice\nis recorded in the run manifest as `execution.skipped_interactive: true,\nreason: \u003Cflag|config>`.\n\n1. **Static phase** is always fully fanned out. Spawn one background\n   subagent per lab in the planned list (capped at\n   `execution.static_fanout_concurrency`). **Pass `model:\n   \u003Cexecution.model.resolved.static_subagent>`** on each `Agent` tool\n   call (typically `haiku` under the `optimized` preset). Each subagent\n   does the markdown-only checks: parser-spec validation, image-ref\n   resolution, external-URL link-check via `WebFetch`, TOC-anchor\n   sanity, prereq sanity, self-consistency between the Use Cases\n   Covered table and the real scene headings. Subagents write their\n   findings as `runs\u002F\u003Crun-id>\u002Flabs\u002F\u003Cslug>\u002Ffindings-static.json` AND a\n   sidecar `runs\u002F\u003Crun-id>\u002Flabs\u002F\u003Cslug>\u002Fscene-fingerprints.json` (shape\n   hash, identifier tokens, raw-excerpt line numbers — see\n   `references\u002Fcross-lab-consistency.md` for the schema). No browser,\n   no tenant state, no GitHub writes.\n\n   The lab-parser pass that produces `steps.json` for each lab spawns a\n   separate subagent with `model:\n   \u003Cexecution.model.resolved.lab_parser>` (typically `sonnet` — the\n   parser benefits from a stronger model than the static checks).\n\n1a. **Cross-lab consistency fan-in.** After every per-lab static\n    subagent in step 1 has returned, run a single fan-in pass with\n    `model: \u003Cexecution.model.resolved.cross_lab>` (typically `sonnet`)\n    that reads\n    every `scene-fingerprints.json` in scope, groups scenes by shape\n    hash, and emits drift findings for divergent identifier tokens (e.g.\n    `Address 1: State\u002FProvince` in one lab vs `Address1: State or\n    Providence` in a sibling). Full algorithm in\n    `references\u002Fcross-lab-consistency.md`. Output:\n    `runs\u002F\u003Crun-id>\u002Fcross-lab-consistency.json` (summary) plus\n    per-lab drift findings appended to each affected lab's\n    `findings-static.json` (flags: `parser_warning: true`,\n    `cross_lab_drift: true`, severity always `low`).\n\n    For a single-lab run (`scope_labs` size 1), the fan-in reads the\n    most recent prior-run `scene-fingerprints.json` for every other lab\n    in `lab_metadata.*.id` (the full all-labs catalog). Labs that have\n    never been audited contribute nothing — the issue body documents\n    this discovery limit.\n\n    Skip when `--dry-run` is set, OR when `--static-only` AND scope size\n    1 AND no prior runs on disk, OR when\n    `judge-config.yml.consistency.cross_lab_enabled: false`. Record\n    `consistency.cross_lab_status` in the run manifest either way.\n\n2. **Interactive phase** (REQUIRED by default — see the callout above)\n   topologically sorts the planned labs against `lab_dependencies`. Each\n   entry under `lab_dependencies` defines a `chain` (ordered list of slugs\n   that must run serially because each lab's tenant artifacts are read by\n   the next). Independent labs run in parallel — but only up to\n   `execution.fanout_concurrency` browser sessions per training account,\n   since concurrent UI activity in the same tenant can collide (e.g. two\n   labs both renaming the same default agent).\n\n   The default `fanout_concurrency: 1` preserves the legacy strict-serial\n   behavior. Raising it requires either (a) a workshop event whose labs\n   genuinely don't share tenant state, or (b) provisioning one training\n   account per concurrent slot.\n\n3. The orchestrator merges static and interactive findings for each lab\n   into a single `findings.json` at the end of the lab's interactive\n   pass. The static `findings-static.json` is also retained for\n   debugging.\n\n### Phase 2 — Per-lab loop (interactive UI execution via per-UC subagents)\n\nThis phase is the **core deliverable** of the audit. The orchestrator drives\na real browser through every step the lab tells the learner to perform,\nusing the account chosen in Phase 1.5, and compares what the live UI does\nto what the lab markdown says. Skip this phase and you have a documentation\naudit, not a lab audit.\n\n#### Why per-UC subagents (and not per-lab or per-step)\n\nThe interactive phase is the most context-heavy part of the run by far —\neach step generates 2 accessibility snapshots, 1 screenshot, console +\nnetwork logs, and an LLM-judge call. At ~50 steps × 11 labs the\norchestrator's conversation context would overflow long before the audit\ncompleted.\n\n**Granularity choice: per Use Case (the `### Use Case #N:` boundaries in\nthe lab markdown).** Each UC is 5–20 steps, self-contained (\"create a\nlearning assistant\" \u002F \"build a sales assistant\"), and runs as a\n**dedicated subagent**. The orchestrator only ever sees the UC's return\nsummary (counts, status, findings JSON path) — not the per-step\nsnapshots. Per-step granularity is too fine (subagent boot overhead\ndominates and intra-UC state — \"the agent I just created is in the\nleft-nav now\" — would have to be reconstructed each time). Per-lab\ngranularity is too coarse (subagent itself overflows on a 50-step lab).\n\n#### Browser handoff: shared MCP browser process\n\nThe orchestrator signs in **once** in Phase 1.5. The Playwright MCP\nserver keeps the browser process alive across subagent boundaries — a\nper-UC subagent that calls `mcp__plugin_playwright_playwright__*` (or your host's\nbrowser tool — see references\u002Fhost-tools.md) reuses\nthe same browser tab the orchestrator left it in. The subagent's first\nreal step is typically `_browser_navigate` to the URL the UC's first\nparser step expects, then a `_browser_snapshot` to confirm state.\n\nNo re-auth, no session export\u002Fimport, no second sign-in. The session\ncookies are tenant-side, not subagent-side.\n\n#### Per-UC state handoff: `uc-state.yml`\n\nUCs inside the same lab share tenant artifacts (UC#1 creates an agent\nnamed \"Copilot Teacher\"; UC#3 tests that agent; the judge for UC#3 needs\nto know \"Copilot Teacher\" was the chosen name). Each UC subagent writes\n`runs\u002F\u003Crun-id>\u002Flabs\u002F\u003Cslug>\u002Fuc-\u003CN>-state.yml`:\n\n```yaml\nuc_id: usecase-1\nfinished_at: \u003Ciso>\nstatus: complete | error | partial\nctx_vars:             # variables the next UC's judge can use as CTX_VARS\n  agent_name: \"Copilot Teacher\"\n  topic_names: [\"Welcome\", \"PolicyLookup\"]\n  knowledge_sources: [\"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fmicrosoft-365-copilot\u002F\"]\n  solution_name: \"Travel Tools\"     # any artifact name set in this UC\nbrowser_left_at:      # for resumes\n  url: \"https:\u002F\u002Fm365.cloud.microsoft\u002Fchat\u002F...\"\n  scene_completed: \"Share and test your agent\"\nfindings_count:\n  high: 0\n  medium: 0\n  low: 0\n```\n\nThe next UC's subagent reads all prior `uc-*-state.yml` files in the lab\ndir and merges their `ctx_vars` into its per-step judge `CTX_VARS` input\n(see `references\u002Fllm-judge-prompts.md` §A inputs). Resumable: a failed\nUC N can be retried in isolation as long as UC N-1's state file exists.\n\n#### Per-lab orchestration steps (executed by the orchestrator)\n\nFor each lab slug in the interactive-phase execution plan (respecting\ntopological order from Phase 1.7):\n\n1. **Mark the lab `running`** in `manifest.yml`.\n2. **Parse the lab** (`references\u002Flab-parser-spec.md`). Write the step\n   tree to `runs\u002F\u003Crun-id>\u002Flabs\u002F\u003Cslug>\u002Fsteps.json`.\n   - On `--dry-run`, stop here for this lab. Otherwise continue.\n   - If `phase_mix: static` for this run, this whole loop is skipped at\n     the Phase 1.7 plan level — the orchestrator never reaches Phase 2.\n3. **Slice steps.json by use case.** For each use case in\n   `steps.json.use_cases[]`, write\n   `runs\u002F\u003Crun-id>\u002Flabs\u002F\u003Cslug>\u002Fuc-\u003CN>-steps.json` containing only that\n   UC's scenes and steps. This is the subagent's input.\n4. **For each use case in order (serial)**:\n   - **Scene-boundary auth probe** at the start of UC N (cookbook\n     §scene-boundary-auth-probe). If expired, halt; mark lab `error,\n     reason: auth_expired`; tell user how to resume. Do NOT spawn the\n     subagent if auth failed.\n   - **Spawn a per-UC subagent** (background or foreground depending on\n     orchestrator preference — see \"Spawning\" below for the prompt\n     shape). Pass `model: \u003Cmanifest.interview.execution.model.resolved.uc_subagent>`\n     on the `Agent` tool call so the subagent runs on the model selected\n     by Phase 1.5 Q2a (e.g. `sonnet` under `optimized`, `opus` under\n     `opus`). Pass the subagent: the UC's `uc-\u003CN>-steps.json` path, the\n     paths of all prior `uc-*-state.yml` files in this lab dir, the\n     run-id, the lab slug, the lab-level transcript path, the\n     allowed-tools list scoped to `mcp__plugin_playwright_playwright__*` (or your host's\n     browser tool — see references\u002Fhost-tools.md)\n     + `Read` + `Write` + the per-step judge invocation, AND the\n     resolved `judge` + `critique` model values from the manifest so the\n     subagent's internal per-step judge call also runs on the right\n     model.\n   - **Wait for the subagent to return.** Its summary tells the\n     orchestrator: status (complete\u002Ferror\u002Fpartial), findings counts by\n     severity, the UC-state file path. The orchestrator does NOT need\n     the per-step snapshots — those are written to disk by the subagent\n     and only the judge reads them.\n   - **On error**: read the subagent's `uc-\u003CN>-state.yml.status`. If\n     `error, reason: auth_expired`, halt the whole lab. If\n     `error, reason: ui_blocker`, mark the UC failed and decide whether\n     to continue to UC N+1 (default: continue — a blocker in UC#1 may\n     not affect UC#2's independent flow) or halt the lab (when\n     `lab_dependencies` says the next UC depends on this UC's artifacts).\n   - **On partial**: the subagent retried `transient` failures and\n     captured findings, but couldn't finish the UC's scenes. Same\n     handling as `error` based on whether downstream UCs depend on the\n     unfinished state.\n5. **Merge UC findings into the lab's `findings.json`.** Concatenate\n   each `uc-\u003CN>-findings.json` into the single lab-level `findings.json`\n   the issue-filer reads. Merge with the static-phase\n   `findings-static.json` (if `phase_mix: both` or `interactive` with a\n   prior static run on disk).\n6. **End-of-lab disposition** — **always file an issue AND open a fix PR** when findings exist:\n   - If `findings.json` has any finding with `confidence >=\n     judge-config.yml.confidence.min_to_include_in_issue` AND the lab is\n     not in `non_deterministic_lab_slugs` (or `--force-issue` was\n     passed):\n     1. Invoke `mcs-lab-issue-filer` (sub-skill) with `model:\n        \u003Cexecution.model.resolved.issue_filer>` — files (or comments on)\n        the GitHub issue and returns the issue number.\n     2. **Then** invoke `mcs-lab-fix-pr-filer` (sub-skill) with that issue\n        number and `model: \u003Cexecution.model.resolved.fix_pr_filer>` —\n        applies the findings' `suggested_correction` diffs to the lab\n        markdown, copies any `proposed_screenshot_replacement` images\n        into `labs\u002F\u003Cslug>\u002Fimages\u002F`, and ships them as a PR. **PR dedup is\n        scoped to OPEN PRs only:**\n        - If an **open** fix-PR for the slug already exists (same author,\n          mergeable — resolved from Phase 1.4 `existing-state.yml` or a\n          head-prefix `gh pr list` query), the run's commit is **appended**\n          to that PR — no duplicate is opened.\n        - Otherwise (no open PR — including when prior PRs were merged or\n          closed), a **new** PR is opened on a run-unique branch\n          `{branch_prefix}\u002Ffix-\u003Cslug>-content-audit-\u003Crun-id>` from `main`, titled\n          `\u003Cslug>: fix audit findings from #\u003Cissue-number>` with body\n          `Closes #\u003Cissue-number>`. A merged\u002Fclosed prior PR never blocks a\n          new one — each run with fresh findings gets its own PR.\n     3. If screenshots were refreshed and an open fix-PR already exists\n        for the slug, invoke `mcs-lab-pr-appender` (sub-skill) with\n        `model: \u003Cexecution.model.resolved.pr_appender>` for the\n        screenshots-only refresh path.\n   - Otherwise: append a clean entry to `runs\u002F\u003Crun-id>\u002Fclean-labs.yml`.\n   - Either way: append the per-lab summary entry to\n     `runtime\u002Faudit-history.yml` (`references\u002Faudit-log-schema.md`).\n   - Mark lab `done`, `issue_filed`, or `issue_and_pr_filed` in\n     `manifest.yml`.\n   \n   > **Why both an issue AND a PR?** The issue documents the *why* (audit\n   > run, evidence, screenshots, links to run artifacts) so maintainers can\n   > triage confidently. The PR ships the *what* (concrete markdown edits +\n   > screenshot replacements) so fixes are immediately reviewable and\n   > mergeable without copy\u002Fpaste.\n7. **Pause** for `judge-config.yml.execution.min_seconds_between_labs`\n   before the next lab.\n\n#### Spawning: what the per-UC subagent's prompt looks like\n\nThe orchestrator hands each per-UC subagent a self-contained prompt with:\n\n- Lab slug, UC id, run id, account user_id (for context only — the\n  browser is already signed in).\n- Path to `uc-\u003CN>-steps.json` (input).\n- Paths to all `uc-\u003C1..N-1>-state.yml` for prior UCs in this lab\n  (read-only).\n- Path where the subagent must write `uc-\u003CN>-state.yml` and\n  `uc-\u003CN>-findings.json`.\n- The dispatch table from `references\u002Fplaywright-cookbook.md#tool-mapping`.\n- The judge prompt template from `references\u002Fllm-judge-prompts.md#A`.\n- Explicit rules:\n  - **The browser is already signed in and on a URL related to the prior\n    UC (or the lab's landing URL for UC#1).** Navigate as the first\n    step requires; never call sign-in flow.\n  - **Execute the lab by following the written steps and clicking the\n    actual UI controls — NEVER navigate by URL to shortcut a step, and\n    NEVER treat a URL seen in a screenshot as a navigation target\n    (issue #40).** Lab screenshots (`![...](images\u002F*.png)`) illustrate\n    the *expected result*; they are not instructions. The only legitimate\n    `_browser_navigate` calls are the ones the step text explicitly tells\n    the learner to make (e.g. \"go to make.powerapps.com\"). Reaching a\n    later page via a synthesized deep-link hides the very instruction\n    drift the audit exists to catch (a renamed\u002Fmoved\u002Fmissing control), so\n    if the named control can't be found, record a **finding**\n    (expected-vs-actual) instead of working around it with a URL.\n  - **Verify identity on every new first-party surface (issue #39).**\n    Before driving Copilot Studio, Power Apps, the Power Platform admin\n    center, Teams, or Outlook, confirm the page is signed in as the\n    *exact redeemed workshop user* (`account.user_id`), not merely \"signed\n    in\" — the audit browser can silently SSO into a different\n    OS-broker\u002FWindows account. If the surface shows a different user\n    (e.g. Power Apps defaulting to a prior run's `DEV - User XXXX`), halt\n    that UC with `error, reason: account_mismatch` and let the\n    orchestrator resolve it (full logout → re-login as the redeemed user)\n    rather than URL-hacking into the right environment.\n  - **Save snapshots to disk by default** —\n    `_browser_snapshot({filename: \"snapshots\u002F\u003Cstep-id>-before.yml\"})`\n    instead of returning inline. The judge reads from disk. This is the\n    single biggest context-saver for the subagent itself.\n  - **Save screenshots to\n    `runs\u002F\u003Crun-id>\u002Flabs\u002F\u003Cslug>\u002Fscreenshots\u002F\u003Cstep-id>.png`** as before.\n  - **Write `uc-\u003CN>-state.yml` at the end** with all ctx_vars set\n    during the UC.\n  - **Return to the orchestrator** only: status, findings counts,\n    `uc-\u003CN>-state.yml` path. Do NOT echo back snapshots or transcripts.\n  - **Connection-class failures** follow the network-retry policy and\n    if exhausted, halt with `error, reason: network_unstable` so the\n    orchestrator can `AskUserQuestion` the user.\n### Phase 3 — Wrap-up\n\n1. Close the browser (`mcp__plugin_playwright_playwright__browser_close` (or your host's browser tool — see references\u002Fhost-tools.md)).\n2. Print a summary to the user: total labs, count by status, list of filed issue URLs **and PR URLs**, run-id for future `\u002Faudit-report` lookups.\n3. Write `runs\u002F\u003Crun-id>\u002Fmanifest.yml` final state.\n\n## Resume flow (`--resume \u003Crun-id>`)\n\nWhen `--resume \u003Crun-id>` is passed:\n\n1. Read `runs\u002F\u003Crun-id>\u002Fmanifest.yml`.\n2. Determine which labs are `pending`, `running` (interrupted mid-run), or `error`.\n3. For `done`\u002F`issue_filed`\u002F`issue_and_pr_filed`\u002F`skipped` labs: keep as-is.\n4. For `running` or `error` (interrupted mid-lab): resume at the\n   **UC-level**. List `runs\u002F\u003Crun-id>\u002Flabs\u002F\u003Cslug>\u002Fuc-*-state.yml` files;\n   the first UC without a state file is where Phase 2 restarts for this\n   lab. Findings already on disk (`uc-\u003CN>-findings.json` for completed\n   UCs) are preserved; new UCs append. If a UC partially completed\n   (`uc-\u003CN>-state.yml.status: partial`), the orchestrator MAY re-run\n   that UC from scratch (since UC-level idempotency is best-effort —\n   the tenant may already have the agent the UC was supposed to create)\n   or skip ahead based on the partial state. Default behavior is to\n   re-run the partial UC and let its judge mark the \"create\" steps\n   `cannot_verify` if the artifact already exists.\n5. For `pending`: run as a fresh lab.\n6. Phase 1.5 interview under `--resume`: inherit `phase_mix` and\n   `scope_labs` from the prior manifest (skip Q2 and Q3). Re-ask the\n   account question (Q1) unless `account_prompt_mode` permits skipping\n   AND the cached `expires_at` is still in the future. A resume after\n   the cache expired requires fresh credentials, no exceptions. A\n   resume with `account_prompt_mode: always` still re-prompts — the\n   user may have meant to redeem a different account for the resumed run.\n\n## Important rules\n\n- **Run the interactive phase by default.** Static analysis alone is not a\n  complete audit. Skip Phase 2 only when the user has explicitly passed\n  `--static-only` or set `execution.require_interactive_phase: false`. Either\n  case must be recorded in the run manifest.\n- **Always prompt at Phase 1.5 unless explicitly told not to.** Default\n  `account_prompt_mode: always` means every fresh run asks \"use cached vs.\n  redeem new\", regardless of how recently the cache was written. This is\n  the user's safety net against running against the wrong tenant.\n- **Never modify the mcs-labs repo.** Only read its `_labs\u002F` and `_data\u002F` directories.\n- **Never commit, push, branch, or PR.** Issues only.\n- **Never run `copilot-studio-cleanup` or any agent-deletion command** as part of an audit run. Tenant hygiene is the user's responsibility (see [[feedback_no_cleanup_in_test_automation]]).\n- **Never log secrets.** The credential blob is DPAPI-encrypted; passwords never appear in transcripts, audit history, issue bodies, or `manifest.yml`. The first 4 chars of the workshop code may appear as `workshop_code_hint`.\n- **Halt loudly on auth_expired.** Don't try to silently re-auth mid-run — that's how data loss happens.\n- **Trust the critique pass.** If critique downgrades a finding to `pass` or `cannot_verify`, drop it from the issue.\n\n## What to do when stuck\n\n- **Parser couldn't classify a step**: emit a `parser_warning` finding (severity: low) and continue. Don't halt the whole lab.\n- **Playwright timeout on `_browser_wait_for`** (UI took too long but the page is reachable): capture diagnostics, mark the step `transient`, retry once. If still failing, record as a finding with `outcome: broken, severity: high, confidence: 0.6`. This is a UI-side problem, not a connection problem — keep going.\n- **Network \u002F connection error** (DNS failure, `net::ERR_*`, navigation timeout to a page that should resolve quickly, repeated `net::ERR_INTERNET_DISCONNECTED`, or any failed-network entry that recurs across consecutive steps): treat as a **connection class** failure, distinct from a UI timeout. The retry policy is:\n  1. Retry the failing operation up to `execution.network_retry_count` times (default `3`), pausing `execution.network_retry_backoff_seconds` between attempts (default `5`, `10`, `20` — exponential).\n  2. If all retries fail, **halt the lab** (mark `status: paused, reason: network_unstable`) and prompt the user via `AskUserQuestion`:\n     - Question: `Network looks unstable — what should we do?`\n     - Options:\n       - `[Recommended] Retry now — connection is back` — description: `Re-attempts the failing step and continues the lab from where we paused.`\n       - `Wait \u003CN> seconds and retry` — description: `Sleeps for execution.network_wait_seconds (default 120) before retrying. Use this if the outage is ongoing.`\n       - `Skip this lab` — description: `Marks the lab status: error, reason: network_unstable and continues with the next lab in the plan.`\n       - `Abort the run` — description: `Halts the entire run. Resume later with \u002Faudit-bootcamp --resume \u003Crun-id> when the connection is stable.`\n  3. Do NOT silently keep retrying past the cap, and do NOT record a connection-class failure as a lab finding — it's an environment issue, not a lab issue. Lab findings are reserved for things the lab author can fix.\n  4. Record every connection-class pause in `runs\u002F\u003Crun-id>\u002Ftranscript.md` with the timestamp, retry count, and the user's chosen response.\n- **Judge call fails**: retry once with a JSON-only reminder. If still failing, log the step as `error` in the transcript but continue with the next step — don't halt the lab over one bad judge call.\n- **Issue creation fails**: write a clear error to `runs\u002F\u003Crun-id>\u002Flabs\u002F\u003Cslug>\u002Ftranscript.md`, set `status: error, reason: gh_unavailable`, keep the rendered `issue-body.md` for the user to file manually.\n\n## What success looks like\n\nAfter a full bootcamp run:\n- `runtime\u002Faudit-history.yml` has 11 new entries (one per lab).\n- `runtime\u002Fruns\u002F\u003Crun-id>\u002F` contains parsed steps, findings, screenshots, transcripts, and rendered issue bodies for every lab.\n- 0–11 GitHub issues filed (typically 1–4 in practice; many labs pass cleanly).\n- The user has a one-line summary in chat: `Audited 11 labs in \u003Cduration>. 7 pass, 3 filed issues, 1 error (auth expired in \u003Cslug>, resume with \u002Faudit-bootcamp --resume \u003Crun-id>).`\n",{"name":5,"description":45,"allowed-tools":46},"Audit Microsoft Copilot Studio labs end-to-end for any event or workshop. Resolves the mcs-labs repo location dynamically (clones\u002Fupdates it as needed), enumerates auditable scopes from the repo's `_events\u002F` and `_workshops\u002F` collections, drives Playwright through every numbered step of each lab in the chosen scope, compares the live UI to the written instructions with an LLM judge, and either files a GitHub issue (one per lab with findings) or appends a clean-pass entry to a local audit log. Use this skill when the user says \"audit the bootcamp\", \"audit a workshop\", \"run the lab auditor\", \"test the mcs-labs labs\", or invokes any `\u002Faudit-*` command from this plugin.\n",[47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69],"Read","Glob","Grep","Write","Edit","Bash(gh issue list:*)","Bash(gh auth status:*)","Bash(gh repo view:*)","PowerShell","AskUserQuestion","mcp__plugin_playwright_playwright__browser_navigate","mcp__plugin_playwright_playwright__browser_snapshot","mcp__plugin_playwright_playwright__browser_take_screenshot","mcp__plugin_playwright_playwright__browser_click","mcp__plugin_playwright_playwright__browser_type","mcp__plugin_playwright_playwright__browser_fill_form","mcp__plugin_playwright_playwright__browser_select_option","mcp__plugin_playwright_playwright__browser_press_key","mcp__plugin_playwright_playwright__browser_wait_for","mcp__plugin_playwright_playwright__browser_evaluate","mcp__plugin_playwright_playwright__browser_console_messages","mcp__plugin_playwright_playwright__browser_network_requests","mcp__plugin_playwright_playwright__browser_close","main","aae0865a11e7bbbce9d74b0b6b043d9945c3335f","skills\u002Fmcs-lab-auditor",2,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FBootcampLabTestPlugin","https:\u002F\u002Fgithub.com\u002Fmicrosoft","https:\u002F\u002Fopensource.microsoft.com","Open source projects and samples from Microsoft","2026-06-20 03:15:17",{"source":8,"skillName":5,"commands":80,"prompt":90,"zipAvailable":91},[81,84,87],{"tool":82,"command":83},"npx","npx skills add microsoft\u002FBootcampLabTestPlugin --skill mcs-lab-auditor",{"tool":85,"command":86},"bunx","bunx skills add microsoft\u002FBootcampLabTestPlugin --skill mcs-lab-auditor",{"tool":88,"command":89},"pnpm","pnpm dlx skills add microsoft\u002FBootcampLabTestPlugin --skill mcs-lab-auditor","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FBootcampLabTestPlugin\u002Fblob\u002Fmain\u002Fskills\u002Fmcs-lab-auditor\u002FSKILL.md",true,[93,99,103,107,111,115,119,123,127,131,135,139,143,147,151],{"path":94,"resourceType":95,"resourceTypeDesc":96,"sizeBytes":97,"sourceHtmlUrl":98},"references\u002Faudit-log-schema.md","REFERENCE","参考文档",4601,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FBootcampLabTestPlugin\u002Fblob\u002Fmain\u002Fskills\u002Fmcs-lab-auditor\u002Freferences\u002Faudit-log-schema.md",{"path":100,"resourceType":95,"resourceTypeDesc":96,"sizeBytes":101,"sourceHtmlUrl":102},"references\u002Fcross-lab-consistency.md",11678,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FBootcampLabTestPlugin\u002Fblob\u002Fmain\u002Fskills\u002Fmcs-lab-auditor\u002Freferences\u002Fcross-lab-consistency.md",{"path":104,"resourceType":95,"resourceTypeDesc":96,"sizeBytes":105,"sourceHtmlUrl":106},"references\u002Fdeployment.md",10020,"https:\u002F\u002Fgithub.com\u002Fdavila7\u002Fclaude-code-templates\u002Fblob\u002Fmain\u002Fcli-tool\u002Fcomponents\u002Fskills\u002Fai-research\u002Finference-serving-sglang\u002Freferences\u002Fdeployment.md",{"path":108,"resourceType":95,"resourceTypeDesc":96,"sizeBytes":109,"sourceHtmlUrl":110},"references\u002Ffinding-schema.md",6337,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FBootcampLabTestPlugin\u002Fblob\u002Fmain\u002Fskills\u002Fmcs-lab-auditor\u002Freferences\u002Ffinding-schema.md",{"path":112,"resourceType":95,"resourceTypeDesc":96,"sizeBytes":113,"sourceHtmlUrl":114},"references\u002Fhost-tools.md",1737,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FBootcampLabTestPlugin\u002Fblob\u002Fmain\u002Fskills\u002Fmcs-lab-auditor\u002Freferences\u002Fhost-tools.md",{"path":116,"resourceType":95,"resourceTypeDesc":96,"sizeBytes":117,"sourceHtmlUrl":118},"references\u002Flab-parser-spec.md",14328,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FBootcampLabTestPlugin\u002Fblob\u002Fmain\u002Fskills\u002Fmcs-lab-auditor\u002Freferences\u002Flab-parser-spec.md",{"path":120,"resourceType":95,"resourceTypeDesc":96,"sizeBytes":121,"sourceHtmlUrl":122},"references\u002Flab-resources-spec.md",8857,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FBootcampLabTestPlugin\u002Fblob\u002Fmain\u002Fskills\u002Fmcs-lab-auditor\u002Freferences\u002Flab-resources-spec.md",{"path":124,"resourceType":95,"resourceTypeDesc":96,"sizeBytes":125,"sourceHtmlUrl":126},"references\u002Fllm-judge-prompts.md",9478,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FBootcampLabTestPlugin\u002Fblob\u002Fmain\u002Fskills\u002Fmcs-lab-auditor\u002Freferences\u002Fllm-judge-prompts.md",{"path":128,"resourceType":95,"resourceTypeDesc":96,"sizeBytes":129,"sourceHtmlUrl":130},"references\u002Fplaywright-cookbook.md",20875,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FBootcampLabTestPlugin\u002Fblob\u002Fmain\u002Fskills\u002Fmcs-lab-auditor\u002Freferences\u002Fplaywright-cookbook.md",{"path":132,"resourceType":95,"resourceTypeDesc":96,"sizeBytes":133,"sourceHtmlUrl":134},"references\u002Fplugin-self-improvement.md",15397,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FBootcampLabTestPlugin\u002Fblob\u002Fmain\u002Fskills\u002Fmcs-lab-auditor\u002Freferences\u002Fplugin-self-improvement.md",{"path":136,"resourceType":95,"resourceTypeDesc":96,"sizeBytes":137,"sourceHtmlUrl":138},"references\u002Fpr-append-flow.md",5371,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FBootcampLabTestPlugin\u002Fblob\u002Fmain\u002Fskills\u002Fmcs-lab-auditor\u002Freferences\u002Fpr-append-flow.md",{"path":140,"resourceType":95,"resourceTypeDesc":96,"sizeBytes":141,"sourceHtmlUrl":142},"references\u002Fradix-attention.md",11005,"https:\u002F\u002Fgithub.com\u002Fdavila7\u002Fclaude-code-templates\u002Fblob\u002Fmain\u002Fcli-tool\u002Fcomponents\u002Fskills\u002Fai-research\u002Finference-serving-sglang\u002Freferences\u002Fradix-attention.md",{"path":144,"resourceType":95,"resourceTypeDesc":96,"sizeBytes":145,"sourceHtmlUrl":146},"references\u002Fstructured-generation.md",13468,"https:\u002F\u002Fgithub.com\u002Fdavila7\u002Fclaude-code-templates\u002Fblob\u002Fmain\u002Fcli-tool\u002Fcomponents\u002Fskills\u002Fai-research\u002Finference-serving-sglang\u002Freferences\u002Fstructured-generation.md",{"path":148,"resourceType":95,"resourceTypeDesc":96,"sizeBytes":149,"sourceHtmlUrl":150},"references\u002Fworkshop-redemption-chatbot.md",24774,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FBootcampLabTestPlugin\u002Fblob\u002Fmain\u002Fskills\u002Fmcs-lab-auditor\u002Freferences\u002Fworkshop-redemption-chatbot.md",{"path":152,"resourceType":95,"resourceTypeDesc":96,"sizeBytes":153,"sourceHtmlUrl":154},"references\u002Fworkshop-redemption.md",8437,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FBootcampLabTestPlugin\u002Fblob\u002Fmain\u002Fskills\u002Fmcs-lab-auditor\u002Freferences\u002Fworkshop-redemption.md",[156,174,183,192,202],{"skillNo":157,"name":158,"nameZh":6,"description":159,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":160,"categories":161,"keywords":162,"publishedAt":40},"01M69BVHVKRMK12R15T156SRG5","mcs-lab-pr-appender","Append screenshot updates from a lab audit run onto an EXISTING open fix-PR branch in the active instance's lab repo (microsoft\u002Fmcs-labs by default). Replaces matched image files in place, makes one commit, pushes, and comments on the PR. Never creates a new branch and never opens a new PR. Invoked by mcs-lab-auditor by default whenever (a) the orchestrator's existing-state probe found an open PR for the slug AND (b) the lab produced new screenshot artifacts. Suppressed by --no-update-screenshots \u002F --no-append-to-pr (CLI) or `issues.pr_append.enabled_by_default: false` (config). Should NOT be invoked directly by the user.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FBootcampLabTestPlugin\u002Fblob\u002Fmain\u002Fskills\u002Fmcs-lab-pr-appender\u002FSKILL.md",[],[163,164,165,166,167,38,168,169,170,171,9,172,173,33],"mcs lab pr appender","pr","artifacts","git","update","git commit","image","screenshots","cli","commit","config",{"skillNo":175,"name":176,"nameZh":6,"description":177,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":178,"categories":179,"keywords":180,"publishedAt":40},"01M69BVFWN5Q952B3SHKVY7863","mcs-lab-new-lab-pr","Open a PR on the active instance's lab repo (microsoft\u002Fmcs-labs by default) that adds a NEW lab built by mcs-lab-builder. Stages the assembled `labs\u002F\u003Cslug>\u002FREADME.md` + screenshots, applies the registration entry (root `lab-config.yml` + generator, or direct `_data\u002Flab-config.yml` + `_labs\u002F\u003Cslug>.md` writes — per the detected mechanism), commits everything in one commit on a run-unique branch off `origin\u002Fmain`, and opens the PR. Invoked by mcs-lab-builder at B7 — NOT directly by the user. Do not use this for audit fixes (that is mcs-lab-fix-pr-filer, which patches an existing lab).","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FBootcampLabTestPlugin\u002Fblob\u002Fmain\u002Fskills\u002Fmcs-lab-new-lab-pr\u002FSKILL.md",[],[164,181,182,170,9,172,173,38,166],"mcs lab new lab pr","create",{"skillNo":184,"name":185,"nameZh":6,"description":186,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":187,"categories":188,"keywords":189,"publishedAt":40},"01M69BVEA7WF4B3FQE0AAA43XC","mcs-lab-issue-filer","Convert a lab's findings.json into a GitHub issue body and file it against the active instance's lab repo (microsoft\u002Fmcs-labs by default). Checks for an existing open issue with matching labels and comments on it instead of duplicating. Invoked by mcs-lab-auditor after a lab completes with at least one judge-recorded finding above the confidence threshold. Should NOT be invoked directly by the user — it expects a fully-populated run directory and assumes the orchestrator has already filtered out cannot_verify and below-threshold findings.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FBootcampLabTestPlugin\u002Fblob\u002Fmain\u002Fskills\u002Fmcs-lab-issue-filer\u002FSKILL.md",[],[190,9,182,191,38,29],"mcs lab issue filer","directory",{"skillNo":193,"name":194,"nameZh":6,"description":195,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":196,"categories":197,"keywords":198,"publishedAt":40},"01M69BVC29V68FGJXNX1QX6M6P","mcs-lab-fix-pr-filer","After mcs-lab-issue-filer has filed (or commented on) the audit issue for a lab, apply the findings' suggested_correction diffs to the lab markdown and copy any proposed_screenshot_replacement images into labs\u002F\u003Cslug>\u002Fimages\u002F. If an OPEN fix-PR for this slug already exists (same author, mergeable), append the commit to it; otherwise open a NEW PR on a run-unique branch `{branch_prefix}\u002Ffix-\u003Cslug>-content-audit-\u003Crun-id>`. Title `\u003Cslug>: fix audit findings from #\u003Cissue-number>`, body `Closes #\u003Cissue-number>`. Dedup is scoped to OPEN PRs only — a merged or closed prior PR never blocks a new one. Should NOT be invoked directly by the user — the orchestrator calls it after mcs-lab-issue-filer returns the issue number.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FBootcampLabTestPlugin\u002Fblob\u002Fmain\u002Fskills\u002Fmcs-lab-fix-pr-filer\u002FSKILL.md",[],[199,164,200,172,33,168,182,38,201,166],"mcs lab fix pr filer","diffs","git branch",{"skillNo":203,"name":204,"nameZh":6,"description":205,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":206,"categories":207,"keywords":210,"publishedAt":40},"01M69BV8YA1TEFTPGZXQ1JP8X1","mcs-lab-builder","Interactively BUILD a new Microsoft Copilot Studio lab for the mcs-labs repo, end-to-end. Get a workshop test account, drive Playwright to the Copilot Studio Home page, then capture a lab step-by-step — writing instructions, tips, and screenshots and confirming each step with the user — assemble it into a sibling-formatted `labs\u002F\u003Cslug>\u002FREADME.md`, re-run the finished lab through the existing audit engine as a quality gate, and open a PR on the active instance's lab repo (microsoft\u002Fmcs-labs by default). Event\u002Fworkshop-agnostic: a lab is built and tested standalone; event attachment is optional. Use when the user says \"build a new lab\", \"author a lab\", or invokes `\u002Fbuild-lab`.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FBootcampLabTestPlugin\u002Fblob\u002Fmain\u002Fskills\u002Fmcs-lab-builder\u002FSKILL.md",[208,209],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},{"categoryNo":24,"categoryName":25,"taxonomyType":21,"taxonomyTypeDesc":22},[211,182,212,170,30,31,9,32,33,34,164,213,35,36,37,38,166,39,28],"mcs lab builder","writing","quality gate"]