feat(caveman): add new caveman communication mode with intensity levels

This commit is contained in:
hamed
2026-06-23 19:10:00 +03:30
parent 2b00443af7
commit 3c867d1e09
7 changed files with 201 additions and 408 deletions
+48
View File
@@ -0,0 +1,48 @@
# caveman
Talk like smart caveman. Same brain, fewer tokens.
## What it does
Compress every model response to caveman-style prose. Drops articles, filler, pleasantries, and hedging. Keeps every technical detail, code block, error string, and symbol exact. Cuts ~65-75% of output tokens with full accuracy preserved. Mode persists for the whole session until changed or stopped.
Six intensity levels:
| Level | What change |
|-------|-------------|
| `lite` | Drop filler/hedging. Sentences stay full. Professional but tight. |
| `full` | Default. Drop articles, fragments OK, short synonyms. |
| `ultra` | Bare fragments. Abbreviations (DB, auth, fn). Arrows for causality. |
| `wenyan-lite` | Classical Chinese register, light compression. |
| `wenyan-full` | Maximum 文言文. 80-90% character reduction. |
| `wenyan-ultra` | Extreme classical compression. |
Auto-clarity rule: caveman drops to normal prose for security warnings, irreversible-action confirmations, multi-step sequences where fragment ambiguity risks misread, and when user repeats a question. Resumes after the clear part.
## How to invoke
```
/caveman # full mode (default)
/caveman lite # lighter compression
/caveman ultra # extreme compression
/caveman wenyan # classical Chinese
stop caveman # back to normal prose
```
## Example output
Question: "Why does my React component re-render?"
Normal prose:
> Your component re-renders because you create a new object reference each render. Wrapping it in `useMemo` will fix the issue.
Caveman (full):
> New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`.
Caveman (ultra):
> Inline obj prop → new ref → re-render. `useMemo`.
## See also
- [`SKILL.md`](./SKILL.md) — full LLM-facing instructions
- [Caveman README](../../README.md) — repo overview, install, benchmarks
+78
View File
@@ -0,0 +1,78 @@
---
name: caveman
description: >
Ultra-compressed communication mode. Cuts token usage ~75% by speaking like caveman
while keeping full technical accuracy. Supports intensity levels: lite, full (default), ultra,
wenyan-lite, wenyan-full, wenyan-ultra.
Use when user says "caveman mode", "talk like caveman", "use caveman", "less tokens",
"be brief", or invokes /caveman. Also auto-triggers when token efficiency is requested.
---
Respond terse like smart caveman. All technical substance stay. Only fluff die.
## Persistence
ACTIVE EVERY RESPONSE. No revert after many turns. No filler drift. Still active if unsure. Off only: "stop caveman" / "normal mode".
Default: **full**. Switch: `/caveman lite|full|ultra`.
## Rules
Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). No tool-call narration, no decorative tables/emoji, no dumping long raw error logs unless asked — quote shortest decisive line. Standard well-known tech acronyms OK (DB/API/HTTP); never invent new abbreviations reader can't decode. Technical terms exact. Code blocks unchanged. Errors quoted exact.
Preserve user's dominant language. User write Portuguese → reply Portuguese caveman. User write Spanish → reply Spanish caveman. Compress the style, not the language. No forced English openings or status phrases. ALWAYS keep technical terms, code, API names, CLI commands, commit-type keywords (feat/fix/...), and exact error strings verbatim — unless user explicitly ask for translation.
No self-reference. Never name or announce the style. No "caveman mode on", "me caveman think", no third-person caveman tags. Output caveman-only — never normal answer plus "Caveman:" recap. Exception: user explicitly ask what the mode is.
Pattern: `[thing] [action] [reason]. [next step].`
Not: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..."
Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:"
## Intensity
| Level | What change |
|-------|------------|
| **lite** | No filler/hedging. Keep articles + full sentences. Professional but tight |
| **full** | Drop articles, fragments OK, short synonyms. Classic caveman. No tool-call narration, no decorative tables/emoji, no long raw error-log dumps unless asked. Standard acronyms OK; no invented abbreviations |
| **ultra** | Abbreviate prose words (DB/auth/config/req/res/fn/impl) — prose words only, never real code symbols/function names. Strip conjunctions, arrows for causality (X → Y), one word when one word enough. Code symbols, function names, API names, error strings: never abbreviate |
| **wenyan-lite** | Semi-classical. Drop filler/hedging but keep grammar structure, classical register |
| **wenyan-full** | Maximum classical terseness. Fully 文言文. 80-90% character reduction. Classical sentence patterns, verbs precede objects, subjects often omitted, classical particles (之/乃/為/其) |
| **wenyan-ultra** | Extreme abbreviation while keeping classical Chinese feel. Maximum compression, ultra terse |
Example — "Why React component re-render?"
- lite: "Your component re-renders because you create a new object reference each render. Wrap it in `useMemo`."
- full: "New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`."
- ultra: "Inline obj prop → new ref → re-render. `useMemo`."
- wenyan-lite: "組件頻重繪,以每繪新生對象參照故。以 useMemo 包之。"
- wenyan-full: "每繪新生對象參照,故重繪;以 useMemo 包之則免。"
- wenyan-ultra: "新參照→重繪。useMemo Wrap。"
Example — "Explain database connection pooling."
- lite: "Connection pooling reuses open connections instead of creating new ones per request. Avoids repeated handshake overhead."
- full: "Pool reuse open DB connections. No new connection per request. Skip handshake overhead."
- ultra: "Pool = reuse DB conn. Skip handshake → fast under load."
- wenyan-full: "池reuse open connection。不每req新開。skip handshake overhead。"
- wenyan-ultra: "池reuse conn。skip handshake → fast。"
## Auto-Clarity
Drop caveman when:
- Security warnings
- Irreversible action confirmations
- Multi-step sequences where fragment order or omitted conjunctions risk misread
- Compression itself creates technical ambiguity (e.g., `"migrate table drop column backup first"` — order unclear without articles/conjunctions)
- User asks to clarify or repeats question
Resume caveman after clear part done.
Example — destructive op:
> **Warning:** This will permanently delete all rows in the `users` table and cannot be undone.
> ```sql
> DROP TABLE users;
> ```
> Caveman resume. Verify backup exist first.
## Boundaries
Code/commits/PRs: write normal. "stop caveman" or "normal mode": revert. Level persist until changed or session end.
@@ -1,69 +0,0 @@
---
name: add-admin-endpoint
description: Add a new paginated admin API endpoint to AdminApiController. Use when the user wants to add a backend admin list, stats, or action endpoint — things like "add an endpoint for X", "create an admin API for Y", "I need a route that lists Z".
---
## Target file
`src/Admin/Controller/AdminApiController.php`
All admin endpoints live here. The class already has `#[IsGranted('ROLE_ADMIN')]` and injects `EntityManagerInterface $em`.
## Checklist
1. **Stats endpoint** (optional but standard): a separate `#[Route('/api/v1/admin/{entity}/stats')]` method that returns counts via raw SQL (`$this->em->getConnection()->fetchOne()`). Return with `$this->success([...])`.
2. **List endpoint**: use QueryBuilder with `->getArrayResult()` — never load full entities for list queries (entity getters may not exist for all fields). Pattern:
```php
#[Route('/api/v1/admin/{entities}', methods: ['GET'])]
public function list{Entity}(Request $request): JsonResponse
{
$page = max(1, (int) $request->query->get('page', 1));
$limit = min(100, max(1, (int) $request->query->get('limit', 15)));
$search = trim((string) $request->query->get('search', ''));
$qb = $this->em->createQueryBuilder()
->select('e.id, e.uuid, e.someField, e.createdAt')
->from(SomeEntity::class, 'e');
if ($search !== '') {
$qb->andWhere('e.name LIKE :s')->setParameter('s', "%$search%");
}
$total = (clone $qb)->select('COUNT(e.id)')->getQuery()->getSingleScalarResult();
$items = $qb
->orderBy('e.id', 'DESC')
->setFirstResult(($page - 1) * $limit)
->setMaxResults($limit)
->getQuery()
->getArrayResult();
return $this->paginated($items, (int) $total, $page, $limit);
}
```
3. **Action endpoints** (toggle status, etc.) follow this shape:
```php
#[Route('/api/v1/admin/{entities}/{uuid}/status', methods: ['POST'])]
public function toggle{Entity}Status(string $uuid): JsonResponse
{
$entity = $this->em->getRepository(SomeEntity::class)->findOneBy(['uuid' => $uuid]);
if (!$entity) return $this->error('NOT_FOUND', 'Entity not found', 404);
$entity->setIsActive(!$entity->getIsActive());
$this->em->flush();
return $this->success(['is_active' => $entity->getIsActive()]);
}
```
## Critical rules
- **Always use `getArrayResult()`** for list queries. Never call entity getters inside admin list methods.
- `createdAt` and `updatedAt` are Unix integer timestamps — do not format them in PHP, let the frontend handle it.
- For JOINs to categories (city, state, specialty), use LEFT JOIN in DQL and select the name field directly into the array result.
- Response shape for lists: `$this->paginated($items, $total, $page, $limit)` — frontend reads `data?.data` for items and `data?.meta?.totalRecords` for count.
- Add `use` imports for any new entity class at the top of the file.
- Run `/sync-db` only if a new entity or column was added as part of this change.
-220
View File
@@ -1,220 +0,0 @@
---
name: add-admin-page
description: Add a new admin React page to the frontend. Use when the user wants a new admin panel page — list views, detail pages, management UIs. Triggers on "add a page for X", "create an admin page", "I need a UI for managing Y", "build the frontend for Z".
---
## Files to create/modify
| Action | Path |
|--------|------|
| Create | `assets/admin/pages/{Name}Page.tsx` |
| Edit | `assets/admin/types/index.ts` — add the TypeScript interface |
| Edit | `assets/admin/App.tsx` — add the route |
## Step 1 — Add the TypeScript type
Add to `assets/admin/types/index.ts`:
```ts
export interface SomeName {
uuid: string;
// ... fields matching the backend array result
}
```
## Step 2 — Register the route
In `assets/admin/App.tsx`, add inside the `<AdminLayout>` routes block:
```tsx
import SomeNamePage from './pages/SomeNamePage';
// ...
<Route path="some-names" element={<SomeNamePage />} />
<Route path="some-names/:uuid" element={<SomeNameDetailPage />} /> {/* if detail page needed */}
```
## Step 3 — Create the page
Standard list page pattern:
```tsx
import React, { useState, useEffect } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
import { MagnifyingGlassIcon, PlusIcon, EyeIcon, TrashIcon, ArrowPathIcon, CheckCircleIcon, XCircleIcon } from '@heroicons/react/24/outline';
import { toast } from 'sonner';
import { api } from '../lib/api';
import type { ApiResponse, PaginatedResponse } from '../lib/api';
import { formatDate } from '../lib/utils';
import ConfirmDialog from '../components/ui/ConfirmDialog';
import Pagination from '../components/ui/Pagination';
import type { SomeName } from '../types';
export default function SomeNamePage() {
const navigate = useNavigate();
const qc = useQueryClient();
const [page, setPage] = useState(1);
const [limit] = useState(15);
const [searchInput, setSearchInput] = useState('');
const [search, setSearch] = useState('');
const [deleteTarget, setDeleteTarget] = useState<SomeName | null>(null);
// Debounced search — always 350 ms
useEffect(() => {
const t = setTimeout(() => { setSearch(searchInput); setPage(1); }, 350);
return () => clearTimeout(t);
}, [searchInput]);
// List query
const listQ = useQuery({
queryKey: ['admin-some-names', page, limit, search],
queryFn: () => {
const p = new URLSearchParams({ page: String(page), limit: String(limit) });
if (search) p.set('search', search);
return api.get<PaginatedResponse<SomeName>>(`/api/v1/admin/some-names?${p}`);
},
});
const items = listQ.data?.data ?? [];
const total = listQ.data?.meta?.totalRecords ?? 0;
// Stats query (if stats endpoint exists)
const statsQ = useQuery({
queryKey: ['admin-some-names-stats'],
queryFn: () => api.get<ApiResponse<{ total: number; active: number }>>('/api/v1/admin/some-names/stats'),
staleTime: 30_000,
});
// IMPORTANT: stats data may be double-nested depending on backend shape.
// Use this pattern to handle both cases:
const stats = (statsQ.data?.data as any)?.data ?? statsQ.data?.data;
// Delete mutation
const deleteMut = useMutation({
mutationFn: (uuid: string) => api.delete<ApiResponse<null>>(`/api/v1/some-names/${uuid}`),
onSuccess: () => {
toast.success('حذف شد');
setDeleteTarget(null);
qc.invalidateQueries({ queryKey: ['admin-some-names'] });
},
onError: (e: Error) => toast.error(e.message),
});
// Toggle status mutation
const toggleMut = useMutation({
mutationFn: (uuid: string) => api.post<ApiResponse<{ is_active: boolean }>>(`/api/v1/admin/some-names/${uuid}/status`, {}),
onSuccess: () => {
toast.success('وضعیت تغییر کرد');
qc.invalidateQueries({ queryKey: ['admin-some-names'] });
},
onError: (e: Error) => toast.error(e.message),
});
return (
<div className="fade-in">
{/* Header */}
<div className="card-title-row" style={{ marginBottom: 'var(--gap)' }}>
<div>
<h1 className="section-title">عنوان صفحه</h1>
<div className="muted" style={{ fontSize: 13, marginTop: 3 }}>توضیح کوتاه</div>
</div>
<button className="btn primary sm" onClick={() => navigate('/admin/some-names/new')}>
<PlusIcon style={{ width: 15, height: 15 }} /> افزودن
</button>
</div>
{/* KPI cards — only if stats endpoint exists */}
{/* <div className="stat-grid"> ... </div> */}
{/* Main card */}
<div className="card">
{/* Toolbar */}
<div className="card-pad" style={{ paddingBottom: 0 }}>
<div className="toolbar">
<div className="field" style={{ minWidth: 240 }}>
<MagnifyingGlassIcon style={{ width: 17, height: 17 }} />
<input value={searchInput} onChange={(e) => setSearchInput(e.target.value)} placeholder="جستجو..." />
</div>
<div className="spacer" />
<button className="btn ghost sm" onClick={() => listQ.refetch()} disabled={listQ.isFetching}>
<ArrowPathIcon style={{ width: 15, height: 15 }} />
</button>
</div>
</div>
{/* Table */}
<div className="table-wrap">
<table className="t">
<thead>
<tr>
<th>نام</th>
<th>وضعیت</th>
<th>تاریخ</th>
<th></th>
</tr>
</thead>
<tbody>
{listQ.isLoading && Array.from({ length: 5 }).map((_, i) => (
<tr key={i}>{Array.from({ length: 4 }).map((_, j) => (
<td key={j}><div className="skeleton" style={{ height: 14, borderRadius: 6, width: '60%' }} /></td>
))}</tr>
))}
{!listQ.isLoading && items.length === 0 && (
<tr><td colSpan={4}><div className="empty">موردی یافت نشد</div></td></tr>
)}
{!listQ.isLoading && items.map((item) => (
<tr key={item.uuid} style={{ cursor: 'pointer' }} onClick={() => navigate(`/admin/some-names/${item.uuid}`)}>
<td>{/* render fields */}</td>
<td>
<span className={`badge ${(item as any).is_active ? 'green' : 'gray'}`}>
<span className="bdot" />
{(item as any).is_active ? 'فعال' : 'غیرفعال'}
</span>
</td>
<td className="muted">{formatDate((item as any).created_at)}</td>
<td onClick={(e) => e.stopPropagation()}>
<div className="row-actions">
<button className="mini-btn" onClick={() => navigate(`/admin/some-names/${item.uuid}`)}>
<EyeIcon style={{ width: 16, height: 16 }} />
</button>
<button className="mini-btn" onClick={() => toggleMut.mutate(item.uuid)} disabled={toggleMut.isPending}>
{(item as any).is_active
? <XCircleIcon style={{ width: 16, height: 16 }} />
: <CheckCircleIcon style={{ width: 16, height: 16 }} />}
</button>
<button className="mini-btn danger" onClick={() => setDeleteTarget(item)}>
<TrashIcon style={{ width: 16, height: 16 }} />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
<Pagination page={page} total={total} limit={limit} onPageChange={setPage} />
</div>
<ConfirmDialog
open={!!deleteTarget}
title="حذف"
message={`آیا از حذف این مورد اطمینان دارید؟`}
confirmLabel="حذف"
danger
loading={deleteMut.isPending}
onConfirm={() => deleteTarget && deleteMut.mutate(deleteTarget.uuid)}
onCancel={() => setDeleteTarget(null)}
/>
</div>
);
}
```
## Critical rules
- **Never use `data?.data?.data`** unless the endpoint is a `$this->success(['data' => ...])` double-nest. Standard `$this->success($array)` → extract with `data?.data`. `$this->paginated()` → items at `data?.data`, total at `data?.meta?.totalRecords`.
- Stats from `$this->success($stats)` may still be double-nested in older endpoints — use `(statsQ.data?.data as any)?.data ?? statsQ.data?.data` to handle both.
- Category API (`/api/v1/categorys/{bundle}`) is always triple-nested: extract with `data?.data?.data ?? []`.
- Search debounce is always 350ms via `setTimeout` in a `useEffect`.
- Query keys follow the format `['admin-entity-name', page, limit, search, ...filters]`.
- After creating the page, run `ddev exec yarn dev` to check for TypeScript errors.
-15
View File
@@ -1,15 +0,0 @@
---
name: sync-db
description: Run the standard Doctrine migration cycle after any entity change. Use whenever an entity is added or modified, a new column/relation is needed, or the user says "migrate", "sync the database", "generate migration", or asks to apply schema changes.
disable-model-invocation: true
---
Run these three commands in sequence and report the output of each step:
```bash
ddev exec php bin/console doctrine:migrations:diff --no-interaction
ddev exec php bin/console doctrine:migrations:migrate --no-interaction
ddev exec php bin/console cache:clear
```
If `migrations:diff` reports "No changes detected", skip `migrations:migrate` and say so. If any step fails, stop and show the full error output.
+64 -104
View File
@@ -1313,6 +1313,25 @@ function MyPatientsPageInner() {
);
}
function patientAvatar(name: string, size = 28) {
return (
<div
style={{
width: size, height: size, borderRadius: "50%", flexShrink: 0,
background: "linear-gradient(145deg, #f8945a, #f0682a)",
color: "#fff", display: "flex", alignItems: "center",
justifyContent: "center", fontWeight: 700, fontSize: size * 0.42,
}}
>
{(name === "—" ? "؟" : name).charAt(0)}
</div>
);
}
function fileNumber(record: PatientRecord): string {
return `P-${record.uuid.slice(0, 8).toUpperCase()}`;
}
function PatientCard({
record,
onOpen,
@@ -1322,122 +1341,63 @@ function PatientCard({
}) {
const name = getPatientName(record);
const phone = getPatientPhone(record);
const nationalCode = getPatientNationalCode(record);
const infoRow = (label: string, value: React.ReactNode) => (
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", fontSize: 13 }}>
<span style={{ color: "var(--text-3)" }}>{label}:</span>
<span dir="ltr" style={{ color: "var(--text-2)" }}>{value}</span>
</div>
);
return (
<div
className="card"
style={{ padding: 16, display: "flex", flexDirection: "column", gap: 12, cursor: "pointer" }}
onClick={onOpen}
>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<span className="mini-btn" style={{ pointerEvents: "none" }}>···</span>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<b style={{ fontSize: 14 }}>{name}</b>
{patientAvatar(name)}
</div>
</div>
{infoRow("شماره پرونده", fileNumber(record))}
{infoRow("موبایل", phone)}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", fontSize: 13 }}>
<span style={{ color: "var(--text-3)" }}>برچسبها:</span>
<span style={{ color: "var(--text-3)" }}></span>
</div>
</div>
);
}
function PatientRow({
record,
onOpen,
}: {
record: PatientRecord;
onOpen: () => void;
}) {
const name = getPatientName(record);
return (
<div
className="card"
style={{
padding: 16,
display: "flex",
flexDirection: "column",
gap: 14,
padding: "12px 16px", display: "flex", alignItems: "center", gap: 14,
cursor: "pointer",
transition: "border-color .15s, box-shadow .15s",
}}
onClick={onOpen}
>
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
<div
className="avatar"
style={{
width: 44,
height: 44,
borderRadius: "50%",
background:
"linear-gradient(145deg, #f8945a, #f0682a)",
color: "#fff",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontWeight: 700,
fontSize: 17,
flexShrink: 0,
}}
>
{(name === "—" ? "؟" : name).charAt(0)}
</div>
<div style={{ minWidth: 0, flex: 1 }}>
<div
style={{
fontWeight: 600,
fontSize: 15,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{name}
</div>
<div
style={{
fontSize: 12,
color: "var(--text-3)",
marginTop: 2,
}}
>
{formatDate(record.created_at)}
</div>
</div>
</div>
<div
style={{
display: "flex",
flexDirection: "column",
gap: 7,
fontSize: 13,
color: "var(--text-2)",
}}
>
<div
style={{ display: "flex", alignItems: "center", gap: 7 }}
>
<PhoneIcon
style={{
width: 14,
color: "var(--text-3)",
flexShrink: 0,
}}
/>
<span dir="ltr">{phone}</span>
</div>
<div
style={{ display: "flex", alignItems: "center", gap: 7 }}
>
<IdentificationIcon
style={{
width: 14,
color: "var(--text-3)",
flexShrink: 0,
}}
/>
<span dir="ltr">
{nationalCode ?? (
<span style={{ color: "var(--text-3)" }}>
کد ملی ثبت نشده
</span>
)}
</span>
</div>
</div>
{patientAvatar(name, 38)}
<b style={{ fontSize: 14, minWidth: 140 }}>{name}</b>
<span style={{ fontSize: 13, color: "var(--text-3)" }} dir="ltr">{fileNumber(record)}</span>
<span style={{ fontSize: 13, color: "var(--text-2)", flex: 1 }} dir="ltr">{getPatientPhone(record)}</span>
<button
className="btn primary sm"
style={{
width: "100%",
justifyContent: "center",
display: "flex",
alignItems: "center",
gap: 5,
}}
onClick={(e) => {
e.stopPropagation();
onOpen();
}}
onClick={(e) => { e.stopPropagation(); onOpen(); }}
>
<FolderOpenIcon style={{ width: 14 }} />
مشاهده پرونده
<FolderOpenIcon style={{ width: 14 }} /> مشاهده
</button>
</div>
);
+11
View File
@@ -0,0 +1,11 @@
{
"version": 1,
"skills": {
"caveman": {
"source": "juliusbrussee/caveman",
"sourceType": "github",
"skillPath": "skills/caveman/SKILL.md",
"computedHash": "1902fa0b569912d0c05736d8d98a72097d9b82719aac88c0c1d03bb546f9176d"
}
}
}