Skip to content

add/testing tools orchestration - #213

Draft
kdenney wants to merge 20 commits into
add/testing-tools-executionfrom
add/testing-tools-orchestration
Draft

kdenney wants to merge 20 commits into
add/testing-tools-executionfrom
add/testing-tools-orchestration

Conversation

@kdenney

@kdenney kdenney commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

🎟️ Tracking

📔 Objective

@github-actions

github-actions Bot commented Aug 21, 2026 •

Copy link
Copy Markdown

Claude Code validation

Result: Issues found

Validated the bitwarden-testing-tools plugin changes against this pull request's own merge base c869f1b (base branch add/testing-tools-execution). Plugin validation, skill review, and the configuration and security review all ran. Nothing blocks plugin loading, no secrets, and no new path from contributor-controlled input to a shell — the verdict is driven by one tool grant that is wider than the changeset justifies (Finding 1).

Re-run note. The branch was rebased after the previous review (every commit now carries the same 2026-10-09T18:22Z timestamp, 12 minutes after those comments were posted), and the merge base moved from f57a05a to c869f1b. The content under review is unchanged: all four findings raised inline on this pull request still reproduce verbatim at HEAD, and their threads are unresolved, so they are listed below rather than re-posted. No new inline comments were added this round.

Not covered: validate-plugin-structure.sh, validate-marketplace.sh, and validate-version-bump.sh run as dedicated workflow steps before this review and report their own results there — see the job log and check status. The skill-reviewer agent has no Bash tool, so it could not scope itself with git diff; it reviewed whole-file and the merge-base fence was applied afterwards.

Scope. This is a stacked pull request. The CI changed-file list carries 80 paths (the whole testing-tools stack measured against main), while the pull request's own diff is 28 files. Findings are anchored to the merge base. Concretely: the six AGENT.md files changed only their version: line (1.5.0 → 1.6.0); hooks/hooks.json and both hook scripts have no diff at all; references/untrusted-source-policy.md is not in this diff; and of the seven SKILL.md files in the CI list, only start-playwright-test/SKILL.md is new here. The agent and hook buckets were read rather than skipped — there was simply nothing the changeset introduced in them.

Findings
  • ⚠️ IMPORTANT: Finding 1: Write granted unscoped on a line that pins every Bash grant to an exact script path, in the one skill that handles untrusted agent output (plugins/bitwarden-testing-tools/skills/start-playwright-test/SKILL.md:5) (already raised on the pull request)
  • ⚠️ IMPORTANT: Finding 2: lint:guardrail is chained into pnpm run lint, but CI never invokes pnpm run lint, so the drift check never runs (package.json:31) (already raised on the pull request)
  • ⚠️ IMPORTANT: Finding 3: Recorded eval inventory lists eight in-plugin skills when ten ship, undercutting the trigger readings it qualifies (plugins/bitwarden-testing-tools/skills/start-playwright-test/evals/README.md:25) (already raised on the pull request)
  • ⚠️ IMPORTANT: Finding 4: Pipeline documented as eight tasks with the wrong concurrency pair; the SKILL.md added here defines nine, so every README table row is off by one (plugins/bitwarden-testing-tools/CHANGELOG.md:11, also README.md:31,137,141-148) (already raised on the pull request)
  • 🎨 SUGGESTED: Finding 5: Near-miss documented against qa-testing-notes, a skill that exists nowhere in this repository (plugins/bitwarden-testing-tools/skills/start-playwright-test/evals/README.md:9)
  • 🎨 SUGGESTED: Finding 6: The streaming fast path gates on ("Skill", "Read") while the new agent detection lives only on the complete-message path (plugins/bitwarden-testing-tools/evals/run_real_eval.py:98)
  • 🎨 SUGGESTED: Finding 7: Description never contains "Playwright", "browser", or "end-to-end", though every sibling it competes with leads with the tool name (plugins/bitwarden-testing-tools/skills/start-playwright-test/SKILL.md:3)
  • 🎨 SUGGESTED: Finding 8: Agent granted unscoped when the pipeline dispatches exactly six named agents (plugins/bitwarden-testing-tools/skills/start-playwright-test/SKILL.md:5)
  • 🎨 SUGGESTED: Finding 9: allowed-tools omits Skill and Bash(date:*), both of which the pipeline's own steps need, so an unattended run stops for two permission prompts (plugins/bitwarden-testing-tools/skills/start-playwright-test/SKILL.md:5)
  • 🎨 SUGGESTED: Finding 10: Report path written unquoted, against the single-quoting rule this same file states in Tasks 2 and 9 (plugins/bitwarden-testing-tools/skills/start-playwright-test/SKILL.md:308)
  • 🎨 SUGGESTED: Finding 11: Three test suites added by this PR are invoked by nothing (plugins/bitwarden-testing-tools/scripts/validate-guardrail.test.sh:1)
  • 🎨 SUGGESTED: Finding 12: The new guardrail check is asymmetric — it enforces three conditions on each AGENT.md but only one on the orchestrator SKILL.md (plugins/bitwarden-testing-tools/scripts/validate-guardrail.sh:37)
  • 🎨 SUGGESTED: Finding 13: Tasks 4 and 5 persist agent output with no fence check, while Task 2 validates its fence before persisting (plugins/bitwarden-testing-tools/skills/start-playwright-test/SKILL.md:130,147)
  • 🎨 SUGGESTED: Finding 14: Task 9 has no branch for a missing SERVICES fence, at the one point the file is otherwise careful about command-line inputs (plugins/bitwarden-testing-tools/skills/start-playwright-test/SKILL.md:264)
  • 🎨 SUGGESTED: Finding 15: Changelog lists the untrusted-source policy under 1.6.0 Added, but it pre-dates this changeset (plugins/bitwarden-testing-tools/CHANGELOG.md:15)

