From ac91484f2609b30eb38e1cad38e0c9053db3b29e Mon Sep 17 00:00:00 2001 From: Ovid Date: Tue, 30 Jun 2026 13:20:13 +0400 Subject: [PATCH 01/19] Add design: bundle PAAD skills into a Kiro power Validated via brainstorming + adversarial pushback. Key decisions: single repo (POWER.md + steering/ at the paad repo root, since GitHub installs require POWER.md at root), generated from SKILL.md as the single source of truth, manual inclusion mode (slash-command invocation), and a scripts/kiro-keywords.yaml sidecar for Kiro keywords (testing showed claude plugin validate ignores SKILL.md frontmatter, so keywords stay out of the skills). No conflict with the existing Claude Code plugin: it lives in plugins/paad/, leaving the repo root as a pure marketplace root. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../2026-06-30-paad-kiro-power-design.md | 294 ++++++++++++++++++ 1 file changed, 294 insertions(+) create mode 100644 docs/plans/2026-06-30-paad-kiro-power-design.md diff --git a/docs/plans/2026-06-30-paad-kiro-power-design.md b/docs/plans/2026-06-30-paad-kiro-power-design.md new file mode 100644 index 0000000..bb1cc7f --- /dev/null +++ b/docs/plans/2026-06-30-paad-kiro-power-design.md @@ -0,0 +1,294 @@ +# Design: Bundle PAAD skills into a Kiro Power + +**Date:** 2026-06-30 +**Status:** Design v2 — revised after adversarial pushback review (findings folded in below) +**Goal:** Distribute PAAD's skills to Kiro IDE users as an installable Kiro "power," maintained alongside the existing Claude Code plugin from a single source of truth. + +## Background + +PAAD is a Claude Code plugin marketplace (`github.com/Ovid/paad`) distributing 9 skills: +`agentic-a11y`, `agentic-architecture`, `agentic-review`, `alignment`, `fix-architecture`, +`help`, `makefile`, `pushback`, `vibe`. Each skill is a pure prompt/instruction skill — a +`SKILL.md` with `name` + `description` frontmatter, markdown instructions, and a graphviz +digraph. No executable tools or MCP servers. + +A Kiro **power** bundles tools, workflows, and best practices that the Kiro IDE activates +on-demand. Verified structure (from https://kiro.dev/docs/powers/create/ and +https://kiro.dev/docs/steering/): + +- **POWER.md** (required) — frontmatter (`name`, `displayName`, `description`, `keywords`, + `author`) + onboarding/steering body. Power-level `keywords` drive when the power activates. +- **mcp.json** (optional) — MCP server config. **Not needed** for PAAD (no tools/servers). +- **steering/** (optional) — workflow-specific markdown guidance files. + +Powers install via the Kiro IDE, kiro.dev, or GitHub repo URLs ("Add Custom Power → Import +from GitHub" / "Local Directory" with an absolute path to the power directory). + +**Verified install constraint (load-bearing):** for GitHub-URL installs, *"The power must have +a valid `POWER.md` file in the repository root"* (https://kiro.dev/docs/powers/installation/). +There is **no subdirectory/path support** for GitHub imports. Kiro consumes the committed +files directly (it does not build from source) and updates by refreshing from the remote repo. +This single fact drives the distribution decision below. + +**Verified discovery mechanism:** manual steering files are discovered two native ways +(https://kiro.dev/docs/steering/): typing `#steering-file-name` in chat, and — importantly — +*"Manual steering files also appear as slash commands — type `/` in chat to see and select +them."* The native `/` slash-command list is the discovery primitive; we do **not** need to +invent a POWER.md dispatcher to surface `#names`. + +## Decisions (from brainstorming, revised after pushback) + +1. **Goal:** Reach Kiro users — a real, maintained second distribution channel. +2. **Granularity:** One `paad` power; each skill becomes a steering file (one install, one + identity). +3. **Sync model:** Generated from `SKILL.md` (single source of truth). Kiro artifacts are + generated and committed, never hand-edited. +4. **Inclusion mode:** All steering files use `inclusion: manual` — surfaced as native `/` + slash commands (and `#` references), the truest equivalent of deliberate `/paad:` + invocation, with no skill-level auto-firing of a gated multi-agent audit. +5. **Distribution = single repo, `POWER.md` at the `paad` repo root (REVISED twice).** + - v1 was `powers/paad/` subdirectory — *rejected*: GitHub-URL installs require `POWER.md` + at the **repository root** (verified), and a subdirectory cannot be installed. + - v2 was a dedicated `Ovid/paad-kiro` repo — *also workable, but rejected for ceremony*: it + adds a second repo and a cross-repo publish step. + - **v3 (chosen): generate `POWER.md` + `steering/` directly into the existing `paad` repo + root.** Kiro's only hard requirement is a valid `POWER.md` at the repo root, which `paad` + already has. Kiro users install `github.com/Ovid/paad`; Kiro reads `POWER.md` + `steering/` + and ignores everything else in the repo. + - **Why:** lowest ceremony (no second repo, no submodule, no cross-repo push), and the + strongest drift/provenance story because source and generated artifact share one git + history — `git diff --exit-code` and commit history alone prove the power matches the + `SKILL.md` files that produced it. + - **Trade-off accepted:** the marketplace repo root carries Kiro files alongside + `.claude-plugin/`, and Kiro consumers pull the whole repo (harmless — Kiro reads only + POWER.md + steering). + - **One caveat to confirm before build:** that Kiro tolerates unrelated extra files at the + repo root (very likely — the docs state only the POWER.md-at-root requirement, not an + exclusivity rule — but verify empirically with a test install). + +## Verified: Kiro steering inclusion modes + +Frontmatter **must be the very first content** in the file — no blank lines or content before it. + +| Mode | Syntax | Activation | +|---|---|---| +| always | `inclusion: always` | Every interaction | +| fileMatch | `inclusion: fileMatch` + `fileMatchPattern: "..."` | When editing matching files | +| **manual** | `inclusion: manual` | Only when user types `#steering-file-name` | +| auto | `inclusion: auto` + `name:` + `description:` | When the request matches `description` | + +PAAD uses **manual** for all steering files (decision 4). `auto` was considered (it is the +native analog of Claude Code's description-based skill activation) but rejected because PAAD's +gated workflows should not auto-fire from conversation alone. Manual files are invoked +deliberately via the native `/` slash-command list or `#` — no dispatcher needed. + +**Residual noise caveat (from pushback):** the *power itself* still activates on its +power-level `keywords` (POWER.md frontmatter). So a broad aggregate keyword set re-introduces +"noisy activation" at the power level — the very thing manual mode avoids at the skill level. +This is a real tension, not fully eliminated; mitigation is a *curated, narrow* aggregate +keyword set (see open questions), not a naive union of every skill's keywords. Even when the +power activates, no gated workflow runs until the user explicitly invokes its slash command. + +## Repo layout (single repo) + +Everything lives in `github.com/Ovid/paad`. `POWER.md` + `steering/` sit at the root +(required for GitHub-URL install) and are generated; only `SKILL.md` is hand-edited. + +``` +paad/ ← Kiro installs this repo; reads root POWER.md + steering/ +├── POWER.md ← GENERATED, root (required for GitHub-URL install) +├── steering/ ← GENERATED +│ ├── agentic-a11y.md +│ ├── agentic-architecture.md +│ ├── agentic-review.md +│ ├── alignment.md +│ ├── fix-architecture.md +│ ├── makefile.md +│ ├── pushback.md +│ └── vibe.md ← 8 files: all skills EXCEPT help (see edge cases) +├── plugins/paad/skills/*/SKILL.md ← canonical source (unchanged, hand-edited) +├── scripts/ +│ └── build-kiro-power. ← generator: reads SKILL.md → writes POWER.md + steering/ +├── .claude-plugin/ ← Claude Code marketplace (Kiro ignores) +├── CLAUDE.md, README.md, docs/ ← (Kiro ignores) +``` + +Workflow: edit a `SKILL.md` → run the generator (`make kiro`) → commit `POWER.md` + `steering/` +in the same repo. No second repo, no push to elsewhere, no submodule. Kiro users who already +installed the power get updates via Kiro's "refresh from remote." + +## Claude Code coexistence check (no conflict) + +Verified against https://code.claude.com/docs/en/plugins. The key structural fact that makes +Option D safe: **the Claude Code plugin is not at the repo root — it lives at `plugins/paad/`.** +So the repo root is purely a *marketplace* root, whose only artifact Claude Code reads is +`.claude-plugin/marketplace.json`. + +- **Reserved names are scoped to the *plugin* root, not the repo root.** Claude Code recognizes + `skills/`, `commands/`, `agents/`, `hooks/`, `monitors/`, `bin/`, `.mcp.json`, `.lsp.json`, + `settings.json`, and `.claude-plugin/` **at the plugin root** (`plugins/paad/`). None of these + is `POWER.md` or `steering/`, and none is read from the marketplace repo root anyway. +- **`POWER.md` + `steering/` at the repo root collide with nothing.** They sit alongside + `.claude-plugin/`, `plugins/`, `README.md`, `docs/` — all ignored by the other tool. Claude + scans `plugins/paad/` for plugin components; Kiro reads root `POWER.md` + `steering/`. Fully + orthogonal directories. +- **The `paad` name reused by both** (Kiro power `name: paad`, Claude plugin `name: paad`) is + fine — different tools, different namespaces; it's intentional shared branding. +- **Two residual empirical checks** (folded into "must-resolve" below): + 1. Run `claude plugin validate .` *after* adding root `POWER.md` + `steering/` to confirm + marketplace validation tolerates the extra root entries (expected to pass — marketplaces + routinely carry README/LICENSE/docs at root). + 2. ~~`keywords`-in-frontmatter tolerance~~ — **RESOLVED 2026-06-30**: testing showed + `claude plugin validate` doesn't inspect `SKILL.md` frontmatter at all (it passes even on + bogus keys), so the question is moot — we use a sidecar and leave `SKILL.md` untouched. + See "Keywords source: sidecar file" above. + +## The mapping (per skill) + +| SKILL.md | → | Kiro power | +|---|---|---| +| `skills//SKILL.md` body | → | `steering/.md` body | +| frontmatter `name`, `description` | → | aggregated into POWER.md "when to load" mapping | +| keywords (from **sidecar**, see below) | → | aggregated into POWER.md `keywords` | +| ```` ```dot ```` digraphs | → | copied verbatim (agent guidance — valid markdown) | +| `$ARGUMENTS` usage | → | rewritten to "the user may provide a path/scope" prose | + +### Keywords source: sidecar file (DECIDED — was: SKILL.md frontmatter) + +Keywords live in a generator-side sidecar `scripts/kiro-keywords.yaml`, keyed by skill name — +**not** in `SKILL.md` frontmatter. + +```yaml +# scripts/kiro-keywords.yaml +agentic-architecture: [architecture, coupling, structure, design review, tech debt] +pushback: [pushback, spec review, requirements, scope, feasibility] +# ...one line per skill +``` + +**Why the sidecar, not frontmatter (resolved 2026-06-30 by test — pushback SERIOUS-5):** +- `claude plugin validate` was run with `keywords` added, and again with a deliberately *bogus* + frontmatter key. **Both passed** — proving `validate` only checks `plugin.json` and does + **not** inspect `SKILL.md` frontmatter at all. So validation can never confirm that an unknown + frontmatter key is runtime-safe; there is no clean green signal to rely on. +- Keywords are a *Kiro* concern, not a PAAD-skill concern — keeping them in the generator keeps + `SKILL.md` byte-for-byte untouched (zero risk to the Claude plugin) and is cleaner separation. +- Single-source-of-truth is preserved: the sidecar is the canonical source for the *Kiro-only* + keyword metadata, versioned next to the generator that consumes it. +- The generator **warns** if a skill in `skills/` has no entry in the sidecar (catches drift + when a new skill is added). + +## Generated POWER.md shape + +```yaml +--- +name: paad +displayName: PAAD — Architecture, Review & Quality Skills +description: Multi-agent architecture analysis, code review, accessibility, and quality workflows. +keywords: [architecture, review, accessibility, a11y, pushback, vibe, alignment, makefile, ...] +author: Ovid +--- +``` + +Body: **onboarding** (what PAAD is) plus a generated **"When to load steering files" +mapping** — the agent-facing routing pattern the Kiro docs describe (workflow → file). This is +derived from each skill's `name` + `description` (the `paad:help` content, generated, so it +cannot drift). Discovery for the *user* is the native `/` slash-command list, not a hand-rolled +index — the POWER.md mapping exists so Kiro's agent loads the right steering file per request, +not as a user-facing `#name` directory (correcting the v1 "dispatcher" framing). + +## Generated steering file shape + +```yaml +--- +inclusion: manual +--- +``` +…followed by the SKILL.md body (with `$ARGUMENTS` and cross-skill-reference transforms). +The `inclusion: manual` block MUST be the literal first content — no blank line before it. + +## Generator behavior + +`scripts/build-kiro-power.*` reads `plugins/paad/skills/*/SKILL.md` and writes `POWER.md` + +`steering/` at the repo root: + +1. **Per skill →** `steering/.md`: prepend `inclusion: manual` frontmatter, then the + body with transforms: + - `$ARGUMENTS` → "the user may provide a path/scope after invoking this guide" + (**known parity loss — pushback MODERATE-8:** Kiro manual invocation has no `$ARGUMENTS` + substitution, so scoped skills like `agentic-architecture src/` degrade from a structured + contract to a prose hope. Must be tested: does the agent reliably pick up a path the user + types after the slash command? Document as a known limitation in the power's README.) + - cross-skill refs ("run `/paad:agentic-architecture`") → "use the `/agentic-architecture` + slash command" (or `#agentic-architecture`) + - digraphs copied verbatim +2. **Aggregate →** `POWER.md`: frontmatter (`name`, `displayName`, `description`, aggregated + `keywords` from `scripts/kiro-keywords.yaml`, `author`) + onboarding + the generated + "when to load steering files" mapping. +3. **Provenance stamp:** embed the source commit into the generated output (e.g. a + `` line in `POWER.md`). In the + single-repo model the shared git history already records provenance, but the stamp makes a + freshly-installed power self-describing and pins exactly which `SKILL.md` revision produced + it. (Note: the generator must read the SHA from git at run time, then write deterministic + output — keep the stamp out of the idempotency `git diff` comparison, or the diff check + will flip on every commit.) + +### Edge cases + +- A skill with no entry in `scripts/kiro-keywords.yaml` → generator **warns** (so a new skill + is not shipped with no way to surface in the power's keyword set). +- `help` skill → becomes the POWER.md index itself; **no separate steering file** (redundant + in Kiro — the power's onboarding *is* the help). +- Cross-skill references rewritten from `/paad:` to `#`. + +## Testing / validation + +- **Idempotency:** running the generator twice produces no diff. +- **CI/Make drift check:** regenerate and `git diff --exit-code -- POWER.md steering/` — fails + if anyone hand-edited the generated files or forgot to regenerate after a `SKILL.md` change. + Same repo, so this is a trivial one-line check that mechanically enforces single-source-of-truth. +- **Claude side:** `claude plugin validate .` and `claude plugin validate ./plugins/paad` + remain the source of truth for the plugin. +- **Kiro side:** lightweight YAML-frontmatter lint enforcing the "frontmatter is first line" + rule for steering files and POWER.md. + +## Out of scope (YAGNI) + +- No `mcp.json` (PAAD skills have no tools/servers). +- No `auto`/`fileMatch` inclusion modes (manual only). +- No per-skill inclusion-mode field (all manual; revisit only if a skill genuinely warrants + seamless activation). + +## Must-resolve before build (from pushback) + +- ~~Verify `keywords` frontmatter tolerance~~ **DONE (2026-06-30)** — tested; `claude plugin + validate` ignores `SKILL.md` frontmatter entirely, so we use a `scripts/kiro-keywords.yaml` + sidecar and leave `SKILL.md` untouched. (SERIOUS-5 closed) +- **Measure steering/POWER.md size against Kiro's actual limit before building.** Data already + exists: `agentic-a11y` ≈ 392 lines, `agentic-architecture` ≈ 278; total skill body ≈ 2,237 + lines. If Kiro caps steering-file length, the "one big steering file per skill" mapping + breaks for the large skills and they need splitting. Test empirically; do not defer. (omission) +- **Curate the power-level keyword set** (narrow, not a union) to limit noisy power activation + while staying discoverable. Resolve before build, not after. (MODERATE-7) +- **Pick the generator runtime** (`build-kiro-power.`) — affects CI wiring and the + idempotency/drift-check tests. Bash vs Node vs a Make target. (omission) +- **Confirm Kiro tolerates extra root files** (Decision #5 caveat) — a test install of + `github.com/Ovid/paad` should succeed despite `.claude-plugin/`, `plugins/`, `docs/` at root. + +## Open questions (lower stakes) + +- Does Kiro render/respect graphviz `dot` blocks in steering, or are they inert text? (Inert + is acceptable — they're agent guidance, not user-facing — but worth confirming.) +- **Versioning/update story for the Kiro side.** CLAUDE.md mandates semver bumps in + `plugin.json` + `marketplace.json`; the power's `POWER.md` should carry a matching version, + and updates reach Kiro users via "refresh from remote" on each commit to `paad`. +- **Security/trust note** for the power README: installing from a GitHub URL injects steering + that dispatches multi-agent workflows — same trust boundary as the Claude plugin. + +## Sources + +- https://kiro.dev/docs/powers/create/ +- https://kiro.dev/docs/powers/ +- https://kiro.dev/docs/powers/installation/ +- https://kiro.dev/docs/steering/ +- https://code.claude.com/docs/en/plugins (Claude Code coexistence check) +- https://github.com/kirodotdev/powers/blob/main/power-builder/POWER.md From 223298c5984620b6d86f8e071a958073471639d7 Mon Sep 17 00:00:00 2001 From: Ovid Date: Tue, 30 Jun 2026 21:41:35 +0400 Subject: [PATCH 02/19] Revise Kiro power design: reconcile with existing path, add impl plan Folds in pushback + alignment review findings: - Reconcile with the already-shipping Kiro/Antigravity path (convert_skills.py -> kiro_and_antigravity/): power supplements the legacy .kiro/skills/ copy path (coexistence), one shared body-cleaning core, exclude makefile to match legacy skip set. - Confirm/resolve empirical questions from Kiro docs: no per-file size cap, $ARGUMENTS has no slash-command equivalent (documented limitation), POWER.md mirrors plugin.json version. - Keep single-repo as recommended (POWER.md at paad root) with a quick busy-root test gate and a dedicated paad-kiro repo as fallback. - Add an ordered implementation plan (Phase 0 gates -> generator -> drift check -> lint -> docs -> ship) with a TDD task breakdown for code phases. - Alignment fixes: legacy-output regression guard, post-generation claude plugin validate, help-sourced POWER.md onboarding. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../2026-06-30-paad-kiro-power-design.md | 323 +++++++++++++++--- 1 file changed, 276 insertions(+), 47 deletions(-) diff --git a/docs/plans/2026-06-30-paad-kiro-power-design.md b/docs/plans/2026-06-30-paad-kiro-power-design.md index bb1cc7f..1b93c90 100644 --- a/docs/plans/2026-06-30-paad-kiro-power-design.md +++ b/docs/plans/2026-06-30-paad-kiro-power-design.md @@ -1,7 +1,9 @@ # Design: Bundle PAAD skills into a Kiro Power **Date:** 2026-06-30 -**Status:** Design v2 — revised after adversarial pushback review (findings folded in below) +**Status:** Design v3 — revised after a second pushback review reconciling the design with the +*already-shipping* Kiro/Antigravity path (`scripts/convert_skills.py` → `kiro_and_antigravity/`). +Coexistence, shared-generator, makefile-exclusion, and versioning findings folded in below. **Goal:** Distribute PAAD's skills to Kiro IDE users as an installable Kiro "power," maintained alongside the existing Claude Code plugin from a single source of truth. ## Background @@ -36,6 +38,31 @@ This single fact drives the distribution decision below. them."* The native `/` slash-command list is the discovery primitive; we do **not** need to invent a POWER.md dispatcher to surface `#names`. +## Existing Kiro path (this design SUPPLEMENTS it — does not replace it yet) + +PAAD **already ships a Kiro distribution path**, and this power is an *additional, experimental* +channel that coexists with it — not a replacement (decided: pushback Option C): + +- `scripts/convert_skills.py` generates `kiro_and_antigravity/skills/.kiro/skills//SKILL.md` + (Kiro *skills*) plus `.agent/skills//SKILL.md` (Antigravity wrappers), committed to the repo. +- `README.md` documents manual install: users copy `.kiro/skills/*` into their own workspace's + `.kiro/skills/`. These are *copy-source* files, inert in the paad repo until copied. + +**Coexistence stance:** the power is the experimental one-click-install channel; the manual +`.kiro/skills/` copy path remains for users who want file-level control. The legacy path may be +retired later once the power is proven — until then **both are maintained**. + +**No double-load risk:** the legacy artifacts sit at a *nested* path +(`kiro_and_antigravity/skills/.kiro/skills/`), not the workspace-root `.kiro/` that Kiro scans, and +a power import reads only `POWER.md` + `steering/`. Both activating at once requires a user to +deliberately install the power *and* hand-copy the legacy skills into their own workspace — their +choice, not a repo defect. (A 10-second sanity glance during the test install confirms this.) + +**One shared generator core (decided: pushback Issue [2] Option A):** the power generator and +`convert_skills.py` MUST share one body-cleaning core so the two outputs never diverge in *content*, +only in *wrapper*. See "Generator behavior" — the power reuses `convert_skills.py`'s section-exclusion +set verbatim. + ## Decisions (from brainstorming, revised after pushback) 1. **Goal:** Reach Kiro users — a real, maintained second distribution channel. @@ -46,25 +73,29 @@ invent a POWER.md dispatcher to surface `#names`. 4. **Inclusion mode:** All steering files use `inclusion: manual` — surfaced as native `/` slash commands (and `#` references), the truest equivalent of deliberate `/paad:` invocation, with no skill-level auto-firing of a gated multi-agent audit. -5. **Distribution = single repo, `POWER.md` at the `paad` repo root (REVISED twice).** +5. **Distribution = single repo, `POWER.md` at the existing `paad` repo root (RECOMMENDED — + revised twice).** - v1 was `powers/paad/` subdirectory — *rejected*: GitHub-URL installs require `POWER.md` at the **repository root** (verified), and a subdirectory cannot be installed. - - v2 was a dedicated `Ovid/paad-kiro` repo — *also workable, but rejected for ceremony*: it - adds a second repo and a cross-repo publish step. - - **v3 (chosen): generate `POWER.md` + `steering/` directly into the existing `paad` repo - root.** Kiro's only hard requirement is a valid `POWER.md` at the repo root, which `paad` - already has. Kiro users install `github.com/Ovid/paad`; Kiro reads `POWER.md` + `steering/` - and ignores everything else in the repo. - - **Why:** lowest ceremony (no second repo, no submodule, no cross-repo push), and the - strongest drift/provenance story because source and generated artifact share one git - history — `git diff --exit-code` and commit history alone prove the power matches the - `SKILL.md` files that produced it. - - **Trade-off accepted:** the marketplace repo root carries Kiro files alongside - `.claude-plugin/`, and Kiro consumers pull the whole repo (harmless — Kiro reads only - POWER.md + steering). - - **One caveat to confirm before build:** that Kiro tolerates unrelated extra files at the - repo root (very likely — the docs state only the POWER.md-at-root requirement, not an - exclusivity rule — but verify empirically with a test install). + - v2 was a dedicated `Ovid/paad-kiro` repo — *workable, and the safe fallback (see below), but + not the default*: it adds a second repo, a submodule/checkout, and a cross-repo publish push. + - **v3 (CHOSEN): generate `POWER.md` + `steering/` directly into the existing `paad` repo + root.** Kiro's only documented requirement is a valid `POWER.md` at the repo root; `paad` + already is a repo root. Kiro users install `github.com/Ovid/paad`; Kiro reads `POWER.md` + + `steering/`. + - **Why single repo wins:** one place to edit and commit, **shared git history** (so provenance + and the drift check are trivial — no cross-repo stamp needed to span histories), no submodule, + no publish push. The daily simplicity dividend is real and recurring. + - **The one risk, and why it's acceptable:** no *observed* published power shares its root with + unrelated files (the official catalog organizes powers as subdirs). But that is absence of + observation, not a documented prohibition — the docs state only the POWER.md-at-root + *requirement*, no exclusivity rule, and a GitHub-URL import that rejected repos for carrying a + README would reject nearly every real repo. The risk is low **and cheaply verified**: one test + install before relying on it (see "Must-resolve"). + - **Fallback if that test fails:** the dedicated `Ovid/paad-kiro` repo (v2) — a clean + POWER.md-at-root repo matching the catalog convention exactly. The same generator output ships + there instead, with a `paad@` provenance stamp to bridge the now-separate histories. Adopt + only if the busy-root test actually fails. ## Verified: Kiro steering inclusion modes @@ -103,24 +134,30 @@ paad/ ← Kiro installs this repo; reads root POW │ ├── agentic-review.md │ ├── alignment.md │ ├── fix-architecture.md -│ ├── makefile.md │ ├── pushback.md -│ └── vibe.md ← 8 files: all skills EXCEPT help (see edge cases) +│ └── vibe.md ← 7 files: all skills EXCEPT help AND makefile (edge cases) ├── plugins/paad/skills/*/SKILL.md ← canonical source (unchanged, hand-edited) +├── kiro_and_antigravity/ ← legacy Kiro/Antigravity copy-source (coexists; see above) ├── scripts/ -│ └── build-kiro-power. ← generator: reads SKILL.md → writes POWER.md + steering/ +│ ├── convert_skills.py ← EXISTING: SKILL.md → .kiro/skills + .agent/skills (legacy) +│ └── build-kiro-power.py ← power generator; shares body-cleaning core with the above ├── .claude-plugin/ ← Claude Code marketplace (Kiro ignores) ├── CLAUDE.md, README.md, docs/ ← (Kiro ignores) ``` Workflow: edit a `SKILL.md` → run the generator (`make kiro`) → commit `POWER.md` + `steering/` -in the same repo. No second repo, no push to elsewhere, no submodule. Kiro users who already +in the same repo. No second repo, no push elsewhere, no submodule. Kiro users who already installed the power get updates via Kiro's "refresh from remote." +**Fallback (dedicated repo, v2):** if the pre-build test install shows Kiro rejects a busy root, +publish the same generated `POWER.md` + `steering/` to a dedicated `github.com/Ovid/paad-kiro` repo +(clean root) instead, with a `paad@` provenance stamp to bridge the separate histories. Only if +the test fails. + ## Claude Code coexistence check (no conflict) Verified against https://code.claude.com/docs/en/plugins. The key structural fact that makes -Option D safe: **the Claude Code plugin is not at the repo root — it lives at `plugins/paad/`.** +the single-repo plan safe: **the Claude Code plugin is not at the repo root — it lives at `plugins/paad/`.** So the repo root is purely a *marketplace* root, whose only artifact Claude Code reads is `.claude-plugin/marketplace.json`. @@ -184,7 +221,7 @@ pushback: [pushback, spec review, requirements, scope, feasibility] name: paad displayName: PAAD — Architecture, Review & Quality Skills description: Multi-agent architecture analysis, code review, accessibility, and quality workflows. -keywords: [architecture, review, accessibility, a11y, pushback, vibe, alignment, makefile, ...] +keywords: [architecture, review, accessibility, a11y, pushback, vibe, alignment, ...] author: Ovid --- ``` @@ -212,12 +249,23 @@ The `inclusion: manual` block MUST be the literal first content — no blank lin `steering/` at the repo root: 1. **Per skill →** `steering/.md`: prepend `inclusion: manual` frontmatter, then the - body with transforms: - - `$ARGUMENTS` → "the user may provide a path/scope after invoking this guide" - (**known parity loss — pushback MODERATE-8:** Kiro manual invocation has no `$ARGUMENTS` - substitution, so scoped skills like `agentic-architecture src/` degrade from a structured - contract to a prose hope. Must be tested: does the agent reliably pick up a path the user - types after the slash command? Document as a known limitation in the power's README.) + body run through the **shared body-cleaning core** (the same logic `convert_skills.py` uses), + with transforms: + - **Strip non-portable orchestration sections** — reuse `convert_skills.py`'s exclusion set + verbatim: `["Arguments", "Input Resolution", "Pre-flight Checks", "Document classification"]`. + These are Claude-Code-specific scaffolding (e.g. the "search your system prompt for your model + ID" probe, `$ARGUMENTS` resolution logic) that misfires in Kiro. **This closes pushback + Issue [2]:** the power's transforms must not be thinner than the tool already shipping working + Kiro output. A test asserts no `## Pre-flight`/`## Input Resolution` survives in `steering/`. + - `$ARGUMENTS` → an explicit instruction to the user to **name the scope in their chat message** + (e.g. "After invoking this guide, state the path or scope you want reviewed, such as `src/`"). + (**CONFIRMED parity loss — pushback MODERATE-8, RESOLVED 2026-06-30:** Kiro's docs are explicit + that invoking a manual steering file just *"adds the file's contents to your current conversation + context"* — there is **no** argument/parameter/`$ARGUMENTS` mechanism on slash commands at all. + So this is not a "test whether it works" item; it is a known, permanent limitation. The prose + transform must therefore *prompt* the user for scope rather than imply automatic capture, and the + power README documents it. Source: https://kiro.dev/docs/chat/slash-commands/ , + https://kiro.dev/docs/steering/) - cross-skill refs ("run `/paad:agentic-architecture`") → "use the `/agentic-architecture` slash command" (or `#agentic-architecture`) - digraphs copied verbatim @@ -226,11 +274,11 @@ The `inclusion: manual` block MUST be the literal first content — no blank lin "when to load steering files" mapping. 3. **Provenance stamp:** embed the source commit into the generated output (e.g. a `` line in `POWER.md`). In the - single-repo model the shared git history already records provenance, but the stamp makes a - freshly-installed power self-describing and pins exactly which `SKILL.md` revision produced - it. (Note: the generator must read the SHA from git at run time, then write deterministic - output — keep the stamp out of the idempotency `git diff` comparison, or the diff check - will flip on every commit.) + single-repo model the shared git history already records provenance, so this is a nice-to-have + that makes a freshly-installed power self-describing; it becomes *load-bearing* only in the + dedicated-repo fallback (where histories are separate). (Note: the generator reads the SHA from + git at run time, then writes otherwise-deterministic output — keep the stamp out of the + idempotency `git diff` comparison, or the drift check will flip on every commit.) ### Edge cases @@ -238,6 +286,10 @@ The `inclusion: manual` block MUST be the literal first content — no blank lin is not shipped with no way to surface in the power's keyword set). - `help` skill → becomes the POWER.md index itself; **no separate steering file** (redundant in Kiro — the power's onboarding *is* the help). +- `makefile` skill → **excluded** (no steering file), matching the legacy `convert_skills.py` + `skip_names = ["makefile", "help"]`. Decided: pushback Issue [4] Option B — keep the two outputs' + skill sets identical; makefile was intentionally omitted from the shipping Kiro path and the power + follows suit. Net: **7 steering files** (all skills except `help` and `makefile`). - Cross-skill references rewritten from `/paad:` to `#`. ## Testing / validation @@ -245,7 +297,9 @@ The `inclusion: manual` block MUST be the literal first content — no blank lin - **Idempotency:** running the generator twice produces no diff. - **CI/Make drift check:** regenerate and `git diff --exit-code -- POWER.md steering/` — fails if anyone hand-edited the generated files or forgot to regenerate after a `SKILL.md` change. - Same repo, so this is a trivial one-line check that mechanically enforces single-source-of-truth. + Same repo, so this is a trivial one-line check that mechanically enforces single-source-of-truth + (exclude the provenance stamp from the comparison — see Generator behavior #3). (In the + dedicated-repo fallback this runs against the `paad-kiro` checkout instead.) - **Claude side:** `claude plugin validate .` and `claude plugin validate ./plugins/paad` remain the source of truth for the plugin. - **Kiro side:** lightweight YAML-frontmatter lint enforcing the "frontmatter is first line" @@ -263,24 +317,199 @@ The `inclusion: manual` block MUST be the literal first content — no blank lin - ~~Verify `keywords` frontmatter tolerance~~ **DONE (2026-06-30)** — tested; `claude plugin validate` ignores `SKILL.md` frontmatter entirely, so we use a `scripts/kiro-keywords.yaml` sidecar and leave `SKILL.md` untouched. (SERIOUS-5 closed) -- **Measure steering/POWER.md size against Kiro's actual limit before building.** Data already - exists: `agentic-a11y` ≈ 392 lines, `agentic-architecture` ≈ 278; total skill body ≈ 2,237 - lines. If Kiro caps steering-file length, the "one big steering file per skill" mapping - breaks for the large skills and they need splitting. Test empirically; do not defer. (omission) +- ~~**Measure steering/POWER.md size against Kiro's actual limit**~~ **RESOLVED (2026-06-30, + docs + search).** No per-file size cap is documented. Kiro manages size via the *context window*: + it auto-summarizes at ~80% of the model limit, with ~50–60K tokens (~200–240K chars) of working + context (kirodotdev/Kiro #4162 is an open feature request for context-size monitoring → no hard + enforcement today). PAAD's largest skill (`agentic-a11y` ≈ 392 lines) is far under that, and + because every steering file is `inclusion: manual`, only the single invoked file enters context — + never the ~2,237-line aggregate. The "one steering file per skill" mapping holds; no splitting + needed. Source: https://kiro.dev/docs/steering/ , https://github.com/kirodotdev/Kiro/issues/4162 - **Curate the power-level keyword set** (narrow, not a union) to limit noisy power activation while staying discoverable. Resolve before build, not after. (MODERATE-7) -- **Pick the generator runtime** (`build-kiro-power.`) — affects CI wiring and the - idempotency/drift-check tests. Bash vs Node vs a Make target. (omission) -- **Confirm Kiro tolerates extra root files** (Decision #5 caveat) — a test install of - `github.com/Ovid/paad` should succeed despite `.claude-plugin/`, `plugins/`, `docs/` at root. +- ~~**Pick the generator runtime**~~ **RESOLVED (pushback Issue [2])** — Python, reusing + `scripts/convert_skills.py`'s body-cleaning core (`build-kiro-power.py`). No new runtime; the two + generators share one transform core so legacy and power outputs can never diverge in content. +- **Root-file tolerance — one quick pre-build test gate for the single-repo plan (low risk).** + - *Requirement confirmed:* https://kiro.dev/docs/powers/installation/ — "The power must have a valid + `POWER.md` file in the repository root"; no subdirectory/path support. (The `kirodotdev/powers` + monorepo's powers-in-subdirs does NOT contradict this — it has no root POWER.md and is the + first-party *catalog*, consumed via Kiro's built-in browser, not "Import from GitHub URL.") + - *Why low risk:* docs state only the POWER.md-at-root *requirement*, **no exclusivity rule**; a + GitHub-URL import that rejected repos for carrying a README/LICENSE would reject nearly every real + repo, and the catalog repo's own root carries `README`, `CONTRIBUTING`, `.github/`, `.kiro/`. The + "every published power has a minimal root" observation (verified via + `gh api repos/kirodotdev/powers/contents/...` 2026-06-30: `stripe`, `power-builder`) reflects how + the *catalog* organizes subdirs, not an enforced minimalism — absence of observation, not + prohibition. + - *The gate:* before relying on single-repo, do one test install of `github.com/Ovid/paad` and + confirm Kiro reads `POWER.md` + `steering/` despite `.claude-plugin/`, `plugins/`, `docs/`, + `kiro_and_antigravity/`, `scripts/`, `CLAUDE.md` at root. **If it fails → dedicated `Ovid/paad-kiro` + repo fallback (Decision #5).** Source: https://github.com/kirodotdev/powers. + +## Implementation plan + +Ordered so the cheap kill-switch checks (Phase 0) run before any code is written, and each later +phase has a concrete done-condition. Phases 1–3 are the build; 4–5 wire up safety and docs. + +### Phase 0 — Resolve the gates (no code yet) + +These are the "Must-resolve before build" items; none requires writing the generator first. + +1. **Busy-root test install (the one real risk).** Hand-author a *minimal throwaway* `POWER.md` + + `steering/one-file.md` at the `paad` repo root **on a scratch branch**, push, and install + `github.com/Ovid/paad` into Kiro via "Add Custom Power → Import from GitHub." + - **Pass** → Kiro reads `POWER.md` + `steering/` despite `.claude-plugin/`, `plugins/`, `docs/`, + etc. at root → proceed single-repo. + - **Fail** → switch to the dedicated `Ovid/paad-kiro` repo fallback (Decision #5) before Phase 1. + - Delete the scratch branch either way. +2. **Curate the power-level keyword set** (narrow, not a union of all skills' keywords) — draft + `scripts/kiro-keywords.yaml` now; it is an input to Phase 1, not an output. +3. **Re-run `claude plugin validate .` and `claude plugin validate ./plugins/paad`** to capture the + green baseline *before* adding any root files, so a later failure is attributable. + +**Gate:** do not start Phase 1 until #1 has a verdict and #2 exists. + +### Phase 1 — Generator (`scripts/build-kiro-power.py`) + +1. **Refactor the shared body-cleaning core out of `convert_skills.py`** into an importable function + (section-exclusion set `["Arguments", "Input Resolution", "Pre-flight Checks", "Document + classification"]`, `/paad:`-line handling, path neutralization). Both generators import it — this + is pushback Issue [2] Option A; the two outputs must never diverge in body content. + - **Legacy regression guard (alignment Issue [1]):** *before* refactoring, snapshot the current + `kiro_and_antigravity/` tree; *after* refactoring, regenerate via `convert_skills.py` and + `git diff --exit-code` against the snapshot. **Done-condition: zero diff** — the shared-core + extraction must not alter a single byte of the shipping legacy/Antigravity output. +2. **`build-kiro-power.py` reads `plugins/paad/skills/*/SKILL.md`** and, per skill (skipping `help` + and `makefile`), writes `steering/.md`: `inclusion: manual` frontmatter as the literal first + line, then the cleaned body with the power-specific transforms (`$ARGUMENTS` → prose prompt; + cross-skill refs → `#`; digraphs verbatim). +3. **Aggregate `POWER.md`**: frontmatter (`name`, `displayName`, `description`, curated `keywords` + from the sidecar, `author`, version mirrored from `plugin.json`) + **onboarding generated from the + `help` skill's content** (so it can't drift — alignment Issue [3]) + the generated "when to load + steering files" mapping (derived from each skill's `name`/`description`). +4. **Provenance stamp** (``), read from git at run time and + **excluded from the idempotency diff**. +5. **Warn** on any skill in `skills/` missing a `kiro-keywords.yaml` entry. + +**Done when:** running it produces `POWER.md` + 7 `steering/*.md`; a second run yields no diff +(idempotency, stamp excluded); the legacy regression guard (#1) shows zero diff; **and** +`claude plugin validate .` + `claude plugin validate ./plugins/paad` still pass with the new root +files present (alignment Issue [2] — the Claude-side complement to Phase 0.3's baseline). + +### Phase 2 — Make target + drift check + +1. `make kiro` runs the generator. +2. Drift check: `make kiro && git diff --exit-code -- POWER.md steering/` (stamp excluded). Wire it + into whatever CI exists, or document it as a pre-commit step if none. + +**Done when:** a deliberate hand-edit to a generated file makes the drift check fail. + +### Phase 3 — Frontmatter lint + +Lightweight check that every `steering/*.md` and `POWER.md` has YAML frontmatter as the **literal +first content** (no leading blank line) — the one Kiro hard rule. Done when it flags a file with a +blank line before its frontmatter. + +### Phase 4 — Docs + +1. **`README.md`**: add a Kiro *power* install section (install `github.com/Ovid/paad`) alongside the + existing manual `.kiro/skills/` copy section — frame the power as the experimental one-click + channel, the copy path as the existing one (Issue [1] Option C coexistence). +2. **Power README / onboarding**: document the `$ARGUMENTS` limitation (user states scope in chat) + and the security/trust note (installing injects steering that dispatches multi-agent workflows). +3. **`CLAUDE.md`**: add "regenerate the Kiro power after editing a `SKILL.md`" to the skill-change + checklist so the power can't silently drift. + +### Phase 5 — Ship + +Bump `plugin.json` (+ sync `marketplace.json`), regenerate, commit `POWER.md` + `steering/` + +generator + sidecar + docs together, and do a final real install from the pushed repo to confirm +end-to-end. + +### TDD task breakdown (code phases) + +Phase 0 (empirical gates) and Phases 4–5 (docs/ship) are not code and stay as written above. The +code-producing units of Phases 1–3 are recast here in red/green/refactor form (alignment skill, +mandatory step). All tests are generator unit tests run against the real `plugins/paad/skills/` tree +or small fixtures. + +#### Task: Shared body-cleaning core (Phase 1.1) + +**Requirement:** Issue [2] Option A (one transform core) + Issue [1] (legacy output unchanged). + +- **RED** — Write a test that snapshots `convert_skills.py`'s current output for one representative + skill (e.g. `pushback`, which has the excluded `Input Resolution`/`Pre-flight Checks` sections), + then asserts the *extracted* `clean_body()` function reproduces that exact output. Expected failure: + the function doesn't exist yet (ImportError). *If it passes unexpectedly:* the core was already + extracted — verify it's actually shared, not duplicated. +- **GREEN** — Extract `clean_body()` from the inline logic in `convert_skills.py` and have + `convert_skills.py` call it. Minimal: no new behavior, just a lifted function. +- **REFACTOR** — Add the full-tree golden guard (snapshot `kiro_and_antigravity/` → regenerate → + `git diff --exit-code`). Look to consolidate the section-exclusion list into one named constant + both generators import (no copy-paste of the four header names). + +#### Task: Steering file generation (Phase 1.2) + +**Requirement:** Decisions 2 & 4 (one steering file per skill, `inclusion: manual`); edge cases +(skip `help` + `makefile`); the mapping (`$ARGUMENTS`→prose, cross-refs→`#name`, digraphs verbatim). + +- **RED** — Write tests asserting: (a) exactly 7 `steering/*.md` are produced (not 8, not 9); + (b) `help.md` and `makefile.md` are absent; (c) each file's **first line** is `---` then + `inclusion: manual` (no leading blank); (d) a fixture containing `$ARGUMENTS` and `/paad:vibe` + yields the prose-scope prompt and `#vibe` respectively; (e) a ```dot block survives verbatim. + Expected failure: generator doesn't exist. +- **GREEN** — Implement per-skill emission with the three transforms. Simplest mapping that passes. +- **REFACTOR** — Extract the frontmatter-prepend and transform steps into named helpers; ensure the + power transforms layer *on top of* the shared `clean_body()` rather than re-implementing any of it. + +#### Task: POWER.md aggregation (Phase 1.3–1.5) + +**Requirement:** "Generated POWER.md shape"; help-sourced onboarding (Issue [3]); keyword sidecar + +missing-entry warning; version mirrors `plugin.json`. + +- **RED** — Tests asserting: (a) frontmatter has `name/displayName/description/keywords/author` and a + `version` equal to `plugin.json`'s; (b) onboarding text is derived from the `help` skill content + (assert a known phrase from `help/SKILL.md` appears, proving it's sourced, not hand-written); + (c) `keywords` equals the curated sidecar values, not a union of every skill's; (d) a skill with no + sidecar entry emits a warning. Expected failure: aggregator absent. +- **GREEN** — Build `POWER.md` from the sidecar + `help` content + `plugin.json` version. +- **REFACTOR** — Move the version read and git-SHA stamp behind small helpers; confirm the stamp is + injected *after* the content used for the idempotency comparison so the drift check stays stable. + +#### Task: Idempotency + drift check (Phase 2) + +**Requirement:** Testing/validation — "running the generator twice produces no diff"; `make kiro` + +`git diff --exit-code` enforce single-source-of-truth. + +- **RED** — Test that runs the generator twice and asserts the second run produces no change to + `POWER.md` + `steering/` (with the provenance stamp normalized/excluded). Expected failure: + non-deterministic output (e.g. dict ordering, or the raw SHA stamp flipping the diff). +- **GREEN** — Make output deterministic (sorted iteration, stable formatting); exclude the stamp from + the compared content. +- **REFACTOR** — Wire the `make kiro && git diff --exit-code` target; share the "compare ignoring + stamp" logic between the idempotency test and the drift check so they can't disagree. + +#### Task: Frontmatter-first lint (Phase 3) + +**Requirement:** Kiro hard rule — frontmatter must be the literal first content. + +- **RED** — Test the linter against two fixtures: one valid (`---` on line 1) and one with a leading + blank line; assert pass and fail respectively. Expected failure: linter absent. +- **GREEN** — Implement the check over `steering/*.md` + `POWER.md`. +- **REFACTOR** — Reuse the same frontmatter-parse helper the generator uses for writing, so "what we + write" and "what we lint" share one definition of valid frontmatter. ## Open questions (lower stakes) - Does Kiro render/respect graphviz `dot` blocks in steering, or are they inert text? (Inert is acceptable — they're agent guidance, not user-facing — but worth confirming.) -- **Versioning/update story for the Kiro side.** CLAUDE.md mandates semver bumps in - `plugin.json` + `marketplace.json`; the power's `POWER.md` should carry a matching version, - and updates reach Kiro users via "refresh from remote" on each commit to `paad`. +- **Versioning/update story for the Kiro side (RESOLVED — pushback Issue [5] Option A).** + `POWER.md` mirrors the **`plugin.json`** version (the precedence source per CLAUDE.md), written + automatically by the generator so it cannot drift and is never a third hand-maintained source. + This disambiguates "matching version" — note `marketplace.json` (currently 1.0.0) and + `plugin.json` (currently 1.11.0) are already out of sync, so the power explicitly follows + `plugin.json`, not `marketplace.json`. Updates reach Kiro users via "refresh from remote." - **Security/trust note** for the power README: installing from a GitHub URL injects steering that dispatches multi-agent workflows — same trust boundary as the Claude plugin. From 29cdb09f3cb091aeacc76fcb2c3b3e0668ca9c51 Mon Sep 17 00:00:00 2001 From: Ovid Date: Tue, 30 Jun 2026 21:50:23 +0400 Subject: [PATCH 03/19] Add Phase 0.2 keyword sidecar for Kiro power generator Curated narrow power-level keyword set plus per-skill entries (drift warning source). Kiro-only metadata kept out of SKILL.md so the Claude plugin stays byte-for-byte untouched. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/kiro-keywords.yaml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 scripts/kiro-keywords.yaml diff --git a/scripts/kiro-keywords.yaml b/scripts/kiro-keywords.yaml new file mode 100644 index 0000000..a936b02 --- /dev/null +++ b/scripts/kiro-keywords.yaml @@ -0,0 +1,29 @@ +# Kiro power keyword metadata — Kiro-only, consumed by scripts/build-kiro-power.py. +# +# These keywords live HERE, not in SKILL.md frontmatter: keywords are a Kiro concern, +# and keeping them out of SKILL.md leaves the Claude Code plugin byte-for-byte untouched. +# `claude plugin validate` does not inspect SKILL.md frontmatter, so a sidecar is the only +# place a Kiro-only key can be validated/owned. (Design: "Keywords source: sidecar file".) + +# Curated, NARROW keyword set written verbatim into POWER.md frontmatter `keywords`. +# This drives *power-level* activation. It is deliberately a hand-picked subset, NOT the +# union of every skill's keywords, to limit noisy power activation while staying +# discoverable. (Design: "Residual noise caveat" + "Curate the power-level keyword set".) +power: + - architecture + - code review + - accessibility + - requirements + - code quality + +# Per-skill keywords — one entry per SHIPPED steering file (excludes help + makefile). +# The generator WARNS if a shipped skill has no entry here, so a newly added skill cannot +# ship without an explicit decision about how it surfaces. (Design: "Edge cases" — warn.) +skills: + agentic-a11y: [accessibility, a11y, screen reader, WCAG] + agentic-architecture: [architecture, coupling, structure, tech debt] + agentic-review: [code review, branch review, pre-merge] + alignment: [alignment, requirements, design, coverage] + fix-architecture: [fix architecture, remediation, flaws] + pushback: [pushback, spec review, scope, feasibility] + vibe: [vibe coding, quick fix, TDD] From 8dbfd85e5915316bc6408087bd8dd98104f0e188 Mon Sep 17 00:00:00 2001 From: Ovid Date: Tue, 30 Jun 2026 21:56:32 +0400 Subject: [PATCH 04/19] Extract shared body-cleaning core into scripts/skill_body.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task 1 of the Kiro power work: lift convert_skills.py's inline body-cleaning logic into a pure, unit-testable clean_body() so the upcoming power generator (build-kiro-power.py) reuses the exact same section-splitting and section-exclusion rules and the two generators can never diverge on WHICH sections they keep. - New scripts/skill_body.py exposes: - EXCLUDED_SECTIONS: single source of truth for the four dropped section headers (Arguments, Input Resolution, Pre-flight Checks, Document classification) — no more copy-pasted list. - clean_body(content, ...): string in -> string out, no file I/O. The per-section /paad: handling is an injectable step (paad_ref_transform) defaulting to the legacy "strip lines" behavior; the section split + EXCLUDED_SECTIONS drop is fixed and shared. Path neutralization and whitespace collapsing are shared. - convert_skills.py imports clean_body and EXCLUDED_SECTIONS and calls clean_body(content) in place of the inline block. No behavior change. Tests (pytest, under tests/): - test_skill_body.py: clean_body() reproduces the committed pushback body exactly (exercises section exclusion, path neutralization, whitespace), and pins EXCLUDED_SECTIONS. - test_legacy_output_unchanged.py: regression guard that runs the pre-refactor generator (git show HEAD) and the refactored generator against the same source and asserts byte-for-byte identical output. Verified to fail when clean_body() behavior is perturbed. Note: a raw `git diff` after regeneration shows a pre-existing drift in alignment/SKILL.md (source edited 2026-04-03 without regenerating the committed tree); the original HEAD generator produces that same diff, so it is unrelated to this refactor. The committed kiro tree is left untouched here. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 2 + scripts/convert_skills.py | 50 +++--------- scripts/skill_body.py | 110 ++++++++++++++++++++++++++ tests/conftest.py | 12 +++ tests/test_legacy_output_unchanged.py | 109 +++++++++++++++++++++++++ tests/test_skill_body.py | 46 +++++++++++ 6 files changed, 288 insertions(+), 41 deletions(-) create mode 100644 scripts/skill_body.py create mode 100644 tests/conftest.py create mode 100644 tests/test_legacy_output_unchanged.py create mode 100644 tests/test_skill_body.py diff --git a/.gitignore b/.gitignore index 282b612..73d1412 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /scratch/ .DS_Store +__pycache__/ +.pytest_cache/ diff --git a/scripts/convert_skills.py b/scripts/convert_skills.py index 9f71e86..428aa05 100755 --- a/scripts/convert_skills.py +++ b/scripts/convert_skills.py @@ -2,8 +2,13 @@ import os import re +import sys from pathlib import Path +# Make the shared body-cleaning core importable regardless of cwd. +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from skill_body import clean_body # noqa: E402 + # Paths relative to repository root SOURCE_DIR = "plugins/paad/skills" TARGET_DIR = "kiro_and_antigravity/skills" @@ -17,7 +22,6 @@ def convert_skills(): agent_skills_root.mkdir(parents=True, exist_ok=True) skip_names = ["makefile", "help"] - unwanted_headers = ["Arguments", "Input Resolution", "Pre-flight Checks", "Document classification"] for skill_path in Path(SOURCE_DIR).iterdir(): if not skill_path.is_dir() or skill_path.name in skip_names: @@ -38,46 +42,10 @@ def convert_skills(): skill_name = name_match.group(1).strip() if name_match else skill_path.name description = desc_match.group(1).strip() if desc_match else "" - # Split into sections by headers (##) - # We use a non-capturing group for the split but keep the header as part of the next chunk - # Actually splitting by \n## works better if we prepend \n to content - parts = re.split(r'\n(##+ .*)', content) - - # parts[0] is everything before the first ## - cleaned_content = parts[0] - - # Process header/body pairs - for i in range(1, len(parts), 2): - header_line = parts[i] - body = parts[i+1] - - header_text = re.sub(r'^##+\s*', '', header_line).strip() - - # Skip unwanted sections - if any(uh in header_text for uh in unwanted_headers): - continue - - # Neutralize "paad/" paths to ".reviews/" or ".reports/" - body = body.replace("paad/architecture-reviews/", ".reviews/architecture/") - body = body.replace("paad/code-reviews/", ".reviews/code/") - body = body.replace("paad/pushback-reviews/", ".reviews/pushback/") - body = body.replace("paad/alignment-reviews/", ".reviews/alignment/") - body = body.replace("paad/", ".reviews/") - - # Remove entire lines containing /paad: (usually follow-up suggestions or command examples) - body = re.sub(r'^.*\/paad:[a-z0-9-]+.*$', '', body, flags=re.MULTILINE) - - # Additional cleanup for any remaining /paad: mentions just in case - body = re.sub(r'\(?/paad:[a-z0-9-]+\)?', '', body) - - # Clean up trailing whitespace and excessive newlines - body = body.rstrip() + "\n" - - cleaned_content += "\n" + header_line + body - - # Final cleanup for consecutive empty lines - cleaned_content = re.sub(r'\n{3,}', '\n\n', cleaned_content).strip() + "\n" - + # Clean the body using the shared core (section split + exclusion + + # path neutralization + legacy /paad: stripping + whitespace collapse). + cleaned_content = clean_body(content) + # Write Kiro Skill kiro_skill_dir = kiro_skills_root / skill_path.name kiro_skill_dir.mkdir(exist_ok=True) diff --git a/scripts/skill_body.py b/scripts/skill_body.py new file mode 100644 index 0000000..3e95121 --- /dev/null +++ b/scripts/skill_body.py @@ -0,0 +1,110 @@ +"""Shared body-cleaning core for the PAAD skill generators. + +This module owns the *single* definition of WHICH sections are kept or dropped +when a `plugins/paad/skills//SKILL.md` is transformed into a copy-source +skill. Two generators consume it: + + * `convert_skills.py` -> the legacy Kiro/Antigravity tree (strips + cross-skill `/paad:` references entirely). + * `build-kiro-power.py` -> the Kiro power (written in a later task; will + rewrite `/paad:` references to `#` instead + of stripping them). + +Both share the section-splitting, section-exclusion, path-neutralization, and +whitespace-collapsing behavior so the two generators can never disagree about +which sections survive. The only intended difference is the per-section +`/paad:` handling, which is injected via `paad_ref_transform`. + +`clean_body()` is intentionally pure (string in -> string out, no file I/O) so +it is directly unit-testable. +""" + +import re + +# Single source of truth for the section headers that are dropped from every +# generated skill. Both generators import this; do NOT copy-paste the names. +EXCLUDED_SECTIONS = [ + "Arguments", + "Input Resolution", + "Pre-flight Checks", + "Document classification", +] + + +def neutralize_paad_paths(body): + """Rewrite `paad/...` output paths to neutral `.reviews/...` paths. + + Shared by both generators: the copy-source skills must not reference the + plugin's own `paad/` review directories. + """ + body = body.replace("paad/architecture-reviews/", ".reviews/architecture/") + body = body.replace("paad/code-reviews/", ".reviews/code/") + body = body.replace("paad/pushback-reviews/", ".reviews/pushback/") + body = body.replace("paad/alignment-reviews/", ".reviews/alignment/") + body = body.replace("paad/", ".reviews/") + return body + + +def strip_paad_references(body): + """Legacy `/paad:` handling: drop whole lines, then any stray mentions. + + This is the default `paad_ref_transform` and reproduces `convert_skills.py` + exactly. The power generator will supply a different transform that rewrites + `/paad:` to `#` instead of removing it. + """ + # Remove entire lines containing /paad: (usually follow-up suggestions or + # command examples). + body = re.sub(r'^.*\/paad:[a-z0-9-]+.*$', '', body, flags=re.MULTILINE) + # Additional cleanup for any remaining /paad: mentions just in case. + body = re.sub(r'\(?/paad:[a-z0-9-]+\)?', '', body) + return body + + +def clean_body(content, excluded_sections=EXCLUDED_SECTIONS, + paad_ref_transform=strip_paad_references): + """Transform a raw SKILL.md `content` string into a cleaned skill body. + + The pipeline is: + 1. Split into sections by `##`+ headers. + 2. Drop sections whose header matches any `excluded_sections` entry. + 3. For each kept section: neutralize `paad/` paths, apply + `paad_ref_transform` to its `/paad:` references, and trim trailing + whitespace. + 4. Collapse runs of 3+ newlines and ensure a single trailing newline. + + Steps 1-2 (which sections survive) are shared and fixed across generators; + only `paad_ref_transform` is meant to vary. + """ + # Split into sections by headers (##). The capturing group keeps each + # header line as its own element so we can re-pair headers with bodies. + parts = re.split(r'\n(##+ .*)', content) + + # parts[0] is everything before the first ##. + cleaned_content = parts[0] + + # Process header/body pairs. + for i in range(1, len(parts), 2): + header_line = parts[i] + body = parts[i + 1] + + header_text = re.sub(r'^##+\s*', '', header_line).strip() + + # Skip unwanted sections. + if any(uh in header_text for uh in excluded_sections): + continue + + # Neutralize "paad/" paths to ".reviews/". + body = neutralize_paad_paths(body) + + # Apply the generator-specific /paad: reference handling. + body = paad_ref_transform(body) + + # Clean up trailing whitespace and excessive newlines. + body = body.rstrip() + "\n" + + cleaned_content += "\n" + header_line + body + + # Final cleanup for consecutive empty lines. + cleaned_content = re.sub(r'\n{3,}', '\n\n', cleaned_content).strip() + "\n" + + return cleaned_content diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..88f5f32 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,12 @@ +"""Shared pytest fixtures and path helpers for the PAAD generator tests.""" + +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +SCRIPTS_DIR = REPO_ROOT / "scripts" + +# Make `scripts/` importable as a package-free module location so tests can +# `import skill_body` / `import convert_skills` regardless of cwd. +if str(SCRIPTS_DIR) not in sys.path: + sys.path.insert(0, str(SCRIPTS_DIR)) diff --git a/tests/test_legacy_output_unchanged.py b/tests/test_legacy_output_unchanged.py new file mode 100644 index 0000000..e6e7b6f --- /dev/null +++ b/tests/test_legacy_output_unchanged.py @@ -0,0 +1,109 @@ +"""Regression guard: the refactor must not change `convert_skills.py`'s output. + +The load-bearing invariant for Task 1 is that extracting `clean_body()` into +`scripts/skill_body.py` leaves the legacy Kiro/Antigravity generator producing +*byte-for-byte identical* output. This test proves that directly by running two +generators against the same source tree and comparing their full output: + + * the ORIGINAL generator, extracted verbatim from `git show HEAD:...` + * the CURRENT (refactored) generator on disk + +If they ever diverge, `clean_body()` no longer reproduces the original +behavior and this test fails. + +Why compare original-vs-current rather than `git diff` against the committed +tree: the committed tree has a PRE-EXISTING drift (the `alignment` source was +edited without regenerating the tree), so a raw `git diff --exit-code` after +regeneration is non-empty regardless of this refactor. Comparing the two +generators isolates the refactor's effect from that unrelated drift, and the +original generator is the authoritative snapshot of "current behavior". +""" + +import filecmp +import os +import shutil +import subprocess +import sys +from pathlib import Path + +from conftest import REPO_ROOT + +SCRIPTS_REL = "scripts/convert_skills.py" +SKILL_BODY_REL = "scripts/skill_body.py" +SOURCE_REL = "plugins/paad/skills" +TARGET_REL = "kiro_and_antigravity/skills" + + +def _run_generator(script_path, workdir): + """Run a convert_skills.py copy in `workdir`, return CompletedProcess.""" + return subprocess.run( + [sys.executable, str(script_path)], + cwd=workdir, + capture_output=True, + text=True, + ) + + +def _stage_workdir(tmp_path, name, convert_script_text): + """Build an isolated repo-shaped workdir with source + a given generator. + + The current `scripts/skill_body.py` is always copied in because both the + original and refactored generators must import the same shared module under + test (the original generator pre-refactor had no import, so copying it is + harmless for the original and required for the refactored one). + """ + workdir = tmp_path / name + scripts_dir = workdir / "scripts" + scripts_dir.mkdir(parents=True) + + # Copy the full skills source tree (the generator reads from here). + shutil.copytree(REPO_ROOT / SOURCE_REL, workdir / SOURCE_REL) + + # Provide the shared core so refactored imports resolve. + shutil.copy2(REPO_ROOT / SKILL_BODY_REL, scripts_dir / "skill_body.py") + + # Write the generator under test. + (scripts_dir / "convert_skills.py").write_text( + convert_script_text, encoding="utf-8" + ) + + return workdir + + +def _dir_trees_equal(a, b): + """Recursively assert two directory trees are byte-for-byte identical.""" + cmp = filecmp.dircmp(a, b) + assert not cmp.left_only, f"only in {a}: {cmp.left_only}" + assert not cmp.right_only, f"only in {b}: {cmp.right_only}" + # filecmp's shallow=True can miss content changes with equal size/mtime; + # force a full content comparison. + _, mismatch, errors = filecmp.cmpfiles( + a, b, cmp.common_files, shallow=False + ) + assert not mismatch, f"content differs in {a} vs {b}: {mismatch}" + assert not errors, f"comparison errors in {a} vs {b}: {errors}" + for sub in cmp.common_dirs: + _dir_trees_equal(Path(a) / sub, Path(b) / sub) + + +def test_refactored_generator_output_matches_original(tmp_path): + """Refactored convert_skills.py must produce identical output to HEAD's.""" + original_text = subprocess.run( + ["git", "show", f"HEAD:{SCRIPTS_REL}"], + cwd=REPO_ROOT, + capture_output=True, + text=True, + check=True, + ).stdout + current_text = (REPO_ROOT / SCRIPTS_REL).read_text(encoding="utf-8") + + orig_wd = _stage_workdir(tmp_path, "original", original_text) + cur_wd = _stage_workdir(tmp_path, "current", current_text) + + orig_run = _run_generator(orig_wd / "scripts" / "convert_skills.py", orig_wd) + cur_run = _run_generator(cur_wd / "scripts" / "convert_skills.py", cur_wd) + + assert orig_run.returncode == 0, orig_run.stderr + assert cur_run.returncode == 0, cur_run.stderr + + _dir_trees_equal(orig_wd / TARGET_REL, cur_wd / TARGET_REL) diff --git a/tests/test_skill_body.py b/tests/test_skill_body.py new file mode 100644 index 0000000..04d8adf --- /dev/null +++ b/tests/test_skill_body.py @@ -0,0 +1,46 @@ +"""Unit tests for the shared body-cleaning core (`scripts/skill_body.py`). + +These tests pin the CURRENT behavior of `convert_skills.py`'s inline +body-cleaning logic so that the extracted, shared `clean_body()` function can +never silently diverge from it. The committed Kiro tree is the authoritative +snapshot of that behavior: each `.kiro/skills//SKILL.md` is exactly what +the inline logic produces for that skill today. +""" + +from pathlib import Path + +from conftest import REPO_ROOT + +SOURCE_DIR = REPO_ROOT / "plugins" / "paad" / "skills" +KIRO_DIR = REPO_ROOT / "kiro_and_antigravity" / "skills" / ".kiro" / "skills" + + +def _read(path: Path) -> str: + return path.read_text(encoding="utf-8") + + +def test_clean_body_reproduces_pushback_committed_output(): + """`clean_body()` must reproduce the committed pushback body exactly. + + pushback is the representative skill: it exercises section exclusion + (`Input Resolution`), path neutralization (`paad/pushback-reviews/` -> + `.reviews/pushback/`), and whitespace collapsing. + """ + from skill_body import clean_body + + source = _read(SOURCE_DIR / "pushback" / "SKILL.md") + expected = _read(KIRO_DIR / "pushback" / "SKILL.md") + + assert clean_body(source) == expected + + +def test_excluded_sections_constant_lists_the_four_headers(): + """The named constant is the single source of truth for dropped sections.""" + from skill_body import EXCLUDED_SECTIONS + + assert EXCLUDED_SECTIONS == [ + "Arguments", + "Input Resolution", + "Pre-flight Checks", + "Document classification", + ] From 749cc3005d84a434b272757afe0d7bd8d99e6d86 Mon Sep 17 00:00:00 2001 From: Ovid Date: Tue, 30 Jun 2026 22:02:07 +0400 Subject: [PATCH 05/19] Regenerate stale Kiro alignment output to match current source The committed kiro_and_antigravity/ tree was last regenerated 2026-03-20 but alignment/SKILL.md was edited 2026-04-03 (Step 2 'mandatory' -> 'when applicable'). This regenerates the legacy output so it matches current sources, making the literal git-diff regression guard clean and self-maintaining. Pre-existing drift, unrelated to the shared-core refactor. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../skills/.kiro/skills/alignment/SKILL.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/kiro_and_antigravity/skills/.kiro/skills/alignment/SKILL.md b/kiro_and_antigravity/skills/.kiro/skills/alignment/SKILL.md index 25367f5..6d2cfa0 100644 --- a/kiro_and_antigravity/skills/.kiro/skills/alignment/SKILL.md +++ b/kiro_and_antigravity/skills/.kiro/skills/alignment/SKILL.md @@ -179,9 +179,14 @@ Create the `.reviews/alignment/` directory if it doesn't exist. **If documents came from conversation history:** Ask: "The documents aren't saved to files yet. Where should I write them?" Suggest a reasonable path based on project structure. -### Step 2: TDD task rewrite (mandatory) +### Step 2: TDD task rewrite (when applicable) -Once alignment is confirmed, rewrite all action items in red/green/refactor format. This is not optional — it produces better implementations. +Once alignment is confirmed, check whether tasks should be rewritten in red/green/refactor format. **Skip this step if:** + +- Tasks are already in red/green/refactor format +- Tasks don't involve code implementation (e.g., infrastructure provisioning, documentation, design work, data migrations, manual processes) + +If neither condition applies, rewrite action items in red/green/refactor format — it produces better implementations. **Why this works:** From 717c0a43393dce9beab3d7066c07c08eaae7fdcf Mon Sep 17 00:00:00 2001 From: Ovid Date: Tue, 30 Jun 2026 22:04:53 +0400 Subject: [PATCH 06/19] Replace tautological output guard with golden clean_body() check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous tests/test_legacy_output_unchanged.py sourced the "original" generator via `git show HEAD:scripts/convert_skills.py`. Once the refactor was committed, HEAD became the refactored commit, so the test compared the refactored generator against itself and could never fail (mutating clean_body() to emit .BROKEN/ still passed). Rewrite it as a real golden-output guard: for every in-scope source skill (plugins/paad/skills/* minus the skip_names help and makefile), assert clean_body() byte-equals the committed kiro_and_antigravity/skills/.kiro/skills//SKILL.md. This is the golden guard the design called for — read-only (no subprocess, no tree mutation, no artifacts), covering every path replacement and section exclusion across ALL skills, and it can genuinely fail. A sanity test asserts each in-scope skill actually has a committed counterpart so the comparison can't silently cover nothing. Verified mutation-proof: breaking the bare `paad/` -> `.reviews/` replacement now FAILS the guard (caught in agentic-a11y, a skill the old pushback-only test never exercised); reverting restores green. Also refocus tests/test_skill_body.py from a single committed-output snapshot into focused unit tests on small inputs that pin each transform independently: section exclusion, all known path neutralizations, the bare `paad/` fallback, the legacy /paad: line strip, and the injectable paad_ref_transform seam (the future power generator's #name rewrite). This closes the coverage gap where only pushback was exercised. All 14 tests pass; working tree clean (no kiro_and_antigravity/ churn). Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/test_legacy_output_unchanged.py | 142 +++++++++----------------- tests/test_skill_body.py | 114 +++++++++++++++------ 2 files changed, 133 insertions(+), 123 deletions(-) diff --git a/tests/test_legacy_output_unchanged.py b/tests/test_legacy_output_unchanged.py index e6e7b6f..2cab37a 100644 --- a/tests/test_legacy_output_unchanged.py +++ b/tests/test_legacy_output_unchanged.py @@ -1,109 +1,67 @@ -"""Regression guard: the refactor must not change `convert_skills.py`'s output. +"""Golden-output regression guard for the shared body-cleaning core. -The load-bearing invariant for Task 1 is that extracting `clean_body()` into -`scripts/skill_body.py` leaves the legacy Kiro/Antigravity generator producing -*byte-for-byte identical* output. This test proves that directly by running two -generators against the same source tree and comparing their full output: +The load-bearing invariant for Task 1 is that the extracted `clean_body()` +reproduces `convert_skills.py`'s legacy per-skill body output EXACTLY. We prove +that against the committed Kiro tree, which is the authoritative golden output: +each `kiro_and_antigravity/skills/.kiro/skills//SKILL.md` is exactly what +the generator produces for that source skill today. - * the ORIGINAL generator, extracted verbatim from `git show HEAD:...` - * the CURRENT (refactored) generator on disk +For every in-scope source skill (all `plugins/paad/skills/*/SKILL.md` except +the `skip_names` `help` and `makefile`), this asserts: -If they ever diverge, `clean_body()` no longer reproduces the original -behavior and this test fails. + clean_body() == -Why compare original-vs-current rather than `git diff` against the committed -tree: the committed tree has a PRE-EXISTING drift (the `alignment` source was -edited without regenerating the tree), so a raw `git diff --exit-code` after -regeneration is non-empty regardless of this refactor. Comparing the two -generators isolates the refactor's effect from that unrelated drift, and the -original generator is the authoritative snapshot of "current behavior". +This is read-only (no subprocess, no tree mutation, no artifacts), and it +covers every path replacement and section exclusion across ALL skills — not +just one representative. Unlike a `git show HEAD` comparison, it can genuinely +fail if `clean_body()` ever diverges from the committed golden output. """ -import filecmp -import os -import shutil -import subprocess -import sys -from pathlib import Path +import pytest from conftest import REPO_ROOT +from skill_body import clean_body -SCRIPTS_REL = "scripts/convert_skills.py" -SKILL_BODY_REL = "scripts/skill_body.py" -SOURCE_REL = "plugins/paad/skills" -TARGET_REL = "kiro_and_antigravity/skills" +SOURCE_DIR = REPO_ROOT / "plugins" / "paad" / "skills" +KIRO_DIR = REPO_ROOT / "kiro_and_antigravity" / "skills" / ".kiro" / "skills" +# Mirrors convert_skills.py's skip_names: these skills are not emitted to the +# Kiro tree and so have no golden output to compare against. +SKIP_NAMES = {"help", "makefile"} -def _run_generator(script_path, workdir): - """Run a convert_skills.py copy in `workdir`, return CompletedProcess.""" - return subprocess.run( - [sys.executable, str(script_path)], - cwd=workdir, - capture_output=True, - text=True, - ) - - -def _stage_workdir(tmp_path, name, convert_script_text): - """Build an isolated repo-shaped workdir with source + a given generator. - - The current `scripts/skill_body.py` is always copied in because both the - original and refactored generators must import the same shared module under - test (the original generator pre-refactor had no import, so copying it is - harmless for the original and required for the refactored one). - """ - workdir = tmp_path / name - scripts_dir = workdir / "scripts" - scripts_dir.mkdir(parents=True) - - # Copy the full skills source tree (the generator reads from here). - shutil.copytree(REPO_ROOT / SOURCE_REL, workdir / SOURCE_REL) - - # Provide the shared core so refactored imports resolve. - shutil.copy2(REPO_ROOT / SKILL_BODY_REL, scripts_dir / "skill_body.py") - # Write the generator under test. - (scripts_dir / "convert_skills.py").write_text( - convert_script_text, encoding="utf-8" +def _in_scope_skill_names(): + names = sorted( + p.name + for p in SOURCE_DIR.iterdir() + if p.is_dir() + and p.name not in SKIP_NAMES + and (p / "SKILL.md").exists() ) + assert names, "no in-scope source skills found — test setup is wrong" + return names - return workdir +def test_in_scope_skills_have_committed_golden_output(): + """Sanity: every in-scope source skill has a committed .kiro counterpart. -def _dir_trees_equal(a, b): - """Recursively assert two directory trees are byte-for-byte identical.""" - cmp = filecmp.dircmp(a, b) - assert not cmp.left_only, f"only in {a}: {cmp.left_only}" - assert not cmp.right_only, f"only in {b}: {cmp.right_only}" - # filecmp's shallow=True can miss content changes with equal size/mtime; - # force a full content comparison. - _, mismatch, errors = filecmp.cmpfiles( - a, b, cmp.common_files, shallow=False + Guards against the golden comparison silently covering nothing if the tree + layout changes. + """ + for name in _in_scope_skill_names(): + assert (KIRO_DIR / name / "SKILL.md").exists(), ( + f"missing committed golden output for in-scope skill {name!r}" + ) + + +@pytest.mark.parametrize("name", _in_scope_skill_names()) +def test_clean_body_matches_committed_golden_output(name): + """clean_body(source) must byte-equal the committed .kiro golden output.""" + source = (SOURCE_DIR / name / "SKILL.md").read_text(encoding="utf-8") + golden = (KIRO_DIR / name / "SKILL.md").read_text(encoding="utf-8") + + assert clean_body(source) == golden, ( + f"clean_body() output for {name!r} no longer matches the committed " + f"Kiro golden output; the shared core has diverged from the legacy " + f"generator behavior." ) - assert not mismatch, f"content differs in {a} vs {b}: {mismatch}" - assert not errors, f"comparison errors in {a} vs {b}: {errors}" - for sub in cmp.common_dirs: - _dir_trees_equal(Path(a) / sub, Path(b) / sub) - - -def test_refactored_generator_output_matches_original(tmp_path): - """Refactored convert_skills.py must produce identical output to HEAD's.""" - original_text = subprocess.run( - ["git", "show", f"HEAD:{SCRIPTS_REL}"], - cwd=REPO_ROOT, - capture_output=True, - text=True, - check=True, - ).stdout - current_text = (REPO_ROOT / SCRIPTS_REL).read_text(encoding="utf-8") - - orig_wd = _stage_workdir(tmp_path, "original", original_text) - cur_wd = _stage_workdir(tmp_path, "current", current_text) - - orig_run = _run_generator(orig_wd / "scripts" / "convert_skills.py", orig_wd) - cur_run = _run_generator(cur_wd / "scripts" / "convert_skills.py", cur_wd) - - assert orig_run.returncode == 0, orig_run.stderr - assert cur_run.returncode == 0, cur_run.stderr - - _dir_trees_equal(orig_wd / TARGET_REL, cur_wd / TARGET_REL) diff --git a/tests/test_skill_body.py b/tests/test_skill_body.py index 04d8adf..ea3b377 100644 --- a/tests/test_skill_body.py +++ b/tests/test_skill_body.py @@ -1,46 +1,98 @@ -"""Unit tests for the shared body-cleaning core (`scripts/skill_body.py`). +"""Focused unit tests for the shared body-cleaning core (`scripts/skill_body.py`). -These tests pin the CURRENT behavior of `convert_skills.py`'s inline -body-cleaning logic so that the extracted, shared `clean_body()` function can -never silently diverge from it. The committed Kiro tree is the authoritative -snapshot of that behavior: each `.kiro/skills//SKILL.md` is exactly what -the inline logic produces for that skill today. +These pin the individual transform behaviors of `clean_body()` on small, +self-contained inputs (no file I/O), documenting the core's contract: + + * which sections survive (EXCLUDED_SECTIONS), + * `paad/...` path neutralization (including the bare `paad/` fallback), + * the legacy `/paad:` line-stripping default, + * the injectable `paad_ref_transform` seam for the future power generator. + +The full-tree golden comparison against the committed Kiro output lives in +`test_legacy_output_unchanged.py`; this file isolates each rule so a failure +points at a specific transform. """ -from pathlib import Path +from skill_body import EXCLUDED_SECTIONS, clean_body -from conftest import REPO_ROOT -SOURCE_DIR = REPO_ROOT / "plugins" / "paad" / "skills" -KIRO_DIR = REPO_ROOT / "kiro_and_antigravity" / "skills" / ".kiro" / "skills" +def test_excluded_sections_constant_lists_the_four_headers(): + """The named constant is the single source of truth for dropped sections.""" + assert EXCLUDED_SECTIONS == [ + "Arguments", + "Input Resolution", + "Pre-flight Checks", + "Document classification", + ] -def _read(path: Path) -> str: - return path.read_text(encoding="utf-8") +def test_excluded_sections_are_dropped_kept_sections_survive(): + content = ( + "# Title\n\nIntro.\n\n" + "## Arguments\n\nargs body\n\n" + "## Input Resolution\n\nresolution body\n\n" + "## Pre-flight Checks\n\npreflight body\n\n" + "## Document classification\n\nclass body\n\n" + "## Phase 1\n\nkept body\n" + ) + out = clean_body(content) + assert "## Phase 1" in out + assert "kept body" in out + for dropped in ("## Arguments", "args body", "## Input Resolution", + "resolution body", "## Pre-flight Checks", "preflight body", + "## Document classification", "class body"): + assert dropped not in out -def test_clean_body_reproduces_pushback_committed_output(): - """`clean_body()` must reproduce the committed pushback body exactly. - pushback is the representative skill: it exercises section exclusion - (`Input Resolution`), path neutralization (`paad/pushback-reviews/` -> - `.reviews/pushback/`), and whitespace collapsing. - """ - from skill_body import clean_body +def test_known_paad_paths_are_neutralized(): + content = ( + "# T\n\n## Body\n\n" + "Write to paad/pushback-reviews/x.md and paad/code-reviews/y.md " + "and paad/architecture-reviews/z.md and paad/alignment-reviews/w.md.\n" + ) + out = clean_body(content) - source = _read(SOURCE_DIR / "pushback" / "SKILL.md") - expected = _read(KIRO_DIR / "pushback" / "SKILL.md") + assert ".reviews/pushback/x.md" in out + assert ".reviews/code/y.md" in out + assert ".reviews/architecture/z.md" in out + assert ".reviews/alignment/w.md" in out + assert "paad/" not in out - assert clean_body(source) == expected +def test_bare_paad_path_falls_back_to_reviews(): + """The catch-all `paad/` -> `.reviews/` replacement (not covered by pushback).""" + content = "# T\n\n## Body\n\nSee paad/something-else/file.md here.\n" + out = clean_body(content) -def test_excluded_sections_constant_lists_the_four_headers(): - """The named constant is the single source of truth for dropped sections.""" - from skill_body import EXCLUDED_SECTIONS + assert ".reviews/something-else/file.md" in out + assert "paad/" not in out - assert EXCLUDED_SECTIONS == [ - "Arguments", - "Input Resolution", - "Pre-flight Checks", - "Document classification", - ] + +def test_legacy_strip_removes_paad_reference_lines(): + content = ( + "# T\n\n## Body\n\n" + "Keep this line.\n" + "Follow up with /paad:agentic-review when done.\n" + "Keep this too.\n" + ) + out = clean_body(content) + + assert "Keep this line." in out + assert "Keep this too." in out + assert "/paad:" not in out + assert "agentic-review" not in out + + +def test_paad_ref_transform_is_injectable(): + """The /paad: handling is a seam: a custom transform replaces the default.""" + content = "# T\n\n## Body\n\nSee /paad:vibe for details.\n" + + def rewrite_to_anchor(body): + import re + return re.sub(r'/paad:([a-z0-9-]+)', r'#\1', body) + + out = clean_body(content, paad_ref_transform=rewrite_to_anchor) + + assert "#vibe" in out + assert "/paad:" not in out From 181d77a9e73e4d0d7b45a414d2f44296dd641138 Mon Sep 17 00:00:00 2001 From: Ovid Date: Tue, 30 Jun 2026 22:09:34 +0400 Subject: [PATCH 07/19] Harden shared core: None-sentinel default, drop dead import, seam note Apply three minor code-quality items to de-risk the seam the next task (build-kiro-power.py) consumes: - clean_body() now uses the None-sentinel pattern for excluded_sections instead of a mutable module-level list as the default argument. EXCLUDED_SECTIONS stays a list (the named-constant assertion is intact) and the generated output is unchanged (zero-diff guard still clean). - Remove the dead `import os` from convert_skills.py (unused; pre-existing). - Add a one-line note on neutralize_paad_paths clarifying it is NOT a seam: path neutralization is shared-and-fixed for both generators, unlike the injectable paad_ref_transform. Prevents the next author assuming symmetry. All 14 tests pass; `python3 scripts/convert_skills.py` then `git diff --exit-code -- kiro_and_antigravity/` is ZERO diff. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/convert_skills.py | 1 - scripts/skill_body.py | 12 ++++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/scripts/convert_skills.py b/scripts/convert_skills.py index 428aa05..769bb15 100755 --- a/scripts/convert_skills.py +++ b/scripts/convert_skills.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -import os import re import sys from pathlib import Path diff --git a/scripts/skill_body.py b/scripts/skill_body.py index 3e95121..df330a8 100644 --- a/scripts/skill_body.py +++ b/scripts/skill_body.py @@ -35,7 +35,9 @@ def neutralize_paad_paths(body): """Rewrite `paad/...` output paths to neutral `.reviews/...` paths. Shared by both generators: the copy-source skills must not reference the - plugin's own `paad/` review directories. + plugin's own `paad/` review directories. NOTE: this is NOT a seam — path + neutralization is fixed and identical for both generators. Only + `paad_ref_transform` (the `/paad:` handling) is meant to vary between them. """ body = body.replace("paad/architecture-reviews/", ".reviews/architecture/") body = body.replace("paad/code-reviews/", ".reviews/code/") @@ -60,10 +62,13 @@ def strip_paad_references(body): return body -def clean_body(content, excluded_sections=EXCLUDED_SECTIONS, +def clean_body(content, excluded_sections=None, paad_ref_transform=strip_paad_references): """Transform a raw SKILL.md `content` string into a cleaned skill body. + `excluded_sections` defaults to the module-level `EXCLUDED_SECTIONS` via a + None sentinel (avoids the mutable-default-argument pitfall). + The pipeline is: 1. Split into sections by `##`+ headers. 2. Drop sections whose header matches any `excluded_sections` entry. @@ -75,6 +80,9 @@ def clean_body(content, excluded_sections=EXCLUDED_SECTIONS, Steps 1-2 (which sections survive) are shared and fixed across generators; only `paad_ref_transform` is meant to vary. """ + if excluded_sections is None: + excluded_sections = EXCLUDED_SECTIONS + # Split into sections by headers (##). The capturing group keeps each # header line as its own element so we can re-pair headers with bodies. parts = re.split(r'\n(##+ .*)', content) From 548d0109b7da1bb3e6707143346893715f75e2d2 Mon Sep 17 00:00:00 2001 From: Ovid Date: Tue, 30 Jun 2026 22:18:57 +0400 Subject: [PATCH 08/19] Add Kiro power steering-file generator (Task 2) Generate one `steering/.md` per in-scope PAAD skill from `plugins/paad/skills/*/SKILL.md`. The 7 files (all skills except `help` and `makefile`) are shaped as `---\ninclusion: manual\n---` frontmatter (literal first content, no leading blank, no BOM) followed by the SKILL.md body. The body is the source run through the shared `clean_body()` core plus three power-specific transforms layered on top (not re-implementing section exclusion / path neutralization): - cross-skill refs `/paad:` -> `#` (injected as clean_body's paad_ref_transform, replacing the legacy line-stripping; also re-run over the whole body to catch intro-prose refs that clean_body's section-only transform misses, e.g. fix-architecture) - surviving prose `$ARGUMENTS` -> a "state the scope in your chat message" prompt (the `If no $ARGUMENTS provided` idiom is rewritten as a clean conditional); digraph `$ARGUMENTS` is left untouched - ```dot digraphs pass through verbatim (protected from the $ARGUMENTS transform) The source name:/description: frontmatter is stripped (it belongs in POWER.md, a later task). Output is idempotent and pure (build_steering_file is string->string; generate writes the files). Tests cover: exactly 7 files, help/makefile absent, frontmatter-first shape with single blank line, cross-ref rewrite (sections + intro), $ARGUMENTS->prose, and verbatim digraph survival. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/build-kiro-power.py | 180 +++++++++++++++ steering/agentic-a11y.md | 350 ++++++++++++++++++++++++++++++ steering/agentic-architecture.md | 252 +++++++++++++++++++++ steering/agentic-review.md | 157 ++++++++++++++ steering/alignment.md | 223 +++++++++++++++++++ steering/fix-architecture.md | 231 ++++++++++++++++++++ steering/pushback.md | 192 ++++++++++++++++ steering/vibe.md | 154 +++++++++++++ tests/test_kiro_power_steering.py | 230 ++++++++++++++++++++ 9 files changed, 1969 insertions(+) create mode 100644 scripts/build-kiro-power.py create mode 100644 steering/agentic-a11y.md create mode 100644 steering/agentic-architecture.md create mode 100644 steering/agentic-review.md create mode 100644 steering/alignment.md create mode 100644 steering/fix-architecture.md create mode 100644 steering/pushback.md create mode 100644 steering/vibe.md create mode 100644 tests/test_kiro_power_steering.py diff --git a/scripts/build-kiro-power.py b/scripts/build-kiro-power.py new file mode 100644 index 0000000..070f87d --- /dev/null +++ b/scripts/build-kiro-power.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 +"""Generate the Kiro power's `steering/` files from the PAAD skills. + +Each in-scope `plugins/paad/skills//SKILL.md` becomes a +`steering/.md` whose body is the source SKILL.md run through the SHARED +body-cleaning core (`skill_body.clean_body`) plus three POWER-SPECIFIC +transforms layered on top: + + 1. Cross-skill refs `/paad:` -> `#` (injected as `clean_body`'s + `paad_ref_transform`, *replacing* the legacy line-stripping — the line is + kept, only the token is rewritten). + 2. Surviving prose `$ARGUMENTS` -> a prompt telling the user to state the + scope in their chat message. Kiro slash commands have NO argument + mechanism, so the prose must prompt, not imply auto-capture. Applied AFTER + `clean_body` because it is power-specific, not part of the shared core. + 3. ```dot digraphs pass through verbatim — `clean_body` already preserves + them, and transform #2 deliberately skips fenced `dot` blocks so digraph + `$ARGUMENTS` node labels are not rewritten. + +The source SKILL.md's own `name:`/`description:` frontmatter is STRIPPED; each +steering file is prefixed with the literal `---\ninclusion: manual\n---` +frontmatter as its first content (no leading blank line, no BOM). The +`name:`/`description:` metadata is surfaced in POWER.md (a later task), not here. + +This file is Task 2: steering files only. POWER.md aggregation is Task 3. +""" + +import re +import sys +from pathlib import Path + +# Make the shared body-cleaning core importable regardless of cwd. +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from skill_body import clean_body # noqa: E402 + +REPO_ROOT = Path(__file__).resolve().parent.parent +SOURCE_DIR = REPO_ROOT / "plugins" / "paad" / "skills" +STEERING_DIR = REPO_ROOT / "steering" + +# Mirrors convert_skills.py's skip_names: these two skills are not emitted. +# `help` becomes POWER.md's index; `makefile` is intentionally omitted to keep +# the power's skill set identical to the legacy Kiro output. +SKIP_NAMES = {"help", "makefile"} + +STEERING_FRONTMATTER = "---\ninclusion: manual\n---\n" + +# A self-contained ```dot ... ``` fenced block. Used to protect digraphs from +# the `$ARGUMENTS` prose transform (digraphs are copied verbatim). +_DOT_BLOCK = re.compile(r"```dot\n.*?\n```", re.DOTALL) + +# Leading YAML frontmatter (`---\n...\n---`) at the very start of a body. +# `clean_body` retains whatever precedes the first `##`, which includes the +# source SKILL.md's `name:`/`description:` frontmatter. Steering files replace +# that with their own `inclusion: manual` frontmatter, so we strip it here. +_LEADING_FRONTMATTER = re.compile(r"\A---\n.*?\n---\n", re.DOTALL) + + +def rewrite_cross_skill_refs(text): + """Power `paad_ref_transform`: rewrite `/paad:` -> `#`. + + Unlike the legacy `strip_paad_references`, this keeps the surrounding line + and only rewrites the reference token, so cross-skill mentions become Kiro + `#` steering references. + """ + return re.sub(r"/paad:([a-z0-9-]+)", r"#\1", text) + + +def strip_leading_frontmatter(body): + """Remove the source SKILL.md's leading `---...---` frontmatter block. + + `clean_body` keeps the source's `name:`/`description:` frontmatter (it + precedes the first `##`); steering files replace it with their own + `inclusion: manual` frontmatter, so we drop the original here. The body then + starts at the skill's `# Title` h1. + """ + return _LEADING_FRONTMATTER.sub("", body, count=1) + + +def rewrite_arguments_to_scope_prompt(body): + """Rewrite surviving prose `$ARGUMENTS` tokens into a scope prompt. + + Kiro manual steering files have no argument mechanism, so any `$ARGUMENTS` + that survives `clean_body` (most live in excluded sections and are already + gone) must prompt the user to state the scope in their chat message rather + than imply automatic capture. + + Digraph `$ARGUMENTS` (inside ```dot blocks) is left untouched — digraphs are + copied verbatim — so this protects fenced `dot` blocks before rewriting. + """ + scope_prose = ( + "the path or scope you state in your chat message (for example, `src/`)" + ) + + # Protect ```dot blocks: stash them, rewrite, then restore. + stash = [] + + def _stash(match): + stash.append(match.group(0)) + return f"\x00DOT{len(stash) - 1}\x00" + + protected = _DOT_BLOCK.sub(_stash, body) + + # Special-case the `If no `$ARGUMENTS` provided` idiom (the real vibe.md + # phrasing): a blunt noun substitution yields the ungrammatical "If no the + # path or scope ... provided". Rewrite the whole clause as a clean + # conditional that still prompts the user for scope. + protected = re.sub( + r"If no `?\$ARGUMENTS`?\s+(?:is\s+|are\s+|was\s+)?provided", + "If you don't state a scope in your chat message", + protected, + ) + + # Replace any remaining `$ARGUMENTS` (optionally backtick-wrapped) with the + # prose noun phrase. + protected = re.sub(r"`?\$ARGUMENTS`?", scope_prose, protected) + + # Restore the digraphs verbatim. + def _restore(match): + return stash[int(match.group(1))] + + return re.sub(r"\x00DOT(\d+)\x00", _restore, protected) + + +def prepend_steering_frontmatter(body): + """Prefix `body` with the literal `inclusion: manual` frontmatter. + + The result's first content is `---\\ninclusion: manual\\n---` — no leading + blank line, no BOM — followed by exactly one blank line and then the body's + `# Title`. Leading whitespace left by frontmatter-stripping is trimmed so the + separator is never doubled. + """ + return STEERING_FRONTMATTER + "\n" + body.lstrip("\n") + + +def build_steering_file(source_content, skill_name): + """Return the full `steering/.md` content for one source skill. + + Pure (string in -> string out). The power transforms layer ON TOP of the + shared `clean_body()` — section exclusion, path neutralization, and + whitespace collapsing all come from `clean_body`; this function only adds + the power-specific cross-ref / `$ARGUMENTS` / frontmatter handling. + """ + body = clean_body(source_content, paad_ref_transform=rewrite_cross_skill_refs) + body = strip_leading_frontmatter(body) + # `clean_body` only applies `paad_ref_transform` to `##` sections, so a + # `/paad:` in the pre-`##` intro (e.g. fix-architecture) would survive. + # Re-run the rewrite over the whole body to catch those. Idempotent: a + # `#name` already rewritten is left unchanged. + body = rewrite_cross_skill_refs(body) + body = rewrite_arguments_to_scope_prompt(body) + return prepend_steering_frontmatter(body) + + +def _in_scope_skill_names(source_dir): + return sorted( + p.name + for p in Path(source_dir).iterdir() + if p.is_dir() + and p.name not in SKIP_NAMES + and (p / "SKILL.md").exists() + ) + + +def generate(source_dir=SOURCE_DIR, out_dir=STEERING_DIR): + """Write one `steering/.md` per in-scope skill into `out_dir`.""" + source_dir = Path(source_dir) + out_dir = Path(out_dir) + out_dir.mkdir(parents=True, exist_ok=True) + + for name in _in_scope_skill_names(source_dir): + source = (source_dir / name / "SKILL.md").read_text(encoding="utf-8") + (out_dir / f"{name}.md").write_text( + build_steering_file(source, name), encoding="utf-8" + ) + print(f"Wrote steering/{name}.md") + + +if __name__ == "__main__": + generate() + print("Steering generation complete.") diff --git a/steering/agentic-a11y.md b/steering/agentic-a11y.md new file mode 100644 index 0000000..7e3eac6 --- /dev/null +++ b/steering/agentic-a11y.md @@ -0,0 +1,350 @@ +--- +inclusion: manual +--- + +# Accessibility Audit + +Multi-agent accessibility audit of user-facing code across any platform. Dispatches specialist agents in parallel — each focused on a different disability category — verifies findings to filter false positives, maps every issue to specific accessibility criteria, and produces a persistent report with concrete fix recommendations. + +**Conformance target:** WCAG 2.2 AA as baseline (applied via WCAG2ICT for non-web platforms). AAA criteria are flagged as bonus recommendations. Platform-specific guidelines (Apple HIG Accessibility, Android Accessibility, Xbox Accessibility Guidelines) are referenced where applicable. + +**This is a technique skill.** Follow the phases in order. Do not skip verification. + +## Phase 1: Reconnaissance + +Run these steps and collect results: + +### 1. Platform detection + +Classify the project into one or more platforms from the table above. A project may span multiple platforms (e.g., a web app with a CLI tool, or a React Native app with web support). Record all detected platforms — specialists will receive this classification. + +### 2. Tech stack identification + +Per detected platform: + +- **Web:** Frameworks (React, Vue, Angular, Svelte, Next.js, etc.), CSS approach (Tailwind, CSS Modules, styled-components, etc.), component libraries (MUI, Radix, Headless UI, Bootstrap — note built-in a11y support) +- **iOS:** SwiftUI vs UIKit vs mixed, any a11y wrapper libraries +- **Android:** Jetpack Compose vs View system vs mixed, any a11y libraries +- **React Native:** version, any a11y libraries (e.g., `react-native-a11y`) +- **Flutter:** version, any a11y packages +- **Desktop:** framework and toolkit +- **CLI:** argument parser, output formatting library, terminal UI library (if any) +- **Game:** engine, UI system (in-engine UI, HTML overlay, custom) + +### 3. Inventory user-facing code + +Collect all files that produce user-facing output or handle user interaction, grouped by platform. + +### 4. Check for existing a11y tooling + +- **Web:** `eslint-plugin-jsx-a11y`, `eslint-plugin-vuejs-accessibility`, `axe-core`, `pa11y`, Lighthouse configs +- **iOS:** Accessibility Inspector usage, XCTest accessibility tests +- **Android:** Accessibility Scanner, Espresso accessibility checks, `AccessibilityChecks.enable()` +- **React Native:** `@testing-library/react-native` accessibility queries +- **Flutter:** `SemanticsDebugger`, accessibility-related widget tests +- **CLI:** structured output tests, `--no-color` support tests +- **Game:** accessibility options menu, remapping tests +- **CI:** any automated a11y checks in CI pipeline +- Note what's present — specialists should not re-flag issues that existing tooling already catches, unless the tooling is misconfigured or findings are suppressed + +### 5. Scan for steering files + +`CLAUDE.md`, `AGENTS.md`, any a11y-specific documentation or guidelines. + +### 6. Estimate scope size + +- **Small:** <20 user-facing files +- **Medium:** 20-100 user-facing files +- **Large:** 100+ user-facing files + +### 7. Build manifest + +Files to audit, grouped for specialists, annotated with detected platform(s). + +**Steering file caveat:** Include in every agent prompt: "Steering files (CLAUDE.md, etc.) describe conventions but may be stale. If you find a contradiction between steering files and actual code, flag it as a finding." + +## Phase 2: Specialist Audit (Parallel) + +Dispatch these agents simultaneously using the Agent tool. Each receives: the file manifest, detected platform(s), steering file contents, existing a11y tooling notes, and their specialist focus. + +### Core specialists (always dispatched) + +| Agent | Focus | Who it helps | +|-------|-------|-------------| +| **Screen Reader & Assistive Tech** | Programmatic exposure of UI semantics so assistive technologies can convey structure, content, and state to users | Blind and low-vision users using screen readers (VoiceOver, TalkBack, NVDA, JAWS, Orca) | +| **Visual & Color** | Sufficient contrast, independence from color alone, support for magnification/text scaling, visible focus/selection indicators | Low-vision users, colorblind users | +| **Keyboard & Motor** | Complete operability without fine motor control — keyboard, switch, sip-and-puff, eye tracking, voice control — with adequate target sizes and no traps | Motor-impaired users, switch access users, sip-and-puff users, voice control users | +| **Cognitive & Learning** | Consistent, predictable interfaces with clear feedback, error recovery, plain language, and no unnecessary cognitive load | Users with cognitive disabilities, learning disabilities, attention disorders, memory impairments | +| **Multimedia & Temporal** | Alternatives for audio/visual content, safe motion/animation, adequate time, no seizure triggers | Deaf/hard-of-hearing users, vestibular disorders, photosensitive epilepsy, users who need more time | + +### Conditional specialist + +Dispatch when the platform has framework-specific a11y pitfalls (web frameworks, SwiftUI, Compose, Flutter, React Native, game engines): + +| Agent | Focus | Input | +|-------|-------|-------| +| **Platform-Specific Patterns** | Framework/platform-specific a11y pitfalls, misuse of platform a11y APIs, navigation/routing a11y, dynamic content patterns | File manifest + platform/framework detection results | + +### Agent prompt template + +Each specialist agent prompt must include: +- The list of files to audit (from their manifest group) +- Contents of files in their audit scope +- The detected platform(s) +- Steering file contents with the staleness caveat +- Existing a11y tooling notes +- Instruction: "You are an accessibility specialist focused on [FOCUS AREA]. The detected platform(s) for this project: [PLATFORMS]. Your goal is to find accessibility barriers that affect [USER GROUP]. For each finding report: file:line, what's wrong, which accessibility criterion it violates (WCAG 2.2 criterion where applicable, or platform-specific guideline), the conformance level (A/AA/AAA), who is affected and how, a concrete code-level fix, and your confidence (0-100). Only report findings with confidence >= 60." + +### Platform-specific checks per specialist + +Include the relevant platform section(s) in each specialist's prompt based on detected platform(s). + +#### Screen Reader & Assistive Tech + +**Web:** (1) Correct ARIA roles, states, and properties on custom components; (2) semantic HTML (headings, landmarks, lists, tables) over generic `div`/`span`; (3) heading hierarchy without skipped levels; (4) meaningful alt text (not filenames or "image"); (5) programmatically associated form labels; (6) `aria-live` regions for dynamic content; (7) link/button purpose clear from text or accessible name; (8) status messages via `role="status"` or `aria-live`. + +**iOS:** (1) `accessibilityLabel` set on all interactive and meaningful elements; (2) `accessibilityTraits` / SwiftUI `.accessibilityAddTraits()` correctly applied (`.button`, `.header`, `.image`, etc.); (3) `accessibilityHint` for non-obvious actions; (4) `isAccessibilityElement` grouping — containers group related info, decorative elements excluded; (5) `accessibilityValue` for sliders/progress; (6) `UIAccessibility.post(.screenChanged, ...)` and `.layoutChanged` notifications for dynamic updates; (7) custom actions via `accessibilityCustomActions`; (8) SwiftUI: `.accessibilityElement(children: .combine)` for logical grouping. + +**Android:** (1) `contentDescription` on all interactive and meaningful elements; (2) `importantForAccessibility` set correctly — decorative elements marked `no`; (3) `labelFor` connecting labels to inputs; (4) `accessibilityLiveRegion` for dynamic content; (5) `accessibilityHeading` on section headers; (6) Compose: `semantics { }` block with appropriate properties, `contentDescription`, `heading()`, `stateDescription`; (7) custom `AccessibilityNodeInfo` actions where needed; (8) `ViewCompat.setAccessibilityDelegate` for custom views. + +**React Native:** (1) `accessibilityLabel` on all interactive elements; (2) `accessibilityRole` set correctly (`button`, `header`, `link`, `image`, etc.); (3) `accessibilityState` for toggles, disabled, expanded states; (4) `accessibilityHint` for non-obvious actions; (5) `accessibilityLiveRegion` for dynamic updates; (6) `accessibilityElementsHidden` for decorative content; (7) `AccessibilityInfo.announceForAccessibility()` for dynamic state changes. + +**Flutter:** (1) `Semantics` widget wrapping meaningful UI with `label`, `button`, `header`, `image` properties; (2) `excludeSemantics: true` on decorative elements; (3) `MergeSemantics` for logically grouped content; (4) `SemanticsService.announce()` for dynamic updates; (5) `tooltip` properties on `IconButton` and similar; (6) `semanticsLabel` on `Text` where displayed text differs from meaning. + +**CLI:** (1) Structured, parseable output (not just visual formatting with boxes/lines); (2) information not conveyed solely by position/layout — screen readers linearize output; (3) progress indication via text updates, not just spinners or progress bars that rely on cursor repositioning; (4) error messages written to stderr with clear text (not just color/emoji). + +**Game:** (1) UI elements have text alternatives available for screen reader narration mode; (2) menu items narrated with name, type, and state; (3) game state changes announced; (4) narration option in settings if not always-on. + +#### Visual & Color + +**Web:** Contrast ratios — AA: 4.5:1 normal text, 3:1 large text (18pt+/14pt+ bold), 3:1 UI components and graphics. AAA: 7:1/4.5:1. Check CSS custom properties and theme values. Flag color-only meaning (error states, status indicators, required fields, links distinguished only by color). Reflow at 320px width. Text spacing override support. `prefers-contrast` media query support. + +**iOS:** (1) Dynamic Type support (`UIFontMetrics`, SwiftUI `.dynamicTypeSize`), test with all type sizes including accessibility sizes; (2) Bold Text support (`UIAccessibility.isBoldTextEnabled`); (3) Increase Contrast (`UIAccessibility.isDarkerSystemColorsEnabled`); (4) Reduce Transparency support; (5) sufficient contrast ratios in custom themes (same ratios as web); (6) color-only information has shape/icon/text alternatives; (7) SF Symbols accessibility variants. + +**Android:** (1) `sp` units for text (scales with user preference); (2) test at 200% font scale; (3) High Contrast Text setting support; (4) sufficient contrast in custom themes; (5) color-only meaning has alternatives; (6) Magnification gesture compatibility (no content hidden at 200%+ zoom); (7) custom views render correctly with font scaling. + +**React Native:** (1) Font scaling support — not disabled via `allowFontScaling={false}` unless justified; (2) layout accommodates scaled text without overflow/truncation; (3) contrast ratios on all custom-themed elements; (4) color-only meaning has alternatives. + +**Flutter:** (1) `MediaQuery.textScaleFactorOf(context)` respected — UI tested at 2.0 scale; (2) contrast ratios met in custom `ThemeData`; (3) color-only meaning has alternatives; (4) `MediaQuery.boldTextOf(context)` support; (5) `MediaQuery.highContrastOf(context)` support. + +**CLI:** (1) `--no-color` flag or `NO_COLOR` env var support; (2) information not conveyed by color alone — use labels, prefixes, symbols alongside color (e.g., `[ERROR]` not just red text); (3) supports `TERM` detection for capability; (4) works in high-contrast terminal themes. + +**Game:** (1) Colorblind mode(s) — at minimum deuteranopia (red-green), ideally also protanopia and tritanopia; (2) UI element outlines/patterns supplement color; (3) font size options or UI scaling; (4) high contrast UI option; (5) brightness/gamma controls; (6) important game elements distinguishable by shape, not just color. + +#### Keyboard & Motor + +**Web:** (1) Every interactive element reachable and activatable via keyboard; (2) no keyboard traps (focus can escape every modal, dropdown, overlay); (3) logical focus order matching visual layout; (4) visible focus indicators (not suppressed by `outline: none` without replacement); (5) skip navigation links; (6) custom widgets follow WAI-ARIA Authoring Practices keyboard patterns; (7) click targets at least 24x24 CSS px (AA), recommend 44x44 (AAA); (8) drag-and-drop has click/keyboard alternative; (9) single-character shortcuts remappable or only active on focus; (10) `pointer-events`, `user-select` not disabling expected interactions. + +**iOS:** (1) Full Keyboard Access support — all actions reachable via external keyboard; (2) Switch Control compatibility — all interactive elements in accessibility tree with correct order; (3) AssistiveTouch custom gesture alternatives for complex gestures; (4) minimum 44x44pt tap targets (Apple HIG); (5) no gesture-only actions without alternatives (e.g., swipe-to-delete must have edit button); (6) custom gesture recognizers don't block Switch Control; (7) `accessibilityActivationPoint` set for non-standard hit areas. + +**Android:** (1) Full keyboard navigation — `android:focusable`, logical `nextFocusDown/Up/Left/Right`; (2) Switch Access compatibility — all interactive elements focusable with adequate ordering; (3) minimum 48x48dp touch targets (Material Design); (4) no gesture-only actions without alternatives; (5) `android:importantForAccessibility` not hiding interactive elements; (6) custom views implement `onKeyDown`/`onKeyUp` for keyboard users; (7) TalkBack gesture alternatives for complex interactions. + +**React Native:** (1) `accessible={true}` on all interactive elements; (2) `accessibilityActions` for custom actions; (3) `onMagicTap`, `onEscape` handlers where appropriate (iOS); (4) minimum touch target sizes (48x48dp Android / 44x44pt iOS); (5) gesture-based interactions have tap alternatives. + +**Flutter:** (1) All interactive widgets have `Semantics` with tap/long press actions; (2) minimum touch targets via `MaterialTapTargetSize.padded`; (3) `FocusNode` and `FocusTraversalGroup` for logical keyboard order; (4) `RawKeyboardListener`/`KeyboardListener` for keyboard shortcuts; (5) no `IgnorePointer`/`AbsorbPointer` hiding accessible interactions; (6) custom `GestureDetector` actions have keyboard alternatives. + +**CLI:** (1) Keyboard-only by nature, but check: interactive prompts (e.g., fuzzy finders, multi-select) navigable with arrow keys and enter; (2) Ctrl-C always exits; (3) no mouse-only interactions in TUI; (4) tab completion where appropriate; (5) long-running operations cancellable via keyboard. + +**Game:** (1) Fully remappable controls; (2) multiple input device support (keyboard, mouse, controller, touch); (3) one-handed mode or alternative layouts; (4) no quick-time events without alternatives or adjustable timing; (5) auto-aim/aim assist option; (6) adjustable input sensitivity/dead zones; (7) hold-vs-toggle options for sustained inputs; (8) copilot/co-pilot mode if multiplayer. + +#### Cognitive & Learning + +**All platforms:** (1) Consistent navigation patterns across screens/pages; (2) consistent identification of common elements; (3) clear error messages identifying the problem and suggesting a fix; (4) labels and instructions on all form inputs; (5) predictable behavior — no unexpected context changes on focus or input; (6) adequate time — adjustable/extendable timeouts, warnings before expiry; (7) help in a consistent, findable location; (8) no unnecessary re-entry of previously provided information; (9) accessible authentication — no cognitive function tests (CAPTCHAs) without alternatives. + +**Web additionally:** (1) `lang` attribute on `` and `lang` attributes on content in other languages; (2) multiple ways to find pages (nav, search, sitemap); (3) breadcrumbs or location indicator. + +**iOS/Android additionally:** (1) Back navigation always works predictably; (2) undo support for destructive actions; (3) confirmation dialogs for irreversible operations. + +**CLI additionally:** (1) `--help` with clear, complete documentation; (2) confirmation prompts for destructive commands (with `--yes`/`-y` to skip); (3) meaningful exit codes; (4) consistent flag naming conventions (e.g., `--verbose` not `-v` in one command and `--debug` in another). + +**Game additionally:** (1) Tutorial or onboarding; (2) difficulty options including easy/story mode; (3) objective tracking/quest log; (4) adjustable game speed; (5) pause in single-player; (6) clear visual/audio feedback for actions; (7) option to simplify UI. + +#### Multimedia & Temporal + +**All platforms:** (1) Pre-recorded video has captions; (2) pre-recorded audio has transcript; (3) no content flashes more than 3 times per second; (4) auto-playing media has pause/stop control; (5) moving/blinking/scrolling content can be paused; (6) no time limits, or limits are adjustable/extendable with warning. + +**Web additionally:** `prefers-reduced-motion` media query respected — all CSS animations/transitions have reduced or disabled alternatives. Audio descriptions for pre-recorded video (AA). `