From 727a5afccb4700dbe181eeb2ce337d5ee111afda Mon Sep 17 00:00:00 2001 From: Sean Kennedy Date: Mon, 18 May 2026 16:32:20 -0400 Subject: [PATCH 01/10] chore(agents): scaffold .agents/ directory and docs --- .agents/README.md | 31 ++++++ .agents/docs/commit-format.md | 56 ++++++++++ .agents/docs/doc-format.md | 40 +++++++ .agents/docs/game-domain.md | 98 +++++++++++++++++ .agents/docs/github.md | 55 ++++++++++ .agents/docs/sails-patterns.md | 168 ++++++++++++++++++++++++++++++ .agents/docs/skill-conventions.md | 41 ++++++++ .agents/docs/testing.md | 94 +++++++++++++++++ .agents/docs/vue-patterns.md | 135 ++++++++++++++++++++++++ 9 files changed, 718 insertions(+) create mode 100644 .agents/README.md create mode 100644 .agents/docs/commit-format.md create mode 100644 .agents/docs/doc-format.md create mode 100644 .agents/docs/game-domain.md create mode 100644 .agents/docs/github.md create mode 100644 .agents/docs/sails-patterns.md create mode 100644 .agents/docs/skill-conventions.md create mode 100644 .agents/docs/testing.md create mode 100644 .agents/docs/vue-patterns.md diff --git a/.agents/README.md b/.agents/README.md new file mode 100644 index 000000000..4a9ef6f76 --- /dev/null +++ b/.agents/README.md @@ -0,0 +1,31 @@ +# .agents + +Cross-tool source of truth for Cuttle's agentic infrastructure. All AI assistants load from here via symlinks. + +## What lives here + +``` +skills/ # ctl-* skill prompts, each in their own SKILL.md +agents/ # ctl-* subagent definitions +docs/ # detailed reference docs (skills index into these) +tools/ # shared Node.js utilities called by hooks +``` + +## How it loads + +| Tool | Loads via | +|------|-----------| +| Claude Code | `.claude/skills → .agents/skills`, `.claude/agents → .agents/agents` | +| Gemini CLI | `.gemini/skills → .agents/skills`, `.gemini/agents → .agents/agents` | + +`CLAUDE.md → @AGENTS.md` auto-loads the top-level discovery and safety rules every session. + +## Skill namespace + +All Cuttle skills use the `ctl-` prefix. See `docs/skill-conventions.md` for naming rules. + +## Adding a skill + +1. Create `skills/ctl-/SKILL.md` with the required frontmatter. +2. Keep the file under ~500 lines. +3. If the skill references detailed examples, put those in `docs/` and link from the skill. diff --git a/.agents/docs/commit-format.md b/.agents/docs/commit-format.md new file mode 100644 index 000000000..5d382d1e1 --- /dev/null +++ b/.agents/docs/commit-format.md @@ -0,0 +1,56 @@ +# Commit Format + +Cuttle uses [Conventional Commits](https://www.conventionalcommits.org/) with file/area-based scopes. + +## Format + +``` +(): +``` + +- Subject: lowercase, imperative, no period. +- Scope: the file name (no extension) or the feature area affected. +- Keep subject under 72 characters. + +## Types + +| Type | When | +|------|------| +| `feat` | New user-facing feature | +| `fix` | Bug fix | +| `test` | Adding or updating tests | +| `style` | Visual/style change, no logic | +| `refactor` | Code restructure, no behavior change | +| `chore` | Build, deps, tooling, agents infrastructure | +| `docs` | Documentation only | + +## Scopes mined from git log + +Common scopes extracted from recent history: + +| Scope | Area | +|-------|------| +| `GameView` | `src/routes/game/GameView.vue` | +| `rematch.js` | `api/controllers/game/rematch.js` | +| `sockets` | Socket event handling | +| `lock-game` | `api/helpers/lock-game.js` | +| `announcementData` | `src/routes/home/components/announcementDialog/data/announcementData.js` | +| `deps` | Dependency updates | +| `agents` | `.agents/` infrastructure | + +For new files, use the file name without extension. For features that span multiple files, use the area name. + +## Rules + +- Never bump `package.json` version in a commit — CI handles this automatically via version labels. +- Do not use `git add -A` or `git add .` — stage specific files. +- Do not use `--no-verify`. + +## Examples + +``` +feat(GameView): add scuttle animation for seven card plays +fix(rematch.js): unlock game on forbidden error path +test(basicMoves.spec.js): add draw-past-limit edge case +chore(agents): add ctl-discover skill +``` diff --git a/.agents/docs/doc-format.md b/.agents/docs/doc-format.md new file mode 100644 index 000000000..eecaa9c55 --- /dev/null +++ b/.agents/docs/doc-format.md @@ -0,0 +1,40 @@ +# Doc Format + +House style for all docs in this repo, including `docs/*.md`, `.agents/docs/*.md`, and skill files. + +## Voice + +- Imperative, factual, dry. No marketing language. +- State what something does, not how great it is. +- Omit filler phrases: "Note that", "It is worth mentioning", "Please keep in mind". + +## Structure + +- `#` — document title (one per file). +- `##` — major section. +- `###` — sub-section. +- No level deeper than `###` in most docs; use a list instead. + +## Lists + +- Flat lists for 3+ parallel items. +- Use a table when items have 2+ attributes. +- Keep bullets to one line when possible. + +## Code + +- Inline code for: file paths, variable names, command fragments, JSON keys. +- Fenced blocks for: commands the reader runs, file snippets, API responses. +- Name the language on every fenced block. + +## Cross-references + +- Link to files by repo-relative path: `[game-domain.md](game-domain.md)`. +- Link to skills by name: `ctl-discover`. +- Do not link to line numbers — they rot. + +## What to omit + +- Do not document uncertainty ("may", "might", "could be"). Verify first, then document the fact. +- Do not annotate intent ("this is needed because...") — that belongs in commit messages or PRs. +- Do not repeat information already in `AGENTS.md`; link to it. diff --git a/.agents/docs/game-domain.md b/.agents/docs/game-domain.md new file mode 100644 index 000000000..8c48b7898 --- /dev/null +++ b/.agents/docs/game-domain.md @@ -0,0 +1,98 @@ +# Game Domain Glossary + +Canonical vocabulary for Cuttle. Use these terms in code, tests, and comments. See `docs/game-rules.md` for full rules. + +## Players + +| Term | Meaning | +|------|---------| +| `p0` | Player 0 — the player who was dealt 5 cards and goes first | +| `p1` | Player 1 — the player who was dealt 6 cards | +| `pNum` | Player number: `0` or `1` | + +In code, player objects are stored on `game.p0` and `game.p1`. The `pNum` is used in move payloads and socket events. + +## Cards + +### Ranks (integers 1–13) + +| Rank | Name | Role | +|------|------|------| +| 1 | Ace | One-off: scrap all point cards | +| 2 | Two | One-off: counter a one-off OR scrap a royal/glasses eight | +| 3 | Three | One-off: take one card from scrap pile | +| 4 | Four | One-off: opponent discards two cards | +| 5 | Five | One-off: discard one, draw up to three | +| 6 | Six | One-off: scrap all royals and glasses eights | +| 7 | Seven | One-off: reveal top two deck cards, play one immediately | +| 8 | Eight | Royal: "Glasses Eight" — opponent plays with hand revealed | +| 9 | Nine | One-off: return opponent's field card to their hand (frozen one turn) | +| 10 | Ten | Points only | +| 11 | Jack | Royal: steal an opponent's point card | +| 12 | Queen | Royal: protect your other cards from targeting | +| 13 | King | Royal: reduce points-to-win threshold | + +### Suits (integers 0–3) + +| Suit | Integer | +|------|---------| +| Clubs | 0 (weakest for scuttle tiebreak) | +| Diamonds | 1 | +| Hearts | 2 | +| Spades | 3 (strongest) | + +### Card representation in code + +```js +// In game store / API responses +{ rank: 7, suit: 0 } // 7 of Clubs + +// In GameCard class (src/stores/game.js) +card.name // "7♣️" + +// In Cypress selectors +'[data-player-hand-card=7_0]' // rank_suit +``` + +## Locations + +| Term | Meaning | Selector location | +|------|---------|-------------------| +| `hand` | Cards in a player's hand | `data-player-hand-card` / `data-opponent-hand-card` | +| `points` | Cards played for points | `data-player-point-card` / `data-opponent-point-card` | +| `faceCards` | Royals and glasses eights in play | `data-player-face-card` / `data-opponent-face-card` | +| `scrap` | Scrapped cards pile | — | + +## Actions + +| Term | API slug | When | +|------|----------|------| +| Draw | `draw` | Take top deck card | +| Play for points | `points` | Play Ace–Ten to own field | +| Scuttle | `scuttle` | Capture opponent's lower-ranked point card | +| Play royal/glasses eight | `faceCard` | Play J/Q/K/8 to own field | +| Play one-off | `untargetedOneOff` or `targetedOneOff` | Play A–7 or 9 to scrap pile | +| Jack (steal) | `jack` | Play Jack onto opponent's point card | +| Counter | `counter` | Play Two in response to opponent's one-off | +| Resolve | `resolve` | Resolve a stacked counter chain | + +## Win conditions + +- Default: 21+ points wins. +- With Kings: 1 King → 14pts, 2 Kings → 10pts, 3 Kings → 5pts, 4 Kings → 0pts (instant win on play). +- Three consecutive passes → stalemate. + +## Socket events + +Game state is broadcast per-player via `sails.helpers.broadcastGameEvent(gameId, payload)` or `sails.helpers.gameStates.publishGameState()`. The `change` field in the payload identifies the event type (e.g., `'rematch'`, `'newGameForRematch'`). + +## Key files + +| File | Purpose | +|------|---------| +| `utils/MoveType.json` | Enum of all move type strings | +| `utils/GameStatus.json` | Enum: `CREATED`, `STARTED`, `FINISHED` | +| `utils/GamePhase.json` | Enum for game phase tracking | +| `src/stores/game.js` | Pinia store: full game state, socket event handlers | +| `api/helpers/broadcast-game-event.js` | Broadcasts payload to all game rooms | +| `docs/game-rules.md` | Complete rules for human reference | diff --git a/.agents/docs/github.md b/.agents/docs/github.md new file mode 100644 index 000000000..87d8cd5f2 --- /dev/null +++ b/.agents/docs/github.md @@ -0,0 +1,55 @@ +# GitHub Workflow + +## Branch naming + +From `docs/CONTRIBUTING.md`: + +- Features: `feature/[issue-number-or-description]` +- Bug fixes: `bug/[issue-number-or-description]` + +## Pull requests + +### Version label (required) + +Every PR merged to `main` must have exactly one version label. The label drives the automated version bump via GitHub Actions (`bump-version.yml`): + +| Label | Effect | +|-------|--------| +| `patch-version` | `x.y.Z → x.y.Z+1` | +| `minor-version` | `x.y.z → x.Y+1.0` | +| `major-version` | `x.y.z → X+1.0.0` | + +The label is applied by the core team, not the contributor. However, the PR author should suggest the appropriate level in the PR description. + +Do not manually bump `package.json` — CI commits the bump after the PR merges. + +### PR template + +Fill the template completely. Link the PR to its issue with `Closes #N` in the body. + +### Draft PRs + +CI checks are skipped for draft PRs (the `draft` job exits 1 immediately). Convert to "Ready for review" only when lint and unit tests pass locally. + +### Automated version flow + +1. PR merges to `main` → "Prepare Version Bump" workflow fires. +2. "Prepare Version Bump" reads the version label, writes `version.txt` artifact. +3. "Bump Version" workflow reads artifact, runs `npm version `, commits, tags, pushes to `main`, creates GitHub release. + +## Labels + +Version labels: `patch-version`, `minor-version`, `major-version` + +Other common labels are for categorization (`bug`, `enhancement`, `documentation`). These do not affect versioning. + +## `gh` CLI usage + +All GitHub operations use the `gh` CLI. Do not add Octokit or other GitHub libraries. + +```bash +gh pr create --title "..." --body "..." +gh pr list --state open +gh pr view +gh label list +``` diff --git a/.agents/docs/sails-patterns.md b/.agents/docs/sails-patterns.md new file mode 100644 index 000000000..d34bf5be2 --- /dev/null +++ b/.agents/docs/sails-patterns.md @@ -0,0 +1,168 @@ +# Sails.js Patterns + +Detailed reference for backend patterns in Cuttle. See `ctl-sails-patterns` skill for quick lookups. + +## Controller actions + +File: `api/controllers//.js` + +```js +module.exports = async function (req, res) { + let game; + try { + const { usr: userId } = req.session; + let { gameId } = req.params; + gameId = Number(gameId); + const { someField } = req.body; + + game = await sails.helpers.lockGame(gameId); + + // ... business logic ... + + await sails.helpers.unlockGame(game.lock); + return res.ok({ result }); + } catch (err) { + try { + await sails.helpers.unlockGame(game.lock); + } catch (_) {} + + const message = err.raw?.message ?? err; + switch (err?.code) { + case CustomErrorType.FORBIDDEN: + return res.forbidden({ message }); + default: + return res.serverError({ message }); + } + } +}; +``` + +Canonical example: `api/controllers/game/rematch.js` + +## Helpers + +File: `api/helpers/.js` + +```js +module.exports = { + friendlyName: 'Helper name', + description: 'What it does', + inputs: { + gameId: { type: 'number', required: true }, + payload: { type: 'ref', required: true }, + }, + sync: true, // omit for async helpers + fn: ({ gameId, payload }, exits) => { + try { + // ... logic ... + return exits.success(result); + } catch (err) { + return exits.error(err); + } + }, +}; +``` + +Canonical example: `api/helpers/broadcast-game-event.js` + +### Calling helpers + +Cuttle uses positional calling (not `.with()`): + +```js +// Positional (project convention) +sails.helpers.broadcastGameEvent(gameId, payload); +await sails.helpers.lockGame(oldGameId); + +// Named (.with()) — valid but not used in this codebase +await sails.helpers.lockGame.with({ gameId: oldGameId }); +``` + +For async helpers, `await` the call. For `sync: true` helpers, no `await`. + +## Policies + +File: `api/policies/.js` + +```js +module.exports = function (req, res, next) { + const { session } = req; + const userIsValid = session.usr && typeof session.usr === 'number'; + if (session.loggedIn && userIsValid) { + return next(); + } + return res.status(401).json({ message: 'Please log in and try again' }); +}; +``` + +Canonical example: `api/policies/isLoggedIn.js` + +Policy chains are configured in `config/policies.js`. Policies run in order before the controller. + +## Models + +File: `api/models/.js` + +```js +module.exports = { + attributes: { + name: { type: 'string', required: true }, + status: { type: 'number', defaultsTo: 0 }, + p0: { model: 'user' }, + p1: { model: 'user' }, + }, + // Lifecycle callbacks (optional) + beforeCreate(values, proceed) { return proceed(); }, +}; +``` + +## Socket broadcasting + +Two patterns: + +**Symmetric broadcast** (same payload to all rooms): +```js +sails.helpers.broadcastGameEvent(gameId, payload); +// broadcasts to: game__p0, game__p1, game__spectator +``` + +**Asymmetric broadcast** (different view per player, hides hidden info): +```js +const { p0State, p1State, spectatorState } = await sails.helpers.gameStates.createSocketEvents(game, fullGame); +sails.sockets.broadcast(`game_${gameId}_p0`, 'game', p0State); +sails.sockets.broadcast(`game_${gameId}_p1`, 'game', p1State); +sails.sockets.broadcast(`game_${gameId}_spectator`, 'game', spectatorState); +``` + +## Routes + +`config/routes.js` maps HTTP verbs + paths to controller actions: + +```js +'POST /api/game/:gameId/move': { action: 'game/play-move' }, +'GET /api/game/:gameId': { action: 'game/get-game' }, +``` + +## Error types + +| Class | File | HTTP code | +|-------|------|-----------| +| `ForbiddenError` | `api/errors/forbiddenError.js` | 403 | +| `CustomErrorType` | `api/errors/customErrorType.js` | varies | + +```js +const ForbiddenError = require('../../errors/forbiddenError'); +throw new ForbiddenError('You are not a player in this game!'); +``` + +## Locking pattern + +Games use an advisory lock to prevent concurrent mutations: + +```js +game = await sails.helpers.lockGame(gameId); +// ... make changes ... +await sails.helpers.unlockGame(game.lock); +``` + +Always unlock in the `catch` block too (swallow the unlock error, then handle the original error). diff --git a/.agents/docs/skill-conventions.md b/.agents/docs/skill-conventions.md new file mode 100644 index 000000000..66b45bd2e --- /dev/null +++ b/.agents/docs/skill-conventions.md @@ -0,0 +1,41 @@ +# Skill Conventions + +## Naming + +- Prefix: `ctl-` for all Cuttle skills. +- Kebab-case after the prefix: `ctl-game-domain`, `ctl-sails-patterns`. +- Verb-first for workflow skills: `ctl-git-commit`, `ctl-plan-work`. +- Noun-first for reference/lookup skills: `ctl-game-domain`, `ctl-test-patterns`. + +## File structure + +Each skill lives at `skills/ctl-/SKILL.md`. + +Required frontmatter: + +```yaml +--- +name: ctl- +description: "" +model: inherit +allowed-tools: Read, Grep, Glob, Bash +--- +``` + +- `description` is what the dispatcher reads to decide whether to route here. Start with the trigger phrase, not a generic label. +- `model: inherit` defers to the session model. Override only when the skill requires a specific capability. +- `allowed-tools` — list only what the skill actually needs. + +## Size cap + +~500 lines per SKILL.md. If a skill grows past this, move detailed examples to `docs/` and link from the skill with a one-liner. + +## Trigger writing + +Good: `"Invoke on 'where does X live', 'find existing pattern', 'how do I discover'. Glob/grep recipes for finding code by area."` + +Bad: `"Helps with discovery"` — not actionable for a dispatcher. + +## Subagent definitions + +Reviewer subagents live in `agents/ctl-.md`. Use the same frontmatter shape. Reviewers always output a structured report: verdict, evidence, line references, recommended action. diff --git a/.agents/docs/testing.md b/.agents/docs/testing.md new file mode 100644 index 000000000..7bf7204d4 --- /dev/null +++ b/.agents/docs/testing.md @@ -0,0 +1,94 @@ +# Testing + +Cuttle uses two test frameworks: + +- **Cypress** for end-to-end (E2E) tests in `tests/e2e/` +- **Vitest** for unit tests in `tests/unit/` + +## Vitest — dual config + +| Script | Config | Scope | +|--------|--------|-------| +| `npm run test:unit:client` | `vitest.config.mjs` | `tests/unit/**/*.spec.{js,ts}` (excludes sails) | +| `npm run test:unit:sails` | `tests/unit/vitest-sails.config.mjs` | `tests/unit/specs/sails/**/*` | +| `npm run test:unit` | runs both above | full unit suite | +| `npm run test:agents` | `.agents/tools/vitest.config.mjs` | `.agents/tools/__tests__/**/*.spec.mjs` | + +Sails tests require a running Sails instance (setup in `tests/unit/setup-sails.vitest.js`). They run sequentially (`maxWorkers: 1, isolate: false`). + +Client tests use `environment: 'node'` and can run in parallel. + +## Cypress — E2E helpers + +Helpers are in `tests/e2e/support/helpers.js` and `commands.js`. + +### `cy.setupGameAsP0()` / `cy.setupGameAsP1()` + +Runs in `beforeEach`. Automatically: +1. Signs up two accounts. +2. Creates and joins a game as both players. +3. Readies both players and waits for the game to load. + +### `cy.loadGameFixture(fixture)` + +Loads a specific game state. All fields are optional. + +```js +cy.loadGameFixture({ + p0Hand: [Card.ACE_OF_CLUBS, Card.SEVEN_OF_CLUBS], + p0Points: [Card.TWO_OF_CLUBS], + p0FaceCards: [Card.KING_OF_SPADES], + p1Hand: [Card.TEN_OF_CLUBS], + p1Points: [Card.SIX_OF_HEARTS], + p1FaceCards: [Card.QUEEN_OF_HEARTS], + topCard: Card.FIVE_OF_DIAMONDS, + secondCard: Card.EIGHT_OF_SPADES, + scrap: [Card.TWO_OF_HEARTS], +}); +``` + +### `assertGameState(playerNum, fixture)` + +Asserts the full game state from a player's perspective. `playerNum` is `0` or `1`. + +```js +assertGameState(0, { + p0Hand: [Card.ACE_OF_CLUBS], + p0Points: [Card.TWO_OF_CLUBS, Card.SEVEN_OF_CLUBS], + ... +}); +``` + +## Card data selectors + +Format: `data---card=_` + +- ``: `player` (the current player) or `opponent` +- ``: `hand`, `point`, `face-card` +- ``: integer 1–13 +- ``: integer 0–3 (Clubs=0, Diamonds=1, Hearts=2, Spades=3) + +```js +cy.get('[data-player-hand-card=7_0]').click(); // 7 of Clubs in hand +cy.get('[data-move-choice=scuttle]').click(); // choose scuttle +cy.get('[data-opponent-point-card=6_2]').click(); // opponent's 6 of Hearts +``` + +## Move data selectors + +Format: `data-move-choice=` + +Move names match the API slug: `points`, `scuttle`, `untargetedOneOff`, `targetedOneOff`, `jack`, `faceCard`, `draw`. + +## Test structure + +E2E specs are in `tests/e2e/specs/`: + +- `in-game/` — in-game moves and interactions +- `out-of-game/` — lobby, stats, profile +- `playground/` — development/debug specs + +Unit specs are in `tests/unit/specs/`: + +- `*.spec.js` — client-side (Vue, Pinia, utilities) +- `sails/` — server-side Sails.js logic diff --git a/.agents/docs/vue-patterns.md b/.agents/docs/vue-patterns.md new file mode 100644 index 000000000..f4295bae3 --- /dev/null +++ b/.agents/docs/vue-patterns.md @@ -0,0 +1,135 @@ +# Vue Patterns + +Detailed reference for frontend patterns in Cuttle. See `ctl-vue-patterns` skill for quick lookups. + +## Component structure + +Cuttle uses Vue 3 with ` + + +``` + +## File locations + +| Type | Path | +|------|------| +| Route entry point | `src/routes//View.vue` | +| Page-specific components | `src/routes//components/` | +| Shared components | `src/components/` | +| Stores | `src/stores/` | +| Router | `src/router.js` | + +## Pinia stores + +All stores use the composition API form (not options API): + +```js +import { ref, computed } from 'vue'; +import { defineStore } from 'pinia'; + +export const useGameStore = defineStore('game', () => { + // State + const id = ref(null); + const status = ref(null); + + // Getters + const isStarted = computed(() => status.value === GameStatus.STARTED); + + // Actions + function setGame(gameData) { + id.value = gameData.id; + status.value = gameData.status; + } + + return { id, status, isStarted, setGame }; +}); +``` + +Canonical example: `src/stores/game.js` + +### Store discovery + +```bash +grep -r "defineStore" src/stores/ +``` + +## Imports and aliases + +| Alias | Resolves to | +|-------|-------------| +| `@` | `src/` | +| `_` | project root | + +```js +import { useGameStore } from '@/stores/game'; +import MoveType from '../../utils/MoveType.json'; // relative from src/ +import { version } from '_/package.json'; // from root +``` + +## Vuetify usage + +- Use Vuetify components (`v-btn`, `v-dialog`, `v-card`, `v-list`, etc.) for all UI. +- Do not write raw HTML buttons or modals. +- Look at `src/components/BaseDialog.vue` for dialog patterns. +- Styles use `sass/variables.scss` for design tokens. + +## Internationalization + +Cuttle uses `vue-i18n`. All user-visible strings go through `$t('key')`: + +```vue + +``` + +Translation files are in `src/i18n/`. + +## Socket connection + +The Sails socket client is initialized in `src/plugins/sails.js` and imported as `io`: + +```js +import { io } from '@/plugins/sails.js'; +``` + +In-game socket events are handled in `src/plugins/sockets/inGameEvents.js`. + +## Router navigation + +```js +import { useRouter } from 'vue-router'; +const router = useRouter(); +router.push({ name: 'game', params: { gameId } }); +``` + +Route names are defined in `src/router.js`. + +## Key constants + +| File | Contents | +|------|---------| +| `utils/MoveType.json` | Move type strings | +| `utils/GameStatus.json` | Game status enums | +| `utils/GamePhase.json` | Game phase enums | +| `utils/local-storage-utils.js` | LocalStorage key constants | From 4626cdfd2b0295a6682e6b6cbe0ba2e97a8b9ab8 Mon Sep 17 00:00:00 2001 From: Sean Kennedy Date: Mon, 18 May 2026 16:45:58 -0400 Subject: [PATCH 02/10] chore(agents): add cross-tool symlinks under .claude/ and .gemini/ --- .claude/agents | 1 + .claude/hooks/git-guardrails.sh | 5 +++ .claude/settings.json | 76 +++++++++++++++++++++++++++++++++ .claude/skills | 1 + .gemini/agents | 1 + .gemini/skills | 1 + .gitignore | 6 ++- 7 files changed, 89 insertions(+), 2 deletions(-) create mode 120000 .claude/agents create mode 100755 .claude/hooks/git-guardrails.sh create mode 100644 .claude/settings.json create mode 120000 .claude/skills create mode 120000 .gemini/agents create mode 120000 .gemini/skills diff --git a/.claude/agents b/.claude/agents new file mode 120000 index 000000000..4c8a5fc93 --- /dev/null +++ b/.claude/agents @@ -0,0 +1 @@ +../.agents/agents \ No newline at end of file diff --git a/.claude/hooks/git-guardrails.sh b/.claude/hooks/git-guardrails.sh new file mode 100755 index 000000000..d1f3d5738 --- /dev/null +++ b/.claude/hooks/git-guardrails.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +# Thin shim — delegates to .agents/tools/git-guardrails.mjs. +# Resolves repo root via git so CWD doesn't matter. +REPO_ROOT="$(git -C "$CLAUDE_PROJECT_DIR" rev-parse --show-toplevel 2>/dev/null || echo "$CLAUDE_PROJECT_DIR")" +exec node "$REPO_ROOT/.agents/tools/git-guardrails.mjs" diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 000000000..14927866c --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,76 @@ +{ + "permissions": { + "allow": [ + "Bash(find *)", + "Bash(ls *)", + "Bash(ls)", + "Bash(grep *)", + "Bash(cat *)", + "Bash(wc *)", + "Bash(echo *)", + "Bash(node -e *)", + "Bash(node --version*)", + "Bash(git status*)", + "Bash(git log*)", + "Bash(git diff*)", + "Bash(git show*)", + "Bash(git branch*)", + "Bash(git rev-parse*)", + "Bash(git remote*)", + "Bash(git fetch*)", + "Bash(git stash list*)", + "Bash(git switch *)", + "Bash(git checkout -b *)", + "Bash(git add *)", + "Bash(git commit *)", + "Bash(npm run lint)", + "Bash(npm run lint:*)", + "Bash(npm run test:unit*)", + "Bash(npm run test:agents*)", + "Bash(npm run build*)", + "Bash(npm run start:*)", + "Bash(npm run:*)", + "Bash(npm view *)", + "Bash(npm info *)", + "Bash(gh pr list*)", + "Bash(gh pr view*)", + "Bash(gh pr diff*)", + "Bash(gh pr checks*)", + "Bash(gh pr create*)", + "Bash(gh issue list*)", + "Bash(gh issue view*)", + "Bash(gh run list*)", + "Bash(gh run view*)", + "Bash(gh label list*)", + "Bash(gh repo view*)", + "Bash(gh api *)", + "Bash(gh status*)", + "WebSearch", + "WebFetch(domain:vitejs.dev)", + "WebFetch(domain:vite.dev)", + "WebFetch(domain:vitest.dev)", + "WebFetch(domain:docs.cypress.io)", + "WebFetch(domain:devtools.vuejs.org)", + "WebFetch(domain:sailsjs.com)", + "WebFetch(domain:vuejs.org)", + "WebFetch(domain:pinia.vuejs.org)", + "WebFetch(domain:github.com)", + "WebFetch(domain:nodejs.org)", + "WebFetch(domain:developer.mozilla.org)" + ], + "deny": [] + }, + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/git-guardrails.sh\"" + } + ] + } + ] + } +} diff --git a/.claude/skills b/.claude/skills new file mode 120000 index 000000000..2b7a412b8 --- /dev/null +++ b/.claude/skills @@ -0,0 +1 @@ +../.agents/skills \ No newline at end of file diff --git a/.gemini/agents b/.gemini/agents new file mode 120000 index 000000000..4c8a5fc93 --- /dev/null +++ b/.gemini/agents @@ -0,0 +1 @@ +../.agents/agents \ No newline at end of file diff --git a/.gemini/skills b/.gemini/skills new file mode 120000 index 000000000..2b7a412b8 --- /dev/null +++ b/.gemini/skills @@ -0,0 +1 @@ +../.agents/skills \ No newline at end of file diff --git a/.gitignore b/.gitignore index 5e3ad3bf1..bb8ec69ba 100644 --- a/.gitignore +++ b/.gitignore @@ -150,7 +150,9 @@ storybook-static # # Files generated by AI assistants ################################################ -.claude* +# Personal/local AI settings (not committed) +.claude/settings.local.json .cursor* -.gemini* +# Track .gemini symlinks but not local overrides +.gemini/settings.local.json From 06786a533880c468e4830717e53bac9e484cad03 Mon Sep 17 00:00:00 2001 From: Sean Kennedy Date: Mon, 18 May 2026 16:47:59 -0400 Subject: [PATCH 03/10] feat(agents): add ctl-discover, ctl-game-domain, ctl-{sails,vue,test}-patterns skills --- .agents/skills/ctl-discover/SKILL.md | 125 +++++++++++++++ .agents/skills/ctl-game-domain/SKILL.md | 108 +++++++++++++ .agents/skills/ctl-sails-patterns/SKILL.md | 177 +++++++++++++++++++++ .agents/skills/ctl-test-patterns/SKILL.md | 176 ++++++++++++++++++++ .agents/skills/ctl-vue-patterns/SKILL.md | 176 ++++++++++++++++++++ 5 files changed, 762 insertions(+) create mode 100644 .agents/skills/ctl-discover/SKILL.md create mode 100644 .agents/skills/ctl-game-domain/SKILL.md create mode 100644 .agents/skills/ctl-sails-patterns/SKILL.md create mode 100644 .agents/skills/ctl-test-patterns/SKILL.md create mode 100644 .agents/skills/ctl-vue-patterns/SKILL.md diff --git a/.agents/skills/ctl-discover/SKILL.md b/.agents/skills/ctl-discover/SKILL.md new file mode 100644 index 000000000..c7d7bc43c --- /dev/null +++ b/.agents/skills/ctl-discover/SKILL.md @@ -0,0 +1,125 @@ +--- +name: ctl-discover +description: "Glob/grep recipes for finding existing patterns by area. Use on 'where does X live', 'find existing pattern for', 'how do I discover', 'search for', 'find files'. Returns file paths and brief descriptions before proposing any code." +model: inherit +allowed-tools: Read, Grep, Glob, Bash +--- + +# Discover + +Pattern-first lookup for Cuttle. Always run discovery before proposing code. + +## Workflow + +1. Identify the area from the request (state, controller, component, test, helper, policy). +2. Run the recipe for that area. +3. Report findings: `Found {N} examples of {pattern}: file:line — description`. +4. Ask "Ready to implement following this pattern?" before writing any code. + +## Recipes by area + +### State management (Pinia stores) + +```bash +grep -r "defineStore" src/stores/ --include="*.js" -l +grep -r "defineStore" src/stores/ --include="*.js" -n +``` + +Look for: store name, state refs, computed getters, returned actions. + +### Frontend components + +```bash +# Shared components +find src/components -name "*.vue" | head -20 + +# Route-specific components +find src/routes -name "*.vue" | head -30 +find src/routes -name "components" -type d + +# A specific route's entry point +ls src/routes// +``` + +Pattern: `src/routes//View.vue` is the entry point; `src/routes//components/` holds page-specific components. + +### Backend controllers + +```bash +find api/controllers -name "*.js" | head -20 +# For a specific action +ls api/controllers/game/ +cat api/controllers/game/rematch.js +``` + +Pattern: `module.exports = async function(req, res) { ... }` with try/catch + lock/unlock. + +### Helpers + +```bash +find api/helpers -name "*.js" | head -20 +grep -r "sails.helpers\." api/controllers/ --include="*.js" -h | sort -u | head -20 +``` + +### Policies + +```bash +ls api/policies/ +grep -r "api/policies" config/policies.js +``` + +### Socket events + +```bash +grep -r "broadcastGameEvent\|publishGameState\|sails.sockets" api/ --include="*.js" -l +grep -r "on('game'" src/ --include="*.js" -l +cat api/helpers/broadcast-game-event.js +``` + +### E2E tests + +```bash +find tests/e2e/specs -name "*.spec.js" | head -20 +ls tests/e2e/specs/in-game/ +cat tests/e2e/support/helpers.js +``` + +### Unit tests + +```bash +find tests/unit/specs -name "*.spec.js" | head -20 +ls tests/unit/specs/sails/ +``` + +### Routes config + +```bash +grep -n "game\|move\|rematch" config/routes.js | head -20 +``` + +### Models + +```bash +ls api/models/ +cat api/models/Game.js +``` + +## Discovery hierarchy (per AGENTS.md) + +1. Existing code (search first) +2. `docs/*.md` for standards +3. Config files (`.eslintrc.js`, `vite.config.mjs`) + +## Output format + +``` +Found {N} examples of {pattern}: +- {file}:{line} — {brief description} +- {file}:{line} — {brief description} + +Common pattern: {summary} + +Ready to implement following this pattern? +``` + +Never skip the "Ready to implement?" gate. diff --git a/.agents/skills/ctl-game-domain/SKILL.md b/.agents/skills/ctl-game-domain/SKILL.md new file mode 100644 index 000000000..637afbecc --- /dev/null +++ b/.agents/skills/ctl-game-domain/SKILL.md @@ -0,0 +1,108 @@ +--- +name: ctl-game-domain +description: "Cuttle game vocabulary index. Use on 'what is a one-off', 'what does scuttle mean', 'what rank is a jack', 'suit order', 'how does the seven work', 'p0 vs p1', 'card selector format', or any domain term lookup. Anti-hallucination guard for game-specific logic." +model: inherit +allowed-tools: Read, Grep +--- + +# Game Domain + +Canonical vocabulary for Cuttle. Full details in `.agents/docs/game-domain.md` and `docs/game-rules.md`. + +## Players + +| Term | Meaning | +|------|---------| +| `p0` | Player dealt 5 cards — goes first | +| `p1` | Player dealt 6 cards | +| `pNum` | Integer: `0` or `1` | + +## Card ranks (1–13) + +| Rank | Name | Type | Effect | +|------|------|------|--------| +| 1 | Ace | One-off | Scrap all point cards (both players) | +| 2 | Two | One-off | Counter a one-off OR scrap a royal/glasses eight | +| 3 | Three | One-off | Take one card from scrap pile to hand | +| 4 | Four | One-off | Opponent discards 2 cards of their choice | +| 5 | Five | One-off | Discard 1, draw up to 3 from deck | +| 6 | Six | One-off | Scrap all royals and glasses eights (both players) | +| 7 | Seven | One-off | Reveal top 2 deck cards, play one immediately | +| 8 | Eight | Royal | "Glasses Eight" — opponent plays with hand revealed | +| 9 | Nine | One-off | Return opponent's field card to hand (frozen 1 turn) | +| 10 | Ten | Points only | 10 points | +| 11 | Jack | Royal | Steal an opponent's point card | +| 12 | Queen | Royal | Protect your other cards from targeting | +| 13 | King | Royal | Reduce points-to-win threshold | + +Number cards (Ace–Ten) score their face value as points when played to the field. + +## Card suits (0–3) + +| Integer | Suit | Scuttle rank | +|---------|------|--------------| +| 0 | Clubs | weakest | +| 1 | Diamonds | | +| 2 | Hearts | | +| 3 | Spades | strongest | + +Suit is the tiebreaker for scuttle: you can scuttle a card of the same rank only if your suit is higher. + +## Game actions + +| Action | API slug | Trigger | +|--------|----------|---------| +| Draw | `draw` | Click deck | +| Play for points | `points` | Select card → click own field | +| Scuttle | `scuttle` | Select higher card → click opponent's point card | +| Royal / Glasses Eight | `faceCard` | Select J/Q/K/8 → click own field | +| One-off (untargeted) | `untargetedOneOff` | Select A/3/4/5/6 → click scrap pile | +| One-off (targeted) | `targetedOneOff` | Select 2/7/9 → click target | +| Jack (steal) | `jack` | Select Jack → click opponent's point card | +| Counter | `counter` | Play Two in response to opponent's one-off | +| Resolve | `resolve` | Resolve counter chain | + +## Card selector format + +``` +data---card=_ +``` + +| Segment | Values | +|---------|--------| +| `` | `player` (current player) or `opponent` | +| `` | `hand`, `point`, `face-card` | +| `` | 1–13 | +| `` | 0–3 | + +Examples: +```js +'[data-player-hand-card=7_0]' // 7 of Clubs in hand +'[data-opponent-point-card=6_2]' // opponent's 6 of Hearts +'[data-move-choice=scuttle]' // scuttle move option +``` + +## Win conditions + +- Default: first to 21+ points. +- 1 King: 14pts to win. 2 Kings: 10pts. 3 Kings: 5pts. 4 Kings: 0pts (win on play). +- Three consecutive passes → stalemate. + +## Terminology to avoid confusing + +| Do not say | Say instead | +|------------|-------------| +| "destroy" | "scrap" | +| "discard pile" | "scrap pile" | +| "play area" | "field" or "points"/"faceCards" depending on card type | +| "special cards" | "royals and glasses eights" | +| "instant effects" | "one-offs" | + +## Key source files + +- `docs/game-rules.md` — full rules for human reference +- `.agents/docs/game-domain.md` — extended glossary with code examples +- `utils/MoveType.json` — all move type strings +- `utils/GameStatus.json` — game status enum +- `src/stores/game.js` — GameCard class, card sorting, socket events +- `api/helpers/broadcast-game-event.js` — socket broadcast pattern diff --git a/.agents/skills/ctl-sails-patterns/SKILL.md b/.agents/skills/ctl-sails-patterns/SKILL.md new file mode 100644 index 000000000..111c0edaa --- /dev/null +++ b/.agents/skills/ctl-sails-patterns/SKILL.md @@ -0,0 +1,177 @@ +--- +name: ctl-sails-patterns +description: "Sails.js backend patterns for Cuttle. Use on 'write a controller', 'add a helper', 'create a policy', 'add a route', 'sails helper syntax', 'lock game', 'broadcast socket event', or any backend/API question." +model: inherit +allowed-tools: Read, Grep, Glob, Bash +--- + +# Sails Patterns + +Quick reference for Cuttle's Sails.js backend. Full details in `.agents/docs/sails-patterns.md`. + +## Before writing any backend code + +```bash +# Find similar controllers +find api/controllers -name "*.js" | head -20 + +# Find existing helper calls +grep -r "sails.helpers\." api/controllers/ --include="*.js" -h | sort -u | head -20 + +# Check existing helpers +ls api/helpers/ + +# Check policy chain +cat config/policies.js +``` + +## Controller template + +File: `api/controllers//.js` + +```js +const GameStatus = require('../../../utils/GameStatus.json'); +const CustomErrorType = require('../../errors/customErrorType'); +const ForbiddenError = require('../../errors/forbiddenError'); + +module.exports = async function (req, res) { + let game; + try { + const { usr: userId } = req.session; + let { gameId } = req.params; + gameId = Number(gameId); + const { fieldName } = req.body; + + game = await sails.helpers.lockGame(gameId); + + // Validate user is a player + const playerIds = [game.p0?.id, game.p1?.id].filter(Boolean); + if (!playerIds.includes(userId)) { + throw new ForbiddenError('You are not a player in this game!'); + } + + // ... business logic ... + + await sails.helpers.unlockGame(game.lock); + return res.ok({ result }); + } catch (err) { + try { await sails.helpers.unlockGame(game.lock); } catch (_) {} + const message = err.raw?.message ?? err; + switch (err?.code) { + case CustomErrorType.FORBIDDEN: return res.forbidden({ message }); + default: return res.serverError({ message }); + } + } +}; +``` + +Canonical example: `api/controllers/game/rematch.js` + +## Helper template + +```js +module.exports = { + friendlyName: 'Helper name', + description: 'What it does in one sentence', + inputs: { + gameId: { type: 'number', required: true }, + }, + // Add `sync: true` only for synchronous helpers + fn: async ({ gameId }, exits) => { + try { + // ... logic ... + return exits.success(result); + } catch (err) { + return exits.error(err); + } + }, +}; +``` + +Canonical example: `api/helpers/broadcast-game-event.js` + +### Calling helpers (positional form — project convention) + +```js +// Async +const game = await sails.helpers.lockGame(gameId); +await sails.helpers.unlockGame(game.lock); + +// Sync +sails.helpers.broadcastGameEvent(gameId, payload); +``` + +Do not use `.with()` — the project uses positional calls consistently. + +## Policy template + +```js +module.exports = function (req, res, next) { + const { session } = req; + const userIsValid = session.usr && typeof session.usr === 'number'; + if (session.loggedIn && userIsValid) { + return next(); + } + return res.status(401).json({ message: 'Please log in and try again' }); +}; +``` + +Canonical example: `api/policies/isLoggedIn.js` + +## Route registration + +File: `config/routes.js` + +```js +'POST /api/game/:gameId/rematch': { action: 'game/rematch' }, +'GET /api/game/:gameId': { action: 'game/get-game' }, +``` + +## Socket broadcasting + +**Symmetric** (same payload to all rooms): +```js +sails.helpers.broadcastGameEvent(gameId, { change: 'eventName', ...data }); +// Broadcasts to: game__p0, game__p1, game__spectator +``` + +**Asymmetric** (per-player views — hides opponent's hidden cards): +```js +const { p0State, p1State, spectatorState } = await sails.helpers.gameStates + .createSocketEvents(game, newFullGame); +sails.sockets.broadcast(`game_${gameId}_p0`, 'game', p0State); +sails.sockets.broadcast(`game_${gameId}_p1`, 'game', p1State); +sails.sockets.broadcast(`game_${gameId}_spectator`, 'game', spectatorState); +``` + +Use asymmetric when the new game state includes hidden information (e.g., opponent's hand). + +## Locking pattern + +Always lock before reading game state, always unlock in catch: + +```js +game = await sails.helpers.lockGame(gameId); +// make changes +await sails.helpers.unlockGame(game.lock); +``` + +## Error types + +```js +const ForbiddenError = require('../../errors/forbiddenError'); +const CustomErrorType = require('../../errors/customErrorType'); + +throw new ForbiddenError('message'); // → res.forbidden() +``` + +## Security rules + +- Every game-mutating route must be protected by `isLoggedIn` policy. +- Always validate that `req.session.usr` matches a player in the game before mutating. +- Never trust client-supplied `userId` — always read from `req.session.usr`. +- No hardcoded secrets; use Sails config. + +## Full reference + +`.agents/docs/sails-patterns.md` diff --git a/.agents/skills/ctl-test-patterns/SKILL.md b/.agents/skills/ctl-test-patterns/SKILL.md new file mode 100644 index 000000000..b5ecbbcd0 --- /dev/null +++ b/.agents/skills/ctl-test-patterns/SKILL.md @@ -0,0 +1,176 @@ +--- +name: ctl-test-patterns +description: "Testing patterns for Cuttle — Cypress E2E and Vitest unit tests. Use on 'write a test', 'add a Cypress spec', 'write a unit test', 'how do I set up a game in tests', 'loadGameFixture', 'assertGameState', 'cy.setupGameAsP0', or any testing question." +model: inherit +allowed-tools: Read, Grep, Glob, Bash +--- + +# Test Patterns + +Quick reference for Cuttle's test infrastructure. Full details in `.agents/docs/testing.md`. + +## Before writing tests + +```bash +# Find similar E2E specs +ls tests/e2e/specs/in-game/ +find tests/e2e/specs -name "*.spec.js" | head -20 + +# Find similar unit tests +find tests/unit/specs -name "*.spec.js" | head -10 + +# Read the test helpers +cat tests/e2e/support/helpers.js +grep -n "Cypress.Commands.add" tests/e2e/support/commands.js | head -20 +``` + +## Cypress E2E — file location + +``` +tests/e2e/specs/in-game/ # in-game moves and interactions +tests/e2e/specs/out-of-game/ # lobby, stats, profile, rankings +tests/e2e/specs/playground/ # development/debug +``` + +## Cypress E2E — basic structure + +```js +describe('Feature name', () => { + beforeEach(() => { + cy.setupGameAsP0(); // or cy.setupGameAsP1() + }); + + it('does something', () => { + cy.loadGameFixture(0, { + p0Hand: [Card.SEVEN_OF_CLUBS], + p0Points: [Card.TWO_OF_CLUBS], + p1Points: [Card.SIX_OF_HEARTS], + }); + + cy.get('[data-player-hand-card=7_0]').click(); + cy.get('[data-move-choice=untargetedOneOff]').click(); + + assertGameState(0, { + p0Hand: [], + p0Points: [Card.TWO_OF_CLUBS], + p1Points: [Card.SIX_OF_HEARTS], + scrap: [Card.SEVEN_OF_CLUBS], + }); + }); +}); +``` + +## `cy.setupGameAsP0()` / `cy.setupGameAsP1()` + +Run in `beforeEach`. Automatically: +1. Signs up two accounts +2. Creates and joins a game +3. Readies both players and waits for load + +## `cy.loadGameFixture(pNum, fixture)` + +Sets the game into a specific state. `pNum` is `0` or `1`. + +```js +cy.loadGameFixture(0, { + p0Hand: [Card.ACE_OF_CLUBS, Card.SEVEN_OF_CLUBS], + p0Points: [Card.TWO_OF_CLUBS, Card.TEN_OF_HEARTS], + p0FaceCards: [Card.KING_OF_SPADES], + p1Hand: [Card.TEN_OF_CLUBS], + p1Points: [Card.SIX_OF_HEARTS], + p1FaceCards: [Card.QUEEN_OF_HEARTS], + topCard: Card.FIVE_OF_DIAMONDS, + secondCard: Card.EIGHT_OF_SPADES, + scrap: [Card.TWO_OF_HEARTS], +}); +``` + +All fields are optional. + +## `assertGameState(pNum, fixture)` + +Asserts full game state from a player's perspective. Same shape as `loadGameFixture`. + +```js +assertGameState(0, { + p0Points: [Card.TWO_OF_CLUBS, Card.SEVEN_OF_CLUBS], + p1Points: [], + scrap: [Card.SIX_OF_HEARTS], +}); +``` + +## Card data selectors + +Format: `data---card=_` + +```js +'[data-player-hand-card=7_0]' // 7 of Clubs in player's hand +'[data-opponent-point-card=6_2]' // opponent's 6 of Hearts on field +'[data-player-face-card=13_3]' // player's King of Spades on field +'[data-move-choice=scuttle]' // scuttle move option button +``` + +## Move selectors + +`[data-move-choice=]` where slug matches the API endpoint: + +`points` · `scuttle` · `faceCard` · `untargetedOneOff` · `targetedOneOff` · `jack` · `counter` · `resolve` · `draw` + +## Making opponent moves + +```js +cy.drawCardOpponent(); +cy.playPointsOpponent(Card.TEN_OF_CLUBS); +cy.playFaceCardOpponent(Card.QUEEN_OF_HEARTS); +cy.playOneOffOpponent(Card.ACE_OF_CLUBS); +``` + +## Vitest unit tests — client + +File: `tests/unit/specs/*.spec.js` + +Config: `vitest.config.mjs` (root) — `include: ['tests/unit/**/*.spec.{js,ts}']` + +```js +import { beforeEach, describe, it, expect, vi } from 'vitest'; + +describe('module name', () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + it('does something', () => { + expect(result).toBe(expected); + }); +}); +``` + +## Vitest unit tests — Sails + +File: `tests/unit/specs/sails/*.spec.js` + +Config: `tests/unit/vitest-sails.config.mjs` — sequential, uses Sails setup file. + +Run with: `npm run test:unit:sails` + +## Agents tests + +File: `.agents/tools/__tests__/*.spec.mjs` + +Config: `.agents/tools/vitest.config.mjs` + +Run with: `npm run test:agents` + +These test pure Node.js logic (no Sails, no Vue). Use ESM imports. + +## TDD workflow (from CONTRIBUTING.md) + +1. Write or update the test first. +2. Run `npm run start:server` in background. +3. Run `npm run e2e:client` (or `e2e:gui` for interactive). +4. Implement until tests pass. +5. Run `npm run lint` and `npm run test:unit`. + +## Full reference + +`.agents/docs/testing.md` diff --git a/.agents/skills/ctl-vue-patterns/SKILL.md b/.agents/skills/ctl-vue-patterns/SKILL.md new file mode 100644 index 000000000..ff268b88a --- /dev/null +++ b/.agents/skills/ctl-vue-patterns/SKILL.md @@ -0,0 +1,176 @@ +--- +name: ctl-vue-patterns +description: "Vue 3 / Pinia / Vuetify frontend patterns for Cuttle. Use on 'write a component', 'add a store', 'Pinia defineStore', 'script setup', 'Vuetify component', 'add a route', 'socket event handler', or any frontend/UI question." +model: inherit +allowed-tools: Read, Grep, Glob, Bash +--- + +# Vue Patterns + +Quick reference for Cuttle's frontend. Full details in `.agents/docs/vue-patterns.md`. + +## Before writing any frontend code + +```bash +# Find similar components +find src/components -name "*.vue" | head -20 +find src/routes -name "*.vue" | head -30 + +# Find similar stores +grep -r "defineStore" src/stores/ --include="*.js" -l + +# Find existing socket event handlers +grep -r "handleInGameEvents\|on('game'" src/ --include="*.js" -l +``` + +## File locations + +| Type | Path | +|------|------| +| Route entry | `src/routes//View.vue` | +| Page components | `src/routes//components/` | +| Shared components | `src/components/` | +| Stores | `src/stores/` | +| Router | `src/router.js` | + +## Vue SFC template + +```vue + + + +``` + +Always use ` + +``` + +Do not hardcode English strings in templates. + +## Vuetify components + +Use Vuetify for all UI elements. Do not write raw HTML buttons/modals/inputs. + +Common components: `v-btn`, `v-card`, `v-dialog`, `v-list`, `v-list-item`, `v-icon`, `v-text-field`, `v-select`, `v-snackbar`, `v-tooltip`. + +## Router navigation + +```js +import { useRouter, useRoute } from 'vue-router'; +const router = useRouter(); +const route = useRoute(); + +// Navigate +router.push({ name: 'game', params: { gameId: id } }); + +// Read params +const gameId = route.params.gameId; +``` + +Route names are in `src/router.js`. + +## Socket connection + +```js +import { io } from '@/plugins/sails.js'; +// io is already configured — call methods directly +io.socket.on('game', (data) => { /* handle */ }); +io.socket.get('/api/game', (body, res) => { /* handle */ }); +``` + +In-game events are handled in `src/plugins/sockets/inGameEvents.js`. + +## Security rules + +- Never use `v-html` with user-supplied content. +- Never expose server-only data (session secrets, DB credentials) in frontend code. + +## Full reference + +`.agents/docs/vue-patterns.md` From 266e228cf7990e235e69d7a6acfd27cd602ae680 Mon Sep 17 00:00:00 2001 From: Sean Kennedy Date: Mon, 18 May 2026 16:49:08 -0400 Subject: [PATCH 04/10] feat(agents): add ctl-{git-commit,github-create-pr,code-review,plan-work} workflow skills --- .agents/skills/ctl-code-review/SKILL.md | 102 +++++++++++++++++++ .agents/skills/ctl-git-commit/SKILL.md | 91 +++++++++++++++++ .agents/skills/ctl-github-create-pr/SKILL.md | 90 ++++++++++++++++ .agents/skills/ctl-plan-work/SKILL.md | 95 +++++++++++++++++ 4 files changed, 378 insertions(+) create mode 100644 .agents/skills/ctl-code-review/SKILL.md create mode 100644 .agents/skills/ctl-git-commit/SKILL.md create mode 100644 .agents/skills/ctl-github-create-pr/SKILL.md create mode 100644 .agents/skills/ctl-plan-work/SKILL.md diff --git a/.agents/skills/ctl-code-review/SKILL.md b/.agents/skills/ctl-code-review/SKILL.md new file mode 100644 index 000000000..540cec403 --- /dev/null +++ b/.agents/skills/ctl-code-review/SKILL.md @@ -0,0 +1,102 @@ +--- +name: ctl-code-review +description: "Review code changes against Cuttle's discover-plan-execute workflow, safety rules, and AGENTS.md conventions. Use on 'review my changes', 'check this diff', 'review this PR', '/ctl-code-review'. Dispatches to subagents for security, architecture, and performance." +model: inherit +allowed-tools: Read, Grep, Glob, Bash, Agent +--- + +# Code Review + +Compliance review for Cuttle. Checks AGENTS.md rules, then dispatches to specialist subagents. + +## Input + +Run against staged diff, a branch, or a PR: + +```bash +# Current branch vs main +git diff main..HEAD + +# Specific PR +gh pr diff +``` + +## Phase 1: Automated checks + +Before reviewing code, verify the basics passed: + +```bash +npm run lint +npm run test:unit +``` + +If either fails, report which checks failed. Do not proceed with review until they're green. + +## Phase 2: AGENTS.md compliance + +Check against AGENTS.md: + +1. **Discovery first** — Were patterns discovered before code was written? Look for evidence of glob/grep discovery in the PR description or commit messages. +2. **Safety rules** — No hardcoded secrets. Session validation present on game-mutating routes. Policies applied (`config/policies.js`). +3. **Pattern consistency** — Controller follows the lock/unlock/catch pattern. Helper follows the `inputs`/`fn`/`exits` structure. Vue SFC uses `