agentsclimarketplace

Test runner

Skill Kanevry/session-orchestrator/skills/test-runner

Loop engineering for AI coding agents — turn ad-hoc sessions into a repeatable research → plan → wave-execute → close loop with verification gates. Runs on Claude Code, Codex CLI, Cursor, and Pi. MIT community plugin.

Install
npx -y skills add Kanevry/session-orchestrator --skill test-runner

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

What its author says it does

Copied from the file, not written here

Use this skill when orchestrating agentic end-to-end tests. Resolves target + profile, dispatches the right driver(s) (playwright for web today, peekaboo for macOS (issue #381)), invokes the ux-evaluator agent (opus, read-only) against driver artifacts, reconciles findings with the open issue tracker via scripts/lib/test-runner/issue-reconcile.mjs, and writes report.md + JSONL roll-up. Wraps upstream tools (no forks). Hard-gates Playwright MCP for browser drive (4× token cost vs CLI per Microsoft's own benchmark).

SKILL.md

16.2 KB, as published. Nobody here has run it

Test Runner Skill

Project-instruction file resolution: CLAUDE.md and AGENTS.md (Codex CLI) are transparent aliases — see skills/_shared/instruction-file-resolution.md. Wherever this skill mentions CLAUDE.md, the alias rule applies.

Soul

Before anything else, read and internalize soul.md in this skill directory. It defines WHO you are — your role as an orchestrator, your delegation boundaries, and your non-negotiable constraints.

Phase 0: Bootstrap Gate

Read skills/_shared/bootstrap-gate.md and execute the gate check. If GATE_CLOSED, invoke skills/bootstrap/SKILL.md and wait for completion. If GATE_OPEN, continue to Phase 1.

<HARD-GATE> Do NOT proceed past Phase 0 if GATE_CLOSED. There is no bypass. Refer to `skills/_shared/bootstrap-gate.md` for the full HARD-GATE constraints. </HARD-GATE>

Phase 1: Read Session Config + Resolve Target / Profile

Read and parse Session Config per skills/_shared/config-reading.md. Store result as $CONFIG.

Test-runner specific fields (parse these specifically):

  • test-runner.default-profile (default: smoke)
  • test-runner.retention-days (default: 30)
  • test-command, typecheck-command, lint-command (used for context only — not driven here)

Target / Profile Resolution

Resolution order (first match wins):

  1. CLI argument --target <name> --profile <name> (explicit, highest priority)
  2. Policy file lookup.orchestrator/policy/test-profiles.json by target name (if present)
  3. Convention-based detection (marker files):
    • playwright.config.{ts,js} present → target type web, dispatch playwright-driver
    • Package.swift present → target type mac, dispatch peekaboo-driver (see skills/peekaboo-driver/SKILL.md)
  4. Fallback → emit error and halt:
    Error: Cannot resolve target — provide --target or add .orchestrator/policy/test-profiles.json
    

Run ID

Generate a run ID immediately after target resolution:

import { makeRunId } from 'scripts/lib/test-runner/artifact-paths.mjs';
const runId = makeRunId(); // e.g. "your-target-app-1715688000123"

All artifact paths in subsequent phases derive from this run ID. Never use ad-hoc paths.

Status Report

After resolution, emit: Test Runner: target=[name] profile=[name] run_id=[runId] driver=[driver]

Phase 2: Driver Dispatch

Determine ${RUN_DIR} from artifact-paths.mjs:runDirPath(runId) before dispatching any driver. All drivers write artifacts under ${RUN_DIR}/.

--since Filtering (when since_ref is provided)

When since_ref is set (passed from the /test --since <git-ref> handoff contract):

  1. Import and call changedFilesSince(since_ref) from scripts/lib/discovery/helpers.mjs.
  2. If the helper throws (ref unresolvable), surface the error to the user and halt.
  3. If the result is [] (no files changed since the ref), emit:
    No files changed since <since_ref>. Skipping test run.
    
    and exit with status 0. Do NOT fall back to a full-repo test run.
  4. If the result is a non-empty array, JSON-stringify it and set TEST_CHANGED_FILES in the driver subprocess environment (see driver invocations below). Driver-side filtering is deferred — drivers receive the env var but do not yet filter by it in this wave.

For each resolved driver:

Web (playwright-driver)

Dispatch via Bash per skills/playwright-driver/SKILL.md. Pass ${RUN_DIR} so the driver writes all artifacts (screenshots, AX dumps, HAR) under it.

# Example invocation shape (exact flags defined by playwright-driver SKILL.md)
TEST_CHANGED_FILES="${CHANGED_FILES_JSON}" node scripts/lib/playwright-driver/runner.mjs \
  --run-dir "${RUN_DIR}" \
  --profile "${PROFILE}" \
  --target "${TARGET}"

Where ${CHANGED_FILES_JSON} is JSON.stringify(changedFiles) when --since was provided, or an empty string otherwise.

Capture exit code. A non-zero exit from Playwright means test failures — these become findings for the UX evaluator. They are NOT a fatal error for the orchestrator. Continue to Phase 3 regardless of exit code.

Log: playwright-driver exited [code] — [N] test files captured under ${RUN_DIR}

macOS (peekaboo-driver)

See skills/peekaboo-driver/SKILL.md for the full dispatch contract, permission probe, and artifact layout.

Pre-dispatch platform check: The driver's Phase 1 gate handles the platform and version checks (darwin + macOS 15.0+) and exits 0 (non-fatal skip) on incompatible systems. The orchestrator does not need to replicate these checks.

Permission probe: The driver runs its own Phase 2 permission probe via peekaboo permissions status --json. If required permissions (Screen Recording, Accessibility) are not granted, the driver surfaces an AUQ and exits 2 on failure. The orchestrator treats exit 2 as a driver-framework error, not a test failure.

Invocation:

# All inputs via environment variables — no positional arguments
RUN_DIR="${RUN_DIR}" TARGET="${TARGET}" PROFILE="${PROFILE}" bash skills/peekaboo-driver/SKILL.md

Any bash-driver shim or report helper you write for this dispatch is subject to .claude/rules/bash-harness-pitfalls.md (grep -c double-print, stdout-capture pollution, file-based aggregate verdicts, perl -pi script-surgery corruption).

Outputs the orchestrator must parse:

ArtifactDescription
${RUN_DIR}/exit_codePlain integer file written by driver before exit
${RUN_DIR}/results.jsonDriver summary: exit_code, scenarios_attempted, scenarios_passed, scenarios_failed
${RUN_DIR}/ax-snapshots/<scenario>.jsonpeekaboo AX-tree output per scenario
${RUN_DIR}/ax-snapshots/glass-modifiers-<ts>.jsonLiquid Glass conformance artifact (consumed by ux-evaluator Check 4)
${RUN_DIR}/screenshots/<step>-<ts>.pngPer-step screenshots (evidence for ux-evaluator findings)
${RUN_DIR}/console.ndjsonDriver log events as NDJSON

Exit-code semantics:

CodeMeaningOrchestrator Action
0All captures succeeded (or platform skip)Record pass or skip, continue to Phase 3
1At least one capture failedFailures become findings (non-fatal) — continue to Phase 3
2Framework error (missing binary, permission denied, OS mismatch)Surface as driver error; still continue to Phase 3 with available artifacts

Capture exit code. Exit 1 (capture failures) produces findings for the UX evaluator — it is NOT fatal for the orchestrator. Exit 2 (framework error) is surfaced in the report but does not halt Phase 3. Continue to Phase 3 regardless of exit code.

Log: peekaboo-driver exited [code] — [N] scenarios captured under ${RUN_DIR}

Phase 3: UX Evaluator Dispatch

Invoke the ux-evaluator agent (agents/ux-evaluator.md) via the Agent tool. The agent reads driver artifacts under ${RUN_DIR}/ and applies skills/test-runner/rubric-v1.md (4 checks). The agent writes findings.jsonl directly to ${RUN_DIR}/findings.jsonl — the coordinator does NOT need to forward findings through prompt context.

Agent({
  description: `UX evaluate run ${runId}`,
  prompt: `<scope: ${RUN_DIR}, rubric: skills/test-runner/rubric-v1.md, output: ${RUN_DIR}/findings.jsonl>`,
  subagent_type: "ux-evaluator",
  run_in_background: false
})

run_in_background: false is mandatory — Phase 4 depends on findings.jsonl being fully written before reconciliation begins.

After the agent completes, verify ${RUN_DIR}/findings.jsonl exists. If missing, emit a warning and skip Phase 4 (no findings to reconcile).

Phase 4: Issue Reconciliation

Read ${RUN_DIR}/findings.jsonl. Use the helpers in scripts/lib/test-runner/issue-reconcile.mjs for all glab/gh interactions — never call glab or gh directly.

Available Helper Functions (issue-reconcile.mjs)

FunctionPurpose
listExistingFindings({glabPath, project, label, maxBuffer})Query the tracker for all open from:test-runner issues; returns {ok, issues[], fingerprints: Set}
createFinding({glabPath, project, fingerprint, title, body, labels, dryRun, maxBuffer})Create a new issue; returns {ok, action: 'create', iid?, command?}
updateFinding({glabPath, project, iid, comment, dryRun, maxBuffer})Add a comment to an existing issue; returns {ok, action: 'comment', command?}
triageDecision(finding, candidates)Pure decision: fingerprint-exact → ignore; Levenshtein ≤ 2 on title → update; else → create
reconcileFinding({finding, existingFingerprints, glabPath, dryRun})Track-A legacy helper — single-finding create-or-noop using a pre-built fingerprint Set

Security notes (ADR-364 §C5, #388, #389):

  • All execFile calls use maxBuffer: 4 MB and shell: false.
  • Bodies > 65 536 bytes are rejected with {ok: false, error: {code: 'BODY_TOO_LARGE', ...}}.
  • **Fingerprint:** literals in free-text fields are replaced with __Fingerprint__ before the authoritative sentinel line is appended (sentinel-injection hardening, #388).

Existing-Fingerprint Dedup

Before iterating findings, build the dedup set once:

import { listExistingFindings } from 'scripts/lib/test-runner/issue-reconcile.mjs';

const listResult = await listExistingFindings({ glabPath });
// listResult.ok === false → log and continue with empty Set (conservative)
const existingFingerprints = listResult.ok ? listResult.fingerprints : new Set();
const existingIssues = listResult.ok ? listResult.issues : [];

If the glab query fails, log the error and proceed with an empty fingerprint set (conservative: may create duplicate issues, safer than silently skipping).

Severity Routing

SeverityAction
criticalAuto-create issue — no AUQ. Label: from:test-runner,priority::critical
highAuto-create issue — no AUQ. Label: from:test-runner,priority::high
mediumBatched AUQ triage (see below)
lowBatched AUQ triage (see below)

For critical and high, call triageDecision(finding, existingIssues) first. If action === 'ignore', skip. If action === 'update', call updateFinding(). If action === 'create', call createFinding().

Batched AUQ Triage (medium / low)

Group medium and low findings and present via a single AskUserQuestion call (mirror the discovery skill's Phase 5 Step 3 pattern):

AskUserQuestion({
  questions: [{
    question: `<N> medium/low findings to triage. How to handle?`,
    header: "Test-runner triage",
    options: [
      {
        label: "Create all (Recommended)",
        description: "File <N> new issues, all with label from:test-runner"
      },
      {
        label: "Review each",
        description: "Walk through findings individually with create/update/ignore"
      },
      {
        label: "Skip all",
        description: "Dismiss medium/low; only critical/high get auto-filed"
      }
    ],
    multiSelect: false
  }]
})

"Create all": For each medium/low finding call triageDecision(finding, existingIssues):

  • ignore → skip silently.
  • update → call updateFinding({iid: target, comment, ...}).
  • create → call createFinding({fingerprint, title, body, labels, ...}).

"Review each": Walk through findings one by one. For each, call triageDecision to get the recommended action, then present a per-finding AUQ with three options (Create / Update existing #<iid> / Skip). Honour the user's selection.

"Skip all": Dismiss all medium/low findings without filing. Log skipped: N medium/low findings (user dismissed).

Reconcile Outcomes

Collect outcomes from reconciliation calls into three buckets:

  • created[] — issue IIDs of newly created issues
  • commented[] — issue IIDs of existing issues that received a new comment
  • noop[] — fingerprints/IIDs where no action was taken (dedup hit or user dismissed)

Pass these buckets to Phase 5.

Phase 5: Report + JSONL Roll-up

report.md

Write ${RUN_DIR}/report.md — a markdown summary of the run. Minimum content:

# Test Run Report

**Run ID:** <runId>
**Target:** <target>
**Profile:** <profile>
**Timestamp:** <ISO 8601>
**Duration:** <ms>ms

## Findings Summary

| Severity | Count |
|----------|-------|
| critical | N |
| high | N |
| medium | N |
| low | N |

## Check Breakdown

| Check | Findings |
|-------|---------|
| <check_name> | N |

## Issue Actions

- **Created:** <IIDs>
- **Commented:** <IIDs>
- **No-op (dedup):** <IIDs>

## Driver Exit Codes

| Driver | Exit Code |
|--------|-----------|
| playwright | N |

JSONL Roll-up

Append one record to .orchestrator/metrics/test-runs.jsonl (path from artifact-paths.mjs:jsonlRollupPath()) via appendJsonlAtomic from scripts/lib/autopilot/telemetry.mjs. Never use appendFileSync — POSIX-atomic guarantee is required (per #376).

Schema v1:

{
  "schema_version": 1,
  "run_id": "your-target-app-1715688000123",
  "timestamp": "2026-05-14T07:30:00Z",
  "target": "your-target-app",
  "profile": "smoke",
  "drivers": ["playwright"],
  "finding_counts": {"critical": 0, "high": 2, "medium": 5, "low": 3},
  "issues": {"created": [388, 389], "commented": [], "noop": [380]},
  "exit_code": 0,
  "duration_ms": 12345
}

exit_code is the orchestrator's own exit code: 0 if no critical/high findings auto-created, 1 otherwise.

Final Status Line

Emit to stdout:

Test Runner complete: run_id=<runId> findings=[total] issues_created=[N] duration=[ms]ms

Anti-Patterns

  • DO NOT invoke Playwright or Peekaboo directly from the coordinator prompt — always dispatch via the wrapper skills (skills/playwright-driver/SKILL.md, skills/peekaboo-driver/SKILL.md).
  • DO NOT inline AX-tree dumps or screenshots into coordinator or agent prompt context — artifacts go to disk under ${RUN_DIR}.
  • DO NOT re-file existing findings — fingerprint-based dedup via issue-reconcile.mjs is mandatory every run.
  • DO NOT use @playwright/mcp for browser drive — R5 hard-gate enforced by scripts/lib/validate/check-playwright-mcp-canary.mjs (4× token cost per Microsoft benchmark).
  • DO NOT call glab or gh directly — always go through scripts/lib/test-runner/issue-reconcile.mjs (execFile + binary allowlist + arg-validation, per ADR-364 §C5).
  • DO NOT write JSONL via appendFileSync — use appendJsonlAtomic from scripts/lib/autopilot/telemetry.mjs.

Critical Rules

  • NEVER auto-merge MRs created by issue-reconcile.
  • NEVER persist artifacts outside ${RUN_DIR} — it is the single source of truth for a run.
  • NEVER call glab/gh directly — always route through issue-reconcile.mjs.
  • ALWAYS use the orchestrator's run-id everywhere (from artifact-paths.mjs:makeRunId()).
  • ALWAYS invoke appendJsonlAtomic for JSONL roll-up — never appendFileSync (POSIX-atomic guarantee, per #376).
  • ALWAYS set run_in_background: false for the ux-evaluator agent dispatch — Phase 4 depends on its output.

Sub-File Reference

FilePurpose
soul.mdOrchestrator identity — read before Phase 0
rubric-v1.mdUX rubric specification (4 checks, fingerprint formula, findings JSON schema) — read by ux-evaluator agent

Implementation Modules

FilePurpose
scripts/lib/test-runner/fingerprint.mjsStable 16-hex fingerprint per finding (deterministic, idempotent)
scripts/lib/test-runner/artifact-paths.mjsRun-dir + artifact path builders — pure functions, no side effects
scripts/lib/test-runner/issue-reconcile.mjsglab/gh dispatch via execFile + binary allowlist (ADR-364 §C5)

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.