Findings 1–4 already carry unresolved inline threads on this pull request; per the re-run rule they were not re-posted. Details for every finding follow.

  • ⚠️ IMPORTANT: Finding 1 detail

    Details and fix

    Every Write this skill performs targets <cwd>/.playwright-testing-artifacts/<slug>/ (Tasks 2, 3, 4, 5, 6, 8 — there are no others). The content written is agent output the skill itself classifies as untrusted-source-derived ("Write the agent's response text verbatim").

    A bare Write pre-approves writing to any path for the turn. So a directive injected into a Jira ticket or a repo file that persuades the orchestrator to write somewhere else — .claude/settings.local.json, a hook script, a workflow file — lands without a prompt. That is the one escalation this skill's own trust boundary is built to stop, and it is the only grant on this line left open: all three Bash grants are pinned to exact script paths.

    Scope it with an Edit(...) rule, which is the documented path-rule form for file modification (there is no documented Write(<path>) rule form, and a Read/Edit path rule is what the docs say covers the Write tool):

    allowed-tools: "Agent, Read, Edit(.playwright-testing-artifacts/**), Bash(mkdir *), Bash(${CLAUDE_PLUGIN_ROOT}/scripts/repo-diff.sh *), Bash(${CLAUDE_SKILL_DIR}/scripts/open_report.py *)"

    Calibration: three sibling skills in this plugin (writing-manual-test-cases, assessing-test-coverage, recommending-test-layers) also grant bare Write, so this follows existing convention rather than departing from it. It is reported because the line is new in this changeset and because this is the one skill in the plugin whose job is handling untrusted agent output.

    Reference: https://code.claude.com/docs/en/permissions#read-and-edit

  • ⚠️ IMPORTANT: Finding 2 detail

    Details and fix

    This adds lint:guardrail and chains it into pnpm run lint on line 28. But .github/workflows/lint.yml never invokes pnpm run lint — it calls the two sub-targets individually:

    • lint.yml:61 → pnpm run lint:prettier
    • lint.yml:67 → pnpm run lint:spelling

    So validate-guardrail.sh runs only when a developer happens to type pnpm lint locally. CHANGELOG.md:15 states the check is "(run in pnpm lint) prevents drift" — in CI it does not run at all, so nothing stops a future edit from removing an agent's untrusted-source guard.

    Add a step to lint.yml alongside the existing two, wired into the same Fail if any linter failed gate:

          - name: Guardrail
            id: guardrail
            continue-on-error: true
            run: pnpm run lint:guardrail

    Then include steps.guardrail.outcome in the gate condition at lint.yml:71. Pairs with Finding 11.

  • ⚠️ IMPORTANT: Finding 3 detail

    Details and fix

    This says "Eight ship in this plugin" and lists eight. The plugin actually ships ten skills — ls plugins/bitwarden-testing-tools/skills/ returns assessing-test-coverage, checking-localhost-web-health, compiling-playwright-report, mapping-services-under-test, recommending-test-layers, running-playwright-tests, scoping-playwright-application-context, start-playwright-test, writing-manual-test-cases, writing-playwright-test-cases.

    recommending-test-layers and writing-manual-test-cases are both absent from the list, so the stated competition is ten skills when it is really twelve (ten in-plugin plus the two dependency skills).

    This matters because line 38 of this same file says "the recorded numbers are only meaningful against this exact inventory." writing-manual-test-cases is specifically the skill the write QA testing notes for this branch so a human can test it manually near-miss is aimed at — and that query timed out on all 7 runs, so its pass already carries no signal. The separation from the one sibling most likely to cross-fire is therefore unmeasured against an inventory that does not list it.

    Add the two missing skills to the list, correct "Eight" to "Ten" and the dependency sentence's count, and re-run trigger-eval.json with a longer --timeout so the two timed-out near-misses produce a real reading.

  • ⚠️ IMPORTANT: Finding 4 detail

    Details and fix

    skills/start-playwright-test/SKILL.md numbers the pipeline Tasks 1–9: parse input, gather context, explore codebase, determine required services, build test cases, compose test plan, verify environment health, execute tests, compile report.

    Three places say eight and name the wrong concurrent pair:

    • This line — "eight-task pipeline … Tasks 3 and 4 are dispatched together"
    • README.md:137 — same two claims
    • README.md:31 — the skills table, "in an eight-task pipeline"

    SKILL.md:118 is explicit: "Tasks 4 and 5 both need only the artifacts from Task 3, so dispatch services-under-test-mapper and playwright-test-case-writer in the same message."

    Because the README table starts at Task 1 = playwright-test-context-gatherer (which is Task 2 in SKILL.md), every row of the README.md:141-148 table is off by one — it does not just undercount, it maps the wrong task number to each agent. The table is also missing a row for the diff-<timestamp>.md artifact (SKILL.md:74-92) and attributes test-results-<timestamp>.json to the runner, though SKILL.md:210,219 has the runner emit segment-<K>-<timestamp>.json with the merge script producing test-results.

    Fix: change "eight-task" to "nine-task" and "Tasks 3 and 4" to "Tasks 4 and 5" in all three places, renumber the README table to match SKILL.md's Tasks 1–9, and add the diff-artifact row.

  • 🎨 SUGGESTED: Finding 5 detail

    Details and fix

    evals/README.md:9 says "qa-testing-notes is a separate skill, so proving no cross-fire is part of the set's job", and CHANGELOG.md:12 repeats it. grep -rn "qa-testing-notes" across the repository returns only those two lines — the skill does not exist here or in either dependency plugin. The manual-QA-notes skill is writing-manual-test-cases, in this same plugin, which is also the skill missing from the inventory in Finding 3.

    Rename the reference to writing-manual-test-cases in both places, or name the external plugin that supplies qa-testing-notes.

  • 🎨 SUGGESTED: Finding 6 detail

    Details and fix

    This PR adds Agent/Task dispatch detection at run_real_eval.py:132, on the assistant (complete-message) branch. The streaming branch at line 98 still gates pending on cb.get("name") in ("Skill", "Read"), so a dispatch is never caught on the partial-message fast path.

    Normally harmless — the assistant event arrives and line 132 fires. It matters on timeout: the run loop is bounded by --timeout, and evals/README.md:42 records that two queries in this very suite "timed out on all 7 runs." Because agent-non-trigger-eval.json is 3/3 should_trigger: false, a dispatch that has begun streaming but whose assistant message has not completed when the timeout fires is silently counted as a PASS — which undercuts the README.md:19 claim that "This suite is a real measurement". The new tests in evals/tests/test_run_real_eval.py exercise only the assistant path, so the gap is untested.

    Add "Agent", "Task" to the tuple at line 98 and extend the needle selection at line 107 to match subagent_type for them. Both the plugin validation and the configuration review flagged this independently.

  • 🎨 SUGGESTED: Finding 7 detail

    Details and fix

    The description reads "Use when you want UI tests planned and run against local Bitwarden web changes". It competes for selection against writing-playwright-test-cases ("Build structured Playwright test cases"), running-playwright-tests, and compiling-playwright-report — all of which name the tool. It is also purely a trigger condition with no capability-first clause and no negative scoping, where mapping-services-under-test solves the same problem with an explicit "Do NOT use it to…" clause.

    The PR's own measurement is consistent with the gap: evals/README.md:42 records the single query in the 0.35–0.65 ambiguity band as generate playwright test cases for this feature and actually run them against localhost at 4/7 — the query whose vocabulary is exactly the two words the description omits, and which matches two siblings almost word-for-word.

    Work the term into the first sentence, e.g. "End-to-end Playwright pipeline for Bitwarden web: plans, executes, and reports UI tests in one run", and add a boundary clause preferring this skill over writing-playwright-test-cases and running-playwright-tests when a request asks for both authoring and executing. Re-run trigger-eval.json after editing, since the recorded 10/10 reading was taken against the current wording.

  • 🎨 SUGGESTED: Finding 8 detail

    Details and fix

    The skill's own table at SKILL.md:34-39 enumerates the complete dispatch set — six agents. A bare Agent grant pre-approves dispatching any agent installed in the session, inheriting whatever tool access that agent carries. Narrowing it costs nothing here because the set is fixed and known:

    Agent(playwright-test-context-gatherer), Agent(playwright-application-context-scoper), Agent(services-under-test-mapper), Agent(playwright-test-case-writer), Agent(localhost-web-health-checker), Agent(playwright-test-runner)
    

    Rated below Finding 1 because an over-broad Agent grant dispatches agents that carry their own permission checks, whereas an over-broad Write writes directly.

    Reference: https://code.claude.com/docs/en/permissions

  • 🎨 SUGGESTED: Finding 9 detail

    Details and fix

    Two tools the pipeline's own steps call are absent from allowed-tools:

    • Skill — SKILL.md:211 and :262 both say "Invoke Skill(bitwarden-testing-tools:compiling-playwright-report) first. It carries the anchored grants for both report scripts, so the commands below run without a permission prompt." The grant that removes the prompt is itself not pre-approved. Three sibling skills in this plugin list Skill(...) entries explicitly.
    • Bash(date:*) — SKILL.md:24 orders a YYYYMMDD-HHmm timestamp generated "once now", and that value keys every artifact filename in the run. Session context supplies the date but not the time. Siblings assessing-test-coverage and recommending-test-layers both carry Bash(date:*) for this reason.

    This is friction, not a block — allowed-tools pre-approves rather than restricts, which is why the stronger form of this claim was dropped (see the dropped-findings section). But both prompts land partway through an otherwise unattended pipeline. Add Skill(bitwarden-testing-tools:compiling-playwright-report) and Bash(date:*) to line 5.

  • 🎨 SUGGESTED: Finding 10 detail

    Details and fix

    The report-open command at line 308 is written unquoted:

    ${CLAUDE_SKILL_DIR}/scripts/open_report.py <artifacts-output-dir>/report-<timestamp>.html
    

    Task 9 at line 274 mandates "single-quoting every value" and Task 2 at line 74 mandates single-quoting the repo path. Not a security hole — the slug is sanitized deny-by-default at line 68 and open_report.py re-validates the path — but <artifacts-output-dir> is rooted at the user's working directory, which may contain a space, which would split into two argv and make open_report.py exit 2 on its usage check.

    Single-quote the path, matching Tasks 2 and 9.

  • 🎨 SUGGESTED: Finding 11 detail

    Details and fix

    scripts/validate-guardrail.test.sh, evals/tests/test_run_real_eval.py, and skills/start-playwright-test/scripts/tests/test_open_report.py are all added by this PR, and no workflow under .github/workflows/ references pytest, unittest, or *.test.sh. test_open_report.py is the security-relevant one: it covers the symlink, .., and symlinked-run-folder escapes that open_report.py's path confinement depends on.

    Add a CI step running python3 -m unittest discover over the plugin's test directories plus bash plugins/bitwarden-testing-tools/scripts/validate-guardrail.test.sh. Pairs naturally with the fix for Finding 2.

  • 🎨 SUGGESTED: Finding 12 detail

    Details and fix

    validate-guardrail.sh:25-33 checks each AGENT.md for three things: the Untrusted source content. heading, the policy reference, and the absence of the stale paragraph. The orchestrator SKILL.md, checked at line 37, gets only the policy-reference grep.

    Since the guardrail block at SKILL.md:43 carries the same heading as the agents do, a reword that drops the heading from the orchestrator would pass the check written to catch exactly that drift — and the orchestrator is the component that reads untrusted feature source first.

    Add a heading check alongside the existing one:

    grep -qF "$HEADING" "$SKILL" || { echo "MISSING guardrail heading: $SKILL"; rc=1; }
  • 🎨 SUGGESTED: Finding 13 detail

    Details and fix

    SKILL.md:58 requires "exactly one well-formed CONTEXT START…CONTEXT END block" before Task 2 persists anything. Task 5 at :145 merely asserts the test-cases response is fenced and :147 writes it verbatim regardless; Task 4 at :130 has no fence statement at all.

    A malformed fence then surfaces two tasks later, after the health checker or runner aborts on it, wasting the Task 7 dispatch and leaving a half-written artifact set behind.

    Add the same "confirm exactly one well-formed fence, otherwise stop and report without persisting" sentence before the Persist step in Tasks 4 and 5.

  • 🎨 SUGGESTED: Finding 14 detail

    Details and fix

    Task 9 at :264 says "Locate the <!-- SERVICES START --> / <!-- SERVICES END --> fence in the test plan and read its ## Required Services bullets", then validates the two extracted values at :266-269 and halts if "either fails". Nothing covers the fence being absent or unterminated, which leaves the model free to improvise a services string at the exact point the file is otherwise careful about what reaches a command line.

    Extend :269 to "If either fails, or the SERVICES fence is absent or unterminated, do not run the render script…".

  • 🎨 SUGGESTED: Finding 15 detail

    Details and fix

    CHANGELOG.md:15 lists the untrusted-source trust boundary under Added for 1.6.0. references/untrusted-source-policy.md is not in this PR's diff against c869f1b, so it pre-dates the changeset, and the six AGENT.md files changed only their version: line — meaning the per-agent guards pre-date it too. What is genuinely new is scripts/validate-guardrail.sh and the orchestrator's own guard block.

    Rescope the entry to the validator script and the orchestrator guardrail, referencing the earlier release for the policy itself.

