From agentic-development-workflow
Generates or upgrades a project's e2e-test skill to a three-tier BDD layer-gate shape. Handles scaffolding, migration from legacy, and cross-tool symlink placement.
How this skill is triggered — by the user, by Claude, or both
Slash command
/agentic-development-workflow:e2e-skill-scaffoldingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Generate a project-local **`e2e-test` skill** in the BDD + layer-gate **three-tier** shape. The generated
references/bdd-journeys.mdreferences/layer-gate-loop.mdreferences/three-tier-model.mdtemplates/derive-verification-recipe.sh.tmpltemplates/e2e-test.SKILL.md.tmpltemplates/journey-00-walking-skeleton.md.tmpltemplates/journeys-README.md.tmpltemplates/layer-gate-evidence.md.tmpltemplates/policy.md.tmpltemplates/preflight.sh.tmpltemplates/seed.sh.tmpltemplates/tool-selection.md.tmplGenerate a project-local e2e-test skill in the BDD + layer-gate three-tier shape. The generated
skill documents tests as natural-language journeys (intent, not click scripts), maps each to a layer
gate, and ships a separate tool-selection guide so the browser/device automation tool is resolved
per environment or pinned by preference. Idempotent — re-run to upgrade a project or refresh after the
standard evolves.
Where this fits:
/aep-onboard → /aep-scaffold ──delegates──► /aep-e2e-skill-scaffolding
│ emits skills/e2e-test/ (canonical)
[ /aep-design → /aep-launch → /aep-build → /aep-wrap ]
▲ build Phase 6-8 run journeys + record evidence; wrap flips the gate
Input: the project's stack (package.json / bts.jsonc), optional product-context.yaml.
Output: real skills/e2e-test/ + .claude/skills/e2e-test + .agents/skills/e2e-test symlinks.
For the conceptual model (why three tiers, how journeys map to gates) read
references/three-tier-model.md and
references/layer-gate-loop.md; for journey authoring conventions,
references/bdd-journeys.md.
The generated skill is project-owned (not from the AEP marketplace), so it is placed canonically by hand. The verified layout that makes one skill visible to Claude Code, Codex, and Pi (Phase 4 creates the two symlinks directly — no migration script, no user-level skill):
skills/e2e-test/ # REAL dir — source of truth, tracked in git as itself
.claude/skills/e2e-test → ../../skills/e2e-test # symlink (Claude Code discovery)
.agents/skills/e2e-test → ../../skills/e2e-test # symlink (Codex / Pi discovery)
This is independent of how the AEP aep-* packages are installed (the skills CLI manages those).
REAL="skills/e2e-test"
LEGACY=".claude/skills/e2e-test" # the old Claude-only placement
if [ -d "$REAL" ] && [ -e "$LEGACY" ] && [ ! -L "$LEGACY" ]; then echo "state: shadow-legacy (resolve $LEGACY first)";
elif [ -d "$REAL" ] && { [ -f "$REAL/policy.md" ] || [ -f "$REAL/journeys/README.md" ]; }; then echo "state: canonical (upgrade in place)";
elif [ -d "$REAL" ]; then echo "state: real-non-bdd (upgrade)";
elif [ -d "$LEGACY" ] && [ ! -L "$LEGACY" ]; then echo "state: thin-legacy (migrate $LEGACY → $REAL, then upgrade)";
else echo "state: absent (create fresh)"; fi
skills/e2e-test AND a real, non-symlink .claude/skills/e2e-test
exist) → resolve the shadow before anything else: merge any content only in .claude/skills/e2e-test
into skills/e2e-test, then git rm -r .claude/skills/e2e-test (or rm -rf if untracked). Phase 4
expose() will REFUSE a real .claude path, so this must be cleared first.mkdir -p skills then move the legacy dir, preserving history when possible:
git mv .claude/skills/e2e-test skills/e2e-test if it's git-tracked, else plain
mv .claude/skills/e2e-test skills/e2e-test (an untracked legacy dir makes git mv fail). Never delete
a real dir without moving its content.policy.md — re-confirm the policy (Phase 2), then add only the missing scaffold files
(policy.md, journeys/README.md + tool-selection.md when the target ≠ none,
00-walking-skeleton.md if no journeys exist, layer-gate-evidence.template.md) and reconcile
SKILL.md.Read the stack to fill template placeholders. Reuse the /aep-scaffold Default Tooling table:
| Placeholder | Source | Default |
|---|---|---|
{{PROJECT_NAME}} | package.json name / repo dir | repo dir name |
{{PKG_MANAGER}} | lockfile (bun.lockb/pnpm-lock.yaml/uv.lock…) | bun |
{{TEST_RUNNER}} | stack (TS→vitest, Py→pytest, Rust→cargo test, Go→go test) | vitest |
{{DEV_SERVER}} | scripts (bun run dev / uv run dev …) | bun run dev |
{{BASE_URL}} | .dev-workflow/ports.env contract | http://localhost:3001 |
{{SERVER_URL}} | .dev-workflow/ports.env contract | http://localhost:3000 |
{{TARGET_TYPE}} | native-uniwind→mobile, tauri/electrobun→desktop, no web frontend (CLI bin OR library/package exports)→cli, else web. Must agree with E2E_TARGET: cli→cli | web |
The policy (which tiers gate a layer, where to dogfood, when) is a per-project decision, not a default
to assume. Propose from the stack, then ask the user to confirm/adjust before rendering policy.md:
| Placeholder | Propose from | Confirm? |
|---|---|---|
{{E2E_TIERS}} | project type → tier table (references/three-tier-model.md): CLI/lib [1,2] (bash journey), API [1,3], web/mobile [1,2,3], config-only [1] | yes |
{{E2E_TARGET}} | CLI tool / library → cli (bash); web frontend + deploy config (wrangler.*, vercel.*) → offer deployed:<url>; web frontend, no deploy → local; no runnable surface (config/schema/docs) → none | yes |
{{E2E_JOURNEY_TIMING}} | cli / local target → pre-merge; deployed:<url> → post-deploy | yes |
{{E2E_LIVE_POLICY}} + {{E2E_MILESTONE_GATES}} | cost-bearing dogfood (live model calls, quota/fee-metered target) → propose milestone_gates_only + name the milestone gates; otherwise every_gate (then free) | yes |
{{E2E_SENSITIVE_PATHS}} | stack scan for auth / payments / migrations / deploy-CI globs → the human-owned deep-tier hard-floor list; rendered as the sensitive_paths: yaml lines in policy.md (one - "<glob>" per line) | yes |
{{SECRET_SCAN_CMD}} / {{SAST_CMD}} | available tooling (gitleaks, semgrep, stack-native scanners) → the inner-loop deterministic security gates; none only by explicit decline | yes |
{{E2E_PREFLIGHT_PROBES}} | target + live_policy → deploy-independent probes (secret names in CI, account fingerprint, env-var wiring) + target-bound probes (reachable, bindings, seedable); cli/TUI → provider auth/reachable, PTY health, fixture repos; rendered as policy.md probe-table rows (probe → precondition → mode → refusal tag) | yes |
Ask plainly, e.g. "This looks like a {type}. Proposed e2e policy: tiers {tiers}, dogfood target
{target}, timing {timing}, live policy {live_policy}. Keep, or adjust (e.g. dogfood against a
deployed Cloudflare URL post-merge)?" Then confirm the sensitive_paths seed list, the security-gate
commands, and the probe sets (/aep-gen-eval → references/verification-economics.md is the canon for
what these govern). Record the confirmed values; they fill policy.md and decide what Phase 3 emits.
Render each templates/*.tmpl with the Phase 2 substitutions into real skills/e2e-test/:
skills/e2e-test/
├── SKILL.md ← templates/e2e-test.SKILL.md.tmpl
├── policy.md ← templates/policy.md.tmpl (every E2E_* token from Phase 2, incl. SENSITIVE_PATHS + PREFLIGHT_PROBES)
├── journeys/ ← omit entirely when E2E_TARGET == none (Tier-2 N/A)
│ ├── README.md ← templates/journeys-README.md.tmpl
│ └── 00-walking-skeleton.md ← templates/journey-00-walking-skeleton.md.tmpl
├── tool-selection.md ← templates/tool-selection.md.tmpl (omit when E2E_TARGET == none)
├── layer-gate-evidence.template.md ← templates/layer-gate-evidence.md.tmpl
└── scripts/
├── seed.sh ← templates/seed.sh.tmpl (chmod +x)
├── preflight.sh ← templates/preflight.sh.tmpl (chmod +x; probe stubs per policy.md)
└── derive-verification-recipe.sh ← templates/derive-verification-recipe.sh.tmpl (chmod +x)
The last two are the runnable reference implementation of the verification-economics canon
(/aep-gen-eval → references/verification-economics.md): preflight.sh realizes the named-refusal
probe sets policy.md declares; derive-verification-recipe.sh is the binding tier derivation
/aep-build Phase 5 runs to emit .dev-workflow/verification-recipe.json. Both are project-owned
and meant to be adapted (precedent: seed.sh, aep-scaffold's audit.sh/converge.sh) — AEP
still ships no runtime.
Conditional on the policy — journeys/ + tool-selection.md are emitted for every dogfoodable
target, including cli (its journeys carry target: cli and dogfood the built binary via bash). Skip
both only when E2E_TARGET == none (no runnable surface). Two branch-specific emit rules:
E2E_TARGET == cli (a CLI binary or a library/package with exports but no web frontend), set
{{TARGET_TYPE}} = cli (not web) and adapt the skeleton to a command invocation — read
references/three-tier-model.md "cli-target skeleton" for the recipe
and why a target: web journey would deadlock the gate.E2E_TARGET == none, render the generated SKILL.md without its Tier-2 section and with no dead
links — read references/three-tier-model.md "none-target rendering"
for the exact substitution.On upgrade, write only files that are absent; for SKILL.md present-but-thin, replace it (it's
generated infrastructure docs, not hand-authored journeys) and tell the user. policy.md is never
silently overwritten — read the existing one, re-confirm with the user (Phase 2), then update it.
chmod +x skills/e2e-test/scripts/seed.sh skills/e2e-test/scripts/preflight.sh \
skills/e2e-test/scripts/derive-verification-recipe.sh
Idempotent and gotcha-safe — skip a correct symlink, fix a wrong one, refuse to clobber a real path (Phase 1 already migrated the legacy real dir):
expose() { # $1 = skill name (e2e-test)
local name="$1"
# Precondition: the real source must exist, else every symlink we create/keep is dangling.
[ -d "skills/$name" ] || { echo "REFUSE: skills/$name missing — emit the real skill dir (Phase 3) first"; return 1; }
for rt in .claude .agents; do
mkdir -p "$rt/skills"
local link="$rt/skills/$name"
if [ -L "$link" ] && [ -e "$link" ]; then
[ "$(readlink "$link")" = "../../skills/$name" ] || { rm -f "$link"; ( cd "$rt/skills" && ln -s "../../skills/$name" "$name" ); }
elif [ -L "$link" ]; then # dangling symlink — repoint it at the (now-present) source
rm -f "$link"; ( cd "$rt/skills" && ln -s "../../skills/$name" "$name" )
elif [ -e "$link" ]; then
echo "REFUSE: $link is a real path — migrate it into skills/ first"; return 1
else
( cd "$rt/skills" && ln -s "../../skills/$name" "$name" )
fi
done
}
expose e2e-test
Verify both resolve to the same source:
readlink -f .claude/skills/e2e-test # → …/skills/e2e-test
readlink -f .agents/skills/e2e-test # → …/skills/e2e-test
The gate is two-phase + covered — scripted_passed (Tier-1 green) → passed (all applicable tiers
green + every acceptance criterion proven + prior-layer journeys replay). See
references/layer-gate-loop.md and
references/three-tier-model.md.
product-context.yaml exists:
layer_gates[] entry uses the canonical enriched shape — status (including
scripted_passed), a coverage block (criteria_total / criteria_covered / uncovered), and
structured evidence (scripted / journeys / matrix). Don't rewrite existing gate state; just
add the missing coverage / evidence keys (back-compat: older gates without them still parse).docs/layer-gates/ exists; seed docs/layer-gates/0.md from
skills/e2e-test/layer-gate-evidence.template.md if absent (never overwrite a filled-in one).docs/layer-gates/<layer>.md (copy the evidence template). The loop works without the YAML state
machine — the two matrices + checklist are the record.# Core files (always emitted):
test -f skills/e2e-test/SKILL.md && test -f skills/e2e-test/policy.md \
&& test -f skills/e2e-test/layer-gate-evidence.template.md \
&& test -x skills/e2e-test/scripts/seed.sh \
&& test -x skills/e2e-test/scripts/preflight.sh \
&& test -x skills/e2e-test/scripts/derive-verification-recipe.sh && echo "core files OK"
# Journey tier (only when E2E_TARGET != none):
if [ -d skills/e2e-test/journeys ]; then
test -f skills/e2e-test/journeys/README.md && test -f skills/e2e-test/tool-selection.md \
&& echo "journey tier OK"
fi
# Syntax-check only — do NOT run seed.sh here: at scaffold time there is no dev server, so a full run
# would block in its wait loop. seed.sh runs for real later via the workspace hook once the server is up.
bash -n skills/e2e-test/scripts/seed.sh && echo "seed.sh syntax OK"
# No unrendered tokens — a leftover {{…}} means a Phase 2 confirmed decision (e.g. sensitive paths,
# preflight probes) was dropped at render time:
! grep -rn '{{' skills/e2e-test/ && echo "tokens rendered OK"
Report to the user: what was created vs. upgraded, the canonical symlinks, the confirmed e2e policy
(policy.md: tiers / target / timing), the chosen {{TARGET_TYPE}} and tool track, and the next step
(write the Layer-0 journey's project specifics, then run it via /aep-build Phase 6 — or, for a
none-target project, prove Layer-0 criteria via Tier-1/Tier-3).
.claude/skills and .agents/skills entries are symlinks only.policy.md stay untouched;
re-confirm the policy with the user, then update it (never silently overwrite). To replace a real
.claude/skills/e2e-test dir, migrate it into skills/ (git mv) first — Phase 4 expose() refuses a
real path.tool-selection.md, never hard-coded in a journey.policy.md is the single source of truth for tiers / target / timing — no copy goes in AGENTS.md;
the skill is canonical cross-tool, so every runtime reads this one file.| Command | What it does |
|---|---|
/aep-build | Phases 6–8 run the journey, compute coverage, record evidence (no gate flip) |
/aep-wrap | Performs the two-phase gate flip (scripted_passed → passed) + asks before advancing |
/aep-dispatch | Blocks the next layer until the prior layer gate is passed |
edit journeys/ | Add a journey per new capability/layer (copy journeys/README.md template) |
npx claudepluginhub memorysaver/agentic-engineering-patterns --plugin patternsScaffolds a new AEP project with OpenSpec, workspace hooks, and BDD e2e-test skill, or audits and converges an existing project's agentic infrastructure into the same canonical layout.
Scaffolds end-to-end testing infrastructure. Useful for setting up e2e tests, auto-activates on 'e2e scaffold' or 'set up e2e testing'.
Scaffolds or migrates test-suite projects (API, E2E/UI, shared library) with archetype selection, layer variants, tag taxonomy, reporter stack, and CI integration. Composes with initialize-harness-project.