WIP Pi
This commit is contained in:
1 parent
933a708f00
commit
7422133054
44 files changed
+3730
No files matched your search
@@ -0,0 +1,2 @@
|
||||
src/aktsource/
|
||||
src/aktsource_testrw/
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@aliou/pi-guardrails@0.13.0/schema.json",
|
||||
"enabled": true,
|
||||
"applyBuiltinDefaults": false,
|
||||
"features": {
|
||||
"pathAccess": true,
|
||||
"permissionGate": true,
|
||||
"policies": true
|
||||
},
|
||||
"pathAccess": {
|
||||
"mode": "allow"
|
||||
},
|
||||
"permissionGate": {
|
||||
"requireConfirmation": false
|
||||
},
|
||||
"policies": {
|
||||
"rules": []
|
||||
},
|
||||
"version": "0.9.0-20260327"
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/aliou/pi-guardrails/main/schema.json",
|
||||
"enabled": true,
|
||||
"applyBuiltinDefaults": false,
|
||||
"features": {
|
||||
"pathAccess": true,
|
||||
"permissionGate": true,
|
||||
"policies": true,
|
||||
"protectEnvFiles": true
|
||||
},
|
||||
"pathAccess": {
|
||||
"mode": "allow"
|
||||
},
|
||||
"permissionGate": {
|
||||
"requireConfirmation": false
|
||||
},
|
||||
"policies": {
|
||||
"rules": []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"packages": [
|
||||
"npm:@ghyper9023/pi-dev-workflow",
|
||||
"git:github.com/JuliusBrussee/caveman",
|
||||
"npm:context-mode"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
# cavecrew
|
||||
|
||||
Decision guide. When to delegate to caveman subagents instead of doing the work inline.
|
||||
|
||||
## What it does
|
||||
|
||||
Tells the main thread when to spawn a caveman-style subagent versus the vanilla equivalent. The win: subagent tool-results inject back into main context verbatim, and caveman output is roughly 1/3 the size of vanilla prose. Across 20 delegations in one session, that is the difference between context exhaustion and finishing the task.
|
||||
|
||||
Three subagents:
|
||||
|
||||
| Subagent | Job | Use when |
|
||||
|----------|-----|----------|
|
||||
| `cavecrew-investigator` | Locate code (read-only) | "Where is X defined / what calls Y / list uses of Z" |
|
||||
| `cavecrew-builder` | Surgical edit, 1-2 files | Scope is obvious, ≤2 files. Refuses 3+ file scope. |
|
||||
| `cavecrew-reviewer` | Diff/file review | One-line findings with severity emoji |
|
||||
|
||||
Use vanilla `Explore` or `Code Reviewer` when you want prose, architecture commentary, or rationale. Use main thread directly for one-line answers and 3+ file refactors.
|
||||
|
||||
This skill is a decision guide, not a slash command. It activates when the conversation mentions delegation.
|
||||
|
||||
## How to invoke
|
||||
|
||||
Triggers on phrases like "delegate to subagent", "use cavecrew", "spawn investigator", "save context", "compressed agent output".
|
||||
|
||||
## Example chaining
|
||||
|
||||
Locate → fix → verify (most common):
|
||||
|
||||
1. `cavecrew-investigator` returns site list (`path:line — symbol — note`)
|
||||
2. Main thread picks 1-2 sites, hands paths to `cavecrew-builder`
|
||||
3. `cavecrew-reviewer` audits the resulting diff
|
||||
|
||||
Parallel scout: spawn 2-3 `cavecrew-investigator` calls in one message with different angles (defs, callers, tests). Aggregate in main.
|
||||
|
||||
## See also
|
||||
|
||||
- [`SKILL.md`](./SKILL.md) — full decision matrix and output contracts
|
||||
- [`agents/cavecrew-investigator.md`](../../agents/cavecrew-investigator.md)
|
||||
- [`agents/cavecrew-builder.md`](../../agents/cavecrew-builder.md)
|
||||
- [`agents/cavecrew-reviewer.md`](../../agents/cavecrew-reviewer.md)
|
||||
- [Caveman README](../../README.md) — repo overview
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
name: cavecrew
|
||||
description: >
|
||||
Decision guide for delegating to caveman-style subagents. Tells the main
|
||||
thread WHEN to spawn `cavecrew-investigator` (locate code), `cavecrew-builder`
|
||||
(1-2 file edit), or `cavecrew-reviewer` (diff review) instead of doing the
|
||||
work inline or using vanilla `Explore`. Subagent output is caveman-compressed
|
||||
so the tool-result injected back into main context is ~60% smaller — main
|
||||
context lasts longer across long sessions.
|
||||
Trigger: "delegate to subagent", "use cavecrew", "spawn investigator/builder/reviewer",
|
||||
"save context", "compressed agent output".
|
||||
---
|
||||
|
||||
Cavecrew = three subagent presets that emit caveman output. Same job as Anthropic defaults (`Explore`, edit-style agents, reviewer); difference is the tool-result they return is compressed, so main context shrinks per delegation.
|
||||
|
||||
## When to use cavecrew vs alternatives
|
||||
|
||||
| Task | Use |
|
||||
|---|---|
|
||||
| "Where is X defined / what calls Y / list uses of Z" | `cavecrew-investigator` |
|
||||
| Same but you also want suggestions/architecture commentary | `Explore` (vanilla) |
|
||||
| Surgical edit, ≤2 files, scope obvious | `cavecrew-builder` |
|
||||
| New feature / 3+ files / cross-cutting refactor | Main thread or `feature-dev:code-architect` |
|
||||
| Review diff, branch, or file for bugs | `cavecrew-reviewer` |
|
||||
| Deep code review with rationale + alternatives | `Code Reviewer` (vanilla) |
|
||||
| One-line answer you already know | Main thread, no subagent |
|
||||
|
||||
Rule of thumb: **if you'd want the subagent's output in 1/3 the tokens, pick cavecrew. If you'd want prose, pick vanilla.**
|
||||
|
||||
## Why this exists (the real win)
|
||||
|
||||
Subagent tool results get injected into main context verbatim. A vanilla `Explore` that returns 2k tokens of prose costs 2k tokens of main-context budget every time. The same finding from `cavecrew-investigator` returns ~700 tokens. Across 20 delegations in one session that's the difference between context exhaustion and finishing the task.
|
||||
|
||||
## Output contracts
|
||||
|
||||
What main thread can rely on per agent:
|
||||
|
||||
**`cavecrew-investigator`**
|
||||
```
|
||||
<Header>:
|
||||
- path:line — `symbol` — short note
|
||||
totals: <counts>.
|
||||
```
|
||||
Or `No match.` Always file-path-first, line-number-attached, backticked symbols. Safe to grep with `path:\d+`.
|
||||
|
||||
**`cavecrew-builder`**
|
||||
```
|
||||
<path:line-range> — <change ≤10 words>.
|
||||
verified: <re-read OK | mismatch @ path:line>.
|
||||
```
|
||||
Or one of: `too-big.` / `needs-confirm.` / `ambiguous.` / `regressed.` (terminal first token).
|
||||
|
||||
**`cavecrew-reviewer`**
|
||||
```
|
||||
path:line: <emoji> <severity>: <problem>. <fix>.
|
||||
totals: N🔴 N🟡 N🔵 N❓
|
||||
```
|
||||
Or `No issues.` Findings sorted file → line ascending.
|
||||
|
||||
## Chaining patterns
|
||||
|
||||
**Locate → fix → verify** (most common):
|
||||
1. `cavecrew-investigator` returns site list.
|
||||
2. Main thread picks 1-2 sites, hands paths to `cavecrew-builder`.
|
||||
3. `cavecrew-reviewer` audits the diff.
|
||||
|
||||
**Parallel scout** (when investigation is broad):
|
||||
Spawn 2-3 `cavecrew-investigator` calls in one message (different angles: defs vs callers vs tests). Aggregate in main thread.
|
||||
|
||||
**Single-shot edit** (when site is already known):
|
||||
Skip investigator. Hand exact path:line to `cavecrew-builder` directly.
|
||||
|
||||
## What NOT to do
|
||||
|
||||
- Don't use `cavecrew-builder` when you don't already know the file. Spawn investigator first or main thread will eat tokens passing context.
|
||||
- Don't chain `cavecrew-investigator → cavecrew-builder` for a 5-file refactor. Builder will return `too-big.` and you'll have wasted a turn.
|
||||
- Don't ask `cavecrew-reviewer` for "general feedback" — it returns findings only, no architecture opinions. Use `Code Reviewer` for that.
|
||||
- Don't expect prose. Cavecrew output is structured, sometimes terse to the point of cryptic. If a human will read it directly, paraphrase.
|
||||
|
||||
## Auto-clarity (inherited)
|
||||
|
||||
Subagents drop caveman → normal English for security warnings, irreversible-action confirmations, and any output where fragment ambiguity could be misread. Resume caveman after.
|
||||
@@ -0,0 +1,44 @@
|
||||
# caveman-commit
|
||||
|
||||
Terse Conventional Commits. Why over what.
|
||||
|
||||
## What it does
|
||||
|
||||
Generates commit messages in Conventional Commits format. Subject ≤50 chars, hard cap 72. Imperative mood. Body only when the *why* is non-obvious or there are breaking changes. No AI attribution, no "this commit does X", no emoji unless the project uses them. Body always required for breaking changes, security fixes, data migrations, and reverts — future debuggers need the context.
|
||||
|
||||
Outputs only the message. Does not stage, commit, or amend.
|
||||
|
||||
## How to invoke
|
||||
|
||||
```
|
||||
/caveman-commit
|
||||
```
|
||||
|
||||
Also triggers on phrases like "write a commit", "commit message", "generate commit".
|
||||
|
||||
## Example output
|
||||
|
||||
Diff: new endpoint for user profile.
|
||||
|
||||
```
|
||||
feat(api): add GET /users/:id/profile
|
||||
|
||||
Mobile client needs profile data without the full user payload
|
||||
to reduce LTE bandwidth on cold-launch screens.
|
||||
|
||||
Closes #128
|
||||
```
|
||||
|
||||
Diff: breaking API rename.
|
||||
|
||||
```
|
||||
feat(api)!: rename /v1/orders to /v1/checkout
|
||||
|
||||
BREAKING CHANGE: clients on /v1/orders must migrate to /v1/checkout
|
||||
before 2026-06-01. Old route returns 410 after that date.
|
||||
```
|
||||
|
||||
## See also
|
||||
|
||||
- [`SKILL.md`](./SKILL.md) — full LLM-facing instructions
|
||||
- [Caveman README](../../README.md) — repo overview
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
name: caveman-commit
|
||||
description: >
|
||||
Ultra-compressed commit message generator. Cuts noise from commit messages while preserving
|
||||
intent and reasoning. Conventional Commits format. Subject ≤50 chars, body only when "why"
|
||||
isn't obvious. Use when user says "write a commit", "commit message", "generate commit",
|
||||
"/commit", or invokes /caveman-commit. Auto-triggers when staging changes.
|
||||
---
|
||||
|
||||
Write commit messages terse and exact. Conventional Commits format. No fluff. Why over what.
|
||||
|
||||
## Rules
|
||||
|
||||
**Subject line:**
|
||||
- `<type>(<scope>): <imperative summary>` — `<scope>` optional
|
||||
- Types: `feat`, `fix`, `refactor`, `perf`, `docs`, `test`, `chore`, `build`, `ci`, `style`, `revert`
|
||||
- Imperative mood: "add", "fix", "remove" — not "added", "adds", "adding"
|
||||
- ≤50 chars when possible, hard cap 72
|
||||
- No trailing period
|
||||
- Match project convention for capitalization after the colon
|
||||
|
||||
**Body (only if needed):**
|
||||
- Skip entirely when subject is self-explanatory
|
||||
- Add body only for: non-obvious *why*, breaking changes, migration notes, linked issues
|
||||
- Wrap at 72 chars
|
||||
- Bullets `-` not `*`
|
||||
- Reference issues/PRs at end: `Closes #42`, `Refs #17`
|
||||
|
||||
**What NEVER goes in:**
|
||||
- "This commit does X", "I", "we", "now", "currently" — the diff says what
|
||||
- "As requested by..." — use Co-authored-by trailer
|
||||
- "Generated with Claude Code" or any AI attribution
|
||||
- Emoji (unless project convention requires)
|
||||
- Restating the file name when scope already says it
|
||||
|
||||
## Examples
|
||||
|
||||
Diff: new endpoint for user profile with body explaining the why
|
||||
- ❌ "feat: add a new endpoint to get user profile information from the database"
|
||||
- ✅
|
||||
```
|
||||
feat(api): add GET /users/:id/profile
|
||||
|
||||
Mobile client needs profile data without the full user payload
|
||||
to reduce LTE bandwidth on cold-launch screens.
|
||||
|
||||
Closes #128
|
||||
```
|
||||
|
||||
Diff: breaking API change
|
||||
- ✅
|
||||
```
|
||||
feat(api)!: rename /v1/orders to /v1/checkout
|
||||
|
||||
BREAKING CHANGE: clients on /v1/orders must migrate to /v1/checkout
|
||||
before 2026-06-01. Old route returns 410 after that date.
|
||||
```
|
||||
|
||||
## Auto-Clarity
|
||||
|
||||
Always include body for: breaking changes, security fixes, data migrations, anything reverting a prior commit. Never compress these into subject-only — future debuggers need the context.
|
||||
|
||||
## Boundaries
|
||||
|
||||
Only generates the commit message. Does not run `git commit`, does not stage files, does not amend. Output the message as a code block ready to paste. "stop caveman-commit" or "normal mode": revert to verbose commit style.
|
||||
@@ -0,0 +1,163 @@
|
||||
<p align="center">
|
||||
<img src="https://em-content.zobj.net/source/apple/391/rock_1faa8.png" width="80" />
|
||||
</p>
|
||||
|
||||
<h1 align="center">caveman-compress</h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>shrink memory file. save token every session.</strong>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
A Claude Code skill that compresses your project memory files (`CLAUDE.md`, todos, preferences) into caveman format — so every session loads fewer tokens automatically.
|
||||
|
||||
Claude read `CLAUDE.md` on every session start. If file big, cost big. Caveman make file small. Cost go down forever.
|
||||
|
||||
## What It Do
|
||||
|
||||
```
|
||||
/caveman-compress CLAUDE.md
|
||||
```
|
||||
|
||||
```
|
||||
CLAUDE.md ← compressed (Claude reads this — fewer tokens every session)
|
||||
CLAUDE.original.md ← human-readable backup (you edit this)
|
||||
```
|
||||
|
||||
Original never lost. You can read and edit `.original.md`. Run skill again to re-compress after edits.
|
||||
|
||||
## Benchmarks
|
||||
|
||||
Real results on real project files:
|
||||
|
||||
| File | Original | Compressed | Saved |
|
||||
|------|----------:|----------:|------:|
|
||||
| `claude-md-preferences.md` | 706 | 285 | **59.6%** |
|
||||
| `project-notes.md` | 1145 | 535 | **53.3%** |
|
||||
| `claude-md-project.md` | 1122 | 636 | **43.3%** |
|
||||
| `todo-list.md` | 627 | 388 | **38.1%** |
|
||||
| `mixed-with-code.md` | 888 | 560 | **36.9%** |
|
||||
| **Average** | **898** | **481** | **46%** |
|
||||
|
||||
All validations passed ✅ — headings, code blocks, URLs, file paths preserved exactly.
|
||||
|
||||
## Before / After
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
|
||||
### 📄 Original (706 tokens)
|
||||
|
||||
> "I strongly prefer TypeScript with strict mode enabled for all new code. Please don't use `any` type unless there's genuinely no way around it, and if you do, leave a comment explaining the reasoning. I find that taking the time to properly type things catches a lot of bugs before they ever make it to runtime."
|
||||
|
||||
</td>
|
||||
<td width="50%">
|
||||
|
||||
### <img src="../../docs/assets/dancing-rock.svg" width="20" height="20" alt="rock"/> Caveman (285 tokens)
|
||||
|
||||
> "Prefer TypeScript strict mode always. No `any` unless unavoidable — comment why if used. Proper types catch bugs early."
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
**Same instructions. 60% fewer tokens. Every. Single. Session.**
|
||||
|
||||
## Security
|
||||
|
||||
`caveman-compress` is flagged as Snyk High Risk due to subprocess and file I/O patterns detected by static analysis. This is a false positive — see [SECURITY.md](./SECURITY.md) for a full explanation of what the skill does and does not do.
|
||||
|
||||
## Install
|
||||
|
||||
Compress is built in with the `caveman` plugin. Install `caveman` once, then use `/caveman-compress`.
|
||||
|
||||
If you need local files, the compress skill lives at:
|
||||
|
||||
```bash
|
||||
caveman-compress/
|
||||
```
|
||||
|
||||
**Requires:** Python 3.10+
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/caveman-compress <filepath>
|
||||
```
|
||||
|
||||
Examples:
|
||||
```
|
||||
/caveman-compress CLAUDE.md
|
||||
/caveman-compress docs/preferences.md
|
||||
/caveman-compress todos.md
|
||||
```
|
||||
|
||||
### What files work
|
||||
|
||||
| Type | Compress? |
|
||||
|------|-----------|
|
||||
| `.md`, `.txt`, `.rst`, `.typ`, `.typst`, `.tex` | ✅ Yes |
|
||||
| Extensionless natural language | ✅ Yes |
|
||||
| `.py`, `.js`, `.ts`, `.json`, `.yaml` | ❌ Skip (code/config) |
|
||||
| `*.original.md` | ❌ Skip (backup files) |
|
||||
|
||||
## How It Work
|
||||
|
||||
```
|
||||
/caveman-compress CLAUDE.md
|
||||
↓
|
||||
detect file type (no tokens)
|
||||
↓
|
||||
Claude compresses (tokens — one call)
|
||||
↓
|
||||
validate output (no tokens)
|
||||
checks: headings, code blocks, URLs, file paths, bullets
|
||||
↓
|
||||
if errors: Claude fixes cherry-picked issues only (tokens — targeted fix)
|
||||
does NOT recompress — only patches broken parts
|
||||
↓
|
||||
retry up to 2 times
|
||||
↓
|
||||
write compressed → CLAUDE.md
|
||||
write original → CLAUDE.original.md
|
||||
```
|
||||
|
||||
Only two things use tokens: initial compression + targeted fix if validation fails. Everything else is local Python.
|
||||
|
||||
## What Is Preserved
|
||||
|
||||
Caveman compress natural language. It never touch:
|
||||
|
||||
- Code blocks (` ``` ` fenced or indented)
|
||||
- Inline code (`` `backtick content` ``)
|
||||
- URLs and links
|
||||
- File paths (`/src/components/...`)
|
||||
- Commands (`npm install`, `git commit`)
|
||||
- Technical terms, library names, API names
|
||||
- Headings (exact text preserved)
|
||||
- Tables (structure preserved, cell text compressed)
|
||||
- Dates, version numbers, numeric values
|
||||
|
||||
## Why This Matter
|
||||
|
||||
`CLAUDE.md` loads on **every session start**. A 1000-token project memory file costs tokens every single time you open a project. Over 100 sessions that's 100,000 tokens of overhead — just for context you already wrote.
|
||||
|
||||
Caveman cut that by ~46% on average. Same instructions. Same accuracy. Less waste.
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────┐
|
||||
│ TOKEN SAVINGS PER FILE █████ 46% │
|
||||
│ SESSIONS THAT BENEFIT ██████████ 100% │
|
||||
│ INFORMATION PRESERVED ██████████ 100% │
|
||||
│ SETUP TIME █ 1x │
|
||||
└────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Part of Caveman
|
||||
|
||||
This skill is part of the [caveman](https://github.com/JuliusBrussee/caveman) toolkit — making Claude use fewer tokens without losing accuracy.
|
||||
|
||||
- **caveman** — make Claude *speak* like caveman (cuts response tokens ~65%)
|
||||
- **caveman-compress** — make Claude *read* less (cuts context tokens ~46%)
|
||||
@@ -0,0 +1,31 @@
|
||||
# Security
|
||||
|
||||
## Snyk High Risk Rating
|
||||
|
||||
`caveman-compress` receives a Snyk High Risk rating due to static analysis heuristics. This document explains what the skill does and does not do.
|
||||
|
||||
### What triggers the rating
|
||||
|
||||
1. **subprocess usage**: The skill calls the `claude` CLI via `subprocess.run()` as a fallback when `ANTHROPIC_API_KEY` is not set. The subprocess call uses a fixed argument list — no shell interpolation occurs. User file content is passed via stdin, not as a shell argument.
|
||||
|
||||
2. **File read/write**: The skill reads the file the user explicitly points it at, compresses it, and writes the result back to the same path. A `.original.md` backup is saved alongside it. No files outside the user-specified path are read or written.
|
||||
|
||||
### What the skill does NOT do
|
||||
|
||||
- Does not execute user file content as code
|
||||
- Does not make network requests except to Anthropic's API (via SDK or CLI)
|
||||
- Does not access files outside the path the user provides
|
||||
- Does not use shell=True or string interpolation in subprocess calls
|
||||
- Does not collect or transmit any data beyond the file being compressed
|
||||
|
||||
### Auth behavior
|
||||
|
||||
If `ANTHROPIC_API_KEY` is set, the skill uses the Anthropic Python SDK directly (no subprocess). If not set, it falls back to the `claude` CLI, which uses the user's existing Claude desktop authentication.
|
||||
|
||||
### File size limit
|
||||
|
||||
Files larger than 500KB are rejected before any API call is made.
|
||||
|
||||
### Reporting a vulnerability
|
||||
|
||||
If you believe you've found a genuine security issue, please open a GitHub issue with the label `security`.
|
||||
@@ -0,0 +1,111 @@
|
||||
---
|
||||
name: caveman-compress
|
||||
description: >
|
||||
Compress natural language memory files (CLAUDE.md, todos, preferences) into caveman format
|
||||
to save input tokens. Preserves all technical substance, code, URLs, and structure.
|
||||
Compressed version overwrites the original file. Human-readable backup saved as FILE.original.md.
|
||||
Trigger: /caveman-compress FILEPATH or "compress memory file"
|
||||
---
|
||||
|
||||
# Caveman Compress
|
||||
|
||||
## Purpose
|
||||
|
||||
Compress natural language files (CLAUDE.md, todos, preferences) into caveman-speak to reduce input tokens. Compressed version overwrites original. Human-readable backup saved as `<filename>.original.md`.
|
||||
|
||||
## Trigger
|
||||
|
||||
`/caveman-compress <filepath>` or when user asks to compress a memory file.
|
||||
|
||||
## Process
|
||||
|
||||
1. The compression scripts live in `scripts/` (adjacent to this SKILL.md). If the path is not immediately available, search for `scripts/__main__.py` next to this SKILL.md.
|
||||
|
||||
2. From the directory containing this SKILL.md, run:
|
||||
|
||||
python3 -m scripts <absolute_filepath>
|
||||
|
||||
3. The CLI will:
|
||||
- detect file type (no tokens)
|
||||
- call Claude to compress
|
||||
- validate output (no tokens)
|
||||
- if errors: cherry-pick fix with Claude (targeted fixes only, no recompression)
|
||||
- retry up to 2 times
|
||||
- if still failing after 2 retries: report error to user, leave original file untouched
|
||||
|
||||
4. Return result to user
|
||||
|
||||
## Compression Rules
|
||||
|
||||
### Remove
|
||||
- Articles: a, an, the
|
||||
- Filler: just, really, basically, actually, simply, essentially, generally
|
||||
- Pleasantries: "sure", "certainly", "of course", "happy to", "I'd recommend"
|
||||
- Hedging: "it might be worth", "you could consider", "it would be good to"
|
||||
- Redundant phrasing: "in order to" → "to", "make sure to" → "ensure", "the reason is because" → "because"
|
||||
- Connective fluff: "however", "furthermore", "additionally", "in addition"
|
||||
|
||||
### Preserve EXACTLY (never modify)
|
||||
- Code blocks (fenced ``` and indented)
|
||||
- Inline code (`backtick content`)
|
||||
- URLs and links (full URLs, markdown links)
|
||||
- File paths (`/src/components/...`, `./config.yaml`)
|
||||
- Commands (`npm install`, `git commit`, `docker build`)
|
||||
- Technical terms (library names, API names, protocols, algorithms)
|
||||
- Proper nouns (project names, people, companies)
|
||||
- Dates, version numbers, numeric values
|
||||
- Environment variables (`$HOME`, `NODE_ENV`)
|
||||
|
||||
### Preserve Structure
|
||||
- All markdown headings (keep exact heading text, compress body below)
|
||||
- Bullet point hierarchy (keep nesting level)
|
||||
- Numbered lists (keep numbering)
|
||||
- Tables (compress cell text, keep structure)
|
||||
- Frontmatter/YAML headers in markdown files
|
||||
|
||||
### Compress
|
||||
- Use short synonyms: "big" not "extensive", "fix" not "implement a solution for", "use" not "utilize"
|
||||
- Fragments OK: "Run tests before commit" not "You should always run tests before committing"
|
||||
- Drop "you should", "make sure to", "remember to" — just state the action
|
||||
- Merge redundant bullets that say the same thing differently
|
||||
- Keep one example where multiple examples show the same pattern
|
||||
|
||||
CRITICAL RULE:
|
||||
Anything inside ``` ... ``` must be copied EXACTLY.
|
||||
Do not:
|
||||
- remove comments
|
||||
- remove spacing
|
||||
- reorder lines
|
||||
- shorten commands
|
||||
- simplify anything
|
||||
|
||||
Inline code (`...`) must be preserved EXACTLY.
|
||||
Do not modify anything inside backticks.
|
||||
|
||||
If file contains code blocks:
|
||||
- Treat code blocks as read-only regions
|
||||
- Only compress text outside them
|
||||
- Do not merge sections around code
|
||||
|
||||
## Pattern
|
||||
|
||||
Original:
|
||||
> You should always make sure to run the test suite before pushing any changes to the main branch. This is important because it helps catch bugs early and prevents broken builds from being deployed to production.
|
||||
|
||||
Compressed:
|
||||
> Run tests before push to main. Catch bugs early, prevent broken prod deploys.
|
||||
|
||||
Original:
|
||||
> The application uses a microservices architecture with the following components. The API gateway handles all incoming requests and routes them to the appropriate service. The authentication service is responsible for managing user sessions and JWT tokens.
|
||||
|
||||
Compressed:
|
||||
> Microservices architecture. API gateway route all requests to services. Auth service manage user sessions + JWT tokens.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- ONLY compress natural language files (.md, .txt, .typ, .typst, .tex, extensionless)
|
||||
- NEVER modify: .py, .js, .ts, .json, .yaml, .yml, .toml, .env, .lock, .css, .html, .xml, .sql, .sh
|
||||
- If file has mixed content (prose + code), compress ONLY the prose sections
|
||||
- If unsure whether something is code or prose, leave it unchanged
|
||||
- Original file is backed up as FILE.original.md before overwriting
|
||||
- Never compress FILE.original.md (skip it)
|
||||
@@ -0,0 +1,9 @@
|
||||
"""Caveman compress scripts.
|
||||
|
||||
This package provides tools to compress natural language markdown files
|
||||
into caveman format to save input tokens.
|
||||
"""
|
||||
|
||||
__all__ = ["cli", "compress", "detect", "validate"]
|
||||
|
||||
__version__ = "1.0.0"
|
||||
@@ -0,0 +1,3 @@
|
||||
from .cli import main
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python3
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
# Support both direct execution and module import
|
||||
try:
|
||||
from .validate import validate
|
||||
except ImportError:
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
from validate import validate
|
||||
|
||||
try:
|
||||
import tiktoken
|
||||
_enc = tiktoken.get_encoding("o200k_base")
|
||||
except ImportError:
|
||||
_enc = None
|
||||
|
||||
|
||||
def count_tokens(text):
|
||||
if _enc is None:
|
||||
return len(text.split()) # fallback: word count
|
||||
return len(_enc.encode(text))
|
||||
|
||||
|
||||
def benchmark_pair(orig_path: Path, comp_path: Path):
|
||||
orig_text = orig_path.read_text()
|
||||
comp_text = comp_path.read_text()
|
||||
|
||||
orig_tokens = count_tokens(orig_text)
|
||||
comp_tokens = count_tokens(comp_text)
|
||||
saved = 100 * (orig_tokens - comp_tokens) / orig_tokens if orig_tokens > 0 else 0.0
|
||||
result = validate(orig_path, comp_path)
|
||||
|
||||
return (comp_path.name, orig_tokens, comp_tokens, saved, result.is_valid)
|
||||
|
||||
|
||||
def print_table(rows):
|
||||
print("\n| File | Original | Compressed | Saved % | Valid |")
|
||||
print("|------|----------|------------|---------|-------|")
|
||||
for r in rows:
|
||||
print(f"| {r[0]} | {r[1]} | {r[2]} | {r[3]:.1f}% | {'✅' if r[4] else '❌'} |")
|
||||
|
||||
|
||||
def main():
|
||||
# Direct file pair: python3 benchmark.py original.md compressed.md
|
||||
if len(sys.argv) == 3:
|
||||
orig = Path(sys.argv[1]).resolve()
|
||||
comp = Path(sys.argv[2]).resolve()
|
||||
if not orig.exists():
|
||||
print(f"❌ Not found: {orig}")
|
||||
sys.exit(1)
|
||||
if not comp.exists():
|
||||
print(f"❌ Not found: {comp}")
|
||||
sys.exit(1)
|
||||
print_table([benchmark_pair(orig, comp)])
|
||||
return
|
||||
|
||||
# Glob mode: repo_root/tests/caveman-compress/
|
||||
# __file__ lives at <repo_root>/skills/caveman-compress/scripts/benchmark.py
|
||||
# Walk up four dirs: scripts → caveman-compress → skills → repo_root.
|
||||
tests_dir = Path(__file__).resolve().parents[3] / "tests" / "caveman-compress"
|
||||
if not tests_dir.exists():
|
||||
print(f"❌ Tests dir not found: {tests_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
rows = []
|
||||
for orig in sorted(tests_dir.glob("*.original.md")):
|
||||
comp = orig.with_name(orig.stem.removesuffix(".original") + ".md")
|
||||
if comp.exists():
|
||||
rows.append(benchmark_pair(orig, comp))
|
||||
|
||||
if not rows:
|
||||
print("No compressed file pairs found.")
|
||||
return
|
||||
|
||||
print_table(rows)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Caveman Compress CLI
|
||||
|
||||
Usage:
|
||||
caveman <filepath>
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
# Force UTF-8 on stdout/stderr before any code can print. Windows consoles
|
||||
# default to cp1252 and crash on the ❌ glyphs in error/validation branches,
|
||||
# masking the real error and leaving the user with a half-compressed file.
|
||||
for _stream in (sys.stdout, sys.stderr):
|
||||
reconfigure = getattr(_stream, "reconfigure", None)
|
||||
if callable(reconfigure):
|
||||
try:
|
||||
reconfigure(encoding="utf-8", errors="replace")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from .compress import compress_file
|
||||
from .detect import detect_file_type, should_compress
|
||||
|
||||
|
||||
def print_usage():
|
||||
print("Usage: caveman <filepath>")
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
print_usage()
|
||||
sys.exit(1)
|
||||
|
||||
filepath = Path(sys.argv[1])
|
||||
|
||||
# Check file exists
|
||||
if not filepath.exists():
|
||||
print(f"❌ File not found: {filepath}")
|
||||
sys.exit(1)
|
||||
|
||||
if not filepath.is_file():
|
||||
print(f"❌ Not a file: {filepath}")
|
||||
sys.exit(1)
|
||||
|
||||
filepath = filepath.resolve()
|
||||
|
||||
# Detect file type
|
||||
file_type = detect_file_type(filepath)
|
||||
|
||||
print(f"Detected: {file_type}")
|
||||
|
||||
# Check if compressible
|
||||
if not should_compress(filepath):
|
||||
print("Skipping: file is not natural language (code/config)")
|
||||
sys.exit(0)
|
||||
|
||||
print("Starting caveman compression...\n")
|
||||
|
||||
try:
|
||||
success = compress_file(filepath)
|
||||
|
||||
if success:
|
||||
print("\nCompression completed successfully")
|
||||
backup_path = filepath.with_name(filepath.stem + ".original.md")
|
||||
print(f"Compressed: {filepath}")
|
||||
print(f"Original: {backup_path}")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("\n❌ Compression failed after retries")
|
||||
sys.exit(2)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\nInterrupted by user")
|
||||
sys.exit(130)
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ Error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,254 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Caveman Memory Compression Orchestrator
|
||||
|
||||
Usage:
|
||||
python scripts/compress.py <filepath>
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
OUTER_FENCE_REGEX = re.compile(
|
||||
r"\A\s*(`{3,}|~{3,})[^\n]*\n(.*)\n\1\s*\Z", re.DOTALL
|
||||
)
|
||||
|
||||
# Filenames and paths that almost certainly hold secrets or PII. Compressing
|
||||
# them ships raw bytes to the Anthropic API — a third-party data boundary that
|
||||
# developers on sensitive codebases cannot cross. detect.py already skips .env
|
||||
# by extension, but credentials.md / secrets.txt / ~/.aws/credentials would
|
||||
# slip through the natural-language filter. This is a hard refuse before read.
|
||||
SENSITIVE_BASENAME_REGEX = re.compile(
|
||||
r"(?ix)^("
|
||||
r"\.env(\..+)?"
|
||||
r"|\.netrc"
|
||||
r"|credentials(\..+)?"
|
||||
r"|secrets?(\..+)?"
|
||||
r"|passwords?(\..+)?"
|
||||
r"|id_(rsa|dsa|ecdsa|ed25519)(\.pub)?"
|
||||
r"|authorized_keys"
|
||||
r"|known_hosts"
|
||||
r"|.*\.(pem|key|p12|pfx|crt|cer|jks|keystore|asc|gpg)"
|
||||
r")$"
|
||||
)
|
||||
|
||||
SENSITIVE_PATH_COMPONENTS = frozenset({".ssh", ".aws", ".gnupg", ".kube", ".docker"})
|
||||
|
||||
SENSITIVE_NAME_TOKENS = (
|
||||
"secret", "credential", "password", "passwd",
|
||||
"apikey", "accesskey", "token", "privatekey",
|
||||
)
|
||||
|
||||
|
||||
def is_sensitive_path(filepath: Path) -> bool:
|
||||
"""Heuristic denylist for files that must never be shipped to a third-party API."""
|
||||
name = filepath.name
|
||||
if SENSITIVE_BASENAME_REGEX.match(name):
|
||||
return True
|
||||
lowered_parts = {p.lower() for p in filepath.parts}
|
||||
if lowered_parts & SENSITIVE_PATH_COMPONENTS:
|
||||
return True
|
||||
# Normalize separators so "api-key" and "api_key" both match "apikey".
|
||||
lower = re.sub(r"[_\-\s.]", "", name.lower())
|
||||
return any(tok in lower for tok in SENSITIVE_NAME_TOKENS)
|
||||
|
||||
|
||||
def strip_llm_wrapper(text: str) -> str:
|
||||
"""Strip outer ```markdown ... ``` fence when it wraps the entire output."""
|
||||
m = OUTER_FENCE_REGEX.match(text)
|
||||
if m:
|
||||
return m.group(2)
|
||||
return text
|
||||
|
||||
from .detect import should_compress
|
||||
from .validate import validate
|
||||
|
||||
MAX_RETRIES = 2
|
||||
|
||||
|
||||
# ---------- Claude Calls ----------
|
||||
|
||||
|
||||
def call_claude(prompt: str) -> str:
|
||||
api_key = os.environ.get("ANTHROPIC_API_KEY")
|
||||
if api_key:
|
||||
try:
|
||||
import anthropic
|
||||
|
||||
client = anthropic.Anthropic(api_key=api_key)
|
||||
msg = client.messages.create(
|
||||
model=os.environ.get("CAVEMAN_MODEL", "claude-sonnet-4-5"),
|
||||
max_tokens=8192,
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
)
|
||||
return strip_llm_wrapper(msg.content[0].text.strip())
|
||||
except ImportError:
|
||||
pass # anthropic not installed, fall back to CLI
|
||||
# Fallback: use claude CLI (handles desktop auth)
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["claude", "--print"],
|
||||
input=prompt,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=True,
|
||||
)
|
||||
return strip_llm_wrapper(result.stdout.strip())
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise RuntimeError(f"Claude call failed:\n{e.stderr}")
|
||||
|
||||
|
||||
def build_compress_prompt(original: str) -> str:
|
||||
return f"""
|
||||
Compress this markdown into caveman format.
|
||||
|
||||
STRICT RULES:
|
||||
- Do NOT modify anything inside ``` code blocks
|
||||
- Do NOT modify anything inside inline backticks
|
||||
- Preserve ALL URLs exactly
|
||||
- Preserve ALL headings exactly
|
||||
- Preserve file paths and commands
|
||||
- Return ONLY the compressed markdown body — do NOT wrap the entire output in a ```markdown fence or any other fence. Inner code blocks from the original stay as-is; do not add a new outer fence around the whole file.
|
||||
|
||||
Only compress natural language.
|
||||
|
||||
TEXT:
|
||||
{original}
|
||||
"""
|
||||
|
||||
|
||||
def build_fix_prompt(original: str, compressed: str, errors: List[str]) -> str:
|
||||
errors_str = "\n".join(f"- {e}" for e in errors)
|
||||
return f"""You are fixing a caveman-compressed markdown file. Specific validation errors were found.
|
||||
|
||||
CRITICAL RULES:
|
||||
- DO NOT recompress or rephrase the file
|
||||
- ONLY fix the listed errors — leave everything else exactly as-is
|
||||
- The ORIGINAL is provided as reference only (to restore missing content)
|
||||
- Preserve caveman style in all untouched sections
|
||||
|
||||
ERRORS TO FIX:
|
||||
{errors_str}
|
||||
|
||||
HOW TO FIX:
|
||||
- Missing URL: find it in ORIGINAL, restore it exactly where it belongs in COMPRESSED
|
||||
- Code block mismatch: find the exact code block in ORIGINAL, restore it in COMPRESSED
|
||||
- Heading mismatch: restore the exact heading text from ORIGINAL into COMPRESSED
|
||||
- Do not touch any section not mentioned in the errors
|
||||
|
||||
ORIGINAL (reference only):
|
||||
{original}
|
||||
|
||||
COMPRESSED (fix this):
|
||||
{compressed}
|
||||
|
||||
Return ONLY the fixed compressed file. No explanation.
|
||||
"""
|
||||
|
||||
|
||||
# ---------- Core Logic ----------
|
||||
|
||||
|
||||
def compress_file(filepath: Path) -> bool:
|
||||
# Resolve and validate path
|
||||
filepath = filepath.resolve()
|
||||
MAX_FILE_SIZE = 500_000 # 500KB
|
||||
if not filepath.exists():
|
||||
raise FileNotFoundError(f"File not found: {filepath}")
|
||||
if filepath.stat().st_size > MAX_FILE_SIZE:
|
||||
raise ValueError(f"File too large to compress safely (max 500KB): {filepath}")
|
||||
|
||||
# Refuse files that look like they contain secrets or PII. Compressing ships
|
||||
# the raw bytes to the Anthropic API — a third-party boundary — so we fail
|
||||
# loudly rather than silently exfiltrate credentials or keys. Override is
|
||||
# intentional: the user must rename the file if the heuristic is wrong.
|
||||
if is_sensitive_path(filepath):
|
||||
raise ValueError(
|
||||
f"Refusing to compress {filepath}: filename looks sensitive "
|
||||
"(credentials, keys, secrets, or known private paths). "
|
||||
"Compression sends file contents to the Anthropic API. "
|
||||
"Rename the file if this is a false positive."
|
||||
)
|
||||
|
||||
print(f"Processing: {filepath}")
|
||||
|
||||
if not should_compress(filepath):
|
||||
print("Skipping (not natural language)")
|
||||
return False
|
||||
|
||||
original_text = filepath.read_text(errors="ignore")
|
||||
backup_path = filepath.with_name(filepath.stem + ".original.md")
|
||||
|
||||
if not original_text.strip():
|
||||
print("❌ Refusing to compress: file is empty or whitespace-only.")
|
||||
return False
|
||||
|
||||
# Check if backup already exists to prevent accidental overwriting
|
||||
if backup_path.exists():
|
||||
print(f"⚠️ Backup file already exists: {backup_path}")
|
||||
print("The original backup may contain important content.")
|
||||
print("Aborting to prevent data loss. Please remove or rename the backup file if you want to proceed.")
|
||||
return False
|
||||
|
||||
# Step 1: Compress
|
||||
print("Compressing with Claude...")
|
||||
compressed = call_claude(build_compress_prompt(original_text))
|
||||
|
||||
if compressed is None or not compressed.strip():
|
||||
print("❌ Compression aborted: Claude returned an empty response.")
|
||||
print(" Original file is untouched (no backup created).")
|
||||
return False
|
||||
|
||||
if compressed.strip() == original_text.strip():
|
||||
print("❌ Compression aborted: output is identical to input.")
|
||||
print(" Likely causes: Claude refused, returned the prompt verbatim, or the file is")
|
||||
print(" already in caveman form. Original file is untouched (no backup created).")
|
||||
return False
|
||||
|
||||
# Save original as backup, then verify the backup readback before
|
||||
# touching the input file. If the filesystem dropped bytes (encoding,
|
||||
# antivirus, disk full), unlink the bad backup and abort instead of
|
||||
# leaving the user with a corrupt backup + compressed primary.
|
||||
backup_path.write_text(original_text)
|
||||
backup_readback = backup_path.read_text(errors="ignore")
|
||||
if backup_readback != original_text:
|
||||
print(f"❌ Backup write verification failed: {backup_path}")
|
||||
print(" In-memory original differs from on-disk backup. Aborting before touching the input file.")
|
||||
try:
|
||||
backup_path.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
return False
|
||||
filepath.write_text(compressed)
|
||||
|
||||
# Step 2: Validate + Retry
|
||||
for attempt in range(MAX_RETRIES):
|
||||
print(f"\nValidation attempt {attempt + 1}")
|
||||
|
||||
result = validate(backup_path, filepath)
|
||||
|
||||
if result.is_valid:
|
||||
print("Validation passed")
|
||||
break
|
||||
|
||||
print("❌ Validation failed:")
|
||||
for err in result.errors:
|
||||
print(f" - {err}")
|
||||
|
||||
if attempt == MAX_RETRIES - 1:
|
||||
# Restore original on failure
|
||||
filepath.write_text(original_text)
|
||||
backup_path.unlink(missing_ok=True)
|
||||
print("❌ Failed after retries — original restored")
|
||||
return False
|
||||
|
||||
print("Fixing with Claude...")
|
||||
compressed = call_claude(
|
||||
build_fix_prompt(original_text, compressed, result.errors)
|
||||
)
|
||||
filepath.write_text(compressed)
|
||||
|
||||
return True
|
||||
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Detect whether a file is natural language (compressible) or code/config (skip)."""
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
# Extensions that are natural language and compressible
|
||||
COMPRESSIBLE_EXTENSIONS = {".md", ".txt", ".markdown", ".rst", ".typ", ".typst", ".tex"}
|
||||
|
||||
# Extensions that are code/config and should be skipped
|
||||
SKIP_EXTENSIONS = {
|
||||
".py", ".js", ".ts", ".tsx", ".jsx", ".json", ".yaml", ".yml",
|
||||
".toml", ".env", ".lock", ".css", ".scss", ".html", ".xml",
|
||||
".sql", ".sh", ".bash", ".zsh", ".go", ".rs", ".java", ".c",
|
||||
".cpp", ".h", ".hpp", ".rb", ".php", ".swift", ".kt", ".lua",
|
||||
".dockerfile", ".makefile", ".csv", ".ini", ".cfg",
|
||||
}
|
||||
|
||||
# Patterns that indicate a line is code
|
||||
CODE_PATTERNS = [
|
||||
re.compile(r"^\s*(import |from .+ import |require\(|const |let |var )"),
|
||||
re.compile(r"^\s*(def |class |function |async function |export )"),
|
||||
re.compile(r"^\s*(if\s*\(|for\s*\(|while\s*\(|switch\s*\(|try\s*\{)"),
|
||||
re.compile(r"^\s*[\}\]\);]+\s*$"), # closing braces/brackets
|
||||
re.compile(r"^\s*@\w+"), # decorators/annotations
|
||||
re.compile(r'^\s*"[^"]+"\s*:\s*'), # JSON-like key-value
|
||||
re.compile(r"^\s*\w+\s*=\s*[{\[\(\"']"), # assignment with literal
|
||||
]
|
||||
|
||||
|
||||
def _is_code_line(line: str) -> bool:
|
||||
"""Check if a line looks like code."""
|
||||
return any(p.match(line) for p in CODE_PATTERNS)
|
||||
|
||||
|
||||
def _is_json_content(text: str) -> bool:
|
||||
"""Check if content is valid JSON."""
|
||||
try:
|
||||
json.loads(text)
|
||||
return True
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
return False
|
||||
|
||||
|
||||
def _is_yaml_content(lines: list[str]) -> bool:
|
||||
"""Heuristic: check if content looks like YAML."""
|
||||
yaml_indicators = 0
|
||||
for line in lines[:30]:
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("---"):
|
||||
yaml_indicators += 1
|
||||
elif re.match(r"^\w[\w\s]*:\s", stripped):
|
||||
yaml_indicators += 1
|
||||
elif stripped.startswith("- ") and ":" in stripped:
|
||||
yaml_indicators += 1
|
||||
# If most non-empty lines look like YAML
|
||||
non_empty = sum(1 for l in lines[:30] if l.strip())
|
||||
return non_empty > 0 and yaml_indicators / non_empty > 0.6
|
||||
|
||||
|
||||
def detect_file_type(filepath: Path) -> str:
|
||||
"""Classify a file as 'natural_language', 'code', 'config', or 'unknown'.
|
||||
|
||||
Returns:
|
||||
One of: 'natural_language', 'code', 'config', 'unknown'
|
||||
"""
|
||||
ext = filepath.suffix.lower()
|
||||
|
||||
# Extension-based classification
|
||||
if ext in COMPRESSIBLE_EXTENSIONS:
|
||||
return "natural_language"
|
||||
if ext in SKIP_EXTENSIONS:
|
||||
return "code" if ext not in {".json", ".yaml", ".yml", ".toml", ".ini", ".cfg", ".env"} else "config"
|
||||
|
||||
# Extensionless files (like CLAUDE.md, TODO) — check content
|
||||
if not ext:
|
||||
try:
|
||||
text = filepath.read_text(errors="ignore")
|
||||
except (OSError, PermissionError):
|
||||
return "unknown"
|
||||
|
||||
lines = text.splitlines()[:50]
|
||||
|
||||
if _is_json_content(text[:10000]):
|
||||
return "config"
|
||||
if _is_yaml_content(lines):
|
||||
return "config"
|
||||
|
||||
code_lines = sum(1 for l in lines if l.strip() and _is_code_line(l))
|
||||
non_empty = sum(1 for l in lines if l.strip())
|
||||
if non_empty > 0 and code_lines / non_empty > 0.4:
|
||||
return "code"
|
||||
|
||||
return "natural_language"
|
||||
|
||||
return "unknown"
|
||||
|
||||
|
||||
def should_compress(filepath: Path) -> bool:
|
||||
"""Return True if the file is natural language and should be compressed."""
|
||||
if not filepath.is_file():
|
||||
return False
|
||||
# Skip backup files
|
||||
if filepath.name.endswith(".original.md"):
|
||||
return False
|
||||
return detect_file_type(filepath) == "natural_language"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python detect.py <file1> [file2] ...")
|
||||
sys.exit(1)
|
||||
|
||||
for path_str in sys.argv[1:]:
|
||||
p = Path(path_str).resolve()
|
||||
file_type = detect_file_type(p)
|
||||
compress = should_compress(p)
|
||||
print(f" {p.name:30s} type={file_type:20s} compress={compress}")
|
||||
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env python3
|
||||
import re
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
URL_REGEX = re.compile(r"https?://[^\s)]+")
|
||||
FENCE_OPEN_REGEX = re.compile(r"^(\s{0,3})(`{3,}|~{3,})(.*)$")
|
||||
HEADING_REGEX = re.compile(r"^(#{1,6})\s+(.*)", re.MULTILINE)
|
||||
BULLET_REGEX = re.compile(r"^\s*[-*+]\s+", re.MULTILINE)
|
||||
|
||||
# crude but effective path detection
|
||||
# Requires either a path prefix (./ ../ / or drive letter) or a slash/backslash within the match
|
||||
PATH_REGEX = re.compile(r"(?:\./|\.\./|/|[A-Za-z]:\\)[\w\-/\\\.]+|[\w\-\.]+[/\\][\w\-/\\\.]+")
|
||||
|
||||
|
||||
class ValidationResult:
|
||||
def __init__(self):
|
||||
self.is_valid = True
|
||||
self.errors = []
|
||||
self.warnings = []
|
||||
|
||||
def add_error(self, msg):
|
||||
self.is_valid = False
|
||||
self.errors.append(msg)
|
||||
|
||||
def add_warning(self, msg):
|
||||
self.warnings.append(msg)
|
||||
|
||||
|
||||
def read_file(path: Path) -> str:
|
||||
return path.read_text(errors="ignore")
|
||||
|
||||
|
||||
# ---------- Extractors ----------
|
||||
|
||||
|
||||
def extract_headings(text):
|
||||
return [(level, title.strip()) for level, title in HEADING_REGEX.findall(text)]
|
||||
|
||||
|
||||
def extract_code_blocks(text):
|
||||
"""Line-based fenced code block extractor.
|
||||
|
||||
Handles ``` and ~~~ fences with variable length (CommonMark: closing
|
||||
fence must use same char and be at least as long as opening). Supports
|
||||
nested fences (e.g. an outer 4-backtick block wrapping inner 3-backtick
|
||||
content).
|
||||
"""
|
||||
blocks = []
|
||||
lines = text.split("\n")
|
||||
i = 0
|
||||
n = len(lines)
|
||||
while i < n:
|
||||
m = FENCE_OPEN_REGEX.match(lines[i])
|
||||
if not m:
|
||||
i += 1
|
||||
continue
|
||||
fence_char = m.group(2)[0]
|
||||
fence_len = len(m.group(2))
|
||||
open_line = lines[i]
|
||||
block_lines = [open_line]
|
||||
i += 1
|
||||
closed = False
|
||||
while i < n:
|
||||
close_m = FENCE_OPEN_REGEX.match(lines[i])
|
||||
if (
|
||||
close_m
|
||||
and close_m.group(2)[0] == fence_char
|
||||
and len(close_m.group(2)) >= fence_len
|
||||
and close_m.group(3).strip() == ""
|
||||
):
|
||||
block_lines.append(lines[i])
|
||||
closed = True
|
||||
i += 1
|
||||
break
|
||||
block_lines.append(lines[i])
|
||||
i += 1
|
||||
if closed:
|
||||
blocks.append("\n".join(block_lines))
|
||||
# Unclosed fences are silently skipped — they indicate malformed markdown
|
||||
# and including them would cause false-positive validation failures.
|
||||
return blocks
|
||||
|
||||
|
||||
def extract_urls(text):
|
||||
return set(URL_REGEX.findall(text))
|
||||
|
||||
|
||||
def extract_paths(text):
|
||||
return set(PATH_REGEX.findall(text))
|
||||
|
||||
|
||||
def count_bullets(text):
|
||||
return len(BULLET_REGEX.findall(text))
|
||||
|
||||
|
||||
def extract_inline_codes(text):
|
||||
text_without_fences = re.sub(r"^```[\s\S]*?^```", "", text, flags=re.MULTILINE)
|
||||
text_without_fences = re.sub(r"^~~~[\s\S]*?^~~~", "", text_without_fences, flags=re.MULTILINE)
|
||||
return re.findall(r"`([^`]+)`", text_without_fences)
|
||||
|
||||
|
||||
# ---------- Validators ----------
|
||||
|
||||
|
||||
def validate_headings(orig, comp, result):
|
||||
h1 = extract_headings(orig)
|
||||
h2 = extract_headings(comp)
|
||||
|
||||
if len(h1) != len(h2):
|
||||
result.add_error(f"Heading count mismatch: {len(h1)} vs {len(h2)}")
|
||||
|
||||
if h1 != h2:
|
||||
result.add_warning("Heading text/order changed")
|
||||
|
||||
|
||||
def validate_code_blocks(orig, comp, result):
|
||||
c1 = extract_code_blocks(orig)
|
||||
c2 = extract_code_blocks(comp)
|
||||
|
||||
if c1 != c2:
|
||||
result.add_error("Code blocks not preserved exactly")
|
||||
|
||||
|
||||
def validate_urls(orig, comp, result):
|
||||
u1 = extract_urls(orig)
|
||||
u2 = extract_urls(comp)
|
||||
|
||||
if u1 != u2:
|
||||
result.add_error(f"URL mismatch: lost={u1 - u2}, added={u2 - u1}")
|
||||
|
||||
|
||||
def validate_paths(orig, comp, result):
|
||||
p1 = extract_paths(orig)
|
||||
p2 = extract_paths(comp)
|
||||
|
||||
if p1 != p2:
|
||||
result.add_warning(f"Path mismatch: lost={p1 - p2}, added={p2 - p1}")
|
||||
|
||||
|
||||
def validate_bullets(orig, comp, result):
|
||||
b1 = count_bullets(orig)
|
||||
b2 = count_bullets(comp)
|
||||
|
||||
if b1 == 0:
|
||||
return
|
||||
|
||||
diff = abs(b1 - b2) / b1
|
||||
|
||||
if diff > 0.15:
|
||||
result.add_warning(f"Bullet count changed too much: {b1} -> {b2}")
|
||||
|
||||
|
||||
def validate_inline_codes(orig, comp, result):
|
||||
c1 = Counter(extract_inline_codes(orig))
|
||||
c2 = Counter(extract_inline_codes(comp))
|
||||
|
||||
if c1 != c2:
|
||||
lost = set(c1.keys()) - set(c2.keys())
|
||||
added = set(c2.keys()) - set(c1.keys())
|
||||
for code, count in c1.items():
|
||||
if code in c2 and c2[code] < count:
|
||||
lost.add(f"{code} (lost {count - c2[code]} of {count} occurrences)")
|
||||
if lost:
|
||||
result.add_error(f"Inline code lost: {lost}")
|
||||
if added:
|
||||
result.add_warning(f"Inline code added: {added}")
|
||||
|
||||
|
||||
# ---------- Main ----------
|
||||
|
||||
|
||||
def validate(original_path: Path, compressed_path: Path) -> ValidationResult:
|
||||
result = ValidationResult()
|
||||
|
||||
orig = read_file(original_path)
|
||||
comp = read_file(compressed_path)
|
||||
|
||||
validate_headings(orig, comp, result)
|
||||
validate_code_blocks(orig, comp, result)
|
||||
validate_urls(orig, comp, result)
|
||||
validate_paths(orig, comp, result)
|
||||
validate_bullets(orig, comp, result)
|
||||
validate_inline_codes(orig, comp, result)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# ---------- CLI ----------
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
if len(sys.argv) != 3:
|
||||
print("Usage: python validate.py <original> <compressed>")
|
||||
sys.exit(1)
|
||||
|
||||
orig = Path(sys.argv[1]).resolve()
|
||||
comp = Path(sys.argv[2]).resolve()
|
||||
|
||||
res = validate(orig, comp)
|
||||
|
||||
print(f"\nValid: {res.is_valid}")
|
||||
|
||||
if res.errors:
|
||||
print("\nErrors:")
|
||||
for e in res.errors:
|
||||
print(f" - {e}")
|
||||
|
||||
if res.warnings:
|
||||
print("\nWarnings:")
|
||||
for w in res.warnings:
|
||||
print(f" - {w}")
|
||||
@@ -0,0 +1,38 @@
|
||||
# caveman-help
|
||||
|
||||
Quick-reference card. One shot, no mode change.
|
||||
|
||||
## What it does
|
||||
|
||||
Prints a cheat sheet of all caveman modes, sibling skills, deactivation triggers, and how to set the default mode via env var or config file. One-shot display — does not flip the active mode, write flag files, or persist anything. Use when you forget the slash commands.
|
||||
|
||||
## How to invoke
|
||||
|
||||
```
|
||||
/caveman-help
|
||||
```
|
||||
|
||||
Also triggers on "caveman help", "what caveman commands", "how do I use caveman".
|
||||
|
||||
## Example output
|
||||
|
||||
```
|
||||
Modes:
|
||||
/caveman full (default)
|
||||
/caveman lite lighter
|
||||
/caveman ultra extreme
|
||||
/caveman wenyan classical Chinese
|
||||
|
||||
Skills:
|
||||
/caveman-commit terse Conventional Commits
|
||||
/caveman-review one-line PR comments
|
||||
/caveman-stats session token savings
|
||||
|
||||
Deactivate:
|
||||
"stop caveman" or "normal mode"
|
||||
```
|
||||
|
||||
## See also
|
||||
|
||||
- [`SKILL.md`](./SKILL.md) — full reference card
|
||||
- [Caveman README](../../README.md) — repo overview
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
name: caveman-help
|
||||
description: >
|
||||
Quick-reference card for all caveman modes, skills, and commands.
|
||||
One-shot display, not a persistent mode. Trigger: /caveman-help,
|
||||
"caveman help", "what caveman commands", "how do I use caveman".
|
||||
---
|
||||
|
||||
# Caveman Help
|
||||
|
||||
Display this reference card when invoked. One-shot — do NOT change mode, write flag files, or persist anything. Output in caveman style.
|
||||
|
||||
## Modes
|
||||
|
||||
| Mode | Trigger | What change |
|
||||
|------|---------|-------------|
|
||||
| **Lite** | `/caveman lite` | Drop filler. Keep sentence structure. |
|
||||
| **Full** | `/caveman` | Drop articles, filler, pleasantries, hedging. Fragments OK. Default. |
|
||||
| **Ultra** | `/caveman ultra` | Extreme compression. Bare fragments. Tables over prose. |
|
||||
| **Wenyan-Lite** | `/caveman wenyan-lite` | Classical Chinese style, light compression. |
|
||||
| **Wenyan-Full** | `/caveman wenyan` | Full 文言文. Maximum classical terseness. |
|
||||
| **Wenyan-Ultra** | `/caveman wenyan-ultra` | Extreme. Ancient scholar on a budget. |
|
||||
|
||||
Mode stick until changed or session end.
|
||||
|
||||
## Skills
|
||||
|
||||
| Skill | Trigger | What it do |
|
||||
|-------|---------|-----------|
|
||||
| **caveman-commit** | `/caveman-commit` | Terse commit messages. Conventional Commits. ≤50 char subject. |
|
||||
| **caveman-review** | `/caveman-review` | One-line PR comments: `L42: bug: user null. Add guard.` |
|
||||
| **caveman-compress** | `/caveman-compress <file>` | Compress .md files to caveman prose. Saves ~46% input tokens. |
|
||||
| **caveman-help** | `/caveman-help` | This card. |
|
||||
|
||||
## Deactivate
|
||||
|
||||
Say "stop caveman" or "normal mode". Resume anytime with `/caveman`.
|
||||
|
||||
## Configure Default Mode
|
||||
|
||||
Default mode = `full`. Change it:
|
||||
|
||||
**Environment variable** (highest priority):
|
||||
```bash
|
||||
export CAVEMAN_DEFAULT_MODE=ultra
|
||||
```
|
||||
|
||||
**Config file** (`~/.config/caveman/config.json`):
|
||||
```json
|
||||
{ "defaultMode": "lite" }
|
||||
```
|
||||
|
||||
Set `"off"` to disable auto-activation on session start. User can still activate manually with `/caveman`.
|
||||
|
||||
Resolution: env var > config file > `full`.
|
||||
|
||||
## More
|
||||
|
||||
Full docs: https://github.com/JuliusBrussee/caveman
|
||||
@@ -0,0 +1,33 @@
|
||||
# caveman-review
|
||||
|
||||
One-line PR comments. Location, problem, fix. No throat-clearing.
|
||||
|
||||
## What it does
|
||||
|
||||
Generates code review comments in `L<line>: <severity> <problem>. <fix>.` format. One line per finding. Severity emoji: 🔴 bug, 🟡 risk, 🔵 nit, ❓ question. Drops "I noticed that...", hedging, and restating what the diff already shows. Keeps exact line numbers, backticked symbols, and concrete fixes.
|
||||
|
||||
Auto-clarity: drops terse mode for CVE-class security findings, architectural disagreements, and onboarding contexts where the author needs the *why*. Resumes terse for the rest.
|
||||
|
||||
Output only — does not approve, request changes, or run linters.
|
||||
|
||||
## How to invoke
|
||||
|
||||
```
|
||||
/caveman-review
|
||||
```
|
||||
|
||||
Also triggers on "review this PR", "code review", "review the diff".
|
||||
|
||||
## Example output
|
||||
|
||||
```
|
||||
L42: 🔴 bug: user can be null after .find(). Add guard before .email.
|
||||
L88-140: 🔵 nit: 50-line fn does 4 things. Extract validate/normalize/persist.
|
||||
L23: 🟡 risk: no retry on 429. Wrap in withBackoff(3).
|
||||
L107: ❓ q: why drop the cache here? Reads on next request will miss.
|
||||
```
|
||||
|
||||
## See also
|
||||
|
||||
- [`SKILL.md`](./SKILL.md) — full LLM-facing instructions
|
||||
- [Caveman README](../../README.md) — repo overview
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
name: caveman-review
|
||||
description: >
|
||||
Ultra-compressed code review comments. Cuts noise from PR feedback while preserving
|
||||
the actionable signal. Each comment is one line: location, problem, fix. Use when user
|
||||
says "review this PR", "code review", "review the diff", "/review", or invokes
|
||||
/caveman-review. Auto-triggers when reviewing pull requests.
|
||||
---
|
||||
|
||||
Write code review comments terse and actionable. One line per finding. Location, problem, fix. No throat-clearing.
|
||||
|
||||
## Rules
|
||||
|
||||
**Format:** `L<line>: <problem>. <fix>.` — or `<file>:L<line>: ...` when reviewing multi-file diffs.
|
||||
|
||||
**Severity prefix (optional, when mixed):**
|
||||
- `🔴 bug:` — broken behavior, will cause incident
|
||||
- `🟡 risk:` — works but fragile (race, missing null check, swallowed error)
|
||||
- `🔵 nit:` — style, naming, micro-optim. Author can ignore
|
||||
- `❓ q:` — genuine question, not a suggestion
|
||||
|
||||
**Drop:**
|
||||
- "I noticed that...", "It seems like...", "You might want to consider..."
|
||||
- "This is just a suggestion but..." — use `nit:` instead
|
||||
- "Great work!", "Looks good overall but..." — say it once at the top, not per comment
|
||||
- Restating what the line does — the reviewer can read the diff
|
||||
- Hedging ("perhaps", "maybe", "I think") — if unsure use `q:`
|
||||
|
||||
**Keep:**
|
||||
- Exact line numbers
|
||||
- Exact symbol/function/variable names in backticks
|
||||
- Concrete fix, not "consider refactoring this"
|
||||
- The *why* if the fix isn't obvious from the problem statement
|
||||
|
||||
## Examples
|
||||
|
||||
❌ "I noticed that on line 42 you're not checking if the user object is null before accessing the email property. This could potentially cause a crash if the user is not found in the database. You might want to add a null check here."
|
||||
|
||||
✅ `L42: 🔴 bug: user can be null after .find(). Add guard before .email.`
|
||||
|
||||
❌ "It looks like this function is doing a lot of things and might benefit from being broken up into smaller functions for readability."
|
||||
|
||||
✅ `L88-140: 🔵 nit: 50-line fn does 4 things. Extract validate/normalize/persist.`
|
||||
|
||||
❌ "Have you considered what happens if the API returns a 429? I think we should probably handle that case."
|
||||
|
||||
✅ `L23: 🟡 risk: no retry on 429. Wrap in withBackoff(3).`
|
||||
|
||||
## Auto-Clarity
|
||||
|
||||
Drop terse mode for: security findings (CVE-class bugs need full explanation + reference), architectural disagreements (need rationale, not just a one-liner), and onboarding contexts where the author is new and needs the "why". In those cases write a normal paragraph, then resume terse for the rest.
|
||||
|
||||
## Boundaries
|
||||
|
||||
Reviews only — does not write the code fix, does not approve/request-changes, does not run linters. Output the comment(s) ready to paste into the PR. "stop caveman-review" or "normal mode": revert to verbose review style.
|
||||
@@ -0,0 +1,30 @@
|
||||
# caveman-stats
|
||||
|
||||
Real session token receipts. No AI estimation.
|
||||
|
||||
## What it does
|
||||
|
||||
Reads the current Claude Code session log directly and reports actual input/output token usage plus estimated savings versus a non-caveman baseline. Numbers come from the JSONL session log on disk — the model itself does not compute or estimate them. Output is injected by the `caveman-mode-tracker` hook, which intercepts `/caveman-stats` and returns the formatted stats as a blocked-decision reason.
|
||||
|
||||
Each run also writes a lifetime-savings suffix file used by the statusline badge (`⛏ 12.4k`).
|
||||
|
||||
## How to invoke
|
||||
|
||||
```
|
||||
/caveman-stats
|
||||
```
|
||||
|
||||
## Example output
|
||||
|
||||
```
|
||||
Session: 47 turns
|
||||
Input: 12,304 tokens
|
||||
Output: 3,891 tokens (caveman)
|
||||
Baseline: 11,247 tokens (estimated without caveman)
|
||||
Saved: 7,356 tokens (~65%)
|
||||
```
|
||||
|
||||
## See also
|
||||
|
||||
- [`SKILL.md`](./SKILL.md) — hook contract and mechanics
|
||||
- [Caveman README](../../README.md) — repo overview
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: caveman-stats
|
||||
description: >
|
||||
Show real token usage and estimated savings for the current session.
|
||||
Reads directly from the Claude Code session log — no AI estimation.
|
||||
Triggers on /caveman-stats. Output is injected by the mode-tracker hook;
|
||||
the model itself does not compute the numbers.
|
||||
---
|
||||
|
||||
This skill is delivered by `hooks/caveman-stats.js` (read by `hooks/caveman-mode-tracker.js` on `/caveman-stats`). The model does not need to do anything when this skill fires — the hook returns `decision: "block"` with the formatted stats as the reason. The user sees the numbers immediately.
|
||||
@@ -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
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
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"). Technical terms exact. Code blocks unchanged. Errors quoted exact.
|
||||
|
||||
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 |
|
||||
| **ultra** | Abbreviate prose words (DB/auth/config/req/res/fn/impl), 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 .Wrap之。"
|
||||
- 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.
|
||||
@@ -0,0 +1,300 @@
|
||||
---
|
||||
name: context-mode
|
||||
description: |
|
||||
Use context-mode tools (ctx_execute, ctx_execute_file) instead of Bash/cat when processing
|
||||
large outputs. Triggers: "analyze logs", "summarize output", "process data",
|
||||
"parse JSON", "filter results", "extract errors", "check build output",
|
||||
"analyze dependencies", "process API response", "large file analysis",
|
||||
"page snapshot", "browser snapshot", "DOM structure", "inspect page",
|
||||
"accessibility tree", "Playwright snapshot",
|
||||
"run tests", "test output", "coverage report", "git log", "recent commits",
|
||||
"diff between branches", "list containers", "pod status", "disk usage",
|
||||
"fetch docs", "API reference", "index documentation",
|
||||
"call API", "check response", "query results",
|
||||
"find TODOs", "count lines", "codebase statistics", "security audit",
|
||||
"outdated packages", "dependency tree", "cloud resources", "CI/CD output".
|
||||
Also triggers on ANY MCP tool output that may exceed 20 lines.
|
||||
Subagent routing is handled automatically via PreToolUse hook.
|
||||
---
|
||||
|
||||
# Context Mode: Default for All Large Output
|
||||
|
||||
## MANDATORY RULE
|
||||
|
||||
<context_mode_logic>
|
||||
<mandatory_rule>
|
||||
Default to context-mode for ALL commands. Only use Bash for guaranteed-small-output operations.
|
||||
</mandatory_rule>
|
||||
</context_mode_logic>
|
||||
|
||||
Bash whitelist (safe to run directly):
|
||||
- **File mutations**: `mkdir`, `mv`, `cp`, `rm`, `touch`, `chmod`
|
||||
- **Git writes**: `git add`, `git commit`, `git push`, `git checkout`, `git branch`, `git merge`
|
||||
- **Navigation**: `cd`, `pwd`, `which`
|
||||
- **Process control**: `kill`, `pkill`
|
||||
- **Package management**: `npm install`, `npm publish`, `pip install`
|
||||
- **Simple output**: `echo`, `printf`
|
||||
|
||||
**Everything else → `ctx_execute` or `ctx_execute_file`.** Any command that reads, queries, fetches, lists, logs, tests, builds, diffs, inspects, or calls an external service. This includes ALL CLIs (gh, aws, kubectl, docker, terraform, wrangler, fly, heroku, gcloud, etc.) — there are thousands and we cannot list them all.
|
||||
|
||||
**When uncertain, use context-mode.** Every KB of unnecessary context reduces the quality and speed of the entire session.
|
||||
|
||||
## Decision Tree
|
||||
|
||||
```
|
||||
About to run a command / read a file / call an API?
|
||||
│
|
||||
├── Command is on the Bash whitelist (file mutations, git writes, navigation, echo)?
|
||||
│ └── Use Bash
|
||||
│
|
||||
├── Output MIGHT be large or you're UNSURE?
|
||||
│ └── Use context-mode ctx_execute or ctx_execute_file
|
||||
│
|
||||
├── Fetching web documentation or HTML page?
|
||||
│ └── Use ctx_fetch_and_index → ctx_search
|
||||
│
|
||||
├── Using Playwright (navigate, snapshot, console, network)?
|
||||
│ └── ALWAYS use filename parameter to save to file, then:
|
||||
│ browser_snapshot(filename) → ctx_index(path) or ctx_execute_file(path)
|
||||
│ browser_console_messages(filename) → ctx_execute_file(path)
|
||||
│ browser_network_requests(filename) → ctx_execute_file(path)
|
||||
│ ⚠ browser_navigate returns a snapshot automatically — ignore it,
|
||||
│ use browser_snapshot(filename) for any inspection.
|
||||
│ ⚠ Playwright MCP uses a SINGLE browser instance — NOT parallel-safe.
|
||||
│ For parallel browser ops, use agent-browser via execute instead.
|
||||
│
|
||||
├── Using agent-browser (parallel-safe browser automation)?
|
||||
│ └── Run via execute (shell) — each call gets its own subprocess:
|
||||
│ execute("agent-browser open example.com && agent-browser snapshot -i -c")
|
||||
│ ✓ Supports sessions for isolated browser instances
|
||||
│ ✓ Safe for parallel subagent execution
|
||||
│ ✓ Lightweight accessibility tree with ref-based interaction
|
||||
│
|
||||
├── Processing output from another MCP tool (Context7, GitHub API, etc.)?
|
||||
│ ├── Output already in context from a previous tool call?
|
||||
│ │ └── Use it directly. Do NOT re-index with ctx_index(content: ...).
|
||||
│ ├── Need to search the output multiple times?
|
||||
│ │ └── Save to file via ctx_execute, then ctx_index(path) → ctx_search
|
||||
│ └── One-shot extraction?
|
||||
│ └── Save to file via ctx_execute, then ctx_execute_file(path)
|
||||
│
|
||||
└── Reading a file to analyze/summarize (not edit)?
|
||||
└── Use ctx_execute_file (file loads into FILE_CONTENT, not context)
|
||||
```
|
||||
|
||||
## When to Use Each Tool
|
||||
|
||||
| Situation | Tool | Example |
|
||||
|-----------|------|---------|
|
||||
| Hit an API endpoint | `ctx_execute` | `fetch('http://localhost:3000/api/orders')` |
|
||||
| Run CLI that returns data | `ctx_execute` | `gh pr list`, `aws s3 ls`, `kubectl get pods` |
|
||||
| Run tests | `ctx_execute` | `npm test`, `pytest`, `go test ./...` |
|
||||
| Git operations | `ctx_execute` | `git log --oneline -50`, `git diff HEAD~5` |
|
||||
| Docker/K8s inspection | `ctx_execute` | `docker stats --no-stream`, `kubectl describe pod` |
|
||||
| Read a log file | `ctx_execute_file` | Parse access.log, error.log, build output |
|
||||
| Read a data file | `ctx_execute_file` | Analyze CSV, JSON, YAML, XML |
|
||||
| Read source code to analyze | `ctx_execute_file` | Count functions, find patterns, extract metrics |
|
||||
| Fetch web docs | `ctx_fetch_and_index` | Index React/Next.js/Zod docs, then search |
|
||||
| Playwright snapshot | `browser_snapshot(filename)` → `ctx_index(path)` → `ctx_search` | Save to file, index server-side, query |
|
||||
| Playwright snapshot (one-shot) | `browser_snapshot(filename)` → `ctx_execute_file(path)` | Save to file, extract in sandbox |
|
||||
| Playwright console/network | `browser_*(filename)` → `ctx_execute_file(path)` | Save to file, analyze in sandbox |
|
||||
| MCP output (already in context) | Use directly | Don't re-index — it's already loaded |
|
||||
| MCP output (need multi-query) | `ctx_execute` to save → `ctx_index(path)` → `ctx_search` | Save to file first, index server-side |
|
||||
| Wipe indexed KB content | `ctx_purge(confirm: true)` | Permanently deletes all indexed content |
|
||||
|
||||
## Automatic Triggers
|
||||
|
||||
Use context-mode for ANY of these, without being asked:
|
||||
|
||||
- **API debugging**: "hit this endpoint", "call the API", "check the response", "find the bug in the response"
|
||||
- **Log analysis**: "check the logs", "what errors", "read access.log", "debug the 500s"
|
||||
- **Test runs**: "run the tests", "check if tests pass", "test suite output"
|
||||
- **Git history**: "show recent commits", "git log", "what changed", "diff between branches"
|
||||
- **Data inspection**: "look at the CSV", "parse the JSON", "analyze the config"
|
||||
- **Infrastructure**: "list containers", "check pods", "S3 buckets", "show running services"
|
||||
- **Dependency audit**: "check dependencies", "outdated packages", "security audit"
|
||||
- **Build output**: "build the project", "check for warnings", "compile errors"
|
||||
- **Code metrics**: "count lines", "find TODOs", "function count", "analyze codebase"
|
||||
- **Web docs lookup**: "look up the docs", "check the API reference", "find examples"
|
||||
|
||||
## Language Selection
|
||||
|
||||
| Situation | Language | Why |
|
||||
|-----------|----------|-----|
|
||||
| HTTP/API calls, JSON | `javascript` | Native fetch, JSON.parse, async/await |
|
||||
| Data analysis, CSV, stats | `python` | csv, statistics, collections, re |
|
||||
| Shell commands with pipes | `shell` | grep, awk, jq, native tools |
|
||||
| File pattern matching | `shell` | find, wc, sort, uniq |
|
||||
|
||||
## Search Query Strategy
|
||||
|
||||
- BM25 uses **OR semantics** — results matching more terms rank higher automatically
|
||||
- Use 2-4 specific technical terms per query
|
||||
- **Always use `source` parameter** when multiple docs are indexed to avoid cross-source contamination
|
||||
- Partial match works: `source: "Node"` matches `"Node.js v22 CHANGELOG"`
|
||||
- **Always use `queries` array** — batch ALL search questions in ONE call:
|
||||
- `ctx_search(queries: ["transform pipe", "refine superRefine", "coerce codec"], source: "Zod")`
|
||||
- NEVER make multiple separate ctx_search() calls — put all queries in one array
|
||||
|
||||
## External Documentation
|
||||
|
||||
- **Always use `ctx_fetch_and_index`** for external docs — NEVER `cat` or `ctx_execute` with local paths for packages you don't own
|
||||
- For GitHub-hosted projects, use the raw URL: `https://raw.githubusercontent.com/org/repo/main/CHANGELOG.md`
|
||||
- After indexing, use the `source` parameter in search to scope results to that specific document
|
||||
|
||||
## Critical Rules
|
||||
|
||||
1. **Always console.log/print your findings.** stdout is all that enters context. No output = wasted call.
|
||||
2. **Write analysis code, not just data dumps.** Don't `console.log(JSON.stringify(data))` — analyze first, print findings.
|
||||
3. **Be specific in output.** Print bug details with IDs, line numbers, exact values — not just counts.
|
||||
4. **For files you need to EDIT**: Use the normal Read tool. context-mode is for analysis, not editing.
|
||||
5. **For Bash whitelist commands only**: Use Bash for file mutations, git writes, navigation, process control, package install, and echo. Everything else goes through context-mode.
|
||||
6. **Never use `ctx_index(content: large_data)`.** Use `ctx_index(path: ...)` to read files server-side. The `content` parameter sends data through context as a tool parameter — use it only for small inline text.
|
||||
7. **Always use `filename` parameter** on Playwright tools (`browser_snapshot`, `browser_console_messages`, `browser_network_requests`). Without it, the full output enters context.
|
||||
8. **Don't re-index data already in context.** If an MCP tool returned data in a previous response, it's already loaded — use it directly or save to file first.
|
||||
|
||||
## Sandboxed Data Workflow
|
||||
|
||||
<sandboxed_data_workflow>
|
||||
<critical_rule>
|
||||
When using tools that support saving to a file: ALWAYS use the 'filename' parameter.
|
||||
NEVER return large raw datasets directly to context.
|
||||
</critical_rule>
|
||||
<workflow>
|
||||
LargeDataTool(filename: "path") → mcp__context-mode__ctx_index(path: "path") → ctx_search()
|
||||
</workflow>
|
||||
</sandboxed_data_workflow>
|
||||
|
||||
This is the universal pattern for context preservation regardless of
|
||||
the source tool (Playwright, GitHub API, AWS CLI, etc.).
|
||||
|
||||
## Examples
|
||||
|
||||
### Debug an API endpoint
|
||||
```javascript
|
||||
const resp = await fetch('http://localhost:3000/api/orders');
|
||||
const { orders } = await resp.json();
|
||||
|
||||
const bugs = [];
|
||||
const negQty = orders.filter(o => o.quantity < 0);
|
||||
if (negQty.length) bugs.push(`Negative qty: ${negQty.map(o => o.id).join(', ')}`);
|
||||
|
||||
const nullFields = orders.filter(o => !o.product || !o.customer);
|
||||
if (nullFields.length) bugs.push(`Null fields: ${nullFields.map(o => o.id).join(', ')}`);
|
||||
|
||||
console.log(`${orders.length} orders, ${bugs.length} bugs found:`);
|
||||
bugs.forEach(b => console.log(`- ${b}`));
|
||||
```
|
||||
|
||||
### Analyze test output
|
||||
```shell
|
||||
npm test 2>&1
|
||||
echo "EXIT=$?"
|
||||
```
|
||||
|
||||
### Check GitHub PRs
|
||||
```shell
|
||||
gh pr list --json number,title,state,reviewDecision --jq '.[] | "\(.number) [\(.state)] \(.title) — \(.reviewDecision // "no review")"'
|
||||
```
|
||||
|
||||
### Read and analyze a large file
|
||||
```python
|
||||
# FILE_CONTENT is pre-loaded by ctx_execute_file
|
||||
import json
|
||||
data = json.loads(FILE_CONTENT)
|
||||
print(f"Records: {len(data)}")
|
||||
# ... analyze and print findings
|
||||
```
|
||||
|
||||
## Browser & Playwright Integration
|
||||
|
||||
**When a task involves Playwright snapshots, screenshots, or page inspection, ALWAYS route through file → sandbox.**
|
||||
|
||||
Playwright `browser_snapshot` returns 10K–135K tokens of accessibility tree data. Calling it without `filename` dumps all of that into context. Passing the output to `ctx_index(content: ...)` sends it into context a SECOND time as a parameter. Both are wrong.
|
||||
|
||||
**The key insight**: `browser_snapshot` has a `filename` parameter that saves to file instead of returning to context. `ctx_index` has a `path` parameter that reads files server-side. `ctx_execute_file` processes files in a sandbox. **None of these touch context.**
|
||||
|
||||
### Workflow A: Snapshot → File → Index → Search (multiple queries)
|
||||
|
||||
```
|
||||
Step 1: browser_snapshot(filename: "/tmp/playwright-snapshot.md")
|
||||
→ saves to file, returns ~50B confirmation (NOT 135K tokens)
|
||||
|
||||
Step 2: ctx_index(path: "/tmp/playwright-snapshot.md", source: "Playwright snapshot")
|
||||
→ reads file SERVER-SIDE, indexes into FTS5, returns ~80B confirmation
|
||||
|
||||
Step 3: ctx_search(queries: ["login form email password"], source: "Playwright")
|
||||
→ returns only matching chunks (~300B)
|
||||
```
|
||||
|
||||
**Total context: ~430B** instead of 270K tokens. Real 99% savings.
|
||||
|
||||
### Workflow B: Snapshot → File → Execute File (one-shot extraction)
|
||||
|
||||
```
|
||||
Step 1: browser_snapshot(filename: "/tmp/playwright-snapshot.md")
|
||||
→ saves to file, returns ~50B confirmation
|
||||
|
||||
Step 2: ctx_execute_file(path: "/tmp/playwright-snapshot.md", language: "javascript", code: "
|
||||
const links = [...FILE_CONTENT.matchAll(/- link \"([^\"]+)\"/g)].map(m => m[1]);
|
||||
const buttons = [...FILE_CONTENT.matchAll(/- button \"([^\"]+)\"/g)].map(m => m[1]);
|
||||
const inputs = [...FILE_CONTENT.matchAll(/- textbox|- checkbox|- radio/g)];
|
||||
console.log('Links:', links.length, '| Buttons:', buttons.length, '| Inputs:', inputs.length);
|
||||
console.log('Navigation:', links.slice(0, 10).join(', '));
|
||||
")
|
||||
→ processes in sandbox, returns ~200B summary
|
||||
```
|
||||
|
||||
**Total context: ~250B** instead of 135K tokens.
|
||||
|
||||
### Workflow C: Console & Network (save to file if large)
|
||||
|
||||
```
|
||||
browser_console_messages(level: "error", filename: "/tmp/console.md")
|
||||
→ ctx_execute_file(path: "/tmp/console.md", ...) or ctx_index(path: "/tmp/console.md", ...)
|
||||
|
||||
browser_network_requests(includeStatic: false, filename: "/tmp/network.md")
|
||||
→ ctx_execute_file(path: "/tmp/network.md", ...) or ctx_index(path: "/tmp/network.md", ...)
|
||||
```
|
||||
|
||||
### CRITICAL: Why `filename` + `path` is mandatory
|
||||
|
||||
| Approach | Context cost | Correct? |
|
||||
|----------|-------------|----------|
|
||||
| `browser_snapshot()` → raw into context | **135K tokens** | NO |
|
||||
| `browser_snapshot()` → `ctx_index(content: raw)` | **270K tokens** (doubled!) | NO |
|
||||
| `browser_snapshot(filename)` → `ctx_index(path)` → `ctx_search` | **~430B** | YES |
|
||||
| `browser_snapshot(filename)` → `ctx_execute_file(path)` | **~250B** | YES |
|
||||
|
||||
### Key Rule
|
||||
|
||||
> **ALWAYS use `filename` parameter when calling `browser_snapshot`, `browser_console_messages`, or `browser_network_requests`.**
|
||||
> Then process via `ctx_index(path: ...)` or `ctx_execute_file(path: ...)` — never `ctx_index(content: ...)`.
|
||||
>
|
||||
> Data flow: **Playwright → file → server-side read → context**. Never: **Playwright → context → ctx_index(content) → context again**.
|
||||
|
||||
## Subagent Usage
|
||||
|
||||
Subagents automatically receive context-mode tool routing via a PreToolUse hook. You do NOT need to manually add tool names to subagent prompts — the hook injects them. Just write natural task descriptions.
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
- Using `curl http://api/endpoint` via Bash → 50KB floods context. Use `ctx_execute` with fetch instead.
|
||||
- Using `cat large-file.json` via Bash → entire file in context. Use `ctx_execute_file` instead.
|
||||
- Using `gh pr list` via Bash → raw JSON in context. Use `ctx_execute` with `--jq` filter instead.
|
||||
- Piping Bash output through `| head -20` → you lose the rest. Use `ctx_execute` to analyze ALL data and print summary.
|
||||
- Narrowing `ctx_execute` output upstream of capture → `ctx_execute` captures, `ctx_search` filters; merging the layers drops data that the index never sees. See `references/anti-patterns.md` §8.
|
||||
- Running `npm test` via Bash → full test output in context. Use `ctx_execute` to capture and summarize.
|
||||
- Calling `browser_snapshot()` WITHOUT `filename` parameter → 135K tokens flood context. **Always** use `browser_snapshot(filename: "/tmp/snap.md")`.
|
||||
- Calling `browser_console_messages()` or `browser_network_requests()` WITHOUT `filename` → entire output floods context. **Always** use the `filename` parameter.
|
||||
- Passing ANY large data to `ctx_index(content: ...)` → data enters context as a parameter. **Always** use `ctx_index(path: ...)` to read server-side. The `content` parameter should only be used for small inline text you're composing yourself.
|
||||
- Calling an MCP tool (Context7 `query-docs`, GitHub API, etc.) then passing the response to `ctx_index(content: response)` → **doubles** context usage. The response is already in context — use it directly or save to file first.
|
||||
- Ignoring `browser_navigate` auto-snapshot → navigation response includes a full page snapshot. Don't rely on it for inspection — call `browser_snapshot(filename)` separately.
|
||||
- Expecting `ctx_stats` to reset or wipe anything → `ctx_stats` is read-only (shows stats only). Use `ctx_purge(confirm: true)` to permanently delete all indexed content.
|
||||
|
||||
## Reference Files
|
||||
|
||||
- [JavaScript/TypeScript Patterns](./references/patterns-javascript.md)
|
||||
- [Python Patterns](./references/patterns-python.md)
|
||||
- [Shell Patterns](./references/patterns-shell.md)
|
||||
- [Anti-Patterns & Common Mistakes](./references/anti-patterns.md)
|
||||
@@ -0,0 +1,283 @@
|
||||
# Anti-Patterns: Common Mistakes with execute / execute_file
|
||||
|
||||
Avoid these pitfalls when using context-mode tools.
|
||||
|
||||
---
|
||||
|
||||
## 1. Using execute for Small Outputs (< 20 Lines)
|
||||
|
||||
**Problem:** `execute` adds overhead (LLM summarization call). For small outputs, Bash is faster and cheaper.
|
||||
|
||||
```
|
||||
BAD — wasteful use of execute:
|
||||
Tool: execute
|
||||
code: "echo $(node --version)"
|
||||
language: shell
|
||||
|
||||
GOOD — just use Bash:
|
||||
Tool: Bash
|
||||
command: node --version
|
||||
```
|
||||
|
||||
**Rule:** If the output fits comfortably in your context window (under ~20 lines), use Bash directly. Reserve `execute` for outputs that would bloat context or need intelligent summarization.
|
||||
|
||||
More examples of "just use Bash":
|
||||
- `git status` — usually 5-10 lines
|
||||
- `ls -la` — directory listing
|
||||
- `cat .env.example` — small config file
|
||||
- `pwd`, `whoami`, `which node`
|
||||
- `wc -l src/index.ts` — single line output
|
||||
|
||||
---
|
||||
|
||||
## 2. Forgetting to Print Output
|
||||
|
||||
**Problem:** `execute` captures stdout. If your code doesn't print anything, the summary will be empty or meaningless.
|
||||
|
||||
```javascript
|
||||
// BAD — no output:
|
||||
const fs = require('fs');
|
||||
const data = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
||||
const deps = Object.keys(data.dependencies);
|
||||
// Nothing printed! The LLM sees empty stdout.
|
||||
|
||||
// GOOD — explicit output:
|
||||
const fs = require('fs');
|
||||
const data = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
||||
const deps = Object.keys(data.dependencies);
|
||||
console.log(`Dependencies (${deps.length}):`);
|
||||
deps.forEach(d => console.log(` ${d}: ${data.dependencies[d]}`));
|
||||
```
|
||||
|
||||
```python
|
||||
# BAD — computes but never prints:
|
||||
with open('data.json') as f:
|
||||
data = json.load(f)
|
||||
result = [x for x in data if x['status'] == 'error']
|
||||
# result is lost — never printed
|
||||
|
||||
# GOOD — always print results:
|
||||
with open('data.json') as f:
|
||||
data = json.load(f)
|
||||
result = [x for x in data if x['status'] == 'error']
|
||||
print(f"Found {len(result)} errors:")
|
||||
for r in result:
|
||||
print(f" {r['id']}: {r['message']}")
|
||||
```
|
||||
|
||||
**Rule:** Every `execute` script must end with print/console.log of the results you want summarized.
|
||||
|
||||
---
|
||||
|
||||
## 3. Using Bash When JS/Python Would Be Cleaner
|
||||
|
||||
**Problem:** Complex data processing in Bash quickly becomes unreadable and error-prone.
|
||||
|
||||
```shell
|
||||
# BAD — parsing JSON in Bash is fragile:
|
||||
cat data.json | python3 -c "
|
||||
import sys, json
|
||||
data = json.load(sys.stdin)
|
||||
for item in data:
|
||||
if item['status'] == 'error':
|
||||
print(item['id'], item['message'])
|
||||
"
|
||||
# If you're already using Python inline, just use language: python
|
||||
```
|
||||
|
||||
```javascript
|
||||
// GOOD — use the right language for the job:
|
||||
// language: javascript
|
||||
const data = require('./data.json');
|
||||
data.filter(x => x.status === 'error')
|
||||
.forEach(x => console.log(`${x.id}: ${x.message}`));
|
||||
```
|
||||
|
||||
**Rule:** If your Bash script contains inline Python/Node or complex `jq`/`awk` chains, switch to `language: python` or `language: javascript` instead.
|
||||
|
||||
Signs you should switch from shell:
|
||||
- Using `python3 -c` or `node -e` inside the shell script
|
||||
- More than 3 pipes chained together
|
||||
- Using `jq` for complex JSON transformations
|
||||
- Nested loops in Bash
|
||||
- String manipulation beyond simple `cut`/`sed`
|
||||
|
||||
---
|
||||
|
||||
## 4. Loading Entire Files into Context Then Processing
|
||||
|
||||
**Problem:** Reading a 10,000-line file with `Read` tool, then asking about it, wastes your entire context window. Use `execute` to process the file and return only the summary.
|
||||
|
||||
```
|
||||
BAD workflow:
|
||||
1. Read tool: read 'server.log' (10,000 lines loaded into context)
|
||||
2. "Find all errors in this log"
|
||||
→ 10,000 lines consumed context for a question that needs ~20 lines of output
|
||||
|
||||
GOOD workflow:
|
||||
1. execute with language: python
|
||||
code: |
|
||||
with open('server.log') as f:
|
||||
errors = [l for l in f if 'ERROR' in l]
|
||||
print(f"Total errors: {len(errors)}")
|
||||
for e in errors[-20:]:
|
||||
print(e.strip())
|
||||
summary_prompt: "Categorize errors and report frequency"
|
||||
→ Only the summary enters context
|
||||
```
|
||||
|
||||
```
|
||||
BAD workflow:
|
||||
1. Read tool: read 'package-lock.json' (20,000 lines)
|
||||
2. "What version of lodash is installed?"
|
||||
|
||||
GOOD workflow:
|
||||
1. execute with language: javascript
|
||||
code: |
|
||||
const lock = require('./package-lock.json');
|
||||
const find = (deps, name) => {
|
||||
if (deps[name]) return deps[name].version;
|
||||
for (const [, dep] of Object.entries(deps)) {
|
||||
if (dep.dependencies) {
|
||||
const v = find(dep.dependencies, name);
|
||||
if (v) return v;
|
||||
}
|
||||
}
|
||||
};
|
||||
console.log(`lodash: ${find(lock.dependencies, 'lodash') || 'not found'}`);
|
||||
summary_prompt: "Report the installed version of lodash"
|
||||
```
|
||||
|
||||
**Rule:** If a file is over 200 lines and you only need specific data from it, use `execute` to extract what you need rather than reading the whole file into context.
|
||||
|
||||
---
|
||||
|
||||
## 5. Not Using JSON.stringify for Structured Output
|
||||
|
||||
**Problem:** Printing objects without serialization gives `[object Object]` in JavaScript.
|
||||
|
||||
```javascript
|
||||
// BAD — prints [object Object]:
|
||||
const pkg = require('./package.json');
|
||||
console.log(pkg.dependencies);
|
||||
// Output: [object Object]
|
||||
|
||||
// GOOD — serialize properly:
|
||||
const pkg = require('./package.json');
|
||||
console.log(JSON.stringify(pkg.dependencies, null, 2));
|
||||
// Output: { "react": "^18.2.0", "next": "^14.0.0", ... }
|
||||
```
|
||||
|
||||
```javascript
|
||||
// BAD — loses structure in arrays:
|
||||
const items = [{name: 'a', value: 1}, {name: 'b', value: 2}];
|
||||
console.log(items);
|
||||
// May print unhelpfully
|
||||
|
||||
// GOOD — format as table:
|
||||
const items = [{name: 'a', value: 1}, {name: 'b', value: 2}];
|
||||
console.log('Name | Value');
|
||||
console.log('------|------');
|
||||
items.forEach(i => console.log(`${i.name.padEnd(5)} | ${i.value}`));
|
||||
// Or use JSON.stringify:
|
||||
console.log(JSON.stringify(items, null, 2));
|
||||
```
|
||||
|
||||
**Rule:** Always use `JSON.stringify(data, null, 2)` for objects/arrays in JavaScript, or format as a readable table. In Python, use `json.dumps(data, indent=2)` or `pprint.pprint(data)`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Timeout Too Short for Network Operations
|
||||
|
||||
**Problem:** Default timeout may be too short for API calls, builds, or test suites.
|
||||
|
||||
```
|
||||
BAD — will timeout on API calls:
|
||||
Tool: execute
|
||||
code: |
|
||||
const resp = await fetch('https://api.slow-service.com/data');
|
||||
console.log(await resp.json());
|
||||
language: javascript
|
||||
timeout_ms: 5000 ← API may take 10+ seconds
|
||||
|
||||
GOOD — generous timeout for network:
|
||||
Tool: execute
|
||||
code: |
|
||||
const resp = await fetch('https://api.slow-service.com/data');
|
||||
console.log(JSON.stringify(await resp.json(), null, 2));
|
||||
language: javascript
|
||||
timeout_ms: 30000 ← 30 seconds for network calls
|
||||
```
|
||||
|
||||
**Recommended timeouts:**
|
||||
| Operation | timeout_ms |
|
||||
|-----------|-----------|
|
||||
| File reading/parsing | 5000 - 10000 |
|
||||
| Local computation | 10000 |
|
||||
| Single API request | 15000 - 30000 |
|
||||
| Paginated API calls | 30000 - 60000 |
|
||||
| npm install / build | 120000 |
|
||||
| Full test suite | 120000 - 300000 |
|
||||
|
||||
**Rule:** Always consider what your script does and set `timeout_ms` accordingly. Network calls and builds need significantly more time than file operations.
|
||||
|
||||
---
|
||||
|
||||
## 7. Not Using summary_prompt Effectively
|
||||
|
||||
**Problem:** Without a good `summary_prompt`, the LLM summarization may focus on irrelevant details.
|
||||
|
||||
```
|
||||
BAD — vague or missing summary_prompt:
|
||||
summary_prompt: "Summarize this"
|
||||
→ May focus on the wrong aspects
|
||||
|
||||
GOOD — specific and actionable:
|
||||
summary_prompt: "Report the count of failing tests, list each failure with its file path and error message, and identify any patterns in the failures"
|
||||
```
|
||||
|
||||
**Tips for effective summary_prompt:**
|
||||
- Be specific about what data points you need
|
||||
- Ask for counts and metrics, not just descriptions
|
||||
- Request actionable insights ("suggest fixes", "identify patterns")
|
||||
- Mention the format you want ("list as bullet points", "group by category")
|
||||
|
||||
---
|
||||
|
||||
## 8. `ctx_execute` Captures, `ctx_search` Filters — Don't Merge the Layers
|
||||
|
||||
`ctx_execute` and `ctx_search` are two layers, not one. `ctx_execute` exists to **capture** full output into the index. `ctx_search` exists to **filter** what was captured. When you narrow the output *inside* `ctx_execute` — at the shell layer, in script logic, anywhere upstream of capture — the dropped lines never reach the index. `ctx_search` cannot recover what was never written. You've spent the capture budget and lost the data you'd want to query later, for no context-window benefit: large stdout is already auto-indexed, not returned inline.
|
||||
|
||||
The mental model:
|
||||
|
||||
```
|
||||
┌──────────────────────┐ ┌──────────────────────┐
|
||||
│ ctx_execute │ ───▶ │ ctx_search │
|
||||
│ (capture layer) │ │ (filter layer) │
|
||||
│ │ │ │
|
||||
│ produces full │ │ queries the │
|
||||
│ output into index │ │ captured index │
|
||||
└──────────────────────┘ └──────────────────────┘
|
||||
▲ ▲
|
||||
│ │
|
||||
Job: capture Job: narrow
|
||||
Do NOT narrow here. Do all narrowing here.
|
||||
```
|
||||
|
||||
**Rule:** Treat `ctx_execute`'s output as write-once to the index. Run the command in full and let it index. Do every narrowing step downstream, via `ctx_search`. If you find yourself trimming inside `ctx_execute`, you are doing the filter layer's job in the capture layer — stop and move the narrowing to a `ctx_search` call.
|
||||
|
||||
**Why the layer separation matters:** the index is what survives across calls and across sessions. Anything you discard before the index is gone permanently from this session's queryable surface. Anything you keep is queryable, repeatedly, with different questions, at zero re-execution cost.
|
||||
|
||||
---
|
||||
|
||||
## Summary Checklist
|
||||
|
||||
Before using `execute`, verify:
|
||||
|
||||
- [ ] Output will be > 20 lines (otherwise use Bash)
|
||||
- [ ] Script prints all results to stdout
|
||||
- [ ] Objects are serialized with JSON.stringify / json.dumps
|
||||
- [ ] Timeout matches the operation type
|
||||
- [ ] Language matches the task (JS for JSON/API, Python for data, Shell for pipes)
|
||||
- [ ] summary_prompt is specific and actionable
|
||||
- [ ] Not loading a file into context that could be processed inside execute
|
||||
@@ -0,0 +1,298 @@
|
||||
# JavaScript / TypeScript Patterns for execute
|
||||
|
||||
Practical patterns for using `execute` with `language: javascript`.
|
||||
All examples assume Node.js runtime with native fetch (Node 18+).
|
||||
|
||||
---
|
||||
|
||||
## API Response Processing
|
||||
|
||||
### Fetch and summarize a REST API
|
||||
|
||||
```javascript
|
||||
// execute: Analyze API health endpoint
|
||||
const resp = await fetch('https://api.example.com/health');
|
||||
const data = await resp.json();
|
||||
|
||||
console.log('=== Service Health ===');
|
||||
console.log(`Status: ${data.status}`);
|
||||
console.log(`Uptime: ${data.uptime}`);
|
||||
console.log(`Timestamp: ${data.timestamp}`);
|
||||
|
||||
if (data.services) {
|
||||
console.log('\n=== Service Components ===');
|
||||
for (const [name, info] of Object.entries(data.services)) {
|
||||
console.log(` ${name}: ${info.status} (latency: ${info.latency_ms}ms)`);
|
||||
}
|
||||
}
|
||||
|
||||
if (data.errors && data.errors.length > 0) {
|
||||
console.log('\n=== Recent Errors ===');
|
||||
data.errors.slice(0, 10).forEach(e => {
|
||||
console.log(` [${e.timestamp}] ${e.code}: ${e.message}`);
|
||||
});
|
||||
}
|
||||
```
|
||||
> summary_prompt: "Report overall health, list any degraded services, and highlight errors"
|
||||
|
||||
### Paginated API collection
|
||||
|
||||
```javascript
|
||||
// execute: Fetch all open issues from GitHub API
|
||||
const owner = 'org';
|
||||
const repo = 'project';
|
||||
let page = 1;
|
||||
let allIssues = [];
|
||||
|
||||
while (true) {
|
||||
const resp = await fetch(
|
||||
`https://api.github.com/repos/${owner}/${repo}/issues?state=open&per_page=100&page=${page}`,
|
||||
{ headers: { 'Accept': 'application/vnd.github.v3+json' } }
|
||||
);
|
||||
const issues = await resp.json();
|
||||
if (issues.length === 0) break;
|
||||
allIssues.push(...issues);
|
||||
page++;
|
||||
}
|
||||
|
||||
console.log(`Total open issues: ${allIssues.length}\n`);
|
||||
|
||||
// Group by labels
|
||||
const byLabel = {};
|
||||
allIssues.forEach(issue => {
|
||||
issue.labels.forEach(label => {
|
||||
byLabel[label.name] = (byLabel[label.name] || 0) + 1;
|
||||
});
|
||||
});
|
||||
|
||||
console.log('=== Issues by Label ===');
|
||||
Object.entries(byLabel)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.forEach(([label, count]) => console.log(` ${label}: ${count}`));
|
||||
|
||||
// Oldest issues
|
||||
console.log('\n=== 10 Oldest Issues ===');
|
||||
allIssues
|
||||
.sort((a, b) => new Date(a.created_at) - new Date(b.created_at))
|
||||
.slice(0, 10)
|
||||
.forEach(i => console.log(` #${i.number} (${i.created_at.slice(0,10)}): ${i.title}`));
|
||||
```
|
||||
> summary_prompt: "Summarize issue distribution by label, highlight stale issues, suggest priorities"
|
||||
> timeout_ms: 30000
|
||||
|
||||
---
|
||||
|
||||
## JSON Data Analysis
|
||||
|
||||
### Analyze a large JSON config file
|
||||
|
||||
```javascript
|
||||
const fs = require('fs');
|
||||
const data = JSON.parse(fs.readFileSync('tsconfig.json', 'utf8'));
|
||||
|
||||
console.log('=== TSConfig Analysis ===');
|
||||
console.log(`Target: ${data.compilerOptions?.target}`);
|
||||
console.log(`Module: ${data.compilerOptions?.module}`);
|
||||
console.log(`Strict: ${data.compilerOptions?.strict}`);
|
||||
console.log(`Paths aliases: ${Object.keys(data.compilerOptions?.paths || {}).length}`);
|
||||
|
||||
if (data.compilerOptions?.paths) {
|
||||
console.log('\n=== Path Aliases ===');
|
||||
for (const [alias, targets] of Object.entries(data.compilerOptions.paths)) {
|
||||
console.log(` ${alias} -> ${targets.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (data.include) console.log(`\nInclude: ${data.include.join(', ')}`);
|
||||
if (data.exclude) console.log(`Exclude: ${data.exclude.join(', ')}`);
|
||||
if (data.references) {
|
||||
console.log(`\nProject References: ${data.references.length}`);
|
||||
data.references.forEach(r => console.log(` ${r.path}`));
|
||||
}
|
||||
```
|
||||
> summary_prompt: "Report compiler strictness, module system, and any unusual configuration"
|
||||
|
||||
### Diff two JSON files
|
||||
|
||||
```javascript
|
||||
const fs = require('fs');
|
||||
const a = JSON.parse(fs.readFileSync('config.prod.json', 'utf8'));
|
||||
const b = JSON.parse(fs.readFileSync('config.staging.json', 'utf8'));
|
||||
|
||||
function diffObjects(obj1, obj2, path = '') {
|
||||
const allKeys = new Set([...Object.keys(obj1 || {}), ...Object.keys(obj2 || {})]);
|
||||
for (const key of allKeys) {
|
||||
const fullPath = path ? `${path}.${key}` : key;
|
||||
if (!(key in (obj1 || {}))) {
|
||||
console.log(`+ ${fullPath}: ${JSON.stringify(obj2[key])}`);
|
||||
} else if (!(key in (obj2 || {}))) {
|
||||
console.log(`- ${fullPath}: ${JSON.stringify(obj1[key])}`);
|
||||
} else if (typeof obj1[key] === 'object' && typeof obj2[key] === 'object') {
|
||||
diffObjects(obj1[key], obj2[key], fullPath);
|
||||
} else if (JSON.stringify(obj1[key]) !== JSON.stringify(obj2[key])) {
|
||||
console.log(`~ ${fullPath}: ${JSON.stringify(obj1[key])} -> ${JSON.stringify(obj2[key])}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('=== Config Diff: prod vs staging ===');
|
||||
diffObjects(a, b);
|
||||
```
|
||||
> summary_prompt: "List all configuration differences between prod and staging environments"
|
||||
|
||||
---
|
||||
|
||||
## Package.json / Lock File Analysis
|
||||
|
||||
### Dependency audit
|
||||
|
||||
```javascript
|
||||
const fs = require('fs');
|
||||
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
||||
|
||||
const deps = Object.entries(pkg.dependencies || {});
|
||||
const devDeps = Object.entries(pkg.devDependencies || {});
|
||||
|
||||
console.log(`Package: ${pkg.name}@${pkg.version}`);
|
||||
console.log(`Dependencies: ${deps.length}`);
|
||||
console.log(`DevDependencies: ${devDeps.length}`);
|
||||
|
||||
// Find non-pinned versions
|
||||
console.log('\n=== Non-Pinned Dependencies ===');
|
||||
[...deps, ...devDeps].forEach(([name, version]) => {
|
||||
if (version.startsWith('^') || version.startsWith('~') || version === '*') {
|
||||
console.log(` ${name}: ${version}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Find duplicated categories
|
||||
console.log('\n=== Scripts ===');
|
||||
Object.entries(pkg.scripts || {}).forEach(([name, cmd]) => {
|
||||
console.log(` ${name}: ${cmd}`);
|
||||
});
|
||||
|
||||
// Workspace detection
|
||||
if (pkg.workspaces) {
|
||||
console.log('\n=== Monorepo Workspaces ===');
|
||||
const ws = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces.packages || [];
|
||||
ws.forEach(w => console.log(` ${w}`));
|
||||
}
|
||||
```
|
||||
> summary_prompt: "Report dependency health: unpinned versions, total count, any security concerns from package names"
|
||||
|
||||
### Lock file drift detection
|
||||
|
||||
```javascript
|
||||
const fs = require('fs');
|
||||
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
||||
|
||||
let lockExists = { npm: false, yarn: false, pnpm: false };
|
||||
try { fs.accessSync('package-lock.json'); lockExists.npm = true; } catch {}
|
||||
try { fs.accessSync('yarn.lock'); lockExists.yarn = true; } catch {}
|
||||
try { fs.accessSync('pnpm-lock.yaml'); lockExists.pnpm = true; } catch {}
|
||||
|
||||
console.log('=== Lock File Status ===');
|
||||
Object.entries(lockExists).forEach(([mgr, exists]) => {
|
||||
console.log(` ${mgr}: ${exists ? 'PRESENT' : 'missing'}`);
|
||||
});
|
||||
|
||||
const activeLocks = Object.entries(lockExists).filter(([, v]) => v);
|
||||
if (activeLocks.length > 1) {
|
||||
console.log('\nWARNING: Multiple lock files detected! This causes inconsistent installs.');
|
||||
}
|
||||
if (activeLocks.length === 0) {
|
||||
console.log('\nWARNING: No lock file found! Dependencies are not reproducible.');
|
||||
}
|
||||
|
||||
// Check engines
|
||||
if (pkg.engines) {
|
||||
console.log('\n=== Required Engines ===');
|
||||
Object.entries(pkg.engines).forEach(([e, v]) => console.log(` ${e}: ${v}`));
|
||||
}
|
||||
```
|
||||
> summary_prompt: "Report lock file health and any warnings about package management"
|
||||
|
||||
---
|
||||
|
||||
## File Content Parsing
|
||||
|
||||
### Parse and summarize a large markdown file
|
||||
|
||||
```javascript
|
||||
const fs = require('fs');
|
||||
const content = fs.readFileSync('CHANGELOG.md', 'utf8');
|
||||
const lines = content.split('\n');
|
||||
|
||||
const sections = [];
|
||||
let currentSection = null;
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('## ')) {
|
||||
if (currentSection) sections.push(currentSection);
|
||||
currentSection = { title: line.replace('## ', ''), items: 0, breaking: 0 };
|
||||
} else if (currentSection && line.startsWith('- ')) {
|
||||
currentSection.items++;
|
||||
if (line.toLowerCase().includes('breaking') || line.toLowerCase().includes('BREAKING')) {
|
||||
currentSection.breaking++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (currentSection) sections.push(currentSection);
|
||||
|
||||
console.log(`Total versions: ${sections.length}\n`);
|
||||
console.log('=== Recent Versions ===');
|
||||
sections.slice(0, 10).forEach(s => {
|
||||
const warn = s.breaking > 0 ? ` [${s.breaking} BREAKING]` : '';
|
||||
console.log(` ${s.title}: ${s.items} changes${warn}`);
|
||||
});
|
||||
|
||||
const totalBreaking = sections.reduce((sum, s) => sum + s.breaking, 0);
|
||||
if (totalBreaking > 0) {
|
||||
console.log(`\nTotal breaking changes across all versions: ${totalBreaking}`);
|
||||
}
|
||||
```
|
||||
> summary_prompt: "Summarize recent releases, highlight breaking changes, report release cadence"
|
||||
|
||||
---
|
||||
|
||||
## Test Output Parsing
|
||||
|
||||
### Run tests and extract failures
|
||||
|
||||
```javascript
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
let output;
|
||||
try {
|
||||
output = execSync('npx jest --json 2>/dev/null', { encoding: 'utf8', maxBuffer: 50 * 1024 * 1024 });
|
||||
} catch (e) {
|
||||
output = e.stdout || '';
|
||||
}
|
||||
|
||||
try {
|
||||
const results = JSON.parse(output);
|
||||
console.log(`=== Test Results ===`);
|
||||
console.log(`Suites: ${results.numPassedTestSuites} passed, ${results.numFailedTestSuites} failed`);
|
||||
console.log(`Tests: ${results.numPassedTests} passed, ${results.numFailedTests} failed`);
|
||||
console.log(`Time: ${(results.testResults || []).reduce((s, t) => s + (t.endTime - t.startTime), 0)}ms`);
|
||||
|
||||
const failures = (results.testResults || []).filter(t => t.status === 'failed');
|
||||
if (failures.length > 0) {
|
||||
console.log('\n=== Failed Tests ===');
|
||||
failures.forEach(suite => {
|
||||
console.log(`\nSuite: ${suite.name}`);
|
||||
(suite.assertionResults || [])
|
||||
.filter(a => a.status === 'failed')
|
||||
.forEach(a => {
|
||||
console.log(` FAIL: ${a.ancestorTitles.join(' > ')} > ${a.title}`);
|
||||
console.log(` ${(a.failureMessages || []).join('\n ').slice(0, 200)}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
console.log('Could not parse JSON output. Raw output:');
|
||||
console.log(output.slice(0, 5000));
|
||||
}
|
||||
```
|
||||
> summary_prompt: "Report test pass/fail counts, list each failing test with its error message"
|
||||
> timeout_ms: 60000
|
||||
@@ -0,0 +1,304 @@
|
||||
# Python Patterns for execute
|
||||
|
||||
Practical patterns for using `execute` with `language: python`.
|
||||
All examples use Python standard library only (no pip installs required).
|
||||
|
||||
---
|
||||
|
||||
## Data Processing with json Module
|
||||
|
||||
### Analyze a large JSON dataset
|
||||
|
||||
```python
|
||||
import json
|
||||
|
||||
with open('data/users.json') as f:
|
||||
users = json.load(f)
|
||||
|
||||
print(f"Total users: {len(users)}")
|
||||
|
||||
# Group by status
|
||||
from collections import Counter
|
||||
statuses = Counter(u.get('status', 'unknown') for u in users)
|
||||
print("\n=== Users by Status ===")
|
||||
for status, count in statuses.most_common():
|
||||
print(f" {status}: {count}")
|
||||
|
||||
# Find anomalies
|
||||
inactive_with_recent = [
|
||||
u for u in users
|
||||
if u.get('status') == 'inactive' and u.get('last_login', '') > '2025-01-01'
|
||||
]
|
||||
if inactive_with_recent:
|
||||
print(f"\n=== Anomaly: {len(inactive_with_recent)} inactive users with recent logins ===")
|
||||
for u in inactive_with_recent[:10]:
|
||||
print(f" {u['email']} - last login: {u['last_login']}")
|
||||
|
||||
# Field completeness
|
||||
fields = ['name', 'email', 'phone', 'address']
|
||||
print("\n=== Field Completeness ===")
|
||||
for field in fields:
|
||||
filled = sum(1 for u in users if u.get(field))
|
||||
pct = (filled / len(users)) * 100 if users else 0
|
||||
print(f" {field}: {filled}/{len(users)} ({pct:.1f}%)")
|
||||
```
|
||||
> summary_prompt: "Report user distribution, data quality issues, and any anomalies found"
|
||||
|
||||
### Merge and compare two JSON configs
|
||||
|
||||
```python
|
||||
import json
|
||||
|
||||
with open('config.default.json') as f:
|
||||
defaults = json.load(f)
|
||||
with open('config.local.json') as f:
|
||||
local = json.load(f)
|
||||
|
||||
def compare(d1, d2, path=""):
|
||||
diffs = []
|
||||
all_keys = set(list(d1.keys()) + list(d2.keys()))
|
||||
for key in sorted(all_keys):
|
||||
full_path = f"{path}.{key}" if path else key
|
||||
if key not in d1:
|
||||
diffs.append(f" + {full_path} = {json.dumps(d2[key])}")
|
||||
elif key not in d2:
|
||||
diffs.append(f" - {full_path} = {json.dumps(d1[key])}")
|
||||
elif isinstance(d1[key], dict) and isinstance(d2[key], dict):
|
||||
diffs.extend(compare(d1[key], d2[key], full_path))
|
||||
elif d1[key] != d2[key]:
|
||||
diffs.append(f" ~ {full_path}: {json.dumps(d1[key])} -> {json.dumps(d2[key])}")
|
||||
return diffs
|
||||
|
||||
diffs = compare(defaults, local)
|
||||
print(f"Config differences: {len(diffs)}")
|
||||
if diffs:
|
||||
print("\n=== Changes (local overrides) ===")
|
||||
for d in diffs:
|
||||
print(d)
|
||||
else:
|
||||
print("No differences found — local matches defaults.")
|
||||
```
|
||||
> summary_prompt: "List all local config overrides and flag any potentially dangerous changes"
|
||||
|
||||
---
|
||||
|
||||
## CSV / Log File Analysis
|
||||
|
||||
### Analyze a CSV file
|
||||
|
||||
```python
|
||||
import csv
|
||||
from collections import Counter, defaultdict
|
||||
from datetime import datetime
|
||||
|
||||
with open('data/transactions.csv') as f:
|
||||
reader = csv.DictReader(f)
|
||||
rows = list(reader)
|
||||
|
||||
print(f"Total records: {len(rows)}")
|
||||
print(f"Columns: {', '.join(rows[0].keys()) if rows else 'none'}")
|
||||
|
||||
# Summary statistics for numeric column
|
||||
amounts = [float(r['amount']) for r in rows if r.get('amount')]
|
||||
if amounts:
|
||||
print(f"\n=== Amount Statistics ===")
|
||||
print(f" Min: ${min(amounts):,.2f}")
|
||||
print(f" Max: ${max(amounts):,.2f}")
|
||||
print(f" Mean: ${sum(amounts)/len(amounts):,.2f}")
|
||||
print(f" Median: ${sorted(amounts)[len(amounts)//2]:,.2f}")
|
||||
print(f" Total: ${sum(amounts):,.2f}")
|
||||
|
||||
# Group by category
|
||||
if 'category' in rows[0]:
|
||||
by_cat = defaultdict(list)
|
||||
for r in rows:
|
||||
by_cat[r['category']].append(float(r.get('amount', 0)))
|
||||
print("\n=== By Category ===")
|
||||
for cat, vals in sorted(by_cat.items(), key=lambda x: -sum(x[1])):
|
||||
print(f" {cat}: {len(vals)} txns, total ${sum(vals):,.2f}")
|
||||
```
|
||||
> summary_prompt: "Summarize transaction patterns, highlight outliers, report category distribution"
|
||||
|
||||
### Parse application logs
|
||||
|
||||
```python
|
||||
import re
|
||||
from collections import Counter
|
||||
from datetime import datetime
|
||||
|
||||
error_pattern = re.compile(r'\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] (\w+): (.+)')
|
||||
|
||||
levels = Counter()
|
||||
errors_by_type = Counter()
|
||||
hourly = Counter()
|
||||
|
||||
with open('app.log') as f:
|
||||
for line in f:
|
||||
match = error_pattern.match(line.strip())
|
||||
if match:
|
||||
timestamp, level, message = match.groups()
|
||||
levels[level] += 1
|
||||
hour = timestamp[:13]
|
||||
hourly[hour] += 1
|
||||
if level in ('ERROR', 'FATAL'):
|
||||
# Extract error class
|
||||
err_type = message.split(':')[0].strip()
|
||||
errors_by_type[err_type] += 1
|
||||
|
||||
print("=== Log Level Distribution ===")
|
||||
for level, count in levels.most_common():
|
||||
print(f" {level}: {count}")
|
||||
|
||||
print("\n=== Top Error Types ===")
|
||||
for err, count in errors_by_type.most_common(10):
|
||||
print(f" {err}: {count}")
|
||||
|
||||
print("\n=== Hourly Activity (last 24h) ===")
|
||||
for hour, count in sorted(hourly.items())[-24:]:
|
||||
bar = '#' * min(count // 10, 50)
|
||||
print(f" {hour}: {count:>5} {bar}")
|
||||
```
|
||||
> summary_prompt: "Report error rates, identify the most common failures, and note any traffic spikes"
|
||||
|
||||
---
|
||||
|
||||
## Text Extraction and Summarization
|
||||
|
||||
### Extract TODOs and FIXMEs from codebase
|
||||
|
||||
```python
|
||||
import os
|
||||
import re
|
||||
|
||||
pattern = re.compile(r'(TODO|FIXME|HACK|XXX|WARN)[:\s](.+)', re.IGNORECASE)
|
||||
results = []
|
||||
|
||||
for root, dirs, files in os.walk('src'):
|
||||
# Skip node_modules and hidden dirs
|
||||
dirs[:] = [d for d in dirs if not d.startswith('.') and d != 'node_modules']
|
||||
for fname in files:
|
||||
if fname.endswith(('.ts', '.tsx', '.js', '.jsx', '.py')):
|
||||
filepath = os.path.join(root, fname)
|
||||
with open(filepath) as f:
|
||||
for i, line in enumerate(f, 1):
|
||||
match = pattern.search(line)
|
||||
if match:
|
||||
results.append({
|
||||
'file': filepath,
|
||||
'line': i,
|
||||
'type': match.group(1).upper(),
|
||||
'text': match.group(2).strip()
|
||||
})
|
||||
|
||||
from collections import Counter
|
||||
by_type = Counter(r['type'] for r in results)
|
||||
|
||||
print(f"Total annotations found: {len(results)}\n")
|
||||
print("=== By Type ===")
|
||||
for t, c in by_type.most_common():
|
||||
print(f" {t}: {c}")
|
||||
|
||||
print("\n=== All Items ===")
|
||||
for r in results:
|
||||
print(f" [{r['type']}] {r['file']}:{r['line']} — {r['text'][:100]}")
|
||||
```
|
||||
> summary_prompt: "Categorize TODOs by urgency, group by file area, suggest which to address first"
|
||||
|
||||
### Summarize a large text/markdown file
|
||||
|
||||
```python
|
||||
with open('ARCHITECTURE.md') as f:
|
||||
content = f.read()
|
||||
|
||||
lines = content.split('\n')
|
||||
print(f"Total lines: {len(lines)}")
|
||||
print(f"Total words: {len(content.split())}")
|
||||
|
||||
# Extract structure
|
||||
headings = [(i+1, line) for i, line in enumerate(lines) if line.startswith('#')]
|
||||
print(f"Sections: {len(headings)}\n")
|
||||
|
||||
print("=== Document Structure ===")
|
||||
for line_num, heading in headings:
|
||||
level = len(heading) - len(heading.lstrip('#'))
|
||||
indent = ' ' * (level - 1)
|
||||
print(f" {indent}{heading.strip()} (line {line_num})")
|
||||
|
||||
# Extract code blocks
|
||||
import re
|
||||
code_blocks = re.findall(r'```(\w+)?', content)
|
||||
if code_blocks:
|
||||
from collections import Counter
|
||||
langs = Counter(b for b in code_blocks if b)
|
||||
print(f"\n=== Code Blocks: {len(code_blocks)} total ===")
|
||||
for lang, count in langs.most_common():
|
||||
print(f" {lang}: {count}")
|
||||
|
||||
# Print first 50 lines for content preview
|
||||
print("\n=== Content Preview (first 50 lines) ===")
|
||||
for line in lines[:50]:
|
||||
print(line)
|
||||
```
|
||||
> summary_prompt: "Summarize the document structure, key architectural decisions, and main components described"
|
||||
|
||||
---
|
||||
|
||||
## File Comparison
|
||||
|
||||
### Compare two source files
|
||||
|
||||
```python
|
||||
import difflib
|
||||
|
||||
with open('src/auth/login.ts') as f:
|
||||
old_lines = f.readlines()
|
||||
with open('src/auth/login.new.ts') as f:
|
||||
new_lines = f.readlines()
|
||||
|
||||
diff = list(difflib.unified_diff(old_lines, new_lines, fromfile='login.ts', tofile='login.new.ts', lineterm=''))
|
||||
|
||||
additions = sum(1 for l in diff if l.startswith('+') and not l.startswith('+++'))
|
||||
deletions = sum(1 for l in diff if l.startswith('-') and not l.startswith('---'))
|
||||
|
||||
print(f"Changes: +{additions} -{deletions}\n")
|
||||
|
||||
if diff:
|
||||
print("=== Diff ===")
|
||||
for line in diff:
|
||||
print(line)
|
||||
else:
|
||||
print("Files are identical.")
|
||||
```
|
||||
> summary_prompt: "Describe the functional changes between the old and new versions"
|
||||
|
||||
### Find duplicate content across files
|
||||
|
||||
```python
|
||||
import os
|
||||
import hashlib
|
||||
from collections import defaultdict
|
||||
|
||||
file_hashes = defaultdict(list)
|
||||
|
||||
for root, dirs, files in os.walk('src'):
|
||||
dirs[:] = [d for d in dirs if not d.startswith('.') and d != 'node_modules']
|
||||
for fname in files:
|
||||
if fname.endswith(('.ts', '.tsx', '.js', '.jsx')):
|
||||
filepath = os.path.join(root, fname)
|
||||
with open(filepath, 'rb') as f:
|
||||
content_hash = hashlib.md5(f.read()).hexdigest()
|
||||
file_hashes[content_hash].append(filepath)
|
||||
|
||||
duplicates = {h: files for h, files in file_hashes.items() if len(files) > 1}
|
||||
|
||||
if duplicates:
|
||||
print(f"Found {len(duplicates)} sets of duplicate files:\n")
|
||||
for h, files in duplicates.items():
|
||||
print(f" Hash: {h[:8]}...")
|
||||
for f in files:
|
||||
print(f" {f}")
|
||||
print()
|
||||
else:
|
||||
print("No duplicate files found.")
|
||||
```
|
||||
> summary_prompt: "List all duplicate files and suggest which copies to remove"
|
||||
@@ -0,0 +1,277 @@
|
||||
# Shell Patterns for execute
|
||||
|
||||
Practical patterns for using `execute` with `language: shell`.
|
||||
Best for piping, filtering, and leveraging native OS tools.
|
||||
|
||||
---
|
||||
|
||||
## Build Output Filtering
|
||||
|
||||
### Capture build errors only
|
||||
|
||||
```shell
|
||||
npm run build 2>&1 | tee /tmp/build-output.txt
|
||||
EXIT_CODE=${PIPESTATUS[0]}
|
||||
|
||||
echo "=== Build Result ==="
|
||||
echo "Exit code: $EXIT_CODE"
|
||||
|
||||
if [ "$EXIT_CODE" -ne 0 ]; then
|
||||
echo ""
|
||||
echo "=== Errors ==="
|
||||
grep -iE '(error|failed|FAIL)' /tmp/build-output.txt | head -50
|
||||
echo ""
|
||||
echo "=== Warnings ==="
|
||||
grep -iE '(warning|warn)' /tmp/build-output.txt | head -20
|
||||
else
|
||||
echo "Build succeeded."
|
||||
echo ""
|
||||
echo "=== Warnings (if any) ==="
|
||||
grep -iE '(warning|warn)' /tmp/build-output.txt | head -10
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Output Size ==="
|
||||
wc -l < /tmp/build-output.txt | xargs -I{} echo "{} total lines of output"
|
||||
rm -f /tmp/build-output.txt
|
||||
```
|
||||
> summary_prompt: "Report build success/failure, list all errors with file paths, and count warnings"
|
||||
> timeout_ms: 120000
|
||||
|
||||
### TypeScript compilation check
|
||||
|
||||
```shell
|
||||
npx tsc --noEmit 2>&1 | tee /tmp/tsc-output.txt
|
||||
EXIT_CODE=${PIPESTATUS[0]}
|
||||
|
||||
echo "=== TypeScript Check ==="
|
||||
echo "Exit code: $EXIT_CODE"
|
||||
|
||||
TOTAL_ERRORS=$(grep -c 'error TS' /tmp/tsc-output.txt 2>/dev/null || echo 0)
|
||||
echo "Total errors: $TOTAL_ERRORS"
|
||||
|
||||
if [ "$TOTAL_ERRORS" -gt 0 ]; then
|
||||
echo ""
|
||||
echo "=== Errors by Code ==="
|
||||
grep -oP 'error TS\d+' /tmp/tsc-output.txt | sort | uniq -c | sort -rn | head -20
|
||||
|
||||
echo ""
|
||||
echo "=== Errors by File ==="
|
||||
grep 'error TS' /tmp/tsc-output.txt | cut -d'(' -f1 | sort | uniq -c | sort -rn | head -20
|
||||
|
||||
echo ""
|
||||
echo "=== First 30 Errors ==="
|
||||
grep 'error TS' /tmp/tsc-output.txt | head -30
|
||||
fi
|
||||
|
||||
rm -f /tmp/tsc-output.txt
|
||||
```
|
||||
> summary_prompt: "Report type error count, most common error codes, and most affected files"
|
||||
> timeout_ms: 60000
|
||||
|
||||
---
|
||||
|
||||
## Test Result Summarization
|
||||
|
||||
### Jest test summary
|
||||
|
||||
```shell
|
||||
npx jest --verbose 2>&1 | tee /tmp/test-output.txt
|
||||
EXIT_CODE=${PIPESTATUS[0]}
|
||||
|
||||
echo ""
|
||||
echo "=== Test Summary ==="
|
||||
echo "Exit code: $EXIT_CODE"
|
||||
|
||||
# Extract summary line
|
||||
grep -E '(Tests:|Test Suites:|Snapshots:|Time:)' /tmp/test-output.txt
|
||||
|
||||
echo ""
|
||||
echo "=== Failed Tests ==="
|
||||
grep -A 2 'FAIL ' /tmp/test-output.txt | head -40
|
||||
|
||||
echo ""
|
||||
echo "=== Slow Tests (if reported) ==="
|
||||
grep -i 'slow' /tmp/test-output.txt | head -10
|
||||
|
||||
rm -f /tmp/test-output.txt
|
||||
```
|
||||
> summary_prompt: "Report pass/fail ratio, list all failing test names with suite, note any slow tests"
|
||||
> timeout_ms: 120000
|
||||
|
||||
### Pytest summary
|
||||
|
||||
```shell
|
||||
python -m pytest --tb=short -q 2>&1 | tee /tmp/pytest-output.txt
|
||||
EXIT_CODE=${PIPESTATUS[0]}
|
||||
|
||||
echo ""
|
||||
echo "=== Pytest Summary ==="
|
||||
echo "Exit code: $EXIT_CODE"
|
||||
|
||||
# Last 20 lines usually contain the summary
|
||||
tail -20 /tmp/pytest-output.txt
|
||||
|
||||
echo ""
|
||||
echo "=== Failures ==="
|
||||
grep -E '(FAILED|ERROR)' /tmp/pytest-output.txt | head -30
|
||||
|
||||
rm -f /tmp/pytest-output.txt
|
||||
```
|
||||
> summary_prompt: "Report test results, list all failures with file and test name"
|
||||
> timeout_ms: 120000
|
||||
|
||||
---
|
||||
|
||||
## Log File Analysis
|
||||
|
||||
### Filter application logs by severity
|
||||
|
||||
```shell
|
||||
LOG_FILE="${1:-/var/log/app.log}"
|
||||
|
||||
echo "=== Log File: $LOG_FILE ==="
|
||||
echo "Total lines: $(wc -l < "$LOG_FILE")"
|
||||
echo ""
|
||||
|
||||
echo "=== Level Distribution ==="
|
||||
grep -oE '\b(DEBUG|INFO|WARN|ERROR|FATAL)\b' "$LOG_FILE" | sort | uniq -c | sort -rn
|
||||
|
||||
echo ""
|
||||
echo "=== Last 20 Errors ==="
|
||||
grep -i 'ERROR\|FATAL' "$LOG_FILE" | tail -20
|
||||
|
||||
echo ""
|
||||
echo "=== Error Timeline (hourly) ==="
|
||||
grep -i 'ERROR' "$LOG_FILE" | grep -oE '\d{4}-\d{2}-\d{2} \d{2}' | sort | uniq -c | tail -24
|
||||
```
|
||||
> summary_prompt: "Report error frequency, identify patterns, and note any error spikes"
|
||||
|
||||
### Analyze access logs
|
||||
|
||||
```shell
|
||||
LOG_FILE="${1:-/var/log/access.log}"
|
||||
|
||||
echo "=== Access Log Summary ==="
|
||||
echo "Total requests: $(wc -l < "$LOG_FILE")"
|
||||
|
||||
echo ""
|
||||
echo "=== HTTP Status Codes ==="
|
||||
awk '{print $9}' "$LOG_FILE" | sort | uniq -c | sort -rn | head -10
|
||||
|
||||
echo ""
|
||||
echo "=== Top 20 Paths ==="
|
||||
awk '{print $7}' "$LOG_FILE" | sort | uniq -c | sort -rn | head -20
|
||||
|
||||
echo ""
|
||||
echo "=== Top 10 IPs ==="
|
||||
awk '{print $1}' "$LOG_FILE" | sort | uniq -c | sort -rn | head -10
|
||||
|
||||
echo ""
|
||||
echo "=== 5xx Errors ==="
|
||||
awk '$9 ~ /^5/' "$LOG_FILE" | tail -20
|
||||
|
||||
echo ""
|
||||
echo "=== Requests per Hour ==="
|
||||
awk '{print $4}' "$LOG_FILE" | cut -d: -f1-2 | sort | uniq -c | tail -24
|
||||
```
|
||||
> summary_prompt: "Report traffic patterns, error rates, most hit endpoints, and suspicious IPs"
|
||||
|
||||
---
|
||||
|
||||
## Directory Size and Structure Analysis
|
||||
|
||||
### Project structure overview
|
||||
|
||||
```shell
|
||||
echo "=== Directory Structure ==="
|
||||
find . -maxdepth 3 -type d \
|
||||
! -path '*/node_modules/*' \
|
||||
! -path '*/.git/*' \
|
||||
! -path '*/dist/*' \
|
||||
! -path '*/.next/*' \
|
||||
! -path '*/__pycache__/*' \
|
||||
| sort
|
||||
|
||||
echo ""
|
||||
echo "=== File Type Distribution ==="
|
||||
find . -type f \
|
||||
! -path '*/node_modules/*' \
|
||||
! -path '*/.git/*' \
|
||||
! -path '*/dist/*' \
|
||||
| sed 's/.*\.//' | sort | uniq -c | sort -rn | head -20
|
||||
|
||||
echo ""
|
||||
echo "=== Largest Files (top 20) ==="
|
||||
find . -type f \
|
||||
! -path '*/node_modules/*' \
|
||||
! -path '*/.git/*' \
|
||||
-exec ls -la {} \; | sort -k5 -rn | head -20 | awk '{print $5, $9}'
|
||||
|
||||
echo ""
|
||||
echo "=== Directory Sizes ==="
|
||||
du -sh */ 2>/dev/null | sort -rh | head -15
|
||||
```
|
||||
> summary_prompt: "Describe the project structure, identify large files that may need attention, report file type distribution"
|
||||
|
||||
### Disk usage investigation
|
||||
|
||||
```shell
|
||||
echo "=== Top-Level Disk Usage ==="
|
||||
du -sh */ 2>/dev/null | sort -rh
|
||||
|
||||
echo ""
|
||||
echo "=== node_modules Size ==="
|
||||
if [ -d "node_modules" ]; then
|
||||
du -sh node_modules
|
||||
echo ""
|
||||
echo "=== Largest node_modules packages ==="
|
||||
du -sh node_modules/*/ 2>/dev/null | sort -rh | head -20
|
||||
else
|
||||
echo "No node_modules directory"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Build Artifacts ==="
|
||||
for dir in dist build .next out .cache; do
|
||||
if [ -d "$dir" ]; then
|
||||
echo " $dir: $(du -sh "$dir" | cut -f1)"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== Git Objects Size ==="
|
||||
if [ -d ".git" ]; then
|
||||
du -sh .git
|
||||
fi
|
||||
```
|
||||
> summary_prompt: "Report total project size, largest contributors, and recommend cleanup targets"
|
||||
|
||||
---
|
||||
|
||||
## Git Analysis
|
||||
|
||||
### Commit activity analysis
|
||||
|
||||
```shell
|
||||
echo "=== Recent Commits (last 30 days) ==="
|
||||
git log --since="30 days ago" --oneline | wc -l | xargs -I{} echo "{} commits in last 30 days"
|
||||
|
||||
echo ""
|
||||
echo "=== Commits by Author ==="
|
||||
git shortlog -sn --since="30 days ago" | head -15
|
||||
|
||||
echo ""
|
||||
echo "=== Most Changed Files (last 30 days) ==="
|
||||
git log --since="30 days ago" --pretty=format: --name-only | sort | uniq -c | sort -rn | head -20
|
||||
|
||||
echo ""
|
||||
echo "=== Branches ==="
|
||||
echo "Local: $(git branch | wc -l | xargs)"
|
||||
echo "Remote: $(git branch -r | wc -l | xargs)"
|
||||
|
||||
echo ""
|
||||
echo "=== Stale Branches (merged, excluding main/master) ==="
|
||||
git branch --merged main 2>/dev/null | grep -v 'main\|master\|\*' | head -10
|
||||
```
|
||||
> summary_prompt: "Report development velocity, active contributors, hotspot files, and cleanup opportunities"
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: ctx-doctor
|
||||
description: |
|
||||
Run context-mode diagnostics. Checks runtimes, hooks, FTS5,
|
||||
plugin registration, npm and marketplace versions.
|
||||
Trigger: /context-mode:ctx-doctor
|
||||
user-invocable: true
|
||||
---
|
||||
|
||||
# Context Mode Doctor
|
||||
|
||||
Run diagnostics and display results directly in the conversation.
|
||||
|
||||
## Instructions
|
||||
|
||||
1. Call the `ctx_doctor` MCP tool directly. It runs all checks server-side and returns a plain-text status report.
|
||||
2. Display the results verbatim — they are already formatted with plain-text status prefixes: `[OK]` PASS, `[FAIL]` FAIL, `[WARN]` WARN. Renderer-safe (no markdown task-list syntax) for cross-client compatibility (e.g., Z.ai GLM).
|
||||
3. **Fallback** (only if MCP tool call fails): Derive the **plugin root** from this skill's base directory (go up 2 levels — remove `/skills/ctx-doctor`), then run with Bash:
|
||||
```
|
||||
CLI="<PLUGIN_ROOT>/cli.bundle.mjs"; [ ! -f "$CLI" ] && CLI="<PLUGIN_ROOT>/build/cli.js"; node "$CLI" doctor
|
||||
```
|
||||
Re-display results verbatim with the same `[OK]`/`[FAIL]`/`[WARN]` prefixes.
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
name: ctx-insight
|
||||
description: |
|
||||
Open the context-mode Insight analytics dashboard in the browser.
|
||||
Shows personal metrics: session activity, tool usage, error rate,
|
||||
parallel work patterns, project focus, and actionable insights.
|
||||
First run installs dependencies (~30s). Subsequent runs open instantly.
|
||||
Trigger: /context-mode:ctx-insight
|
||||
user-invocable: true
|
||||
---
|
||||
|
||||
# Context Mode Insight
|
||||
|
||||
Open the personal analytics dashboard in the browser.
|
||||
|
||||
## Instructions
|
||||
|
||||
1. Call the `ctx_insight` MCP tool (no parameters needed, or pass `port: 4747` to customize). Optional data-dir overrides: `sessionDir`/`insightSessionDir` for `INSIGHT_SESSION_DIR`, and `contentDir`/`insightContentDir` for `INSIGHT_CONTENT_DIR`.
|
||||
2. The tool will:
|
||||
- Copy source files to cache (first run only)
|
||||
- Install dependencies (first run only, ~30s)
|
||||
- Build the dashboard (~1s)
|
||||
- Start a local server
|
||||
- Open the browser
|
||||
3. Display the tool's output to the user — it contains progress steps and the dashboard URL.
|
||||
4. Tell the user:
|
||||
- "Dashboard is running at http://localhost:4747"
|
||||
- "Refresh the page to see updated metrics"
|
||||
- "Dashboard stops automatically when Claude exits. To stop sooner: kill the PID shown above."
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
name: ctx-purge
|
||||
description: |
|
||||
Purge the context-mode knowledge base. Permanently deletes all indexed content
|
||||
and resets session stats. This is destructive and cannot be undone.
|
||||
Trigger: /context-mode:ctx-purge
|
||||
user-invocable: true
|
||||
---
|
||||
|
||||
# Context Mode Purge
|
||||
|
||||
Permanently deletes session data for this project. Two scopes are supported (issue #520):
|
||||
|
||||
- **Project scope** (`scope: "project"`): wipes EVERYTHING — knowledge base, all session DB rows for every session, events markdown, and stats.
|
||||
- **Session scope** (`sessionId: "<id>"` or `scope: "session"`): wipes ONLY the matching session's rows + FTS5 chunks. Sibling sessions, project stats, and the FTS5 store file are preserved.
|
||||
|
||||
## Instructions
|
||||
|
||||
1. **Decide the scope first** with the user:
|
||||
- "Wipe just one session?" → ask for the `sessionId`.
|
||||
- "Wipe the whole project?" → confirm scope:'project' (this is the destructive, irreversible default).
|
||||
2. **Warn the user about scope:'project'**. Everything will be deleted:
|
||||
- FTS5 knowledge base (all indexed content from `ctx_index`, `ctx_fetch_and_index`, `ctx_batch_execute`)
|
||||
- Session events DB (analytics, metadata, resume snapshots) for ALL sessions in the project
|
||||
- Session events markdown file
|
||||
- In-memory session stats + persisted stats file
|
||||
3. Call the `mcp__context-mode__ctx_purge` MCP tool with the chosen parameters:
|
||||
- Scoped: `{ confirm: true, sessionId: "<id>" }` — implies scope:'session'.
|
||||
- Project: `{ confirm: true, scope: "project" }` — explicit destructive form.
|
||||
- Bare `{ confirm: true }` still works but emits a deprecation warning. Prefer the explicit forms.
|
||||
4. Report the result to the user — the response lists exactly what was deleted and (for scoped purges) confirms that other sessions and project stats were preserved.
|
||||
|
||||
## Schema rules
|
||||
|
||||
- `confirm: true` is always required.
|
||||
- `sessionId` and `scope: "project"` together is REJECTED as ambiguous (the sessionId implies session scope; combining with project scope contradicts intent).
|
||||
- `scope: "session"` without `sessionId` throws — sessionId is required.
|
||||
|
||||
## When to Use
|
||||
|
||||
- **Scoped (per-session)**: scratch acceptance scenarios, drill replays, isolating a polluted session without losing the main working session's stats.
|
||||
- **Project**: KB contains stale or incorrect content polluting search results, switching between unrelated projects in the same session, completely fresh start.
|
||||
|
||||
## Important
|
||||
|
||||
- `ctx_purge` is the **only** way to delete session data. No other mechanism exists.
|
||||
- `ctx_stats` is read-only — shows statistics only.
|
||||
- `/clear` and `/compact` do NOT affect any context-mode data.
|
||||
- There is no undo. Re-index content if you need it again.
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
name: ctx-stats
|
||||
description: |
|
||||
Show how much context window context-mode saved this session.
|
||||
Displays token consumption, context savings ratio, and per-tool breakdown.
|
||||
Read-only — shows stats only, no reset capability.
|
||||
To wipe the knowledge base entirely, use ctx_purge instead.
|
||||
Trigger: /context-mode:ctx-stats
|
||||
user-invocable: true
|
||||
---
|
||||
|
||||
# Context Mode Stats
|
||||
|
||||
Show context savings for the current session.
|
||||
|
||||
## Instructions
|
||||
|
||||
1. Call the `mcp__context-mode__ctx_stats` MCP tool (no parameters needed).
|
||||
2. **CRITICAL**: You MUST copy-paste the ENTIRE tool output as markdown text directly into your response message. Do NOT summarize, do NOT collapse, do NOT paraphrase. The user must see the full tables without pressing ctrl+o. Copy every line exactly as returned by the tool.
|
||||
3. After the full output, add ONE sentence highlighting the key savings metric, e.g.:
|
||||
- "context-mode saved **12.4x** — 92% of data stayed in sandbox."
|
||||
- If no data yet: "No context-mode calls yet this session."
|
||||
|
||||
## Purge
|
||||
|
||||
- **`ctx_purge(confirm: true)`** — Permanently deletes all indexed content from the knowledge base. Use `/context-mode:ctx-purge` for this.
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
name: ctx-upgrade
|
||||
description: |
|
||||
Update context-mode from GitHub and fix hooks/settings.
|
||||
Pulls latest, builds, installs, updates npm global, configures hooks.
|
||||
Trigger: /context-mode:ctx-upgrade
|
||||
user-invocable: true
|
||||
---
|
||||
|
||||
# Context Mode Upgrade
|
||||
|
||||
Pull latest from GitHub and reinstall the plugin.
|
||||
|
||||
## Instructions
|
||||
|
||||
1. Call the `ctx_upgrade` MCP tool directly. It returns a shell command to execute.
|
||||
2. Run the returned command using your shell execution tool (Bash, shell_execute, etc.).
|
||||
3. Display results as a markdown checklist:
|
||||
```
|
||||
## context-mode upgrade
|
||||
- [x] Pulled latest from GitHub
|
||||
- [x] Built and installed v1.0.39
|
||||
- [x] Hooks configured
|
||||
- [x] Doctor: all checks PASS
|
||||
```
|
||||
Use `[x]` for success, `[ ]` for failure. Show actual version numbers.
|
||||
4. Tell the user to **restart their session** to pick up the new version.
|
||||
5. **Fallback** (only if MCP tool call fails): Derive the **plugin root** from this skill's base directory (go up 2 levels — remove `/skills/ctx-upgrade`), then run with Bash:
|
||||
```
|
||||
CLI="<PLUGIN_ROOT>/cli.bundle.mjs"; [ ! -f "$CLI" ] && CLI="<PLUGIN_ROOT>/build/cli.js"; node "$CLI" upgrade
|
||||
```
|
||||
@@ -0,0 +1,47 @@
|
||||
# ADR Format
|
||||
|
||||
ADRs live in `docs/adr/` and use sequential numbering: `0001-slug.md`, `0002-slug.md`, etc.
|
||||
|
||||
Create the `docs/adr/` directory lazily — only when the first ADR is needed.
|
||||
|
||||
## Template
|
||||
|
||||
```md
|
||||
# {Short title of the decision}
|
||||
|
||||
{1-3 sentences: what's the context, what did we decide, and why.}
|
||||
```
|
||||
|
||||
That's it. An ADR can be a single paragraph. The value is in recording *that* a decision was made and *why* — not in filling out sections.
|
||||
|
||||
## Optional sections
|
||||
|
||||
Only include these when they add genuine value. Most ADRs won't need them.
|
||||
|
||||
- **Status** frontmatter (`proposed | accepted | deprecated | superseded by ADR-NNNN`) — useful when decisions are revisited
|
||||
- **Considered Options** — only when the rejected alternatives are worth remembering
|
||||
- **Consequences** — only when non-obvious downstream effects need to be called out
|
||||
|
||||
## Numbering
|
||||
|
||||
Scan `docs/adr/` for the highest existing number and increment by one.
|
||||
|
||||
## When to offer an ADR
|
||||
|
||||
All three of these must be true:
|
||||
|
||||
1. **Hard to reverse** — the cost of changing your mind later is meaningful
|
||||
2. **Surprising without context** — a future reader will look at the code and wonder "why on earth did they do it this way?"
|
||||
3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons
|
||||
|
||||
If a decision is easy to reverse, skip it — you'll just reverse it. If it's not surprising, nobody will wonder why. If there was no real alternative, there's nothing to record beyond "we did the obvious thing."
|
||||
|
||||
### What qualifies
|
||||
|
||||
- **Architectural shape.** "We're using a monorepo." "The write model is event-sourced, the read model is projected into Postgres."
|
||||
- **Integration patterns between contexts.** "Ordering and Billing communicate via domain events, not synchronous HTTP."
|
||||
- **Technology choices that carry lock-in.** Database, message bus, auth provider, deployment target. Not every library — just the ones that would take a quarter to swap out.
|
||||
- **Boundary and scope decisions.** "Customer data is owned by the Customer context; other contexts reference it by ID only." The explicit no-s are as valuable as the yes-s.
|
||||
- **Deliberate deviations from the obvious path.** "We're using manual SQL instead of an ORM because X." Anything where a reasonable reader would assume the opposite. These stop the next engineer from "fixing" something that was deliberate.
|
||||
- **Constraints not visible in the code.** "We can't use AWS because of compliance requirements." "Response times must be under 200ms because of the partner API contract."
|
||||
- **Rejected alternatives when the rejection is non-obvious.** If you considered GraphQL and picked REST for subtle reasons, record it — otherwise someone will suggest GraphQL again in six months.
|
||||
@@ -0,0 +1,77 @@
|
||||
# CONTEXT.md Format
|
||||
|
||||
## Structure
|
||||
|
||||
```md
|
||||
# {Context Name}
|
||||
|
||||
{One or two sentence description of what this context is and why it exists.}
|
||||
|
||||
## Language
|
||||
|
||||
**Order**:
|
||||
{A concise description of the term}
|
||||
_Avoid_: Purchase, transaction
|
||||
|
||||
**Invoice**:
|
||||
A request for payment sent to a customer after delivery.
|
||||
_Avoid_: Bill, payment request
|
||||
|
||||
**Customer**:
|
||||
A person or organization that places orders.
|
||||
_Avoid_: Client, buyer, account
|
||||
|
||||
## Relationships
|
||||
|
||||
- An **Order** produces one or more **Invoices**
|
||||
- An **Invoice** belongs to exactly one **Customer**
|
||||
|
||||
## Example dialogue
|
||||
|
||||
> **Dev:** "When a **Customer** places an **Order**, do we create the **Invoice** immediately?"
|
||||
> **Domain expert:** "No — an **Invoice** is only generated once a **Fulfillment** is confirmed."
|
||||
|
||||
## Flagged ambiguities
|
||||
|
||||
- "account" was used to mean both **Customer** and **User** — resolved: these are distinct concepts.
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
- **Be opinionated.** When multiple words exist for the same concept, pick the best one and list the others as aliases to avoid.
|
||||
- **Flag conflicts explicitly.** If a term is used ambiguously, call it out in "Flagged ambiguities" with a clear resolution.
|
||||
- **Keep definitions tight.** One sentence max. Define what it IS, not what it does.
|
||||
- **Show relationships.** Use bold term names and express cardinality where obvious.
|
||||
- **Only include terms specific to this project's context.** General programming concepts (timeouts, error types, utility patterns) don't belong even if the project uses them extensively. Before adding a term, ask: is this a concept unique to this context, or a general programming concept? Only the former belongs.
|
||||
- **Group terms under subheadings** when natural clusters emerge. If all terms belong to a single cohesive area, a flat list is fine.
|
||||
- **Write an example dialogue.** A conversation between a dev and a domain expert that demonstrates how the terms interact naturally and clarifies boundaries between related concepts.
|
||||
|
||||
## Single vs multi-context repos
|
||||
|
||||
**Single context (most repos):** One `CONTEXT.md` at the repo root.
|
||||
|
||||
**Multiple contexts:** A `CONTEXT-MAP.md` at the repo root lists the contexts, where they live, and how they relate to each other:
|
||||
|
||||
```md
|
||||
# Context Map
|
||||
|
||||
## Contexts
|
||||
|
||||
- [Ordering](./src/ordering/CONTEXT.md) — receives and tracks customer orders
|
||||
- [Billing](./src/billing/CONTEXT.md) — generates invoices and processes payments
|
||||
- [Fulfillment](./src/fulfillment/CONTEXT.md) — manages warehouse picking and shipping
|
||||
|
||||
## Relationships
|
||||
|
||||
- **Ordering → Fulfillment**: Ordering emits `OrderPlaced` events; Fulfillment consumes them to start picking
|
||||
- **Fulfillment → Billing**: Fulfillment emits `ShipmentDispatched` events; Billing consumes them to generate invoices
|
||||
- **Ordering ↔ Billing**: Shared types for `CustomerId` and `Money`
|
||||
```
|
||||
|
||||
The skill infers which structure applies:
|
||||
|
||||
- If `CONTEXT-MAP.md` exists, read it to find contexts
|
||||
- If only a root `CONTEXT.md` exists, single context
|
||||
- If neither exists, create a root `CONTEXT.md` lazily when the first term is resolved
|
||||
|
||||
When multiple contexts exist, infer which one the current topic relates to. If unclear, ask.
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
name: grill-with-docs
|
||||
description: Grilling session that challenges your plan against the existing domain model, sharpens terminology, and updates documentation (CONTEXT.md, ADRs) inline as decisions crystallise. Use when user wants to stress-test a plan against their project's language and documented decisions.
|
||||
---
|
||||
|
||||
<what-to-do>
|
||||
|
||||
Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer.
|
||||
|
||||
Ask the questions one at a time, waiting for feedback on each question before continuing.
|
||||
|
||||
If a question can be answered by exploring the codebase, explore the codebase instead.
|
||||
|
||||
</what-to-do>
|
||||
|
||||
<supporting-info>
|
||||
|
||||
## Domain awareness
|
||||
|
||||
During codebase exploration, also look for existing documentation:
|
||||
|
||||
### File structure
|
||||
|
||||
Most repos have a single context:
|
||||
|
||||
```
|
||||
/
|
||||
├── CONTEXT.md
|
||||
├── docs/
|
||||
│ └── adr/
|
||||
│ ├── 0001-event-sourced-orders.md
|
||||
│ └── 0002-postgres-for-write-model.md
|
||||
└── src/
|
||||
```
|
||||
|
||||
If a `CONTEXT-MAP.md` exists at the root, the repo has multiple contexts. The map points to where each one lives:
|
||||
|
||||
```
|
||||
/
|
||||
├── CONTEXT-MAP.md
|
||||
├── docs/
|
||||
│ └── adr/ ← system-wide decisions
|
||||
├── src/
|
||||
│ ├── ordering/
|
||||
│ │ ├── CONTEXT.md
|
||||
│ │ └── docs/adr/ ← context-specific decisions
|
||||
│ └── billing/
|
||||
│ ├── CONTEXT.md
|
||||
│ └── docs/adr/
|
||||
```
|
||||
|
||||
Create files lazily — only when you have something to write. If no `CONTEXT.md` exists, create one when the first term is resolved. If no `docs/adr/` exists, create it when the first ADR is needed.
|
||||
|
||||
## During the session
|
||||
|
||||
### Challenge against the glossary
|
||||
|
||||
When the user uses a term that conflicts with the existing language in `CONTEXT.md`, call it out immediately. "Your glossary defines 'cancellation' as X, but you seem to mean Y — which is it?"
|
||||
|
||||
### Sharpen fuzzy language
|
||||
|
||||
When the user uses vague or overloaded terms, propose a precise canonical term. "You're saying 'account' — do you mean the Customer or the User? Those are different things."
|
||||
|
||||
### Discuss concrete scenarios
|
||||
|
||||
When domain relationships are being discussed, stress-test them with specific scenarios. Invent scenarios that probe edge cases and force the user to be precise about the boundaries between concepts.
|
||||
|
||||
### Cross-reference with code
|
||||
|
||||
When the user states how something works, check whether the code agrees. If you find a contradiction, surface it: "Your code cancels entire Orders, but you just said partial cancellation is possible — which is right?"
|
||||
|
||||
### Update CONTEXT.md inline
|
||||
|
||||
When a term is resolved, update `CONTEXT.md` right there. Don't batch these up — capture them as they happen. Use the format in [CONTEXT-FORMAT.md](./CONTEXT-FORMAT.md).
|
||||
|
||||
`CONTEXT.md` should be totally devoid of implementation details. Do not treat `CONTEXT.md` as a spec, a scratch pad, or a repository for implementation decisions. It is a glossary and nothing else.
|
||||
|
||||
### Offer ADRs sparingly
|
||||
|
||||
Only offer to create an ADR when all three are true:
|
||||
|
||||
1. **Hard to reverse** — the cost of changing your mind later is meaningful
|
||||
2. **Surprising without context** — a future reader will wonder "why did they do it this way?"
|
||||
3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons
|
||||
|
||||
If any of the three is missing, skip the ADR. Use the format in [ADR-FORMAT.md](./ADR-FORMAT.md).
|
||||
|
||||
</supporting-info>
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
name: karpathy-guidelines
|
||||
description: Behavioral guidelines to reduce common LLM coding mistakes. Use when writing, reviewing, or refactoring code to avoid overcomplication, make surgical changes, surface assumptions, and define verifiable success criteria.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Karpathy Guidelines
|
||||
|
||||
Behavioral guidelines to reduce common LLM coding mistakes, derived from [Andrej Karpathy's observations](https://x.com/karpathy/status/2015883857489522876) on LLM coding pitfalls.
|
||||
|
||||
**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
|
||||
|
||||
## 1. Think Before Coding
|
||||
|
||||
**Don't assume. Don't hide confusion. Surface tradeoffs.**
|
||||
|
||||
Before implementing:
|
||||
- State your assumptions explicitly. If uncertain, ask.
|
||||
- If multiple interpretations exist, present them - don't pick silently.
|
||||
- If a simpler approach exists, say so. Push back when warranted.
|
||||
- If something is unclear, stop. Name what's confusing. Ask.
|
||||
|
||||
## 2. Simplicity First
|
||||
|
||||
**Minimum code that solves the problem. Nothing speculative.**
|
||||
|
||||
- No features beyond what was asked.
|
||||
- No abstractions for single-use code.
|
||||
- No "flexibility" or "configurability" that wasn't requested.
|
||||
- No error handling for impossible scenarios.
|
||||
- If you write 200 lines and it could be 50, rewrite it.
|
||||
|
||||
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
|
||||
|
||||
## 3. Surgical Changes
|
||||
|
||||
**Touch only what you must. Clean up only your own mess.**
|
||||
|
||||
When editing existing code:
|
||||
- Don't "improve" adjacent code, comments, or formatting.
|
||||
- Don't refactor things that aren't broken.
|
||||
- Match existing style, even if you'd do it differently.
|
||||
- If you notice unrelated dead code, mention it - don't delete it.
|
||||
|
||||
When your changes create orphans:
|
||||
- Remove imports/variables/functions that YOUR changes made unused.
|
||||
- Don't remove pre-existing dead code unless asked.
|
||||
|
||||
The test: Every changed line should trace directly to the user's request.
|
||||
|
||||
## 4. Goal-Driven Execution
|
||||
|
||||
**Define success criteria. Loop until verified.**
|
||||
|
||||
Transform tasks into verifiable goals:
|
||||
- "Add validation" → "Write tests for invalid inputs, then make them pass"
|
||||
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
|
||||
- "Refactor X" → "Ensure tests pass before and after"
|
||||
|
||||
For multi-step tasks, state a brief plan:
|
||||
```
|
||||
1. [Step] → verify: [check]
|
||||
2. [Step] → verify: [check]
|
||||
3. [Step] → verify: [check]
|
||||
```
|
||||
|
||||
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
name: review-html
|
||||
description: review 代码并输出交互式 HTML 报告。适用于代码审查,git-diff评审。
|
||||
---
|
||||
|
||||
你是一个代码审查助手。用户要求你审查**当前代码库**的改动,具体包括:
|
||||
- `git diff HEAD` 相对于 HEAD 的所有未提交改动(如果无输出,则默认使用最近 1 次 commit 的改动 `git log -p -n 1`)。默认不允许探索整个代码库除非用户明确要求。
|
||||
- 最近 **2(或指定个数)个 commit** 的完整改动(使用 `git log -p -n <N>`)
|
||||
- 完整的整个代码库(需用户明确说明)
|
||||
|
||||
## 硬性输出要求
|
||||
|
||||
**不要输出任何 Markdown 格式(包括代码块标记 ` ```html ` 等)**
|
||||
**直接从 `<!DOCTYPE html>` 开始输出一个完整的、自包含的 HTML 文档。**
|
||||
**使用中文+英文专业名词**
|
||||
|
||||
### 审查问题分类与重要程度
|
||||
|
||||
1. **BUG**:潜在 bug,最高优先级。
|
||||
2. **敏感信息**:敏感信息泄露、敏感文件未加入 `.gitignore`,高优先级。
|
||||
3. **可维护性**:代码解耦、结构清晰、冗余代码优化,中优先级。
|
||||
4. **规范**:代码规范、代码风格,低优先级。
|
||||
5. **语法糖**:适合当前语言的高阶语法优化,维持原逻辑和无 bug > 性能提升大 > 可读性 > 性能提升可忽略 > 缩短代码行数,低优先级。
|
||||
6. **其他**:其他问题。
|
||||
|
||||
### HTML 内容约束
|
||||
|
||||
1. **外观**:简单的现代 CSS(白/灰背景,清晰字体)。全部内联,不引用外部 CDN。
|
||||
2. **交互**:提供折叠/展开功能,默认只展示文件名称和问题数量,点击文件名展开详细审查意见。
|
||||
3. **内容**:每个改动文件至少包含:文件路径、改动摘要(+/- 行数)、具体审查意见(问题分类及描述)。
|
||||
4. **轻量**:总 HTML 大小控制如下——审查整个代码库时上限为 2000 行(若代码库内容较少则仍遵循 700 行限制以保持简洁);其他情况(diff / commit)严格控制在 700 行以内。CSS 和 JS 均为少量内联。
|
||||
|
||||
### 特殊场景:简洁总结与评分
|
||||
|
||||
当审查结果**没有任何重要程度为“高”或“最高”的问题**(即仅包含中、低或无问题)时,使用简洁 HTML 输出,不需要交互式折叠,仅展示:
|
||||
- 改动摘要
|
||||
- 总体评分(满分 100)及简短理由
|
||||
- 如果审查对象是**未提交的改动**,额外提供一条符合 Conventional Commits 规范的 commit message 建议(例如 `fix: 修复xxx`、`feat: 新增xxx`),从改动内容智能生成;若审查对象是已提交的 commit 或整个代码库,则只展示评分与总结,不提供 commit message。
|
||||
|
||||
评分参考标准(严格、公正):
|
||||
- **60 分(及格)**:存在少量中等问题,代码可运行但需改进。
|
||||
- **80 分(良好)**:仅有轻微(低)问题,整体质量较好。
|
||||
- **95 分(优秀)**:几乎无问题,设计清晰。
|
||||
- **99 分(完美)**:无可挑剔,代码典范。
|
||||
(实际得分根据问题数量与严重性严格判定)
|
||||
|
||||
## 执行步骤
|
||||
|
||||
1. 根据用户指令运行 `git diff HEAD` 或 `git log -p -n <N>` 获取改动,或遍历代码库。
|
||||
2. 合并改动,按文件分组。
|
||||
3. 对每个文件进行审查,并生成符合上述要求的 HTML。
|
||||
4. 自动确保 `.pi-dev-output/pi-review/html/` 文件夹存在于项目根目录,若不存在则创建。
|
||||
5. HTML 文件命名格式:`年月日时分-任务简述(极简10词以内)-index.html`,若同名文件已存在则编号递增(如 `index1.html`)。
|
||||
6. 直接输出 HTML 文件到 `.pi-dev-output/pi-review/html/` 目录,**不要添加任何解释性文字**,仅简短说明工作完成和输出文件路径即可。
|
||||
|
||||
请严格遵循以上规则。
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
name: to-prd
|
||||
description: Turn the current conversation context into a PRD document and save it locally. Use when user wants to create a PRD from the current context.
|
||||
---
|
||||
|
||||
This skill takes the current conversation context and codebase understanding and produces a PRD. Do NOT interview the user — just synthesize what you already know.
|
||||
|
||||
## Process
|
||||
|
||||
1. Explore the repo to understand the current state of the codebase, if you haven't already. Use the project's domain glossary vocabulary throughout the PRD, and respect any ADRs in the area you're touching.
|
||||
|
||||
2. Sketch out the major modules you will need to build or modify to complete the implementation. Actively look for opportunities to extract deep modules that can be tested in isolation.
|
||||
|
||||
A deep module (as opposed to a shallow module) is one which encapsulates a lot of functionality in a simple, testable interface which rarely changes.
|
||||
|
||||
Check with the user that these modules match their expectations. Check with the user which modules they want tests written for.
|
||||
|
||||
3. Write the PRD using the template below, then save it to `.pi-dev-output/pi-prd/` directory.
|
||||
|
||||
<prd-template>
|
||||
|
||||
## Problem Statement
|
||||
|
||||
The problem that the user is facing, from the user's perspective.
|
||||
|
||||
## Solution
|
||||
|
||||
The solution to the problem, from the user's perspective.
|
||||
|
||||
## User Stories
|
||||
|
||||
A LONG, numbered list of user stories. Each user story should be in the format of:
|
||||
|
||||
1. As an <actor>, I want a <feature>, so that <benefit>
|
||||
|
||||
<user-story-example>
|
||||
1. As a mobile bank customer, I want to see balance on my accounts, so that I can make better informed decisions about my spending
|
||||
</user-story-example>
|
||||
|
||||
This list of user stories should be extremely extensive and cover all aspects of the feature.
|
||||
|
||||
## Implementation Decisions
|
||||
|
||||
A list of implementation decisions that were made. This can include:
|
||||
|
||||
- The modules that will be built/modified
|
||||
- The interfaces of those modules that will be modified
|
||||
- Technical clarifications from the developer
|
||||
- Architectural decisions
|
||||
- Schema changes
|
||||
- API contracts
|
||||
- Specific interactions
|
||||
|
||||
Do NOT include specific file paths or code snippets. They may end up being outdated very quickly.
|
||||
|
||||
Exception: if a prototype produced a snippet that encodes a decision more precisely than prose can (state machine, reducer, schema, type shape), inline it within the relevant decision and note briefly that it came from a prototype. Trim to the decision-rich parts — not a working demo, just the important bits.
|
||||
|
||||
## Testing Decisions
|
||||
|
||||
A list of testing decisions that were made. Include:
|
||||
|
||||
- A description of what makes a good test (only test external behavior, not implementation details)
|
||||
- Which modules will be tested
|
||||
- Prior art for the tests (i.e. similar types of tests in the codebase)
|
||||
|
||||
## Out of Scope
|
||||
|
||||
A description of the things that are out of scope for this PRD.
|
||||
|
||||
## Further Notes
|
||||
|
||||
Any further notes about the feature.
|
||||
|
||||
</prd-template>
|
||||
Reference in new issue
Block a user