Dropped after documentation check: six findings claimed a frontmatter field, permission form, or slash-command form behaved other than as written. All six were refuted by the official documentation.

  • skills/start-playwright-test/SKILL.md:5 allowed-tools omits Skill, claimed to block the compiling-playwright-report invocation at lines 211/262 and stall Tasks 8 and 9 entirely — "It does not restrict which tools are available: every tool remains callable, and your permission settings still govern tools that are not listed." (https://code.claude.com/docs/en/skills). Reduced to a prompt, which is Finding 9.
  • skills/start-playwright-test/SKILL.md:5 Bash rule form, claimed that Bash(cmd *) with a space is not the documented prefix form and that the plugin's split between cmd * and cmd:* means one spelling never fires — "The :* suffix is an equivalent way to write a trailing wildcard, so Bash(ls:*) matches the same commands as Bash(ls *)." (https://code.claude.com/docs/en/permissions). Raised independently by both checkers; both copies dropped.
  • skills/start-playwright-test/SKILL.md:4 argument-hint, claimed to be a slash-command-only field ignored by skill loading — "| argument-hint | No | Hint shown during autocomplete to indicate expected arguments. Example: [issue-number] or [filename] [format]. |" (https://code.claude.com/docs/en/skills)
  • skills/start-playwright-test/SKILL.md:215,222,274,286, claimed the orchestrator's ${CLAUDE_PLUGIN_ROOT}/skills/compiling-playwright-report/scripts/... command form cannot match compiling-playwright-report's ${CLAUDE_SKILL_DIR}/scripts/... grant — "Claude Code substitutes ${CLAUDE_SKILL_DIR} and ${CLAUDE_PROJECT_DIR} in two places: the skill's markdown content, and Bash rules in the allowed-tools frontmatter." (https://code.claude.com/docs/en/skills). Both sides substitute to the same absolute path.
  • plugins/bitwarden-testing-tools/README.md:50 Agent(<name>), claimed to be an unverified assertion about the permission schema with Task(<name>) as the real spelling — "Use Agent(AgentName) rules to control which subagents Claude can use", "Agent(my-custom-agent) matches a custom subagent named my-custom-agent", with a "deny": ["Agent(Explore)"] example. (https://code.claude.com/docs/en/permissions). The README sentence is accurate as written.
  • plugins/bitwarden-testing-tools/README.md:120,128-132 /start-playwright-test, claimed unresolvable because plugin skills require the /plugin:skill form — "The bare /fancy also invokes the skill unless another command already uses that name." (https://code.claude.com/docs/en/skills). The qualified form is the repo's convention elsewhere, but the bare form documented here works.

Also dropped, on measurement rather than documentation: a claim that SKILL.md is at or past the 3,000-word progressive-disclosure ceiling and needs a references/ split. wc -w reports 2,539 words over 350 lines, inside the 1,000–3,000 target.

Checks run

Check Status
Plugin structure Not run here — dedicated workflow step; see the job log
Marketplace Not run here — dedicated workflow step; see the job log
Version bump Not run here — dedicated workflow step. Checked incidentally and consistent: 1.5.0 → 1.6.0 in marketplace.json, plugin.json, README catalog, and all six AGENT.md, with a matching ## [1.6.0] CHANGELOG entry. MINOR is correct for an added skill
Plugin validation (AI) Ran — manifest, semver, structure, component frontmatter, hook schema, ${CLAUDE_PLUGIN_ROOT} usage, and credential scan all clean; no MCP config present to validate
Skill review (AI) Ran for start-playwright-test/SKILL.md, the only SKILL.md with a diff against the merge base. The other six in the CI list have zero diff here — see the scope note
Configuration & security Ran — agents, hooks, plugin support files (evals/, scripts/, eval JSON), README, CHANGELOG, and root package.json, .cspell.json, .gitignore, .claude-plugin/marketplace.json

What the security review found

No secrets, no credentials, and no settings.local.json in the changeset. No file attempts to direct this review (no CWE-1427 finding). The injection strings in skills/*/evals/behavior-eval.json are declared prompt-injection fixtures inside JSON prompt fields, framed by their purpose field and sibling READMEs, and routed nowhere executable — run_real_eval.py reads only a flat array of {query, should_trigger}, and the gatherer fixture is an object under an evals key in a directory no runner is pointed at. They were treated as data.

The shell-adjacent surfaces this PR adds are tightened rather than widened, and are worth recording as such:

  • SKILL.md:68 sanitizes the slug deny-by-default with an explicit ^[a-z0-9][a-z0-9-]*$ post-check.
  • SKILL.md:74 allowlists affected repos to three literals before any path reaches repo-diff.sh, which re-allowlists by basename.
  • SKILL.md:265-271 regex-checks the services string and allowlists the base URL before the render command line, and passes a ticket key or slug for --plan-name so raw description text never reaches argv. (Finding 14 is the one gap in that check.)
  • scripts/open_report.py replaces a broad open/xdg-open grant with a script enforcing a filename regex, islink and isfile checks, and a realpath grandparent match against the artifacts root, then execs a fixed argument list with no shell. The auto-open path was also checked for stored XSS under file://: render_report.py HTML-escapes every interpolated value.
  • scripts/validate-guardrail.sh takes no runtime input; all greps are grep -qF with fixed strings and quoted paths. Its gate was verified statically to pass — all six AGENT.md files carry both anchors. Finding 12 covers its one asymmetry.

Finding 1 is the one place the changeset leaves a grant open, which is why it carries the verdict.

@kdenney
kdenney force-pushed the add/testing-tools-orchestration branch from 7661e9a to e6ddd98 Compare August 22, 2026 03:28
@kdenney
kdenney force-pushed the add/testing-tools-orchestration branch from e6ddd98 to 397c7b0 Compare August 26, 2026 20:13
@kdenney
kdenney force-pushed the add/testing-tools-orchestration branch from d5f360f to 1645262 Compare August 26, 2026 20:59
@kdenney
kdenney force-pushed the add/testing-tools-orchestration branch from 1645262 to 4cf8e05 Compare August 27, 2026 21:25
@kdenney
kdenney force-pushed the add/testing-tools-orchestration branch 2 times, most recently from 9a52299 to d599e2f Compare August 28, 2026 20:57
@kdenney
kdenney force-pushed the add/testing-tools-orchestration branch from d599e2f to e05bfbb Compare August 28, 2026 21:59
@kdenney
kdenney force-pushed the add/testing-tools-orchestration branch from e05bfbb to 7e45c0b Compare August 31, 2026 17:27
@kdenney
kdenney force-pushed the add/testing-tools-orchestration branch 2 times, most recently from b4ec0a8 to db03cad Compare September 1, 2026 15:45
@kdenney
kdenney force-pushed the add/testing-tools-orchestration branch from db03cad to b10d2bd Compare September 1, 2026 23:25
@kdenney
kdenney force-pushed the add/testing-tools-orchestration branch from b10d2bd to c97db5f Compare September 3, 2026 16:55
@kdenney
kdenney force-pushed the add/testing-tools-orchestration branch 2 times, most recently from 3503de2 to 55c30e2 Compare September 4, 2026 20:43
@kdenney
kdenney force-pushed the add/testing-tools-orchestration branch 2 times, most recently from 1d07e94 to 3553b08 Compare October 8, 2026 20:35
@kdenney
kdenney force-pushed the add/testing-tools-orchestration branch from 3553b08 to d3e1365 Compare October 8, 2026 20:43
@kdenney
kdenney force-pushed the add/testing-tools-orchestration branch from d3e1365 to fa10d1b Compare October 9, 2026 17:57
name: start-playwright-test
description: Use when you want UI tests planned and run against local Bitwarden web changes, starting from a Jira ticket, an implementation plan, or a description of the feature. Requires the Bitwarden local dev environment to already be running; this pipeline verifies services and required feature flags but never starts services or changes flags. Accepts a Jira ticket ID, a Jira browse URL, an implementation plan file path, or a feature description, optionally followed by extra instructions.
argument-hint: "<jira-ticket-id | jira-url | feature-plan-path | feature-description> [extra instructions]"
allowed-tools: "Agent, Read, Write, Bash(mkdir *), Bash(${CLAUDE_PLUGIN_ROOT}/scripts/repo-diff.sh *), Bash(${CLAUDE_SKILL_DIR}/scripts/open_report.py *)"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ IMPORTANT: Write is granted unscoped on a line that carefully scopes every Bash grant.

Details and fix

Every Write this skill performs targets <cwd>/.playwright-testing-artifacts/<slug>/ (Tasks 2, 3, 4, 5, 6, 8 — there are no others). The content written is agent output the skill itself classifies as untrusted-source-derived ("Write the agent's response text verbatim").

A bare Write pre-approves writing to any path for the turn. So a directive injected into a Jira ticket or a repo file that persuades the orchestrator to write somewhere else — .claude/settings.local.json, a hook script, a workflow file — lands without a prompt. That is the one escalation this skill's own trust boundary is built to stop, and it is the only grant on this line left open: all three Bash grants are pinned to exact script paths.

Scope it with an Edit(...) rule, which is the documented path-rule form for file modification (Read and Edit rules; there is no documented Write(<path>) rule form):

allowed-tools: "Agent, Read, Edit(.playwright-testing-artifacts/**), Bash(mkdir *), Bash(${CLAUDE_PLUGIN_ROOT}/scripts/repo-diff.sh *), Bash(${CLAUDE_SKILL_DIR}/scripts/open_report.py *)"

Calibration: three sibling skills in this plugin (writing-manual-test-cases, assessing-test-coverage, recommending-test-layers) also grant bare Write, so this follows existing convention rather than departing from it. It is reported because the line is new in this changeset and because this is the one skill in the plugin whose job is handling untrusted agent output.

Reference: https://code.claude.com/docs/en/permissions#read-and-edit

Comment thread package.json
"lint": "pnpm run lint:prettier && pnpm run lint:spelling && pnpm run lint:guardrail",
"lint:prettier": "prettier --check .",
"lint:spelling": "cspell lint --no-progress --no-summary --gitignore '**'",
"lint:guardrail": "bash plugins/bitwarden-testing-tools/scripts/validate-guardrail.sh",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ IMPORTANT: The new guardrail check never runs in CI, so the drift it is meant to prevent is unguarded.

Details and fix

This adds lint:guardrail and chains it into pnpm run lint on line 28. But .github/workflows/lint.yml never invokes pnpm run lint — it calls the two sub-targets individually:

  • lint.yml:61 → pnpm run lint:prettier
  • lint.yml:67 → pnpm run lint:spelling

So validate-guardrail.sh runs only when a developer happens to type pnpm lint locally. CHANGELOG.md:15 states the check is "(run in pnpm lint) prevents drift" — in CI it does not run at all, so nothing stops a future edit from removing an agent's untrusted-source guard.

Add a step to lint.yml alongside the existing two, wired into the same Fail if any linter failed gate:

      - name: Guardrail
        id: guardrail
        continue-on-error: true
        run: pnpm run lint:guardrail

Then include steps.guardrail.outcome in the gate condition at lint.yml:71.

While you are there: scripts/validate-guardrail.test.sh, evals/tests/test_run_real_eval.py, and skills/start-playwright-test/scripts/tests/test_open_report.py are all added by this PR and are likewise invoked by nothing. test_open_report.py is the security-relevant one — it covers the symlink, .., and symlinked-run-folder escapes in open_report.py.


## Inventory

These readings were recorded against the ten sibling skills the orchestrator competes with for selection (each reading below notes its own run date). Eight ship in this plugin:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ IMPORTANT: The recorded inventory is wrong, which undercuts the trigger readings it qualifies.

Details and fix

This says "Eight ship in this plugin" and lists eight. The plugin actually ships ten skills — ls plugins/bitwarden-testing-tools/skills/ returns:

assessing-test-coverage          recommending-test-layers
checking-localhost-web-health    running-playwright-tests
compiling-playwright-report      scoping-playwright-application-context
mapping-services-under-test      start-playwright-test
                                 writing-manual-test-cases
                                 writing-playwright-test-cases

recommending-test-layers and writing-manual-test-cases are both absent from the list, so the stated competition is ten skills when it is really twelve (ten in-plugin plus the two dependency skills).

This matters because line 38 of this same file says "the recorded numbers are only meaningful against this exact inventory." writing-manual-test-cases is specifically the skill the write QA testing notes for this branch so a human can test it manually near-miss is aimed at — and that query timed out on all 7 runs, so its pass already carries no signal. The separation from the one sibling most likely to cross-fire is therefore unmeasured against an inventory that does not list it.

Add the two missing skills to the list, correct "Eight" to "Ten" and the dependency sentence's count, and re-run trigger-eval.json with a longer --timeout so the two timed-out near-misses produce a real reading.


### Added

- `start-playwright-test`, the pipeline entry point and the only orchestration skill. It accepts a Jira ticket id, a Jira browse URL, an implementation plan path, or a feature description, optionally followed by extra guidance (passed verbatim to the scoper, where it can call for a marketing-initiated or sales-assisted trial). It runs an eight-task pipeline, dispatching six agents and persisting each response verbatim to `.playwright-testing-artifacts/<slug>/`, then renders an HTML report. Tasks 3 and 4 are dispatched together and run concurrently. After gathering context it checks that each affected repo is exactly `clients`, `server`, or `billing-pricing`, then runs `scripts/repo-diff.sh` for each one, under a grant scoped to that script, and writes the changed files to `diff-<timestamp>.md`, which the scoper and mapper read in place of running the script. A required feature flag in the wrong state, reported by the health check, halts the run like any other environment failure, so a run never pauses to ask about a flag. When the report is written, it opens it in the user's browser through `skills/start-playwright-test/scripts/open_report.py`, under a grant scoped to that script; the script opens only a `report-<timestamp>.html` inside a run folder under `.playwright-testing-artifacts/`, using `open` on macOS or `xdg-open` on Linux, and skips other platforms, with unit tests.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ IMPORTANT: The pipeline is documented as eight tasks with the wrong concurrency pair; the SKILL.md added here defines nine.

Details and fix

skills/start-playwright-test/SKILL.md numbers the pipeline Tasks 1–9:

Task 1 Parse input
Task 2 Gather context
Task 3 Explore codebase
Task 4 Determine required services
Task 5 Build test cases
Task 6 Compose test plan
Task 7 Verify environment health
Task 8 Execute tests
Task 9 Compile report

Three places say eight and name the wrong concurrent pair:

  • This line — "eight-task pipeline … Tasks 3 and 4 are dispatched together"
  • README.md:137 — same two claims
  • README.md:31 — the skills table, "in an eight-task pipeline"

SKILL.md:118 is explicit: "Tasks 4 and 5 both need only the artifacts from Task 3, so dispatch services-under-test-mapper and playwright-test-case-writer in the same message."

Because the README table starts at Task 1 = playwright-test-context-gatherer (which is Task 2 in SKILL.md), every row of the README.md:141-148 table is off by one — it does not just undercount, it maps the wrong task number to each agent. The table is also missing a row for the diff-<timestamp>.md artifact (SKILL.md:74-92) and attributes test-results-<timestamp>.json to the runner, though SKILL.md:210,219 has the runner emit segment-<K>-<timestamp>.json with the merge script producing test-results.

Fix: change "eight-task" to "nine-task" and "Tasks 3 and 4" to "Tasks 4 and 5" in all three places, renumber the README table to match SKILL.md's Tasks 1–9, and add the diff-artifact row.

kdenney added 20 commits October 9, 2026 13:22
Squashed net diff of the orchestration layer (start-playwright-test) for a clean reparent.
…tches it

`checking-localhost-web-health` is a skill, which agents invoke/run;
dispatch is for agents (the Agent tool). Reword the README agent row for
localhost-web-health-checker accordingly. No behavior change.
… contract

The untrusted-source guardrail moved its rules into a shared
references/untrusted-source-policy.md, referenced by each agent, so the
per-agent prose no longer contains the old "names this run's fence token"
backstop string. Retarget validate-guardrail.sh to anchor each AGENT.md on
the "Untrusted source content." heading and the UNTRUSTED-SOURCE-<nonce>
fence binding — phrases both the current and earlier guardrail forms share,
so agents can migrate one at a time without turning the stack red — and add
a check that the shared policy file exists. Update the test fixtures to the
new contract and add cases for a missing fence binding, a missing policy
file, and a stale paragraph alongside a valid guardrail.
… the shared policy

The orchestrator prepended a full copy of the untrusted-source rules to
every agent dispatch, duplicating the rules that now live in
references/untrusted-source-policy.md. Slim the prepended block to what
only the orchestrator can supply — this run's fence token and the binding
of the shared policy to the UNTRUSTED-SOURCE-<nonce> region — and defer the
rule text to the policy file, so the whole pipeline has one source of truth
for the guardrail. The nonce generation and fence-binding mechanism are
unchanged.
…ation layer

With the gatherer now distilling and discarding the raw feature source, the
per-run nonce fence has nothing left to protect, so remove it end to end.

- start-playwright-test: drop the run-token generation and the gen-nonce.sh
  grant; rewrite the dispatch guardrail to point at the shared policy with no
  fence or token; in Task 2 replace the "exactly one UNTRUSTED-SOURCE pair"
  check with a CONTEXT-fence + three-section check, and drop the
  non-load-bearing Source Summary note from artifact persistence.
- Delete scripts/gen-nonce.sh and gen-nonce.test.sh.
- validate-guardrail.sh: retarget from the UNTRUSTED-SOURCE fence to the
  shared-policy contract — every AGENT.md carries the "Untrusted source
  content." heading and names references/untrusted-source-policy.md, the
  orchestrator skill names it too, and the policy file exists. Drop the fence
  and gen-nonce checks. Update the test fixtures and cases to match.
- Rewrite the gatherer's injection behavior eval: the injected imperative must
  now appear nowhere in the artifact (there is no fence to reproduce it into)
  and must not be acted on; update its README and expectation count.
- Reword the 1.6.0 changelog trust-boundary bullet to the sanitize-and-discard
  model.
…EADME skills table

The orchestration layer's README skills table omitted the
compiling-playwright-report skill, though the skill ships in the plugin.
Add its row so the table is complete.
Plugin agent discovery loads markdown files under agents/ as agents, so
the gatherer's evals/README.md registered as an unrestricted agent of
its own, with all tools and no frontmatter. Move the eval beside the
orchestrator's agent suites under skills/start-playwright-test/evals/,
where skill discovery reads only SKILL.md, and point the orchestrator's
eval README and the 1.6.0 changelog at it.
…e orchestrator

The scoper and mapper no longer hold Bash, so the orchestrator runs
repo-diff.sh in the main session and writes the changed files for them.
… runs repo-diff.sh

The repo names derive from untrusted feature source and go into a main-session
shell command, where the planning-agent hook does not apply.
Extra instructions were only folded into dispatch prompts at the
orchestrator's discretion and never reached the scoper's skill, so
asking for a marketing or sales-assisted trial after the ticket ID
could silently fall back to the default paid-org signup.
…ght-test

The pipeline now always runs from test plan to execution without pausing,
so the optional review gate and the flag that switched it on are gone.
The health check now reports how many required flags it verified, and a
flag in the wrong state is one of its failures. The orchestrator must
recognize both success forms so it proceeds or halts correctly.
…inishes

The orchestrator now hands the written report to open_report.py, which opens it
with open on macOS or xdg-open on Linux. The skill grants only that script, not
an opener that would accept any file or URL, and the script refuses anything but
a report-<timestamp>.html inside a run folder under .playwright-testing-artifacts.
Task 9 put the SERVICES fence's services string and base URL, agent
output derived from untrusted feature source, into double-quoted render
arguments, where $(...) still expands. They are now checked against
tight patterns and single-quoted, and --plan-name takes the ticket key
or slug rather than raw description text or a file path.

Also stop the run when merge_results.py fails instead of branching on a
status that was never read, drop the orchestrator's instruction to
research referenced tickets itself (the gatherer owns raw source), use
the namespaced compiling-playwright-report skill, narrow the agent
non-trigger suite to the three orchestrator-only agents (the other
three are standalone-capable by design), and bring the agents' version
fields to 1.6.0.
Moves the runner's --agent flag, its unit tests, and the working-tree
recipe up from the foundation layer to beside their only consumer, the
agent non-trigger suite here. The recipe now loads the two vendor plugins
with this one: with only this plugin's directory, claude reports its
dependencies as not installed and loads none of its skills.

Also restores the README's dependency rows, which this layer's first
commit dropped when rebased, drops the eval README's note about the
vendor suites (they moved with their plugins), and records in the
orchestrator inventory that two of its sibling skills come from the
dependency plugins.
…the split inventory

Re-run on 2026-10-07 with the two vendor plugins loaded alongside this one:
10/10 and 10/10. One should-trigger query passed at 4/7, and two
should-not-trigger queries timed out on every run, so their passes carry no
signal. Recorded as measured rather than carrying the 2026-08-01 reading
forward.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant