mcs-lab-auditor
Audit Microsoft Copilot Studio labs end-to-end for any event or workshop. Resolves the mcs-labs repo location dynamically (clones/updates it as needed), enumerates auditable scopes from the repo's `_events/` and `_workshops/` 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 `/audit-*` command from this plugin.
- License 许可
- MIT
- 最近更新
- 2026-06-20
- Stars
- ★ 1
- Forks
- 2
01 前言 Front Matter
- name
- mcs-lab-auditor
- description
- Audit Microsoft Copilot Studio labs end-to-end for any event or workshop. Resolves the mcs-labs repo location dynamically (clones/updates it as needed), enumerates auditable scopes from the repo's `_events/` and `_workshops/` 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 `/audit-*` command from this plugin.
- allowed-tools
- [ "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" ]
02 资源文件 15
- 参考文档references/audit-log-schema.md4.5 KB
- 参考文档references/cross-lab-consistency.md11.4 KB
- 参考文档references/deployment.md9.8 KB
- 参考文档references/finding-schema.md6.2 KB
- 参考文档references/host-tools.md1.7 KB
- 参考文档references/lab-parser-spec.md14.0 KB
- 参考文档references/lab-resources-spec.md8.6 KB
- 参考文档references/llm-judge-prompts.md9.3 KB
- 参考文档references/playwright-cookbook.md20.4 KB
- 参考文档references/plugin-self-improvement.md15.0 KB
- 参考文档references/pr-append-flow.md5.2 KB
- 参考文档references/radix-attention.md10.7 KB
- 参考文档references/structured-generation.md13.2 KB
- 参考文档references/workshop-redemption-chatbot.md24.2 KB
- 参考文档references/workshop-redemption.md8.2 KB
03 SKILL.md 正文
mcs-lab-auditor (orchestration skill)
You 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).
Two write paths. (a) the active instance's lab repo {repo} (e.g. microsoft/mcs-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/BootcampLabTestPlugin — 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/plugin-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/plugin-self-improvement.md for the full procedure including the cascading-step (high-severity) classification.
This file is the orchestrator. It loads the reference files below as needed:
references/lab-parser-spec.md— how to convert a lab's markdown into a step tree.references/lab-resources-spec.md— Lab Resources discovery + pre-flight scrape of per-event SharePoint config values. Used when a lab referencescopilotstudiotraining.sharepoint.com/.../Lab-Assets.aspx(or similar) for connector credentials / endpoint URLs.references/plugin-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) andmicrosoft/BootcampLabTestPlugin(plugin side) as appropriate; cascading-step failures are high-severity.references/playwright-cookbook.md— portal sign-in flow, scene-boundary auth probe, tool mapping per step kind, known quirks.references/workshop-redemption.md— Skillable-style workshop redemption flow.references/workshop-redemption-chatbot.md— chatbot Adaptive Card workshop redemption flow.references/llm-judge-prompts.md— the per-step judge, the second-pass critique, the action classifier.references/finding-schema.md— finding record fields, outcome and severity definitions.references/audit-log-schema.md—audit-history.ymlentry shape.
Read whichever you need before doing the corresponding step. Don't try to keep all of them in your head at once.
Top-level entry points (called from command files)
| Command | What this skill does |
|---|---|
/audit-event [--event <key>] [--resume <id>] [--labs csv] [--no-issue] |
Audit every lab in an event OR workshop. With --event <key> the scope is pinned; without it, Phase 1.5 Q3a picks one. Generic over every scope in the repo's _events/ and _workshops/ collections. |
/audit-bootcamp [--resume <id>] [--labs csv] [--no-issue] |
Shortcut for /audit-event --event bootcamp. Audits every bootcamp lab. Skips Q3 and Q3a in the interview. |
/audit-lab [<slug>] [--no-issue] [--dry-run] |
Audit a single lab. With <slug>, the slug pins scope. Without, Phase 1.5 Q4 picks one from the full all-labs catalog (lab_metadata.*), not constrained to any event. |
/audit-report [<run-id>] |
Summarize audit-history.yml. No browser activity. |
/audit-account [show|redeem|clear] |
Manage the cached test account. |
Run lifecycle (for /audit-bootcamp and /audit-lab)
Phase 1 — Pre-flight (no browser yet)
-
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:
pwsh -NoProfile -File "$env:CLAUDE_PLUGIN_ROOT\scripts\Test-PluginVersion.ps1"It compares the local
.claude-plugin/plugin.jsonversion to the version published onorigin/mainofmicrosoft/BootcampLabTestPlugin. Behavior:plugin up-to-date (vX.Y.Z)→ continue silently.UPDATE AVAILABLE: local vA < published vB …(exit 10) → surface the warning to the user and recommend running/pluginto update before continuing, but do NOT hard-halt — let the user decide (they may be intentionally testing a local build).version check skipped (…)(offline /ghunavailable) → note it and continue. The check is best-effort and never blocks a run.
-
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 futureOpus 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.On non-Opus orchestrator, halt with:
ERROR: mcs-lab-auditor requires the orchestrator to run on Opus. Current session model: <detected model> Switch to Opus (e.g. /model in Claude Code) and re-run the /audit-* command. Lower-tier sub-agents are still supported — see Phase 1.5 Q5 model-preset.Do NOT proceed past Phase 1 step 2 on a non-Opus session.
Resolve the active lab instance (FIRST, before any repo or portal access)
Run once at the start of every audit/build run:
pwsh -NoProfile -File "$env:CLAUDE_PLUGIN_ROOT/scripts/Resolve-LabInstance.ps1" -Mode Json -Instance "<--instance value or empty>"
Parse the JSON and hold these values for the whole run — every sub-skill uses them:
{repo}=.repo(e.g.microsoft/mcs-labs) — the target for ALLgh issue/gh pr/gh repocalls.{branch_prefix}=.branch_prefix(e.g.dewain) — the prefix for ALL created branches. If.branch_prefix_sourceisunresolved, do NOT create any branch; halt and ask the user to setbranch_prefixin theirlab-instances.ymlor authenticategh.{clone_url}/ candidates are consumed byResolve-LabRepo.ps1 -Instance(next step).
Then resolve the repo, passing the instance through:
pwsh -NoProfile -File "$env:CLAUDE_PLUGIN_ROOT/scripts/Resolve-LabRepo.ps1" -Mode Status -Instance "<--instance value or empty>"
Replace the old hardcoded gh repo view microsoft/mcs-labs … permission check with gh repo view {repo} ….
Materialize the active portal
Write the resolved .portal object to runtime/account/active-portal.yml (create the dir if needed). All redemption flows read THIS file, never config/workshop.yml directly. For the default mcs-labs instance this file is a copy of config/workshop.yml, so redemption behaves identically.
-
Resolve the plugin directory and the mcs-labs repo (NO hard-coded paths).
- The plugin's own files are reached via the
${CLAUDE_PLUGIN_ROOT}environment variable Claude Code sets for this plugin — never a hard-codedC:\Users\...\.claude\plugins\...path. All script invocations below use"$env:CLAUDE_PLUGIN_ROOT\scripts\<name>.ps1". - The mcs-labs repo location is resolved, not assumed. Run:
This finds the repo (viapwsh -NoProfile -File "$env:CLAUDE_PLUGIN_ROOT\scripts\Resolve-LabRepo.ps1" -Mode Path -Instance "<--instance value or empty>"$env:MCS_LABS_REPO, thejudge-config.yml.build.registration.mcs_labs_repo_path_candidates, or a built-in candidate list), clonesmicrosoft/mcs-labsinto%USERPROFILE%\.mcs-lab-auditor\mcs-labsif no local copy exists, and pins the working tree toorigin/main(it checks outorigin/maineven 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$REPOand use it for every subsequent read of_labs/,_events/,_workshops/,_data/. On--dry-runor static-only runs you may pass-NoPullto skip the fetch. If resolution fails (clone error, no git), halt with the script's error message. - ASSERT the audited revision is
origin/main(MANDATORY unless-NoPull). A lab audit is only meaningful against the current instructions. After capturing$REPO, confirm the working tree is exactlyorigin/main:
If this printspwsh -NoProfile -Command "git -C '$REPO' fetch --quiet origin; if ((git -C '$REPO' rev-parse HEAD) -ne (git -C '$REPO' rev-parse origin/main)) { 'MISMATCH' } else { 'ok' }"MISMATCH(the clone is on a stale branch, behindorigin/main, or has uncommitted changes blocking the pin), halt with a clear message that names the diverging-commit count and tells the user to commit/stash and re-run — do NOT proceed to parse labs against non-origin/maincontent. Record the audited commit SHA (git -C $REPO rev-parse --short origin/main) inmanifest.ymlso every finding is traceable to an exact instruction revision, and reference that SHA in issue/PR bodies. (Auditing a stale branch silently produces findings that are already fixed onmain— issue #41.)
- The plugin's own files are reached via the
-
Load configs:
runtime/account/active-portal.ymlconfig/judge-config.yml
-
Check
ghauth:gh auth status gh repo view {repo} --json viewerPermissionIf either fails, halt with a clear message before doing anything else.
-
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_configstable (which has drifted out of sync — it is missing newer workshops likeagent-in-a-dayand the academy series and still lists an obsoletemcs-in-a-day-v2). Use the legacy table only as a last-resort fallback when the collections are unreadable.- Run:
This scanspwsh -NoProfile -File "$env:CLAUDE_PLUGIN_ROOT\scripts\Get-EventCatalog.ps1" -RepoRoot "$REPO" -Json$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-matterlabs:array. When a richer$REPO\_data\agendas\<id>.ymlexists, itsschedule[]entries withtype: laboverride the order/labels — but the slug set is the same.- Events are formal curated experiences (bootcamp, the buildathons). Workshops are less formal / on-demand (azure-ai-workshop, mcs-in-a-day, agent-in-a-day, the agent-academy series).
external: truescopes (e.g. the agent-academy workshops) host their labs in another repo (repository), soauditableis 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 itsexternal_url.
- Build the all-labs catalog for single-lab scope (Phase 1.5 Q4):
{slug → { title, … }}fromlab_metadata.*in$REPO\_data\lab-config.yml, with each_labs/<slug>.mdfront-mattertitle:as a fallback. (lab_metadatais still maintained per-lab, so it remains the single-lab picker source.) - Determine which scope drives the active run:
/audit-bootcamp→ scope = thebootcampevent (pinned)./audit-event→ scope = the event/workshop named by--event <key>if passed; otherwise resolved by Phase 1.5 Q3a (scope picker).--eventmatches anidfrom the catalog regardless of whether it is an event or a workshop./audit-lab <slug>→ scope is a single slug; for display, use the first catalog entry whoselabs[]contains the slug, ornoneif the slug belongs to no scope.
- For the active scope (whole event/workshop,
--labs csvsubset, or single slug), confirm$REPO\_labs\<slug>.mdexists for every chosen slug. If not, recordstatus: skipped, reason: lab_file_missingand continue with the next slug — never abort the whole run because one lab is missing.
- Run:
-
Run-start account prompt (see Phase 1.5 below). On
--dry-run, skip this —--dry-runonly exercises the parser and writessteps.jsonper lab. -
Create the run directory:
$run_id = (Get-Date -Format "yyyy-MM-ddTHHmmZ") + "-" + (-join ((1..4) | % { '{0:x}' -f (Get-Random -Maximum 16) })) $run_dir = "runtime/runs/$run_id"Initialize
manifest.ymlwith the planned lab list, allstatus: pending, and the run start timestamp.
Browser-MCP preflight (before any interactive step)
The interactive phase needs a Playwright MCP. Tool names differ per host — see
references/host-tools.md. Before the first browser
action:
- Check your available tools for the Playwright
browser_*actions (Claude:mcp__plugin_playwright_playwright__*; Copilot: the bundledplaywrightserver). - If present → proceed with the interactive phase, calling each action by its host-qualified name.
- If absent → do NOT call a browser tool. Fall back to
--static-onlyfor this run and tell the user how to enable the browser for their host:- Claude Code: enable the
playwright@claude-plugins-officialMCP plugin. - Copilot CLI: the plugin bundles a
playwrightMCP (.github/mcp.json); it needsnpx+ network on first use. Runcopilot mcp listto confirm it loaded.
- Claude Code: enable the
Phase 1.5 — Run-start interview (MANDATORY)
Before any Playwright activity, run an interactive interview to confirm the
scope of the run. The interview is a series of AskUserQuestion calls. Each
question is skipped only when a CLI flag has already provided the answer
— never silently default away a question the user hasn't answered, since
silent defaults have caused real audit runs to execute against the wrong
tenant or to ship a doc-only sweep when the user expected a live audit.
The interview questions, in order (Q1, Q2, Q2a model preset, Q3 scope, Q3a event, Q4 lab):
Q1. Account — which test account?
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.
Governed by judge-config.yml.execution.account_prompt_mode:
always(default): always ask, even if cache is valid. Forces the user to opt in.only_if_expired: skip only if cachedexpires_atis in the future.only_if_missing: skip only if no cached account exists at all. Not recommended.
When asking, use AskUserQuestion. The options shown are conditional on cache state:
| Cache state | Options shown |
|---|---|
No cache (runtime/account/account.meta.json missing) |
Redeem a new workshop code (will prompt for the code), Abort the run |
Cache present but workshop_code.enc missing (legacy cache) |
[Recommended] Use cached: <user_id>, Redeem a new workshop code, Abort the run |
Cache present AND workshop_code.enc present (full cache) |
[Recommended] Use cached: <user_id>, Redeem a new user from the cached workshop code (no code prompt), Redeem a new workshop code (prompt for a different code), Abort |
Option semantics:
Use cached: <user_id>— Reuse the existing DPAPI-cached credentials. No workshop code prompt — the user already exists, there is no need to redeem again. Description:Cached <relative-time> ago. Expires <expires_at or "unknown">.Redeem a new user from the cached workshop code— Only shown whenworkshop_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: <workshop_code_hint>...) without re-asking for it.Redeem a new workshop code— Discards the cached code (if any) and prompts the user for a fresh code viaAskUserQuestion. Description:Prompts for a new workshop code. Use this when the cached code is exhausted or you're switching events.Abort the run— Description:Stop the run before any browser activity.
If the user picks one of the two redemption options:
- Read
runtime/account/active-portal.yml.portal_kind(chatbot | skillable | email). - Determine where the workshop code comes from:
- If the user picked
Redeem a new user from the cached workshop code, decryptruntime/account/workshop_code.encvia DPAPI. NoAskUserQuestionprompt is needed. - Otherwise, prompt the user via
AskUserQuestion:- Question:
What is the workshop code? - 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.
- Question:
- If the user picked
- Dispatch the portal-specific redemption flow. Each flow consumes the workshop code (if required by
workshop_code_required: true) and the deterministic config values fromruntime/account/active-portal.yml.chatbot_account_request_formandruntime/account/active-portal.yml.account_new_password_pattern— no furtherAskUserQuestioncalls during redemption:chatbot→ followreferences/workshop-redemption-chatbot.md(Cards 1–5 + AAD sign-in + first-login password change).skillable(or missing) → followreferences/workshop-redemption.md.email→ submit code on the portal, detect "check your email", then useAskUserQuestionto collect username/password (optional tenant), then continue.
- For all kinds, finish with the same AAD sign-in + caching path:
- DPAPI-encrypt the new password (after the first-login change) into
runtime/account/credential.enc. - DPAPI-encrypt the full workshop code into
runtime/account/workshop_code.encso the user can later chooseRedeem a new user from the cached workshop codewithout retyping it. - Write
runtime/account/account.meta.jsonwithuser_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.
- DPAPI-encrypt the new password (after the first-login change) into
The redemption flow is responsible for first-run portal setup too: if
runtime/account/active-portal.yml.workshop_portal_url is still
REPLACE_ME_ON_FIRST_RUN, it must prompt for Workshop portal URL, validate
^https?://, 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/workshop.yml — and re-materialize runtime/account/active-portal.yml, then continue to the
workshop-code prompt.
When skipping under a non-default account_prompt_mode, record the
decision in the run manifest as account.skip_reason: expires_at_in_future (or similar) so future readers know the cached
account wasn't user-confirmed for this run.
Q2. Phase mix — static, interactive, or both?
Skip this question when ANY of:
--static-onlyCLI flag was passed (mix = static).--interactive-onlyCLI flag was passed (mix = interactive).judge-config.yml.execution.require_interactive_phase: false(mix = static).
When asking, use AskUserQuestion:
- Question:
Which phase(s) should this run execute? - Options:
[Recommended] Both static and interactive— description:Full audit. Static markdown checks fan out per lab, then interactive Playwright drives the live UI for every lab in the plan. Highest cost, most coverage.Static only— description:Markdown parser checks, image-ref resolution, external link probes, prereq sanity. No browser, no tenant state. Fast and cheap.Interactive only— description:Assumes a prior static pass produced findings-static.json for each lab. Drives the live UI and merges with the prior static findings at lab completion. Useful when re-verifying a previously-audited lab in a new product release.
Record the chosen mix as execution.phase_mix: static | interactive | both
in the run manifest. If "static only" or "interactive only", also set
execution.skipped_interactive: true or execution.skipped_static: true
respectively, with reason: cli_flag or reason: interview.
Q2a. Model preset — Optimized per function, or All Opus?
Skip this question when ANY of:
--model-preset <key>CLI flag was passed (optimized|opus|custom).judge-config.yml.execution.model.presetis set to a non-promptvalue (i.e. the config has an explicit default, e.g. set by a previous run).--resume <run-id>was passed (preset is inherited from the prior manifest).
The 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 / fix-PR filer / PR appender sub-skills.
When asking, use AskUserQuestion:
- Question:
Which model preset for sub-agents? - Options:
[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/PR filers on Haiku 4.5. Approx $50 per bootcamp event run. ~85% completion rate.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.Custom (edit config)— description:Halt before redemption so you can edit config/judge-config.yml.execution.model.* per-function overrides, then re-run /audit-*. No subagents spawn this run.
Record 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.
On 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.
Q3. Scope — an event or a single lab?
Skip this question when ANY of:
--labs <csv>CLI flag was passed (scope = csv → the driving event/workshop is the one whoselabs[]is a superset of the csv, ormultiif the csv crosses scopes).- The entry point is
/audit-lab <slug>(slug already names the scope; scope =oneimmediately). - The entry point is
/audit-bootcamp(scope is implicitlyevent=bootcampand the question is skipped). - The entry point is
/audit-event --event <key>(scope isevent=<key>and the question is skipped — but Q3a still runs if no--eventwas passed). --resume <run-id>was passed (scope is inherited from the prior manifest).
When asking, use AskUserQuestion:
- Question:
What should this run audit? - Options:
[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.Just one lab— description:Pick a single lab from the full mcs-labs catalog. Follow-up question lists choices.
Q3a. Event/workshop picker (only if Q3 = "A whole event or workshop")
The picker source is the events + workshops catalog built in Phase 1 step 6 from the repo's _events/ and _workshops/ collections — NOT the legacy event_configs table. Each entry already carries type (event | workshop), id, title, description, auditable, and labs[].
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:
- The most-used scope in
runtime/audit-history.ymlover the last 30 days is the first option, marked[Recommended]. - The two next-most-used (or, if no history, the highest-
labs-count auditable scopes) fill the second and third slots. Other event/workshop (type the id)is always option 4 — picks any remaining scope via "Other" free-text. Validate the typed id against the catalogids; if invalid, re-ask Q3a with a(invalid id: <typed>)prefix.
Label each option with its type so the user can tell events from workshops, e.g. [Event] Architecture Bootcamp (11 labs) / [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/ or _workshops/ file in the repo.
Example option set (regenerate dynamically — do not hardcode):
- Question:
Which event or workshop? - Options:
[Recommended] [Event] Architecture Bootcamp (11 labs)— description: the catalogdescriptionforbootcamp.[Workshop] Agent in a Day (4 labs)— description: the catalogdescriptionforagent-in-a-day.[Workshop] MCS in a Day (4 labs)— description: the catalogdescriptionformcs-in-a-day.Other event/workshop (type the id)— description:Free-text entry. Any id from the catalog, e.g. agent-buildathon-1day, agent-buildathon-1month, azure-ai-workshop.
If 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 / repository and cannot be driven locally, then re-ask Q3a for an auditable scope.
Once the scope is resolved, scope_labs is the labs[] slug list from its catalog entry (agenda-overridden order when _data/agendas/<id>.yml exists).
Q4. One-lab picker (only if Q3 = "Just one lab")
The picker source for a single-lab scope is the full all-labs catalog (lab_metadata.*), NOT one event/workshop'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/workshop includes.
Because AskUserQuestion supports only 2–4 options per question and the catalog has 60+ labs, ask in two steps grouped by lab_metadata.<id>.section:
Q4a — Section. Use AskUserQuestion:
- Question:
Which section is the lab in? - Options:
[Recommended] Core (essential foundations)— description:Build Intelligent Agents, Master Variables, Monitor Performance, etc. ~12 labs.Intermediate (practical applications)— description:Setup for Success, Ask Me Anything, BYOM, MCS Governance, etc. ~10 labs.Advanced & specialized (autonomous + dev)— description:Multi-Agent, Account News, Orchestration, MCP Connectors, Pipelines, etc. ~15 labs.Other (type the slug)— description:Free-text entry. Use this for optional/external labs or when you already know the slug.
Map sections from lab_metadata.<id>.section:
- "Core" = labs where
section: core. - "Intermediate" = labs where
section: intermediate. - "Advanced & specialized" = labs where
section ∈ {advanced, specialized, optional, external}(consolidated to keep the question to 4 options).
Q4b — Lab in the chosen section. Render the section's labs as the AskUserQuestion options, with the lab's title (from lab_metadata.<id>.title or the front-matter as fallback) as the description.
When a section has more than 4 labs (most do), paginate by listing the first 3 entries plus an explicit More labs in <section> (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: <typed>) prefix.
The 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.
For "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.
Read the title from lab_metadata.<id>.title (the source of truth in lab-config.yml) with the front-matter title: of _labs/<slug>.md as a fallback if lab_metadata is unavailable at interview time.
Recording the interview outcome
Append all interview answers to the run manifest before Phase 1.7 starts:
interview:
account_choice: cached | redeemed | aborted
phase_mix: static | interactive | both
model_preset: optimized | opus | custom # Q2a; see execution.model.resolved below for per-function assignments
scope: event | one # "event" → audit a whole event/workshop's lab list; "one" → audit a single slug from the all-labs catalog
scope_type: event | workshop | null # which collection the chosen scope came from; null when scope == one
event: <scope-id|null> # the chosen event/workshop id; null when scope == one (single-lab runs have no driving scope)
scope_labs: [<slug>, ...] # the final list driving Phase 1.7 (always 1+ entries)
skipped_questions: # questions short-circuited by CLI flags or by the entry-point command
- { question: "phase_mix", reason: "cli_flag: --static-only" }
- { question: "model_preset", reason: "cli_flag: --model-preset optimized" }
- { question: "event_picker", reason: "command: /audit-bootcamp" }
execution:
model:
preset: optimized | opus | custom
resolved: # per-function frozen assignments for this run
uc_subagent: sonnet | opus | haiku
judge: sonnet | opus | haiku
critique: sonnet | opus | haiku
static_subagent: sonnet | opus | haiku
cross_lab: sonnet | opus | haiku
lab_parser: sonnet | opus | haiku
issue_filer: sonnet | opus | haiku
fix_pr_filer: sonnet | opus | haiku
pr_appender: sonnet | opus | haiku
After the interview, the orchestrator has:
- A valid signed-in browser context (or
phase_mix: staticso none is needed). - A confirmed
account_user_id. - An explicit
phase_mix. - An explicit
model_presetplus the frozen per-functionexecution.model.resolvedmap. Every Agent / subagent spawn from here on MUST pass the resolved model via the tool'smodelparameter. - An explicit lab list for Phase 1.7's plan.
Record the account itself under account.user_id and account.source: cached | redeemed as before.
Phase 1.6 — Lab Resources discovery and pre-flight scrape (CONDITIONAL)
After Phase 1.5 (so the browser is signed in) but before Phase 1.7, check
whether any lab in the planned list references a per-event Lab
Resources SharePoint page. The full procedure is in
references/lab-resources-spec.md; the orchestrator-side summary:
- For each lab in the planned list, parse
_labs/<slug>.mdfor external URLs matching the Lab Resources pattern (seelab-resources-spec.md§1). If found, recordlab_metadata.lab_resources_urlin the parsed step tree. - If any lab has a Lab Resources URL, perform a single pre-flight
scrape: navigate, capture page text via
browser_evaluate, parse the label-value table perlab-resources-spec.md§3, and write the result toruns/<run-id>/lab-resources.yml. - Skip Phase 1.6 entirely when
--dry-runis set OR the run is static-phase-only (no browser session exists in either case). - Take a debug screenshot at
runs/<run-id>/lab-resources.png(local-only, never uploaded anywhere). - If the scrape fails (page 401/403/404, parser miss, network error),
log the failure, set
lab_resources.status: unavailablein the run manifest, and continue. Labs that don't depend on Lab Resources are unaffected; labs that do will fall back to user-prompt orcannot_verifyperlab-resources-spec.md§6.
Security — non-negotiable. Lab Resources values include real credentials
(passwords, connection secrets, API keys). The orchestrator and every
downstream subagent MUST NOT include any scraped value verbatim in:
issue bodies, PR descriptions, commit messages, audit-history.yml,
manifest.yml, console output, judge prompts, transcripts. Reference
values by key only — e.g. "the password from
lab_resources.labs.setup-for-success.servicenow.password", never the
literal. The cache file runs/<run-id>/lab-resources.yml lives in the
run directory and is excluded from any rendered finding artifact;
runtime/ is gitignored at the repo root. See lab-resources-spec.md §4
for the full security contract.
After Phase 1.6, the orchestrator has the lab-resources cache available
under runs/<run-id>/lab-resources.yml (or knows it's unavailable).
Per-lab interactive subagents read from it when a step's text references
a Lab Resources value.
Phase 1.7 — Plan execution order (fan-out vs serial)
After Phase 1.5 but before Phase 2, build the execution plan from
judge-config.yml.lab_dependencies and the planned lab list.
The static phase is not a substitute for the interactive phase. Static checks can spot typos, broken links, missing images, and structural drift. They cannot spot UI drift in the live product — controls that have been renamed, dialogs that have been added, settings that have moved. The interactive phase is what catches the issues that actually break a learner mid-lab. Doing only the static phase is the audit equivalent of running
tsc --noEmitand calling it tested.
The behavior is governed by
judge-config.yml.execution.require_interactive_phase (default true).
When true, the orchestrator MUST run Phase 2 against the signed-in
training account. The only way to skip Phase 2 is to pass the
--static-only CLI flag (or set the config to false), and either choice
is recorded in the run manifest as execution.skipped_interactive: true, reason: <flag|config>.
-
Static phase is always fully fanned out. Spawn one background subagent per lab in the planned list (capped at
execution.static_fanout_concurrency). Passmodel: <execution.model.resolved.static_subagent>on eachAgenttool call (typicallyhaikuunder theoptimizedpreset). Each subagent does the markdown-only checks: parser-spec validation, image-ref resolution, external-URL link-check viaWebFetch, TOC-anchor sanity, prereq sanity, self-consistency between the Use Cases Covered table and the real scene headings. Subagents write their findings asruns/<run-id>/labs/<slug>/findings-static.jsonAND a sidecarruns/<run-id>/labs/<slug>/scene-fingerprints.json(shape hash, identifier tokens, raw-excerpt line numbers — seereferences/cross-lab-consistency.mdfor the schema). No browser, no tenant state, no GitHub writes.The lab-parser pass that produces
steps.jsonfor each lab spawns a separate subagent withmodel: <execution.model.resolved.lab_parser>(typicallysonnet— the parser benefits from a stronger model than the static checks).
1a. Cross-lab consistency fan-in. After every per-lab static
subagent in step 1 has returned, run a single fan-in pass with
model: <execution.model.resolved.cross_lab> (typically sonnet)
that reads
every scene-fingerprints.json in scope, groups scenes by shape
hash, and emits drift findings for divergent identifier tokens (e.g.
Address 1: State/Province in one lab vs Address1: State or Providence in a sibling). Full algorithm in
references/cross-lab-consistency.md. Output:
runs/<run-id>/cross-lab-consistency.json (summary) plus
per-lab drift findings appended to each affected lab's
findings-static.json (flags: parser_warning: true,
cross_lab_drift: true, severity always low).
For a single-lab run (`scope_labs` size 1), the fan-in reads the
most recent prior-run `scene-fingerprints.json` for every other lab
in `lab_metadata.*.id` (the full all-labs catalog). Labs that have
never been audited contribute nothing — the issue body documents
this discovery limit.
Skip when `--dry-run` is set, OR when `--static-only` AND scope size
1 AND no prior runs on disk, OR when
`judge-config.yml.consistency.cross_lab_enabled: false`. Record
`consistency.cross_lab_status` in the run manifest either way.
-
Interactive phase (REQUIRED by default — see the callout above) topologically sorts the planned labs against
lab_dependencies. Each entry underlab_dependenciesdefines achain(ordered list of slugs that must run serially because each lab's tenant artifacts are read by the next). Independent labs run in parallel — but only up toexecution.fanout_concurrencybrowser sessions per training account, since concurrent UI activity in the same tenant can collide (e.g. two labs both renaming the same default agent).The default
fanout_concurrency: 1preserves the legacy strict-serial behavior. Raising it requires either (a) a workshop event whose labs genuinely don't share tenant state, or (b) provisioning one training account per concurrent slot. -
The orchestrator merges static and interactive findings for each lab into a single
findings.jsonat the end of the lab's interactive pass. The staticfindings-static.jsonis also retained for debugging.
Phase 2 — Per-lab loop (interactive UI execution via per-UC subagents)
This phase is the core deliverable of the audit. The orchestrator drives a real browser through every step the lab tells the learner to perform, using the account chosen in Phase 1.5, and compares what the live UI does to what the lab markdown says. Skip this phase and you have a documentation audit, not a lab audit.
Why per-UC subagents (and not per-lab or per-step)
The interactive phase is the most context-heavy part of the run by far — each step generates 2 accessibility snapshots, 1 screenshot, console + network logs, and an LLM-judge call. At ~50 steps × 11 labs the orchestrator's conversation context would overflow long before the audit completed.
Granularity choice: per Use Case (the ### Use Case #N: boundaries in
the lab markdown). Each UC is 5–20 steps, self-contained ("create a
learning assistant" / "build a sales assistant"), and runs as a
dedicated subagent. The orchestrator only ever sees the UC's return
summary (counts, status, findings JSON path) — not the per-step
snapshots. Per-step granularity is too fine (subagent boot overhead
dominates and intra-UC state — "the agent I just created is in the
left-nav now" — would have to be reconstructed each time). Per-lab
granularity is too coarse (subagent itself overflows on a 50-step lab).
Browser handoff: shared MCP browser process
The orchestrator signs in once in Phase 1.5. The Playwright MCP
server keeps the browser process alive across subagent boundaries — a
per-UC subagent that calls mcp__plugin_playwright_playwright__* (or your host's
browser tool — see references/host-tools.md) reuses
the same browser tab the orchestrator left it in. The subagent's first
real step is typically _browser_navigate to the URL the UC's first
parser step expects, then a _browser_snapshot to confirm state.
No re-auth, no session export/import, no second sign-in. The session cookies are tenant-side, not subagent-side.
Per-UC state handoff: uc-state.yml
UCs inside the same lab share tenant artifacts (UC#1 creates an agent
named "Copilot Teacher"; UC#3 tests that agent; the judge for UC#3 needs
to know "Copilot Teacher" was the chosen name). Each UC subagent writes
runs/<run-id>/labs/<slug>/uc-<N>-state.yml:
uc_id: usecase-1
finished_at: <iso>
status: complete | error | partial
ctx_vars: # variables the next UC's judge can use as CTX_VARS
agent_name: "Copilot Teacher"
topic_names: ["Welcome", "PolicyLookup"]
knowledge_sources: ["https://learn.microsoft.com/en-us/microsoft-365-copilot/"]
solution_name: "Travel Tools" # any artifact name set in this UC
browser_left_at: # for resumes
url: "https://m365.cloud.microsoft/chat/..."
scene_completed: "Share and test your agent"
findings_count:
high: 0
medium: 0
low: 0
The next UC's subagent reads all prior uc-*-state.yml files in the lab
dir and merges their ctx_vars into its per-step judge CTX_VARS input
(see references/llm-judge-prompts.md §A inputs). Resumable: a failed
UC N can be retried in isolation as long as UC N-1's state file exists.
Per-lab orchestration steps (executed by the orchestrator)
For each lab slug in the interactive-phase execution plan (respecting topological order from Phase 1.7):
-
Mark the lab
runninginmanifest.yml. -
Parse the lab (
references/lab-parser-spec.md). Write the step tree toruns/<run-id>/labs/<slug>/steps.json.- On
--dry-run, stop here for this lab. Otherwise continue. - If
phase_mix: staticfor this run, this whole loop is skipped at the Phase 1.7 plan level — the orchestrator never reaches Phase 2.
- On
-
Slice steps.json by use case. For each use case in
steps.json.use_cases[], writeruns/<run-id>/labs/<slug>/uc-<N>-steps.jsoncontaining only that UC's scenes and steps. This is the subagent's input. -
For each use case in order (serial):
- Scene-boundary auth probe at the start of UC N (cookbook
§scene-boundary-auth-probe). If expired, halt; mark lab
error, reason: auth_expired; tell user how to resume. Do NOT spawn the subagent if auth failed. - Spawn a per-UC subagent (background or foreground depending on
orchestrator preference — see "Spawning" below for the prompt
shape). Pass
model: <manifest.interview.execution.model.resolved.uc_subagent>on theAgenttool call so the subagent runs on the model selected by Phase 1.5 Q2a (e.g.sonnetunderoptimized,opusunderopus). Pass the subagent: the UC'suc-<N>-steps.jsonpath, the paths of all prioruc-*-state.ymlfiles in this lab dir, the run-id, the lab slug, the lab-level transcript path, the allowed-tools list scoped tomcp__plugin_playwright_playwright__*(or your host's browser tool — see references/host-tools.md)Read+Write+ the per-step judge invocation, AND the resolvedjudge+critiquemodel values from the manifest so the subagent's internal per-step judge call also runs on the right model.
- Wait for the subagent to return. Its summary tells the orchestrator: status (complete/error/partial), findings counts by severity, the UC-state file path. The orchestrator does NOT need the per-step snapshots — those are written to disk by the subagent and only the judge reads them.
- On error: read the subagent's
uc-<N>-state.yml.status. Iferror, reason: auth_expired, halt the whole lab. Iferror, reason: ui_blocker, mark the UC failed and decide whether to continue to UC N+1 (default: continue — a blocker in UC#1 may not affect UC#2's independent flow) or halt the lab (whenlab_dependenciessays the next UC depends on this UC's artifacts). - On partial: the subagent retried
transientfailures and captured findings, but couldn't finish the UC's scenes. Same handling aserrorbased on whether downstream UCs depend on the unfinished state.
- Scene-boundary auth probe at the start of UC N (cookbook
§scene-boundary-auth-probe). If expired, halt; mark lab
-
Merge UC findings into the lab's
findings.json. Concatenate eachuc-<N>-findings.jsoninto the single lab-levelfindings.jsonthe issue-filer reads. Merge with the static-phasefindings-static.json(ifphase_mix: bothorinteractivewith a prior static run on disk). -
End-of-lab disposition — always file an issue AND open a fix PR when findings exist:
- If
findings.jsonhas any finding withconfidence >= judge-config.yml.confidence.min_to_include_in_issueAND the lab is not innon_deterministic_lab_slugs(or--force-issuewas passed):- Invoke
mcs-lab-issue-filer(sub-skill) withmodel: <execution.model.resolved.issue_filer>— files (or comments on) the GitHub issue and returns the issue number. - Then invoke
mcs-lab-fix-pr-filer(sub-skill) with that issue number andmodel: <execution.model.resolved.fix_pr_filer>— applies the findings'suggested_correctiondiffs to the lab markdown, copies anyproposed_screenshot_replacementimages intolabs/<slug>/images/, and ships them as a PR. PR dedup is scoped to OPEN PRs only:- If an open fix-PR for the slug already exists (same author,
mergeable — resolved from Phase 1.4
existing-state.ymlor a head-prefixgh pr listquery), the run's commit is appended to that PR — no duplicate is opened. - Otherwise (no open PR — including when prior PRs were merged or
closed), a new PR is opened on a run-unique branch
{branch_prefix}/fix-<slug>-content-audit-<run-id>frommain, titled<slug>: fix audit findings from #<issue-number>with bodyCloses #<issue-number>. A merged/closed prior PR never blocks a new one — each run with fresh findings gets its own PR.
- If an open fix-PR for the slug already exists (same author,
mergeable — resolved from Phase 1.4
- If screenshots were refreshed and an open fix-PR already exists
for the slug, invoke
mcs-lab-pr-appender(sub-skill) withmodel: <execution.model.resolved.pr_appender>for the screenshots-only refresh path.
- Invoke
- Otherwise: append a clean entry to
runs/<run-id>/clean-labs.yml. - Either way: append the per-lab summary entry to
runtime/audit-history.yml(references/audit-log-schema.md). - Mark lab
done,issue_filed, orissue_and_pr_filedinmanifest.yml.
Why both an issue AND a PR? The issue documents the why (audit run, evidence, screenshots, links to run artifacts) so maintainers can triage confidently. The PR ships the what (concrete markdown edits + screenshot replacements) so fixes are immediately reviewable and mergeable without copy/paste.
- If
-
Pause for
judge-config.yml.execution.min_seconds_between_labsbefore the next lab.
Spawning: what the per-UC subagent's prompt looks like
The orchestrator hands each per-UC subagent a self-contained prompt with:
- Lab slug, UC id, run id, account user_id (for context only — the browser is already signed in).
- Path to
uc-<N>-steps.json(input). - Paths to all
uc-<1..N-1>-state.ymlfor prior UCs in this lab (read-only). - Path where the subagent must write
uc-<N>-state.ymlanduc-<N>-findings.json. - The dispatch table from
references/playwright-cookbook.md#tool-mapping. - The judge prompt template from
references/llm-judge-prompts.md#A. - Explicit rules:
- The browser is already signed in and on a URL related to the prior UC (or the lab's landing URL for UC#1). Navigate as the first step requires; never call sign-in flow.
- Execute the lab by following the written steps and clicking the
actual UI controls — NEVER navigate by URL to shortcut a step, and
NEVER treat a URL seen in a screenshot as a navigation target
(issue #40). Lab screenshots (
) illustrate the expected result; they are not instructions. The only legitimate_browser_navigatecalls are the ones the step text explicitly tells the learner to make (e.g. "go to make.powerapps.com"). Reaching a later page via a synthesized deep-link hides the very instruction drift the audit exists to catch (a renamed/moved/missing control), so if the named control can't be found, record a finding (expected-vs-actual) instead of working around it with a URL. - Verify identity on every new first-party surface (issue #39).
Before driving Copilot Studio, Power Apps, the Power Platform admin
center, Teams, or Outlook, confirm the page is signed in as the
exact redeemed workshop user (
account.user_id), not merely "signed in" — the audit browser can silently SSO into a different OS-broker/Windows account. If the surface shows a different user (e.g. Power Apps defaulting to a prior run'sDEV - User XXXX), halt that UC witherror, reason: account_mismatchand let the orchestrator resolve it (full logout → re-login as the redeemed user) rather than URL-hacking into the right environment. - Save snapshots to disk by default —
_browser_snapshot({filename: "snapshots/<step-id>-before.yml"})instead of returning inline. The judge reads from disk. This is the single biggest context-saver for the subagent itself. - Save screenshots to
runs/<run-id>/labs/<slug>/screenshots/<step-id>.pngas before. - Write
uc-<N>-state.ymlat the end with all ctx_vars set during the UC. - Return to the orchestrator only: status, findings counts,
uc-<N>-state.ymlpath. Do NOT echo back snapshots or transcripts. - Connection-class failures follow the network-retry policy and
if exhausted, halt with
error, reason: network_unstableso the orchestrator canAskUserQuestionthe user.
Phase 3 — Wrap-up
- Close the browser (
mcp__plugin_playwright_playwright__browser_close(or your host's browser tool — see references/host-tools.md)). - Print a summary to the user: total labs, count by status, list of filed issue URLs and PR URLs, run-id for future
/audit-reportlookups. - Write
runs/<run-id>/manifest.ymlfinal state.
Resume flow (--resume <run-id>)
When --resume <run-id> is passed:
- Read
runs/<run-id>/manifest.yml. - Determine which labs are
pending,running(interrupted mid-run), orerror. - For
done/issue_filed/issue_and_pr_filed/skippedlabs: keep as-is. - For
runningorerror(interrupted mid-lab): resume at the UC-level. Listruns/<run-id>/labs/<slug>/uc-*-state.ymlfiles; the first UC without a state file is where Phase 2 restarts for this lab. Findings already on disk (uc-<N>-findings.jsonfor completed UCs) are preserved; new UCs append. If a UC partially completed (uc-<N>-state.yml.status: partial), the orchestrator MAY re-run that UC from scratch (since UC-level idempotency is best-effort — the tenant may already have the agent the UC was supposed to create) or skip ahead based on the partial state. Default behavior is to re-run the partial UC and let its judge mark the "create" stepscannot_verifyif the artifact already exists. - For
pending: run as a fresh lab. - Phase 1.5 interview under
--resume: inheritphase_mixandscope_labsfrom the prior manifest (skip Q2 and Q3). Re-ask the account question (Q1) unlessaccount_prompt_modepermits skipping AND the cachedexpires_atis still in the future. A resume after the cache expired requires fresh credentials, no exceptions. A resume withaccount_prompt_mode: alwaysstill re-prompts — the user may have meant to redeem a different account for the resumed run.
Important rules
- Run the interactive phase by default. Static analysis alone is not a
complete audit. Skip Phase 2 only when the user has explicitly passed
--static-onlyor setexecution.require_interactive_phase: false. Either case must be recorded in the run manifest. - Always prompt at Phase 1.5 unless explicitly told not to. Default
account_prompt_mode: alwaysmeans every fresh run asks "use cached vs. redeem new", regardless of how recently the cache was written. This is the user's safety net against running against the wrong tenant. - Never modify the mcs-labs repo. Only read its
_labs/and_data/directories. - Never commit, push, branch, or PR. Issues only.
- Never run
copilot-studio-cleanupor any agent-deletion command as part of an audit run. Tenant hygiene is the user's responsibility (see [[feedback_no_cleanup_in_test_automation]]). - 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 asworkshop_code_hint. - Halt loudly on auth_expired. Don't try to silently re-auth mid-run — that's how data loss happens.
- Trust the critique pass. If critique downgrades a finding to
passorcannot_verify, drop it from the issue.
What to do when stuck
- Parser couldn't classify a step: emit a
parser_warningfinding (severity: low) and continue. Don't halt the whole lab. - Playwright timeout on
_browser_wait_for(UI took too long but the page is reachable): capture diagnostics, mark the steptransient, retry once. If still failing, record as a finding withoutcome: broken, severity: high, confidence: 0.6. This is a UI-side problem, not a connection problem — keep going. - Network / connection error (DNS failure,
net::ERR_*, navigation timeout to a page that should resolve quickly, repeatednet::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:- Retry the failing operation up to
execution.network_retry_counttimes (default3), pausingexecution.network_retry_backoff_secondsbetween attempts (default5,10,20— exponential). - If all retries fail, halt the lab (mark
status: paused, reason: network_unstable) and prompt the user viaAskUserQuestion:- Question:
Network looks unstable — what should we do? - Options:
[Recommended] Retry now — connection is back— description:Re-attempts the failing step and continues the lab from where we paused.Wait <N> seconds and retry— description:Sleeps for execution.network_wait_seconds (default 120) before retrying. Use this if the outage is ongoing.Skip this lab— description:Marks the lab status: error, reason: network_unstable and continues with the next lab in the plan.Abort the run— description:Halts the entire run. Resume later with /audit-bootcamp --resume <run-id> when the connection is stable.
- Question:
- 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.
- Record every connection-class pause in
runs/<run-id>/transcript.mdwith the timestamp, retry count, and the user's chosen response.
- Retry the failing operation up to
- Judge call fails: retry once with a JSON-only reminder. If still failing, log the step as
errorin the transcript but continue with the next step — don't halt the lab over one bad judge call. - Issue creation fails: write a clear error to
runs/<run-id>/labs/<slug>/transcript.md, setstatus: error, reason: gh_unavailable, keep the renderedissue-body.mdfor the user to file manually.
What success looks like
After a full bootcamp run:
runtime/audit-history.ymlhas 11 new entries (one per lab).runtime/runs/<run-id>/contains parsed steps, findings, screenshots, transcripts, and rendered issue bodies for every lab.- 0–11 GitHub issues filed (typically 1–4 in practice; many labs pass cleanly).
- The user has a one-line summary in chat:
Audited 11 labs in <duration>. 7 pass, 3 filed issues, 1 error (auth expired in <slug>, resume with /audit-bootcamp --resume <run-id>).
04 同仓库更多 Skills
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/mcs-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 / --no-append-to-pr (CLI) or `issues.pr_append.enabled_by_default: false` (config). Should NOT be invoked directly by the user.
mcs-lab-new-lab-pr
Open a PR on the active instance's lab repo (microsoft/mcs-labs by default) that adds a NEW lab built by mcs-lab-builder. Stages the assembled `labs/<slug>/README.md` + screenshots, applies the registration entry (root `lab-config.yml` + generator, or direct `_data/lab-config.yml` + `_labs/<slug>.md` writes — per the detected mechanism), commits everything in one commit on a run-unique branch off `origin/main`, 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).
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/mcs-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.
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/<slug>/images/. 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}/fix-<slug>-content-audit-<run-id>`. Title `<slug>: fix audit findings from #<issue-number>`, body `Closes #<issue-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.
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/<slug>/README.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/mcs-labs by default). Event/workshop-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 `/build-lab`.