docs(plan): implementation plan for Cyclone skill catalog (9 tasks, 4 phases)
This commit is contained in:
@@ -0,0 +1,826 @@
|
||||
# Cyclone Skill Catalog Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Ship 8 layer-mapped Cyclone skills under `.superpowers/skills/` (one per PR), then update the README to advertise the catalog. No code changes — pure guidance artifacts.
|
||||
|
||||
**Architecture:** Each skill is a single `SKILL.md` (150–250 lines, YAML frontmatter + sections per the spec template). One commit per skill. Phase order matches the spec's build order so each later skill can ship its `## Related skills` section pointing to skills that already exist.
|
||||
|
||||
**Tech Stack:** Markdown, YAML frontmatter. No build step, no runtime, no tests (skills are guidance, not code).
|
||||
|
||||
**Spec:** [`docs/superpowers/specs/2026-06-21-cyclone-skill-catalog-design.md`](../specs/2026-06-21-cyclone-skill-catalog-design.md)
|
||||
|
||||
---
|
||||
|
||||
## File structure
|
||||
|
||||
```
|
||||
.superpowers/
|
||||
└── skills/
|
||||
├── cyclone-spec/
|
||||
│ └── SKILL.md ← Task 1
|
||||
├── cyclone-tests/
|
||||
│ └── SKILL.md ← Task 2
|
||||
├── cyclone-edi/
|
||||
│ └── SKILL.md ← Task 3
|
||||
│ └── references/
|
||||
│ └── parsers.md ← Task 3
|
||||
├── cyclone-tail/
|
||||
│ └── SKILL.md ← Task 4
|
||||
│ └── references/
|
||||
│ └── wire-format.md ← Task 4
|
||||
├── cyclone-store/
|
||||
│ └── SKILL.md ← Task 5
|
||||
├── cyclone-api-router/
|
||||
│ └── SKILL.md ← Task 6
|
||||
├── cyclone-frontend-page/
|
||||
│ └── SKILL.md ← Task 7
|
||||
└── cyclone-cli/
|
||||
└── SKILL.md ← Task 8
|
||||
|
||||
README.md ← Task 9 (catalog section)
|
||||
```
|
||||
|
||||
Each skill is one PR. Each `SKILL.md` follows the template in spec §"Per-skill structure". Optional `references/<file>.md` only when the SKILL.md would otherwise exceed ~200 lines.
|
||||
|
||||
---
|
||||
|
||||
## Shared SKILL.md template (lock this in Task 0, then apply to every Task)
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: <kebab-case>
|
||||
description: "<one-sentence trigger, with Use when: … keywords>"
|
||||
---
|
||||
|
||||
# <Title>
|
||||
|
||||
## When to use
|
||||
- <bullet 1 — concrete situation>
|
||||
- <bullet 2>
|
||||
- <bullet 3>
|
||||
|
||||
## Conventions
|
||||
1. <testable rule>
|
||||
2. <testable rule>
|
||||
3. <testable rule>
|
||||
|
||||
## Patterns
|
||||
\`\`\`<lang>
|
||||
<copy-pasteable skeleton>
|
||||
\`\`\`
|
||||
|
||||
## Anti-patterns
|
||||
- <tempting-but-wrong move>
|
||||
|
||||
## Related skills
|
||||
- `<other-skill>` — <one line>
|
||||
- `<other-skill>` — <one line>
|
||||
```
|
||||
|
||||
YAML frontmatter rules:
|
||||
- `name` must be kebab-case and match the directory name.
|
||||
- `description` is the only thing the auto-loader sees. One sentence, max ~200 chars. Include a `Use when:` clause listing the trigger situations.
|
||||
|
||||
---
|
||||
|
||||
## Task 0: Pre-flight — verify starting state
|
||||
|
||||
**Goal:** Confirm `.superpowers/skills/` does not exist yet, capture a directory snapshot, and verify the catalog can be served from project scope.
|
||||
|
||||
**Files:**
|
||||
- Read: `.superpowers/` (existing project-scoped superpowers directory)
|
||||
- Create (no commit): `/tmp/cyclone-skill-baseline.txt`
|
||||
|
||||
- [ ] **Step 1: Inspect existing `.superpowers/` contents**
|
||||
|
||||
```bash
|
||||
ls -la .superpowers/
|
||||
```
|
||||
|
||||
Expected: shows only `plans/` and `specs/` directories (the canonical superpowers content for this repo). No `skills/` directory yet.
|
||||
|
||||
- [ ] **Step 2: Confirm no stray skill files outside `.superpowers/skills/`**
|
||||
|
||||
```bash
|
||||
find . -path ./node_modules -prune -o -path ./.git -prune -o -path ./backend/.venv -prune -o -name "SKILL.md" -print
|
||||
```
|
||||
|
||||
Expected: no output (or only paths under `.grok/` system directories that are not part of the repo).
|
||||
|
||||
- [ ] **Step 3: Verify the README's skills section is empty (or absent)**
|
||||
|
||||
```bash
|
||||
grep -n -i "skill" README.md | head -10
|
||||
```
|
||||
|
||||
Expected: a few hits for "skill" in unrelated contexts (e.g. "skill set" in marketing copy) but no existing catalog section.
|
||||
|
||||
- [ ] **Step 4: Save the baseline**
|
||||
|
||||
```bash
|
||||
ls .superpowers/ | tee /tmp/cyclone-skill-baseline.txt
|
||||
```
|
||||
|
||||
Expected: `plans\nspecs`.
|
||||
|
||||
No commit. Pre-flight only.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Phase 1, Skill 1 — `cyclone-spec`
|
||||
|
||||
**Goal:** Codify the SP-N superpowers flow that this repo already uses (14 specs, 8 plans on disk). Pure documentation; this skill owns the convention so every later feature goes through it consistently.
|
||||
|
||||
**Files:**
|
||||
- Create: `.superpowers/skills/cyclone-spec/SKILL.md`
|
||||
|
||||
- [ ] **Step 1: Confirm the SP-N flow materials exist**
|
||||
|
||||
```bash
|
||||
ls docs/superpowers/specs/ | wc -l
|
||||
ls docs/superpowers/plans/ | wc -l
|
||||
git log --oneline | grep -E "^(feat|docs|merge): SP[0-9]+" | head -5
|
||||
```
|
||||
|
||||
Expected: ≥ 14 specs, ≥ 8 plans, ≥ 5 SP-N commits in history. Record the numbers for use in the skill.
|
||||
|
||||
- [ ] **Step 2: Create the directory**
|
||||
|
||||
```bash
|
||||
mkdir -p .superpowers/skills/cyclone-spec
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Write the SKILL.md**
|
||||
|
||||
Use the shared template. Frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: cyclone-spec
|
||||
description: "Cyclone SP-N superpowers increment flow — spec → plan → implement → merge. Use when: starting a new numbered feature increment, naming a branch, opening a SP-N PR, or doing the merge dance into main."
|
||||
---
|
||||
```
|
||||
|
||||
Body sections (each must be present, in this order):
|
||||
- `## When to use` — 4 bullets covering: starting a new increment, naming the spec/plan files, opening the PR, performing the merge.
|
||||
- `## Conventions` — at minimum these numbered rules:
|
||||
1. **Numbering.** Reserve the next SP-N number (next after the highest in `git log`). Never reuse a number.
|
||||
2. **Branch.** `sp<N>-<short-kebab-topic>` (e.g. `sp22-line-reconciliation`).
|
||||
3. **Spec path.** `docs/superpowers/specs/YYYY-MM-DD-cyclone-<topic>-design.md`. Status header: `Draft, pending user review`.
|
||||
4. **Plan path.** `docs/superpowers/plans/YYYY-MM-DD-cyclone-<topic>.md`. Header per writing-plans skill.
|
||||
5. **Commit prefix.** `feat(sp<N>): …`, `docs(spec): …`, `docs(plan): …`, `merge: SP<N> … into main`.
|
||||
6. **PR title.** `SP<N> <Topic>` (matches commit history).
|
||||
7. **Merge shape.** Single atomic merge commit into `main` after review; no squash, no rebase.
|
||||
- `## Patterns` — copy-pasteable skeleton for the spec header (Date / Status / Branch / Scope) and the plan header (per writing-plans).
|
||||
- `## Anti-patterns` — at least these:
|
||||
- Don't skip the spec ("it's a small fix"). Small fixes still get a 3-line spec when they introduce a new numbered increment.
|
||||
- Don't squash the merge commit — the SP-N merge commit is the audit trail.
|
||||
- Don't put code in the spec — the spec is the *what*, the plan is the *how*.
|
||||
- `## Related skills` — references to `cyclone-tests` (every spec mentions test impact), `cyclone-edi` / `cyclone-tail` / `cyclone-store` / `cyclone-api-router` / `cyclone-frontend-page` / `cyclone-cli` (each spec touches one or more of these), and the upstream `superpowers:brainstorming` / `superpowers:writing-plans` global skills.
|
||||
|
||||
Keep total under ~200 lines. Use real examples drawn from existing specs (e.g. SP9 multi-payer, SP14 5-lane Inbox) where they illustrate the convention.
|
||||
|
||||
- [ ] **Step 4: Verify frontmatter and structure**
|
||||
|
||||
```bash
|
||||
head -5 .superpowers/skills/cyclone-spec/SKILL.md
|
||||
wc -l .superpowers/skills/cyclone-spec/SKILL.md
|
||||
grep -c "^## " .superpowers/skills/cyclone-spec/SKILL.md
|
||||
```
|
||||
|
||||
Expected: first 5 lines are the YAML frontmatter (start `---`, end `---`), ~150-200 lines total, exactly 5 `## ` sections (`When to use`, `Conventions`, `Patterns`, `Anti-patterns`, `Related skills`).
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add .superpowers/skills/cyclone-spec/SKILL.md
|
||||
git commit -m "feat(sp-skill-catalog): add cyclone-spec skill (SP-N flow)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Phase 1, Skill 2 — `cyclone-tests`
|
||||
|
||||
**Goal:** Codify the pytest + vitest fixture patterns so every later skill can ship its `## Anti-patterns` cleanly. Specifically: how to drop a prodfiles fixture into `backend/tests/fixtures/`, how to write a `.test.tsx` next to a page, how to keep tests deterministic.
|
||||
|
||||
**Files:**
|
||||
- Create: `.superpowers/skills/cyclone-tests/SKILL.md`
|
||||
|
||||
- [ ] **Step 1: Survey the existing fixture layout**
|
||||
|
||||
```bash
|
||||
ls backend/tests/fixtures/ | head -30
|
||||
ls backend/tests/fixtures/ | wc -l
|
||||
ls src/**/*.test.tsx src/**/*.test.ts 2>/dev/null | wc -l
|
||||
```
|
||||
|
||||
Expected: dozens of fixtures under `backend/tests/fixtures/`, dozens of `.test.ts(x)` siblings across `src/`. Record counts for the skill.
|
||||
|
||||
- [ ] **Step 2: Sample one backend pytest and one frontend vitest to cite**
|
||||
|
||||
```bash
|
||||
ls backend/tests/fixtures/ | head -3
|
||||
echo "---"
|
||||
ls src/hooks/*.test.ts | head -3
|
||||
```
|
||||
|
||||
Pick the smallest/most-representative example from each side to cite in the skill's `## Patterns`.
|
||||
|
||||
- [ ] **Step 3: Create the directory and write the SKILL.md**
|
||||
|
||||
```bash
|
||||
mkdir -p .superpowers/skills/cyclone-tests
|
||||
```
|
||||
|
||||
Frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: cyclone-tests
|
||||
description: "Cyclone pytest + vitest fixture patterns, prodfiles layout, backend/tests/fixtures/ conventions, .test.tsx sibling rule. Use when: adding a backend pytest case, adding a frontend vitest test, or wiring in a real-EDI prodfiles sample."
|
||||
---
|
||||
```
|
||||
|
||||
Body sections (each must be present):
|
||||
- `## When to use` — 4 bullets: adding a backend test, adding a frontend test, dropping in a prodfiles fixture, debugging a flaky test.
|
||||
- `## Conventions` — at minimum:
|
||||
1. **Frontend sibling rule.** Every new file in `src/` that contains testable logic gets a `*.test.ts(x)` next to it. `useFoo.ts` → `useFoo.test.ts`.
|
||||
2. **Backend test location.** Tests live under `backend/tests/test_*.py`. Integration tests (FastAPI) follow `test_api_*.py`; pure-unit tests (parsers, validators) follow `test_<module>_*.py`.
|
||||
3. **Prodfiles drop-in.** Real EDI samples live under `docs/prodfiles/<source>/<edi>.txt`. To use one in a test: copy to `backend/tests/fixtures/<test-name>/<edi>.txt` and reference via the existing fixture helper (see `## Patterns`).
|
||||
4. **Determinism.** Use `freezegun` for date-sensitive backend tests; use `vi.useFakeTimers()` for time-sensitive frontend tests. Never rely on `datetime.now()` directly.
|
||||
5. **No network.** Tests must not hit the network. `httpx`/`requests` mocks live in `conftest.py` fixtures.
|
||||
6. **pytest collection.** Run `cd backend && python -m pytest tests/<file>::<name> -v` for the fastest feedback loop. Full suite is the merge gate.
|
||||
- `## Patterns` — three small copy-pasteable skeletons:
|
||||
- One pytest using `tmp_path` + a prodfiles fixture reference.
|
||||
- One vitest `*.test.ts` for a hook using `vi.useFakeTimers()`.
|
||||
- One vitest `*.test.tsx` for a component using `@testing-library/react` + `happy-dom`.
|
||||
- `## Anti-patterns` — at least:
|
||||
- Don't put frontend tests in `src/__tests__/` (legacy location, no longer used).
|
||||
- Don't reach into `docs/prodfiles/` directly from a test — copy to `backend/tests/fixtures/` so tests stay runnable when prodfiles is reorganized.
|
||||
- Don't use `sleep()` for timing — use the deterministic timer tools listed in Conventions.
|
||||
- `## Related skills` — references to all 7 other skills (every domain skill impacts tests) plus `superpowers:test-driven-development` upstream.
|
||||
|
||||
Keep under ~250 lines (this is the most cross-cutting skill, so it's the largest).
|
||||
|
||||
- [ ] **Step 4: Verify**
|
||||
|
||||
```bash
|
||||
head -5 .superpowers/skills/cyclone-tests/SKILL.md
|
||||
wc -l .superpowers/skills/cyclone-tests/SKILL.md
|
||||
grep -c "^## " .superpowers/skills/cyclone-tests/SKILL.md
|
||||
```
|
||||
|
||||
Expected: frontmatter present, 150–250 lines, 5 `## ` sections.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add .superpowers/skills/cyclone-tests/SKILL.md
|
||||
git commit -m "feat(sp-skill-catalog): add cyclone-tests skill (fixture patterns)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Phase 2, Skill 3 — `cyclone-edi`
|
||||
|
||||
**Goal:** Codify Cyclone's EDI parser/validator conventions across 837P/835/999/270/271/277CA/TA1. Highest-leverage skill — 30 parsers, scattered validators, fixture sprawl.
|
||||
|
||||
**Files:**
|
||||
- Create: `.superpowers/skills/cyclone-edi/SKILL.md`
|
||||
- Create: `.superpowers/skills/cyclone-edi/references/parsers.md`
|
||||
|
||||
- [ ] **Step 1: Enumerate the parsers and validator rules**
|
||||
|
||||
```bash
|
||||
ls backend/src/cyclone/parsers/
|
||||
grep -rn "^def parse_" backend/src/cyclone/parsers/ | head -40
|
||||
grep -n "^R[0-9]" backend/src/cyclone/validator*.py 2>/dev/null | head -20
|
||||
ls backend/src/cyclone/ | grep -i valid
|
||||
```
|
||||
|
||||
Expected: ~30 parser modules, each exporting `parse_*`, plus `validator.py` and possibly `validator_835.py`. Record the R-codes (R200, R210, etc.) found.
|
||||
|
||||
- [ ] **Step 2: Sample one parser to cite in the skill**
|
||||
|
||||
Pick the smallest one (e.g. `parse_ta1.py` or `parse_999.py`) and read the first 30 lines. The skill's `## Patterns` section will reference its structure.
|
||||
|
||||
- [ ] **Step 3: Create directories and write the SKILL.md + reference**
|
||||
|
||||
```bash
|
||||
mkdir -p .superpowers/skills/cyclone-edi/references
|
||||
```
|
||||
|
||||
Frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: cyclone-edi
|
||||
description: "Cyclone EDI parser/validator conventions (837P/835/999/270/271/277CA/TA1). Use when: adding or changing a parser, adding a validator rule (R200/R210/NPI Luhn/EIN/CAS), or mapping a new CAS adjustment reason code."
|
||||
---
|
||||
```
|
||||
|
||||
Body sections:
|
||||
- `## When to use` — 4 bullets: adding a parser, adding a validator rule, wiring a new CAS code, debugging a parse failure on a prodfiles sample.
|
||||
- `## Conventions` — at minimum:
|
||||
1. **Parser signature.** Every parser module exports `parse(text: str) -> <TypedResult>` where `<TypedResult>` is a Pydantic model from `backend/src/cyclone/parsers/<edi>_models.py` (or co-located).
|
||||
2. **Segment walk.** Parsers consume `backend/src/cyclone/segments.py` helpers (`Segment`, `Loop`, `next_segment`) — do not parse raw text inline.
|
||||
3. **Validator rules.** Numbered (R200, R210, …) live in `backend/src/cyclone/validator.py` (or `validator_835.py` for 835-specific). Each rule is a function `_rule_R<n>_<name>` raising `ValidationError` with the R-code.
|
||||
4. **NPI / EIN / CAS.** Identity-format rules (NPI Luhn, EIN `XX-XXXXXXX`, CAS adjustment reason codes) live in dedicated modules (`npi.py`, etc.). Don't duplicate the format logic.
|
||||
5. **Prodfiles reuse.** When adding a parser for a new transaction type, ship at least one prodfiles fixture in `backend/tests/fixtures/<edi>/<sample>.txt`.
|
||||
- `## Patterns` — three skeletons:
|
||||
- A minimal `parse_<edi>.py` (using `segments.py`, exporting `parse(text)`).
|
||||
- A validator rule `_rule_R<n>_<name>(ctx) -> None`.
|
||||
- A test that uses a prodfiles fixture (`backend/tests/fixtures/<edi>/<sample>.txt`) and asserts `parse(text).foo == expected`.
|
||||
- `## Anti-patterns` — at least:
|
||||
- Don't re-parse raw X12 strings inside validators — always parse first, validate the typed result.
|
||||
- Don't bake payer-specific logic into the generic parser — payer variations live in `backend/src/cyclone/payers.py` or `<payer>_config.py`.
|
||||
- Don't add a validator rule without an R-code — the R-code is how the UI surfaces the error.
|
||||
- `## Related skills` — references to `cyclone-store` (writes the parsed result), `cyclone-api-router` (exposes it), `cyclone-tests` (fixture drop-in), `cyclone-cli` (the `cyc parse <file>` smoke command).
|
||||
|
||||
Then write `references/parsers.md`: a flat table mapping each parser module to its transaction type, signature, primary fixtures, and any payer-specific variants. Keep under 80 lines.
|
||||
|
||||
- [ ] **Step 4: Verify**
|
||||
|
||||
```bash
|
||||
head -5 .superpowers/skills/cyclone-edi/SKILL.md
|
||||
wc -l .superpowers/skills/cyclone-edi/SKILL.md
|
||||
wc -l .superpowers/skills/cyclone-edi/references/parsers.md
|
||||
grep -c "^## " .superpowers/skills/cyclone-edi/SKILL.md
|
||||
```
|
||||
|
||||
Expected: frontmatter present, SKILL.md ≤ 200 lines, references/parsers.md ≤ 80 lines, exactly 5 `## ` sections in SKILL.md.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add .superpowers/skills/cyclone-edi/
|
||||
git commit -m "feat(sp-skill-catalog): add cyclone-edi skill (parser/validator conventions)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Phase 2, Skill 4 — `cyclone-tail`
|
||||
|
||||
**Goal:** Codify the live-tail wire format and the `useTailStream` + `useMergedTail` + per-resource hook triplet. Concrete drift risk — wire format is in the README but consumed across 3 page-hook pairs.
|
||||
|
||||
**Files:**
|
||||
- Create: `.superpowers/skills/cyclone-tail/SKILL.md`
|
||||
- Create: `.superpowers/skills/cyclone-tail/references/wire-format.md`
|
||||
|
||||
- [ ] **Step 1: Survey the live-tail surface**
|
||||
|
||||
```bash
|
||||
grep -rn "/stream" backend/src/cyclone/api_routers/ src/hooks/ 2>/dev/null | head -20
|
||||
ls src/hooks/use*Stream* src/hooks/use*MergedTail*
|
||||
ls src/hooks/use*Tail* 2>/dev/null
|
||||
grep -rn "snapshot_end\|x-ndjson" backend/src/cyclone/ src/ 2>/dev/null | head -10
|
||||
```
|
||||
|
||||
Expected: 3 `/api/<resource>/stream` endpoints, one `useTailStream.ts`, one `useMergedTail.ts`, multiple `use<X>Stream` consumers.
|
||||
|
||||
- [ ] **Step 2: Read the wire-format spec in the README**
|
||||
|
||||
```bash
|
||||
grep -n -A 30 "Wire format" README.md | head -50
|
||||
```
|
||||
|
||||
This is the canonical definition; the skill will reference it. Record the line range for the `references/wire-format.md` cross-reference.
|
||||
|
||||
- [ ] **Step 3: Create directories and write the SKILL.md + reference**
|
||||
|
||||
```bash
|
||||
mkdir -p .superpowers/skills/cyclone-tail/references
|
||||
```
|
||||
|
||||
Frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: cyclone-tail
|
||||
description: "Cyclone live-tail streaming wire format and the useTailStream / useMergedTail hook triplet. Use when: adding a new streaming list page, changing the wire format, debugging stalled/reconnecting state, or modifying the StatusPill behavior."
|
||||
---
|
||||
```
|
||||
|
||||
Body sections:
|
||||
- `## When to use` — 4 bullets: adding a new streaming page, changing the wire format, debugging stalled/reconnecting, tuning heartbeat/stall timing.
|
||||
- `## Conventions` — at minimum:
|
||||
1. **Wire format.** Newline-delimited JSON. Line shapes: `{"type":"item","data":…}`, `{"type":"snapshot_end","data":{"count":N}}`, `{"type":"heartbeat","data":{"ts":…}}`, `{"type":"item_dropped","data":…}`, `{"type":"error","data":…}`. See `references/wire-format.md`.
|
||||
2. **Hook triplet.** Streaming pages consume three hooks: `useTailStream(url)` (raw stream state), `useMergedTail(items, events)` (combine initial fetch with live events), and a per-resource hook (`useClaims`, `useRemittances`, etc.) that wraps both.
|
||||
3. **Stall threshold.** 30s of total silence (heartbeat included) flips the connection to `stalled` and surfaces a `↻ Reconnect` button. Don't change this without updating the README's "Status pill" table.
|
||||
4. **Snapshot first.** Every stream must emit the current snapshot before any live `item` events. The `snapshot_end` line is the marker the UI uses to flip `StatusPill` from `connecting` to `live`.
|
||||
5. **Content-Type.** `application/x-ndjson`. Never `application/json` for a stream endpoint.
|
||||
- `## Patterns` — three skeletons:
|
||||
- A `useFoo.ts` page-hook skeleton (TanStack Query initial fetch + tail subscription).
|
||||
- A new backend `/api/foo/stream` endpoint signature.
|
||||
- A `StatusPill` consumer wiring.
|
||||
- `## Anti-patterns` — at least:
|
||||
- Don't hand-roll `fetch` + `ReadableStream` parsing in a page — go through `useTailStream` so reconnect/stall handling is consistent.
|
||||
- Don't change the wire format on one endpoint without updating the other two — they share the parser in `src/lib/tail-stream.ts`.
|
||||
- Don't emit `item` events before `snapshot_end` — the UI will duplicate rows.
|
||||
- `## Related skills` — references to `cyclone-frontend-page` (page-level wiring), `cyclone-api-router` (the `/api/<resource>/stream` endpoint), `cyclone-store` (the event contract that drives the stream), `cyclone-tests` (`.test.ts` siblings on every hook in this skill).
|
||||
|
||||
Then write `references/wire-format.md`: copy the relevant README section (with attribution) and add a per-line field reference table (which fields are required, which are optional, parser tolerance). Keep under 80 lines.
|
||||
|
||||
- [ ] **Step 4: Verify**
|
||||
|
||||
```bash
|
||||
head -5 .superpowers/skills/cyclone-tail/SKILL.md
|
||||
wc -l .superpowers/skills/cyclone-tail/SKILL.md
|
||||
wc -l .superpowers/skills/cyclone-tail/references/wire-format.md
|
||||
grep -c "^## " .superpowers/skills/cyclone-tail/SKILL.md
|
||||
```
|
||||
|
||||
Expected: frontmatter present, SKILL.md ≤ 200 lines, wire-format.md ≤ 80 lines, 5 `## ` sections.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add .superpowers/skills/cyclone-tail/
|
||||
git commit -m "feat(sp-skill-catalog): add cyclone-tail skill (live-tail wire format)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Phase 3, Skill 5 — `cyclone-store`
|
||||
|
||||
**Goal:** Codify the store write-paths, the pubsub event contract, and the SP21 store-split boundary map. Aligns with the SP21 store split currently in plan stage — the skill codifies the new boundaries as they land.
|
||||
|
||||
**Files:**
|
||||
- Create: `.superpowers/skills/cyclone-store/SKILL.md`
|
||||
|
||||
- [ ] **Step 1: Read SP21 plan to learn the new module boundaries**
|
||||
|
||||
```bash
|
||||
cat docs/superpowers/plans/2026-06-21-cyclone-store-split.md | head -80
|
||||
```
|
||||
|
||||
Record the module list (`batches`, `inbox`, `acks`, etc.) for use in the skill.
|
||||
|
||||
- [ ] **Step 2: Survey the current pubsub surface**
|
||||
|
||||
```bash
|
||||
grep -rn "_written\|_recorded" backend/src/cyclone/pubsub.py | head -20
|
||||
grep -rn "publish\|subscribe" backend/src/cyclone/store.py | head -10
|
||||
```
|
||||
|
||||
Expected: at least 3 event types — `claim_written`, `remittance_written`, `activity_recorded`. Record exact names.
|
||||
|
||||
- [ ] **Step 3: Create the directory and write the SKILL.md**
|
||||
|
||||
```bash
|
||||
mkdir -p .superpowers/skills/cyclone-store
|
||||
```
|
||||
|
||||
Frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: cyclone-store
|
||||
description: "Cyclone store write-paths, the pubsub event contract (claim_written / remittance_written / activity_recorded), and the SP21 store-split boundary map. Use when: touching store.py, adding a new entity, wiring a new write event, or splitting a store module."
|
||||
---
|
||||
```
|
||||
|
||||
Body sections:
|
||||
- `## When to use` — 4 bullets: adding a new entity, wiring a new write event, debugging a write-path issue, splitting a store module.
|
||||
- `## Conventions` — at minimum:
|
||||
1. **All writes go through `CycloneStore`.** Route handlers and parsers must not write directly to the ORM session.
|
||||
2. **Every write publishes an event.** The event name matches the entity: `claim_written`, `remittance_written`, `activity_recorded`. New entities get `<entity>_written` (or `_recorded` for non-canonical rows).
|
||||
3. **Snapshot shape.** Each entity has a Pydantic serializer in `backend/src/cyclone/store/ui.py` (or its post-SP21 equivalent). The serializer is the single source of truth for what the frontend sees.
|
||||
4. **SP21 boundaries.** Post-split, each domain lives in its own module (`batches`, `inbox`, `acks`, …). Cross-module writes go through `CycloneStore` facade methods, not direct module access.
|
||||
5. **No business logic in route handlers.** A route handler validates input, calls `store.<method>(...)`, publishes the event, returns the serialized result. Anything more belongs in the store or a parser.
|
||||
- `## Patterns` — three skeletons:
|
||||
- A `CycloneStore` write method (`def add_foo(self, foo: Foo) -> FooRecord`).
|
||||
- An event publication in `pubsub.py`.
|
||||
- A new `<entity>_written` handler test that asserts both the row and the event.
|
||||
- `## Anti-patterns` — at least:
|
||||
- Don't read directly from the ORM in a route handler — go through the snapshot serializer.
|
||||
- Don't introduce a new event name without updating the subscriber list (currently `api.py` and `api_routers/`).
|
||||
- Don't merge a write method with its event publication into separate places — they live next to each other so reviewers see both.
|
||||
- `## Related skills` — references to `cyclone-edi` (the parsed result lands here), `cyclone-api-router` (the route that calls the store), `cyclone-tail` (the event drives the stream), `cyclone-tests` (write-path tests).
|
||||
|
||||
Keep under ~200 lines. No `references/` needed unless the boundary map grows.
|
||||
|
||||
- [ ] **Step 4: Verify**
|
||||
|
||||
```bash
|
||||
head -5 .superpowers/skills/cyclone-store/SKILL.md
|
||||
wc -l .superpowers/skills/cyclone-store/SKILL.md
|
||||
grep -c "^## " .superpowers/skills/cyclone-store/SKILL.md
|
||||
```
|
||||
|
||||
Expected: frontmatter present, ≤ 200 lines, 5 `## ` sections.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add .superpowers/skills/cyclone-store/SKILL.md
|
||||
git commit -m "feat(sp-skill-catalog): add cyclone-store skill (write paths + event contract)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Phase 3, Skill 6 — `cyclone-api-router`
|
||||
|
||||
**Goal:** Codify FastAPI router conventions — `api_routers/`, `api_helpers.py` reuse, response shapes, error envelopes. Pairs with `cyclone-store`.
|
||||
|
||||
**Files:**
|
||||
- Create: `.superpowers/skills/cyclone-api-router/SKILL.md`
|
||||
|
||||
- [ ] **Step 1: Survey the existing router layout**
|
||||
|
||||
```bash
|
||||
ls backend/src/cyclone/api_routers/
|
||||
grep -l "APIRouter" backend/src/cyclone/api_routers/*.py
|
||||
head -30 backend/src/cyclone/api_helpers.py
|
||||
```
|
||||
|
||||
Record the router filenames and which helpers exist in `api_helpers.py`.
|
||||
|
||||
- [ ] **Step 2: Sample one router**
|
||||
|
||||
Pick the smallest existing router (likely `acks.py` or `ta1_acks.py`) and read the first 50 lines. The skill's `## Patterns` section will reference its structure.
|
||||
|
||||
- [ ] **Step 3: Create the directory and write the SKILL.md**
|
||||
|
||||
```bash
|
||||
mkdir -p .superpowers/skills/cyclone-api-router
|
||||
```
|
||||
|
||||
Frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: cyclone-api-router
|
||||
description: "Cyclone FastAPI router conventions (api_routers/, api_helpers.py, response shapes, error envelopes). Use when: adding or changing an HTTP endpoint, splitting a route out of api.py, or wiring a new helper into api_helpers.py."
|
||||
---
|
||||
```
|
||||
|
||||
Body sections:
|
||||
- `## When to use` — 4 bullets: adding an endpoint, splitting a route, adding a helper, defining an error response.
|
||||
- `## Conventions` — at minimum:
|
||||
1. **No new top-level routes in `api.py`.** Every endpoint lives in `backend/src/cyclone/api_routers/<topic>.py` as an `APIRouter`.
|
||||
2. **Reuse `api_helpers.py`.** Common response shapes, error envelopes, and parsing helpers live there. Don't duplicate them in a router.
|
||||
3. **Response shape.** Every successful response is a Pydantic model. Errors use the shared `ErrorEnvelope` (from `api_helpers.py`) with `code` and `message`.
|
||||
4. **Mounting.** Routers are mounted in `api.py` with their prefix; keep prefix naming consistent (`/api/<resource>`).
|
||||
5. **Streaming endpoints.** Use `StreamingResponse(media_type="application/x-ndjson")` — see `cyclone-tail` for the wire format.
|
||||
6. **Tests.** Every new endpoint gets a `test_api_<topic>_<verb>.py` test under `backend/tests/`.
|
||||
- `## Patterns` — three skeletons:
|
||||
- A new `APIRouter` skeleton (`router = APIRouter(prefix="/api/foo", tags=["foo"])`).
|
||||
- A `get_one` / `get_list` / `post_one` trio with Pydantic response models.
|
||||
- An error envelope usage example.
|
||||
- `## Anti-patterns` — at least:
|
||||
- Don't import from `cyclone.api` into a router — the dependency runs the other way.
|
||||
- Don't return raw dicts — always Pydantic.
|
||||
- Don't bypass `CycloneStore` to query the ORM directly — see `cyclone-store`.
|
||||
- `## Related skills` — references to `cyclone-store` (most routes call store methods), `cyclone-tail` (streaming endpoints), `cyclone-edi` (parse endpoints), `cyclone-tests` (endpoint tests).
|
||||
|
||||
Keep under ~200 lines.
|
||||
|
||||
- [ ] **Step 4: Verify**
|
||||
|
||||
```bash
|
||||
head -5 .superpowers/skills/cyclone-api-router/SKILL.md
|
||||
wc -l .superpowers/skills/cyclone-api-router/SKILL.md
|
||||
grep -c "^## " .superpowers/skills/cyclone-api-router/SKILL.md
|
||||
```
|
||||
|
||||
Expected: frontmatter present, ≤ 200 lines, 5 `## ` sections.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add .superpowers/skills/cyclone-api-router/SKILL.md
|
||||
git commit -m "feat(sp-skill-catalog): add cyclone-api-router skill (FastAPI conventions)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Phase 4, Skill 7 — `cyclone-frontend-page`
|
||||
|
||||
**Goal:** Codify the React page-component pattern — TanStack Query, `use<X>` hook, drawer, URL state, `.test.tsx` sibling. Generalizes what `cyclone-tail` already covers.
|
||||
|
||||
**Files:**
|
||||
- Create: `.superpowers/skills/cyclone-frontend-page/SKILL.md`
|
||||
|
||||
- [ ] **Step 1: Survey the page layout**
|
||||
|
||||
```bash
|
||||
ls src/pages/
|
||||
echo "---"
|
||||
ls src/components/ui/ | head -20
|
||||
```
|
||||
|
||||
Record the page list and which shadcn-style UI components are in `src/components/ui/`.
|
||||
|
||||
- [ ] **Step 2: Sample one page to cite**
|
||||
|
||||
```bash
|
||||
head -50 src/pages/Claims.tsx
|
||||
```
|
||||
|
||||
Use this in the skill's `## Patterns` to show the canonical page structure.
|
||||
|
||||
- [ ] **Step 3: Create the directory and write the SKILL.md**
|
||||
|
||||
```bash
|
||||
mkdir -p .superpowers/skills/cyclone-frontend-page
|
||||
```
|
||||
|
||||
Frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: cyclone-frontend-page
|
||||
description: "Cyclone React page conventions (TanStack Query, use<X> hook, drawer, URL state, .test.tsx sibling, Layout / PageHeader / Sidebar). Use when: adding a new page, refactoring an existing one, or wiring a drawer into a page."
|
||||
---
|
||||
```
|
||||
|
||||
Body sections:
|
||||
- `## When to use` — 4 bullets: adding a new page, refactoring an existing page, wiring a drawer, sharing state via URL.
|
||||
- `## Conventions` — at minimum:
|
||||
1. **Page shape.** Every page in `src/pages/<Name>.tsx` exports a default `function <Name>()`. It uses `<Layout>`, sets a `<PageHeader>`, and renders a table or list.
|
||||
2. **Data hook.** Each page pairs with a `use<X>` hook in `src/hooks/use<X>.ts` that returns `{ data, isLoading, error, ...tail }`. Pages do not call `fetch` directly.
|
||||
3. **Tail.** If the page shows live data, it consumes the tail-stream pattern via `cyclone-tail`. The hook, not the page, owns the streaming subscription.
|
||||
4. **Drawer.** Drawers (e.g. `ClaimDrawer`, `RemitDrawer`) live in `src/components/<DrawerName>/` and are wired via `useDrawerUrlState` so the URL reflects open state.
|
||||
5. **Tests.** Every page gets a `src/pages/<Name>.test.tsx` sibling. Every hook gets a `src/hooks/use<X>.test.ts` sibling.
|
||||
6. **UI primitives.** Use Radix-backed components from `src/components/ui/`. Don't pull in new UI libraries without discussion.
|
||||
7. **Routing.** Pages register their route in `src/App.tsx`. Lazy-load if the page is heavy.
|
||||
- `## Patterns` — three skeletons:
|
||||
- A minimal `Claims.tsx`-style page (Layout + PageHeader + table + drawer).
|
||||
- A `use<X>` hook skeleton (TanStack Query + tail).
|
||||
- A drawer component skeleton with `useDrawerUrlState`.
|
||||
- `## Anti-patterns` — at least:
|
||||
- Don't `fetch` from inside a page component — go through the hook.
|
||||
- Don't open a drawer via local component state — use `useDrawerUrlState` so deep-links work.
|
||||
- Don't put domain logic in JSX — extract to the hook or a pure helper in `src/lib/`.
|
||||
- `## Related skills` — references to `cyclone-tail` (live-data pages), `cyclone-api-router` (the endpoints pages call), `cyclone-tests` (page + hook tests), `cyclone-edi` (pages that show parsed EDI content).
|
||||
|
||||
Keep under ~200 lines.
|
||||
|
||||
- [ ] **Step 4: Verify**
|
||||
|
||||
```bash
|
||||
head -5 .superpowers/skills/cyclone-frontend-page/SKILL.md
|
||||
wc -l .superpowers/skills/cyclone-frontend-page/SKILL.md
|
||||
grep -c "^## " .superpowers/skills/cyclone-frontend-page/SKILL.md
|
||||
```
|
||||
|
||||
Expected: frontmatter present, ≤ 200 lines, 5 `## ` sections.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add .superpowers/skills/cyclone-frontend-page/SKILL.md
|
||||
git commit -m "feat(sp-skill-catalog): add cyclone-frontend-page skill (React page conventions)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 8: Phase 4, Skill 8 — `cyclone-cli`
|
||||
|
||||
**Goal:** Codify the CLI subcommand conventions in `cli.py` — argparse style, exit codes, smoke-test patterns. Last because nothing else depends on it.
|
||||
|
||||
**Files:**
|
||||
- Create: `.superpowers/skills/cyclone-cli/SKILL.md`
|
||||
|
||||
- [ ] **Step 1: Survey the existing CLI**
|
||||
|
||||
```bash
|
||||
grep -n "add_parser\|sub_parsers\|set_defaults" backend/src/cyclone/cli.py | head -20
|
||||
grep -n "sys.exit\|return [0-9]\|return 1\|return 2" backend/src/cyclone/cli.py | head -10
|
||||
```
|
||||
|
||||
Record the subcommand list and exit-code conventions.
|
||||
|
||||
- [ ] **Step 2: Sample one subcommand**
|
||||
|
||||
Pick the smallest (likely `validate`) and read its 20-30 lines. The skill's `## Patterns` will reference its structure.
|
||||
|
||||
- [ ] **Step 3: Create the directory and write the SKILL.md**
|
||||
|
||||
```bash
|
||||
mkdir -p .superpowers/skills/cyclone-cli
|
||||
```
|
||||
|
||||
Frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: cyclone-cli
|
||||
description: "Cyclone CLI subcommand conventions (cli.py, serve/parse/backup/rotate-key/validate, argparse style, exit codes, smoke tests). Use when: adding a CLI subcommand, changing exit codes, or working on operator-facing commands."
|
||||
---
|
||||
```
|
||||
|
||||
Body sections:
|
||||
- `## When to use` — 4 bullets: adding a subcommand, changing exit codes, adding a smoke test, wiring a security-sensitive command (key rotation, backup).
|
||||
- `## Conventions` — at minimum:
|
||||
1. **Subcommand shape.** Every subcommand is a function `cmd_<name>(args) -> int` in `cli.py`. The `main()` parser dispatches via `set_defaults(func=...)`.
|
||||
2. **Argparse style.** Use `argparse.ArgumentParser` sub-parsers. Long-form flags (`--rotate-key`) preferred over positional for safety.
|
||||
3. **Exit codes.** `0` = success, `1` = user error (bad input, missing file), `2` = operator error (DB locked, key missing), `3` = security error (auth fail, key mismatch). Document in `cmd_<name>` docstring.
|
||||
4. **Smoke test.** Every new subcommand gets a `backend/tests/test_cli_<name>.py` test using `subprocess.run` against the installed `cyc` entrypoint.
|
||||
5. **Security-sensitive commands.** Key rotation (`cyc rotate-key`), backup (`cyc backup`), and any command touching `secrets.py` requires a `--confirm` flag and a dry-run path.
|
||||
- `## Patterns` — three skeletons:
|
||||
- A `cmd_<name>(args)` function with `argparse` setup.
|
||||
- A `subprocess.run` smoke test.
|
||||
- A `--confirm` / `--dry-run` pattern for security-sensitive commands.
|
||||
- `## Anti-patterns` — at least:
|
||||
- Don't `sys.exit()` from inside a subcommand — return the int and let `main()` exit.
|
||||
- Don't print to stdout for errors — use `logging` (or `print(..., file=sys.stderr)` for top-level fatal errors).
|
||||
- Don't add a subcommand without a smoke test in `backend/tests/test_cli_<name>.py`.
|
||||
- `## Related skills` — references to `cyclone-store` (most subcommands touch the store), `cyclone-api-router` (`cyc serve` runs the FastAPI app), `cyclone-edi` (`cyc parse <file>`), `cyclone-tests` (CLI smoke tests).
|
||||
|
||||
Keep under ~200 lines.
|
||||
|
||||
- [ ] **Step 4: Verify**
|
||||
|
||||
```bash
|
||||
head -5 .superpowers/skills/cyclone-cli/SKILL.md
|
||||
wc -l .superpowers/skills/cyclone-cli/SKILL.md
|
||||
grep -c "^## " .superpowers/skills/cyclone-cli/SKILL.md
|
||||
```
|
||||
|
||||
Expected: frontmatter present, ≤ 200 lines, 5 `## ` sections.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add .superpowers/skills/cyclone-cli/SKILL.md
|
||||
git commit -m "feat(sp-skill-catalog): add cyclone-cli skill (CLI conventions)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 9: Final — README catalog section
|
||||
|
||||
**Goal:** Advertise the catalog to human readers and to AI agents reading the README on first contact.
|
||||
|
||||
**Files:**
|
||||
- Modify: `README.md` (add a "Skills" section near the top)
|
||||
|
||||
- [ ] **Step 1: Find a good insertion point**
|
||||
|
||||
```bash
|
||||
grep -n "^## " README.md | head -20
|
||||
```
|
||||
|
||||
Pick a sensible location — typically after the existing "Install" or "Dev" section.
|
||||
|
||||
- [ ] **Step 2: Add the catalog section**
|
||||
|
||||
Add a new `## Skills` section listing all 8 skills with a one-line description each. Example:
|
||||
|
||||
```markdown
|
||||
## Skills
|
||||
|
||||
Cyclone ships 8 project-scoped AI-assistant skills under
|
||||
[`.superpowers/skills/`](.superpowers/skills/). Each one codifies the
|
||||
conventions for a major subsystem so the next contributor (human or
|
||||
AI) gets the lay of the land automatically.
|
||||
|
||||
| Skill | Owns |
|
||||
|-------|------|
|
||||
| [`cyclone-spec`](.superpowers/skills/cyclone-spec/SKILL.md) | The SP-N spec → plan → implement → merge flow. |
|
||||
| [`cyclone-tests`](.superpowers/skills/cyclone-tests/SKILL.md) | pytest + vitest fixture patterns, prodfiles drop-in. |
|
||||
| [`cyclone-edi`](.superpowers/skills/cyclone-edi/SKILL.md) | EDI parser/validator conventions (837P/835/999/270/271/277CA/TA1). |
|
||||
| [`cyclone-tail`](.superpowers/skills/cyclone-tail/SKILL.md) | Live-tail streaming wire format and the hook triplet. |
|
||||
| [`cyclone-store`](.superpowers/skills/cyclone-store/SKILL.md) | Store write-paths, pubsub event contract, SP21 split map. |
|
||||
| [`cyclone-api-router`](.superpowers/skills/cyclone-api-router/SKILL.md) | FastAPI router conventions (`api_routers/`, `api_helpers.py`). |
|
||||
| [`cyclone-frontend-page`](.superpowers/skills/cyclone-frontend-page/SKILL.md) | React page conventions (TanStack Query, drawer, URL state). |
|
||||
| [`cyclone-cli`](.superpowers/skills/cyclone-cli/SKILL.md) | CLI subcommand conventions (`cli.py`, exit codes, smoke tests). |
|
||||
|
||||
Skills auto-load by description match — no slash command needed.
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Verify the section reads cleanly**
|
||||
|
||||
```bash
|
||||
grep -n -A 16 "^## Skills" README.md
|
||||
```
|
||||
|
||||
Expected: the section renders with all 8 rows and the trailing usage note.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add README.md
|
||||
git commit -m "docs(readme): add Skills section linking the 8-skill catalog"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-review (run after writing the plan, before executing)
|
||||
|
||||
- [ ] **Spec coverage.** Skim each section of `docs/superpowers/specs/2026-06-21-cyclone-skill-catalog-design.md`. Confirm:
|
||||
- Spec §"The catalog" → Tasks 1-8 each create the matching skill. ✅
|
||||
- Spec §"Per-skill structure" → enforced by the shared template locked in Task 0. ✅
|
||||
- Spec §"Loading & cross-references" → every Task has a `## Related skills` section. ✅
|
||||
- Spec §"Build order / phasing" → Phase order = Task order (1-2 foundations, 3-4 domain, 5-6 backend, 7-8 frontend+CLI). ✅
|
||||
- Spec §"Verification" → covered by verification steps in each task + Task 9 README addition. ✅
|
||||
- [ ] **Placeholder scan.** No "TBD" / "TODO" / "implement later" in the plan. Each task's body content is specified (frontmatter exact, sections listed, key bullets enumerated).
|
||||
- [ ] **Type / name consistency.**
|
||||
- Skill names: `cyclone-spec`, `cyclone-tests`, `cyclone-edi`, `cyclone-tail`, `cyclone-store`, `cyclone-api-router`, `cyclone-frontend-page`, `cyclone-cli`. Same in every task. ✅
|
||||
- Commit prefix: `feat(sp-skill-catalog): …` everywhere. ✅
|
||||
- Section names: `## When to use`, `## Conventions`, `## Patterns`, `## Anti-patterns`, `## Related skills`. Same in every task. ✅
|
||||
|
||||
Plan passes self-review.
|
||||
Reference in New Issue
Block a user