chore: bootstrap project scaffolding
Add architecture doc (research-wiki/ARCHITECTURE.md), CLAUDE.md with tiered SOP for Fable 5, adapted .claude skills/hooks/settings, package skeleton (src/polygateway), pyproject with import-linter contracts, Makefile, .env.example and smoke test.
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
---
|
||||
name: brainstorming
|
||||
description: "Use before design-level creative work. MANDATORY when the change touches public API, port (Protocol) signatures, or architecture boundaries — these require an approved design and a human gate. For smaller changes (internal implementation, test fixes, tasks inside an approved plan), use your judgment: skip if requirements are already unambiguous."
|
||||
---
|
||||
|
||||
# Brainstorming Ideas Into Designs
|
||||
|
||||
把想法变成经过对比与确认的设计。产出是一份设计文档,不是代码。
|
||||
|
||||
## 触发边界(何时必须用)
|
||||
|
||||
| 情形 | 是否强制 |
|
||||
|---|---|
|
||||
| 变更公共 API、端口(Protocol)签名、架构边界(`ports.py`/`types.py`/`errors.py`、middleware 洋葱层次、依赖铁律) | **强制**,且设计必须经人类确认后才能实施 |
|
||||
| 新增子系统/新组件、里程碑级功能 | **强制** |
|
||||
| 内部实现、测试修复、已批准 plan 内的任务、文档修订 | 自判;需求无歧义即可直接做 |
|
||||
|
||||
不确定属于哪一类时,按"是否会改变库对下游三个项目的承诺"判断:会,就走设计。
|
||||
|
||||
## 核心要求(设计的验收标准)
|
||||
|
||||
一份合格的设计必须包含,缺一即不完整:
|
||||
|
||||
1. **备选方案对比**:至少 2-3 个可行方案、各自权衡、你的推荐及理由。只有一个方案 = 没有做设计。
|
||||
2. **旧版行为审计(仅重写/迁移类任务)**:本库大量工作是从 `reference/` 三项目迁移治理代码。凡替换/重写既有模块,必须列出旧版全部行为(含持久化、崩溃恢复、幂等、断点续跑),逐条标注"保留 / 替换 / 有意放弃"。未声明的隐式丢弃 = bug。
|
||||
3. **非功能维度(逐条回答,允许"不适用"但必须写明)**:
|
||||
- 并发与取消:并发调用下的行为?`CancelledError` 穿透路径?
|
||||
- 降级方向:依赖的后端不可用时,静默降级还是报错?(对照 CLAUDE.md 库铁律)
|
||||
- 幂等与重复:同一操作重复执行是否安全?
|
||||
- 持久化与原子性:什么时候落盘?部分写入会不会损坏数据?
|
||||
4. **错误处理与测试策略**:失败落入哪个错误分类?怎么测?
|
||||
|
||||
## 过程建议(非脚本)
|
||||
|
||||
先查 `research-wiki/ARCHITECTURE.md` 与相关 `reference/` 代码,再提问;问题一次一个、能选择题就选择题;范围过大(多个独立子系统)先拆分再逐个设计。不做任务外的重构与抽象——设计只服务当前目标。
|
||||
|
||||
如需向人类展示视觉对比(布局/图示),可参考 `visual-companion.md`(可选工具,非流程)。
|
||||
|
||||
## 留痕与审批门(不可省略)
|
||||
|
||||
1. **写设计文档**: `research-wiki/designs/YYYY-MM-DD-<topic>-design.md`(≤400 行),并提交 git。
|
||||
2. **自审后送 Codex 独立审**: 用 `/codex:rescue --fresh --wait` 只读审查(需求覆盖、内部一致性、与 ARCHITECTURE.md/CLAUDE.md 的冲突)。逐条核验其意见后就地修订——不盲从。模板见 `spec-document-reviewer-prompt.md`。
|
||||
3. **人类审批门**: 凡触发"强制"档的设计,必须请人类审阅设计文档并明确同意后才进入 `writing-plans`。这是硬门,不因任何理由跳过。
|
||||
4. **Wiki 注册**(`research-wiki/` 存在时):
|
||||
```bash
|
||||
.claude/tools/research_wiki.py add_entity research-wiki/ --type design --id <slug> --title "<title>"
|
||||
.claude/tools/research_wiki.py rebuild_index research-wiki/
|
||||
```
|
||||
在生成页中记录:选定方案、关键理由、**被否决的备选及否决原因**。
|
||||
|
||||
设计获批后的下一步是 `writing-plans`(若达到其触发规模),不要跳到实现类 skill。
|
||||
@@ -0,0 +1,49 @@
|
||||
# Spec Document Reviewer Prompt Template
|
||||
|
||||
Use this template when dispatching a spec document reviewer subagent.
|
||||
|
||||
**Purpose:** Verify the spec is complete, consistent, and ready for implementation planning.
|
||||
|
||||
**Dispatch after:** Spec document is written to docs/superpowers/specs/
|
||||
|
||||
```
|
||||
Task tool (general-purpose):
|
||||
description: "Review spec document"
|
||||
prompt: |
|
||||
You are a spec document reviewer. Verify this spec is complete and ready for planning.
|
||||
|
||||
**Spec to review:** [SPEC_FILE_PATH]
|
||||
|
||||
## What to Check
|
||||
|
||||
| Category | What to Look For |
|
||||
|----------|------------------|
|
||||
| Completeness | TODOs, placeholders, "TBD", incomplete sections |
|
||||
| Consistency | Internal contradictions, conflicting requirements |
|
||||
| Clarity | Requirements ambiguous enough to cause someone to build the wrong thing |
|
||||
| Scope | Focused enough for a single plan — not covering multiple independent subsystems |
|
||||
| YAGNI | Unrequested features, over-engineering |
|
||||
|
||||
## Calibration
|
||||
|
||||
**Only flag issues that would cause real problems during implementation planning.**
|
||||
A missing section, a contradiction, or a requirement so ambiguous it could be
|
||||
interpreted two different ways — those are issues. Minor wording improvements,
|
||||
stylistic preferences, and "sections less detailed than others" are not.
|
||||
|
||||
Approve unless there are serious gaps that would lead to a flawed plan.
|
||||
|
||||
## Output Format
|
||||
|
||||
## Spec Review
|
||||
|
||||
**Status:** Approved | Issues Found
|
||||
|
||||
**Issues (if any):**
|
||||
- [Section X]: [specific issue] - [why it matters for planning]
|
||||
|
||||
**Recommendations (advisory, do not block approval):**
|
||||
- [suggestions for improvement]
|
||||
```
|
||||
|
||||
**Reviewer returns:** Status, Issues (if any), Recommendations
|
||||
@@ -0,0 +1,287 @@
|
||||
# Visual Companion Guide
|
||||
|
||||
Browser-based visual brainstorming companion for showing mockups, diagrams, and options.
|
||||
|
||||
## When to Use
|
||||
|
||||
Decide per-question, not per-session. The test: **would the user understand this better by seeing it than reading it?**
|
||||
|
||||
**Use the browser** when the content itself is visual:
|
||||
|
||||
- **UI mockups** — wireframes, layouts, navigation structures, component designs
|
||||
- **Architecture diagrams** — system components, data flow, relationship maps
|
||||
- **Side-by-side visual comparisons** — comparing two layouts, two color schemes, two design directions
|
||||
- **Design polish** — when the question is about look and feel, spacing, visual hierarchy
|
||||
- **Spatial relationships** — state machines, flowcharts, entity relationships rendered as diagrams
|
||||
|
||||
**Use the terminal** when the content is text or tabular:
|
||||
|
||||
- **Requirements and scope questions** — "what does X mean?", "which features are in scope?"
|
||||
- **Conceptual A/B/C choices** — picking between approaches described in words
|
||||
- **Tradeoff lists** — pros/cons, comparison tables
|
||||
- **Technical decisions** — API design, data modeling, architectural approach selection
|
||||
- **Clarifying questions** — anything where the answer is words, not a visual preference
|
||||
|
||||
A question *about* a UI topic is not automatically a visual question. "What kind of wizard do you want?" is conceptual — use the terminal. "Which of these wizard layouts feels right?" is visual — use the browser.
|
||||
|
||||
## How It Works
|
||||
|
||||
The server watches a directory for HTML files and serves the newest one to the browser. You write HTML content to `screen_dir`, the user sees it in their browser and can click to select options. Selections are recorded to `state_dir/events` that you read on your next turn.
|
||||
|
||||
**Content fragments vs full documents:** If your HTML file starts with `<!DOCTYPE` or `<html`, the server serves it as-is (just injects the helper script). Otherwise, the server automatically wraps your content in the frame template — adding the header, CSS theme, selection indicator, and all interactive infrastructure. **Write content fragments by default.** Only write full documents when you need complete control over the page.
|
||||
|
||||
## Starting a Session
|
||||
|
||||
```bash
|
||||
# Start server with persistence (mockups saved to project)
|
||||
scripts/start-server.sh --project-dir /path/to/project
|
||||
|
||||
# Returns: {"type":"server-started","port":52341,"url":"http://localhost:52341",
|
||||
# "screen_dir":"/path/to/project/.superpowers/brainstorm/12345-1706000000/content",
|
||||
# "state_dir":"/path/to/project/.superpowers/brainstorm/12345-1706000000/state"}
|
||||
```
|
||||
|
||||
Save `screen_dir` and `state_dir` from the response. Tell user to open the URL.
|
||||
|
||||
**Finding connection info:** The server writes its startup JSON to `$STATE_DIR/server-info`. If you launched the server in the background and didn't capture stdout, read that file to get the URL and port. When using `--project-dir`, check `<project>/.superpowers/brainstorm/` for the session directory.
|
||||
|
||||
**Note:** Pass the project root as `--project-dir` so mockups persist in `.superpowers/brainstorm/` and survive server restarts. Without it, files go to `/tmp` and get cleaned up. Remind the user to add `.superpowers/` to `.gitignore` if it's not already there.
|
||||
|
||||
**Launching the server by platform:**
|
||||
|
||||
**Claude Code (macOS / Linux):**
|
||||
```bash
|
||||
# Default mode works — the script backgrounds the server itself
|
||||
scripts/start-server.sh --project-dir /path/to/project
|
||||
```
|
||||
|
||||
**Claude Code (Windows):**
|
||||
```bash
|
||||
# Windows auto-detects and uses foreground mode, which blocks the tool call.
|
||||
# Use run_in_background: true on the Bash tool call so the server survives
|
||||
# across conversation turns.
|
||||
scripts/start-server.sh --project-dir /path/to/project
|
||||
```
|
||||
When calling this via the Bash tool, set `run_in_background: true`. Then read `$STATE_DIR/server-info` on the next turn to get the URL and port.
|
||||
|
||||
**Codex:**
|
||||
```bash
|
||||
# Codex reaps background processes. The script auto-detects CODEX_CI and
|
||||
# switches to foreground mode. Run it normally — no extra flags needed.
|
||||
scripts/start-server.sh --project-dir /path/to/project
|
||||
```
|
||||
|
||||
**Gemini CLI:**
|
||||
```bash
|
||||
# Use --foreground and set is_background: true on your shell tool call
|
||||
# so the process survives across turns
|
||||
scripts/start-server.sh --project-dir /path/to/project --foreground
|
||||
```
|
||||
|
||||
**Other environments:** The server must keep running in the background across conversation turns. If your environment reaps detached processes, use `--foreground` and launch the command with your platform's background execution mechanism.
|
||||
|
||||
If the URL is unreachable from your browser (common in remote/containerized setups), bind a non-loopback host:
|
||||
|
||||
```bash
|
||||
scripts/start-server.sh \
|
||||
--project-dir /path/to/project \
|
||||
--host 0.0.0.0 \
|
||||
--url-host localhost
|
||||
```
|
||||
|
||||
Use `--url-host` to control what hostname is printed in the returned URL JSON.
|
||||
|
||||
## The Loop
|
||||
|
||||
1. **Check server is alive**, then **write HTML** to a new file in `screen_dir`:
|
||||
- Before each write, check that `$STATE_DIR/server-info` exists. If it doesn't (or `$STATE_DIR/server-stopped` exists), the server has shut down — restart it with `start-server.sh` before continuing. The server auto-exits after 30 minutes of inactivity.
|
||||
- Use semantic filenames: `platform.html`, `visual-style.html`, `layout.html`
|
||||
- **Never reuse filenames** — each screen gets a fresh file
|
||||
- Use Write tool — **never use cat/heredoc** (dumps noise into terminal)
|
||||
- Server automatically serves the newest file
|
||||
|
||||
2. **Tell user what to expect and end your turn:**
|
||||
- Remind them of the URL (every step, not just first)
|
||||
- Give a brief text summary of what's on screen (e.g., "Showing 3 layout options for the homepage")
|
||||
- Ask them to respond in the terminal: "Take a look and let me know what you think. Click to select an option if you'd like."
|
||||
|
||||
3. **On your next turn** — after the user responds in the terminal:
|
||||
- Read `$STATE_DIR/events` if it exists — this contains the user's browser interactions (clicks, selections) as JSON lines
|
||||
- Merge with the user's terminal text to get the full picture
|
||||
- The terminal message is the primary feedback; `state_dir/events` provides structured interaction data
|
||||
|
||||
4. **Iterate or advance** — if feedback changes current screen, write a new file (e.g., `layout-v2.html`). Only move to the next question when the current step is validated.
|
||||
|
||||
5. **Unload when returning to terminal** — when the next step doesn't need the browser (e.g., a clarifying question, a tradeoff discussion), push a waiting screen to clear the stale content:
|
||||
|
||||
```html
|
||||
<!-- filename: waiting.html (or waiting-2.html, etc.) -->
|
||||
<div style="display:flex;align-items:center;justify-content:center;min-height:60vh">
|
||||
<p class="subtitle">Continuing in terminal...</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
This prevents the user from staring at a resolved choice while the conversation has moved on. When the next visual question comes up, push a new content file as usual.
|
||||
|
||||
6. Repeat until done.
|
||||
|
||||
## Writing Content Fragments
|
||||
|
||||
Write just the content that goes inside the page. The server wraps it in the frame template automatically (header, theme CSS, selection indicator, and all interactive infrastructure).
|
||||
|
||||
**Minimal example:**
|
||||
|
||||
```html
|
||||
<h2>Which layout works better?</h2>
|
||||
<p class="subtitle">Consider readability and visual hierarchy</p>
|
||||
|
||||
<div class="options">
|
||||
<div class="option" data-choice="a" onclick="toggleSelect(this)">
|
||||
<div class="letter">A</div>
|
||||
<div class="content">
|
||||
<h3>Single Column</h3>
|
||||
<p>Clean, focused reading experience</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option" data-choice="b" onclick="toggleSelect(this)">
|
||||
<div class="letter">B</div>
|
||||
<div class="content">
|
||||
<h3>Two Column</h3>
|
||||
<p>Sidebar navigation with main content</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
That's it. No `<html>`, no CSS, no `<script>` tags needed. The server provides all of that.
|
||||
|
||||
## CSS Classes Available
|
||||
|
||||
The frame template provides these CSS classes for your content:
|
||||
|
||||
### Options (A/B/C choices)
|
||||
|
||||
```html
|
||||
<div class="options">
|
||||
<div class="option" data-choice="a" onclick="toggleSelect(this)">
|
||||
<div class="letter">A</div>
|
||||
<div class="content">
|
||||
<h3>Title</h3>
|
||||
<p>Description</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Multi-select:** Add `data-multiselect` to the container to let users select multiple options. Each click toggles the item. The indicator bar shows the count.
|
||||
|
||||
```html
|
||||
<div class="options" data-multiselect>
|
||||
<!-- same option markup — users can select/deselect multiple -->
|
||||
</div>
|
||||
```
|
||||
|
||||
### Cards (visual designs)
|
||||
|
||||
```html
|
||||
<div class="cards">
|
||||
<div class="card" data-choice="design1" onclick="toggleSelect(this)">
|
||||
<div class="card-image"><!-- mockup content --></div>
|
||||
<div class="card-body">
|
||||
<h3>Name</h3>
|
||||
<p>Description</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Mockup container
|
||||
|
||||
```html
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">Preview: Dashboard Layout</div>
|
||||
<div class="mockup-body"><!-- your mockup HTML --></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Split view (side-by-side)
|
||||
|
||||
```html
|
||||
<div class="split">
|
||||
<div class="mockup"><!-- left --></div>
|
||||
<div class="mockup"><!-- right --></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Pros/Cons
|
||||
|
||||
```html
|
||||
<div class="pros-cons">
|
||||
<div class="pros"><h4>Pros</h4><ul><li>Benefit</li></ul></div>
|
||||
<div class="cons"><h4>Cons</h4><ul><li>Drawback</li></ul></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Mock elements (wireframe building blocks)
|
||||
|
||||
```html
|
||||
<div class="mock-nav">Logo | Home | About | Contact</div>
|
||||
<div style="display: flex;">
|
||||
<div class="mock-sidebar">Navigation</div>
|
||||
<div class="mock-content">Main content area</div>
|
||||
</div>
|
||||
<button class="mock-button">Action Button</button>
|
||||
<input class="mock-input" placeholder="Input field">
|
||||
<div class="placeholder">Placeholder area</div>
|
||||
```
|
||||
|
||||
### Typography and sections
|
||||
|
||||
- `h2` — page title
|
||||
- `h3` — section heading
|
||||
- `.subtitle` — secondary text below title
|
||||
- `.section` — content block with bottom margin
|
||||
- `.label` — small uppercase label text
|
||||
|
||||
## Browser Events Format
|
||||
|
||||
When the user clicks options in the browser, their interactions are recorded to `$STATE_DIR/events` (one JSON object per line). The file is cleared automatically when you push a new screen.
|
||||
|
||||
```jsonl
|
||||
{"type":"click","choice":"a","text":"Option A - Simple Layout","timestamp":1706000101}
|
||||
{"type":"click","choice":"c","text":"Option C - Complex Grid","timestamp":1706000108}
|
||||
{"type":"click","choice":"b","text":"Option B - Hybrid","timestamp":1706000115}
|
||||
```
|
||||
|
||||
The full event stream shows the user's exploration path — they may click multiple options before settling. The last `choice` event is typically the final selection, but the pattern of clicks can reveal hesitation or preferences worth asking about.
|
||||
|
||||
If `$STATE_DIR/events` doesn't exist, the user didn't interact with the browser — use only their terminal text.
|
||||
|
||||
## Design Tips
|
||||
|
||||
- **Scale fidelity to the question** — wireframes for layout, polish for polish questions
|
||||
- **Explain the question on each page** — "Which layout feels more professional?" not just "Pick one"
|
||||
- **Iterate before advancing** — if feedback changes current screen, write a new version
|
||||
- **2-4 options max** per screen
|
||||
- **Use real content when it matters** — for a photography portfolio, use actual images (Unsplash). Placeholder content obscures design issues.
|
||||
- **Keep mockups simple** — focus on layout and structure, not pixel-perfect design
|
||||
|
||||
## File Naming
|
||||
|
||||
- Use semantic names: `platform.html`, `visual-style.html`, `layout.html`
|
||||
- Never reuse filenames — each screen must be a new file
|
||||
- For iterations: append version suffix like `layout-v2.html`, `layout-v3.html`
|
||||
- Server serves newest file by modification time
|
||||
|
||||
## Cleaning Up
|
||||
|
||||
```bash
|
||||
scripts/stop-server.sh $SESSION_DIR
|
||||
```
|
||||
|
||||
If the session used `--project-dir`, mockup files persist in `.superpowers/brainstorm/` for later reference. Only `/tmp` sessions get deleted on stop.
|
||||
|
||||
## Reference
|
||||
|
||||
- Frame template (CSS reference): `scripts/frame-template.html`
|
||||
- Helper script (client-side): `scripts/helper.js`
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
name: commit
|
||||
description: Use when committing code changes - enforces commit message format, safety checks, and prohibits AI signatures
|
||||
---
|
||||
|
||||
# Commit
|
||||
|
||||
Automatically generate a commit message and submit the code.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Check status**: run `git status` to inspect changes (do not use `-uall`)
|
||||
2. **Review diff**: run `git diff --staged` and `git diff` to inspect the exact changes
|
||||
3. **Check style**: run `git log --oneline -10` to review recent commit style
|
||||
4. **Generate message**: create a commit message that follows the rules below
|
||||
5. **Commit**: stage the files and commit
|
||||
|
||||
## Commit Message Rules
|
||||
|
||||
### Format
|
||||
```text
|
||||
<type>: <description>
|
||||
|
||||
[optional body]
|
||||
```
|
||||
|
||||
### Type Values
|
||||
- `feat`: new feature
|
||||
- `fix`: bug fix
|
||||
- `docs`: documentation update
|
||||
- `refactor`: refactor with no functional change
|
||||
- `test`: test-related change
|
||||
- `chore`: build / tooling / configuration
|
||||
- `perf`: performance optimization
|
||||
- `ci`: CI/CD related
|
||||
|
||||
### Rules
|
||||
- The description must be written in English
|
||||
- Keep it under 72 characters
|
||||
- Use the imperative mood, for example "add user authentication" instead of "added user authentication"
|
||||
|
||||
## Safety Rules
|
||||
|
||||
> [!CAUTION]
|
||||
> **Never** add any AI signature to a commit message:
|
||||
> - Do not add `Co-Authored-By: Claude`
|
||||
> - Do not add `🤖 Generated with...`
|
||||
> - Do not add any AI signature or marker
|
||||
|
||||
## Sensitive File Check
|
||||
|
||||
Before committing, check whether any of the following files were accidentally added:
|
||||
- `.env` / `.env.*`
|
||||
- `credentials.json`
|
||||
- `*_secret*`
|
||||
- `*.pem` / `*.key`
|
||||
|
||||
If sensitive files are found, **warn the user** and wait for confirmation.
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Auto-generate a message
|
||||
/commit
|
||||
|
||||
# Specify a message
|
||||
/commit "feat: add user login"
|
||||
```
|
||||
@@ -0,0 +1,251 @@
|
||||
---
|
||||
name: finishing-a-development-branch
|
||||
description: Use when implementation is complete, all tests pass, and you need to decide how to integrate the work - guides completion of development work by presenting structured options for merge, PR, or cleanup
|
||||
---
|
||||
|
||||
# Finishing a Development Branch
|
||||
|
||||
## Overview
|
||||
|
||||
Guide completion of development work by presenting clear options and handling chosen workflow.
|
||||
|
||||
**Core principle:** Verify tests → Detect environment → Present options → Execute choice → Clean up.
|
||||
|
||||
**Announce at start:** "I'm using the finishing-a-development-branch skill to complete this work."
|
||||
|
||||
## The Process
|
||||
|
||||
### Step 1: Verify Tests
|
||||
|
||||
**Before presenting options, verify tests pass:**
|
||||
|
||||
```bash
|
||||
# Run project's test suite
|
||||
conda run -n PolyGateway pytest # 本项目;其他栈: npm test / cargo test / go test
|
||||
```
|
||||
|
||||
**If tests fail:**
|
||||
```
|
||||
Tests failing (<N> failures). Must fix before completing:
|
||||
|
||||
[Show failures]
|
||||
|
||||
Cannot proceed with merge/PR until tests pass.
|
||||
```
|
||||
|
||||
Stop. Don't proceed to Step 2.
|
||||
|
||||
**If tests pass:** Continue to Step 2.
|
||||
|
||||
### Step 2: Detect Environment
|
||||
|
||||
**Determine workspace state before presenting options:**
|
||||
|
||||
```bash
|
||||
GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P)
|
||||
GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P)
|
||||
```
|
||||
|
||||
This determines which menu to show and how cleanup works:
|
||||
|
||||
| State | Menu | Cleanup |
|
||||
|-------|------|---------|
|
||||
| `GIT_DIR == GIT_COMMON` (normal repo) | Standard 4 options | No worktree to clean up |
|
||||
| `GIT_DIR != GIT_COMMON`, named branch | Standard 4 options | Provenance-based (see Step 6) |
|
||||
| `GIT_DIR != GIT_COMMON`, detached HEAD | Reduced 3 options (no merge) | No cleanup (externally managed) |
|
||||
|
||||
### Step 3: Determine Base Branch
|
||||
|
||||
```bash
|
||||
# Try common base branches
|
||||
git merge-base HEAD main 2>/dev/null || git merge-base HEAD master 2>/dev/null
|
||||
```
|
||||
|
||||
Or ask: "This branch split from main - is that correct?"
|
||||
|
||||
### Step 4: Present Options
|
||||
|
||||
**Normal repo and named-branch worktree — present exactly these 4 options:**
|
||||
|
||||
```
|
||||
Implementation complete. What would you like to do?
|
||||
|
||||
1. Merge back to <base-branch> locally
|
||||
2. Push and create a Pull Request
|
||||
3. Keep the branch as-is (I'll handle it later)
|
||||
4. Discard this work
|
||||
|
||||
Which option?
|
||||
```
|
||||
|
||||
**Detached HEAD — present exactly these 3 options:**
|
||||
|
||||
```
|
||||
Implementation complete. You're on a detached HEAD (externally managed workspace).
|
||||
|
||||
1. Push as new branch and create a Pull Request
|
||||
2. Keep as-is (I'll handle it later)
|
||||
3. Discard this work
|
||||
|
||||
Which option?
|
||||
```
|
||||
|
||||
**Don't add explanation** - keep options concise.
|
||||
|
||||
### Step 5: Execute Choice
|
||||
|
||||
#### Option 1: Merge Locally
|
||||
|
||||
```bash
|
||||
# Get main repo root for CWD safety
|
||||
MAIN_ROOT=$(git -C "$(git rev-parse --git-common-dir)/.." rev-parse --show-toplevel)
|
||||
cd "$MAIN_ROOT"
|
||||
|
||||
# Merge first — verify success before removing anything
|
||||
git checkout <base-branch>
|
||||
git pull
|
||||
git merge <feature-branch>
|
||||
|
||||
# Verify tests on merged result
|
||||
<test command>
|
||||
|
||||
# Only after merge succeeds: cleanup worktree (Step 6), then delete branch
|
||||
```
|
||||
|
||||
Then: Cleanup worktree (Step 6), then delete branch:
|
||||
|
||||
```bash
|
||||
git branch -d <feature-branch>
|
||||
```
|
||||
|
||||
#### Option 2: Push and Create PR
|
||||
|
||||
```bash
|
||||
# Push branch
|
||||
git push -u origin <feature-branch>
|
||||
|
||||
# Create PR
|
||||
gh pr create --title "<title>" --body "$(cat <<'EOF'
|
||||
## Summary
|
||||
<2-3 bullets of what changed>
|
||||
|
||||
## Test Plan
|
||||
- [ ] <verification steps>
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
**Do NOT clean up worktree** — user needs it alive to iterate on PR feedback.
|
||||
|
||||
#### Option 3: Keep As-Is
|
||||
|
||||
Report: "Keeping branch <name>. Worktree preserved at <path>."
|
||||
|
||||
**Don't cleanup worktree.**
|
||||
|
||||
#### Option 4: Discard
|
||||
|
||||
**Confirm first:**
|
||||
```
|
||||
This will permanently delete:
|
||||
- Branch <name>
|
||||
- All commits: <commit-list>
|
||||
- Worktree at <path>
|
||||
|
||||
Type 'discard' to confirm.
|
||||
```
|
||||
|
||||
Wait for exact confirmation.
|
||||
|
||||
If confirmed:
|
||||
```bash
|
||||
MAIN_ROOT=$(git -C "$(git rev-parse --git-common-dir)/.." rev-parse --show-toplevel)
|
||||
cd "$MAIN_ROOT"
|
||||
```
|
||||
|
||||
Then: Cleanup worktree (Step 6), then force-delete branch:
|
||||
```bash
|
||||
git branch -D <feature-branch>
|
||||
```
|
||||
|
||||
### Step 6: Cleanup Workspace
|
||||
|
||||
**Only runs for Options 1 and 4.** Options 2 and 3 always preserve the worktree.
|
||||
|
||||
```bash
|
||||
GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P)
|
||||
GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P)
|
||||
WORKTREE_PATH=$(git rev-parse --show-toplevel)
|
||||
```
|
||||
|
||||
**If `GIT_DIR == GIT_COMMON`:** Normal repo, no worktree to clean up. Done.
|
||||
|
||||
**If worktree path is under `.worktrees/`, `worktrees/`, or `~/.config/superpowers/worktrees/`:** Superpowers created this worktree — we own cleanup.
|
||||
|
||||
```bash
|
||||
MAIN_ROOT=$(git -C "$(git rev-parse --git-common-dir)/.." rev-parse --show-toplevel)
|
||||
cd "$MAIN_ROOT"
|
||||
git worktree remove "$WORKTREE_PATH"
|
||||
git worktree prune # Self-healing: clean up any stale registrations
|
||||
```
|
||||
|
||||
**Otherwise:** The host environment (harness) owns this workspace. Do NOT remove it. If your platform provides a workspace-exit tool, use it. Otherwise, leave the workspace in place.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Option | Merge | Push | Keep Worktree | Cleanup Branch |
|
||||
|--------|-------|------|---------------|----------------|
|
||||
| 1. Merge locally | yes | - | - | yes |
|
||||
| 2. Create PR | - | yes | yes | - |
|
||||
| 3. Keep as-is | - | - | yes | - |
|
||||
| 4. Discard | - | - | - | yes (force) |
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
**Skipping test verification**
|
||||
- **Problem:** Merge broken code, create failing PR
|
||||
- **Fix:** Always verify tests before offering options
|
||||
|
||||
**Open-ended questions**
|
||||
- **Problem:** "What should I do next?" is ambiguous
|
||||
- **Fix:** Present exactly 4 structured options (or 3 for detached HEAD)
|
||||
|
||||
**Cleaning up worktree for Option 2**
|
||||
- **Problem:** Remove worktree user needs for PR iteration
|
||||
- **Fix:** Only cleanup for Options 1 and 4
|
||||
|
||||
**Deleting branch before removing worktree**
|
||||
- **Problem:** `git branch -d` fails because worktree still references the branch
|
||||
- **Fix:** Merge first, remove worktree, then delete branch
|
||||
|
||||
**Running git worktree remove from inside the worktree**
|
||||
- **Problem:** Command fails silently when CWD is inside the worktree being removed
|
||||
- **Fix:** Always `cd` to main repo root before `git worktree remove`
|
||||
|
||||
**Cleaning up harness-owned worktrees**
|
||||
- **Problem:** Removing a worktree the harness created causes phantom state
|
||||
- **Fix:** Only clean up worktrees under `.worktrees/`, `worktrees/`, or `~/.config/superpowers/worktrees/`
|
||||
|
||||
**No confirmation for discard**
|
||||
- **Problem:** Accidentally delete work
|
||||
- **Fix:** Require typed "discard" confirmation
|
||||
|
||||
## Red Flags
|
||||
|
||||
**Never:**
|
||||
- Proceed with failing tests
|
||||
- Merge without verifying tests on result
|
||||
- Delete work without confirmation
|
||||
- Force-push without explicit request
|
||||
- Remove a worktree before confirming merge success
|
||||
- Clean up worktrees you didn't create (provenance check)
|
||||
- Run `git worktree remove` from inside the worktree
|
||||
|
||||
**Always:**
|
||||
- Verify tests before offering options
|
||||
- Detect environment before presenting menu
|
||||
- Present exactly 4 options (or 3 for detached HEAD)
|
||||
- Get typed confirmation for Option 4
|
||||
- Clean up worktree for Options 1 & 4 only
|
||||
- `cd` to main repo root before worktree removal
|
||||
- Run `git worktree prune` after removal
|
||||
@@ -0,0 +1,616 @@
|
||||
---
|
||||
name: graphify
|
||||
description: "Use for any question about a codebase, its architecture, file relationships, or project content — especially when graphify-out/ exists, where the question should be treated as a graphify query first. Turns any input (code, docs, papers, images, videos) into a persistent knowledge graph with god nodes, community detection, and query/path/explain tools."
|
||||
trigger: /graphify
|
||||
---
|
||||
|
||||
# /graphify
|
||||
|
||||
Turn any folder of files into a navigable knowledge graph with community detection, an honest audit trail, and three outputs: interactive HTML, GraphRAG-ready JSON, and a plain-language GRAPH_REPORT.md.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/graphify # full pipeline on current directory → Obsidian vault
|
||||
/graphify <path> # full pipeline on specific path
|
||||
/graphify https://github.com/<owner>/<repo> # clone repo then run full pipeline on it
|
||||
/graphify https://github.com/<owner>/<repo> --branch <branch> # clone a specific branch
|
||||
/graphify <url1> <url2> ... # clone multiple repos, build each, merge into one cross-repo graph
|
||||
/graphify <path> --mode deep # thorough extraction, richer INFERRED edges
|
||||
/graphify <path> --update # incremental - re-extract only new/changed files
|
||||
/graphify <path> --directed # build directed graph (preserves edge direction: source→target)
|
||||
/graphify <path> --whisper-model medium # use a larger Whisper model for better transcription accuracy
|
||||
/graphify <path> --cluster-only # rerun clustering on existing graph
|
||||
/graphify <path> --no-viz # skip visualization, just report + JSON
|
||||
/graphify <path> --html # (HTML is generated by default - this flag is a no-op)
|
||||
/graphify <path> --svg # also export graph.svg (embeds in Notion, GitHub)
|
||||
/graphify <path> --graphml # export graph.graphml (Gephi, yEd)
|
||||
/graphify <path> --neo4j # generate graphify-out/cypher.txt for Neo4j
|
||||
/graphify <path> --neo4j-push bolt://localhost:7687 # push directly to Neo4j
|
||||
/graphify <path> --mcp # start MCP stdio server for agent access
|
||||
/graphify <path> --watch # watch folder, auto-rebuild on code changes (no LLM needed)
|
||||
/graphify <path> --wiki # build agent-crawlable wiki (index.md + one article per community)
|
||||
/graphify <path> --obsidian --obsidian-dir ~/vaults/my-project # write vault to custom path (e.g. existing vault)
|
||||
/graphify add <url> # fetch URL, save to ./raw, update graph
|
||||
/graphify add <url> --author "Name" # tag who wrote it
|
||||
/graphify add <url> --contributor "Name" # tag who added it to the corpus
|
||||
/graphify query "<question>" # BFS traversal - broad context
|
||||
/graphify query "<question>" --dfs # DFS - trace a specific path
|
||||
/graphify query "<question>" --budget 1500 # cap answer at N tokens
|
||||
/graphify path "AuthModule" "Database" # shortest path between two concepts
|
||||
/graphify explain "SwinTransformer" # plain-language explanation of a node
|
||||
```
|
||||
|
||||
## What graphify is for
|
||||
|
||||
Drop any folder of code, docs, papers, images, or video into graphify and get a queryable knowledge graph. Persistent across sessions, honest audit trail (EXTRACTED/INFERRED/AMBIGUOUS), community detection surfaces cross-document connections you wouldn't think to ask about.
|
||||
|
||||
## What You Must Do When Invoked
|
||||
|
||||
If the user invoked `/graphify --help` or `/graphify -h` (with no other arguments), print the contents of the `## Usage` section above verbatim and stop. Do not run any commands, do not detect files, do not default the path to `.`. Just print the Usage block and return.
|
||||
|
||||
**Fast path — existing graph:** Before doing anything else, check whether `graphify-out/graph.json` exists. The expected location is `graphify-out/graph.json` relative to the **current working directory** (i.e. the project root where you are running commands). If it exists AND the user's request is a natural-language question about the codebase (e.g. "How does X work?", "What calls Y?", "Trace the data flow through Z") and NOT an explicit rebuild command (`--update`, `--cluster-only`, or a bare path/URL that implies fresh extraction): **skip Steps 1–5 entirely and jump straight to `## For /graphify query`.** Run `graphify query "<question>"` immediately. Do not run detect. Do not check corpus size. Do not ask the user to narrow. The graph is already built — use it.
|
||||
|
||||
If no path was given, use `.` (current directory). Do not ask the user for a path.
|
||||
|
||||
If the path argument starts with `https://github.com/` or `http://github.com/`, treat it as a GitHub URL - run Step 0 before anything else, then continue with the resolved local path.
|
||||
|
||||
Follow these steps in order. Do not skip steps.
|
||||
|
||||
### Step 0 - GitHub repos and multi-path merge (only if a URL or several paths)
|
||||
|
||||
Only when the path is one or more `https://github.com/...` URLs, or several local subfolders to merge. See `references/github-and-merge.md` for the clone, cross-repo merge, and monorepo flow, then continue with the resolved local path. A plain local path skips this step.
|
||||
|
||||
### Step 1 - Ensure graphify is installed
|
||||
|
||||
```bash
|
||||
# Detect the correct Python interpreter (handles uv tool, pipx, venv, system installs)
|
||||
PYTHON=""
|
||||
GRAPHIFY_BIN=$(which graphify 2>/dev/null)
|
||||
# 1. uv tool installs — most reliable on modern Mac/Linux
|
||||
if [ -z "$PYTHON" ] && command -v uv >/dev/null 2>&1; then
|
||||
_UV_PY=$(uv tool run graphifyy python -c "import sys; print(sys.executable)" 2>/dev/null)
|
||||
if [ -n "$_UV_PY" ]; then PYTHON="$_UV_PY"; fi
|
||||
fi
|
||||
# 2. Read shebang from graphify binary (pipx and direct pip installs)
|
||||
if [ -z "$PYTHON" ] && [ -n "$GRAPHIFY_BIN" ]; then
|
||||
_SHEBANG=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!')
|
||||
case "$_SHEBANG" in
|
||||
*[!a-zA-Z0-9/_.-]*) ;;
|
||||
*) "$_SHEBANG" -c "import graphify" 2>/dev/null && PYTHON="$_SHEBANG" ;;
|
||||
esac
|
||||
fi
|
||||
# 3. Fall back to python3
|
||||
if [ -z "$PYTHON" ]; then PYTHON="python3"; fi
|
||||
if ! "$PYTHON" -c "import graphify" 2>/dev/null; then
|
||||
if command -v uv >/dev/null 2>&1; then
|
||||
uv tool install --upgrade graphifyy -q 2>&1 | tail -3
|
||||
_UV_PY=$(uv tool run graphifyy python -c "import sys; print(sys.executable)" 2>/dev/null)
|
||||
if [ -n "$_UV_PY" ]; then PYTHON="$_UV_PY"; fi
|
||||
else
|
||||
"$PYTHON" -m pip install graphifyy -q 2>/dev/null \
|
||||
|| "$PYTHON" -m pip install graphifyy -q --break-system-packages 2>&1 | tail -3
|
||||
fi
|
||||
fi
|
||||
# Write interpreter path for all subsequent steps (persists across invocations)
|
||||
mkdir -p graphify-out
|
||||
"$PYTHON" -c "import sys; open('graphify-out/.graphify_python', 'w', encoding='utf-8').write(sys.executable)"
|
||||
# Save scan root so `graphify update` (no args) knows where to look next time
|
||||
echo "$(cd INPUT_PATH && pwd)" > graphify-out/.graphify_root
|
||||
```
|
||||
|
||||
If the import succeeds, print nothing and move straight to Step 2.
|
||||
|
||||
**In every subsequent bash block, replace `python3` with `$(cat graphify-out/.graphify_python)` to use the correct interpreter.**
|
||||
|
||||
### Step 2 - Detect files
|
||||
|
||||
```bash
|
||||
$(cat graphify-out/.graphify_python) -c "
|
||||
import json
|
||||
from graphify.detect import detect
|
||||
from pathlib import Path
|
||||
result = detect(Path('INPUT_PATH'))
|
||||
print(json.dumps(result, ensure_ascii=False))
|
||||
" > graphify-out/.graphify_detect.json
|
||||
```
|
||||
|
||||
Replace INPUT_PATH with the actual path the user provided. Do NOT cat or print the JSON - read it silently and present a clean summary instead:
|
||||
|
||||
```
|
||||
Corpus: X files · ~Y words
|
||||
code: N files (.py .ts .go ...)
|
||||
docs: N files (.md .txt ...)
|
||||
papers: N files (.pdf ...)
|
||||
images: N files
|
||||
video: N files (.mp4 .mp3 ...)
|
||||
```
|
||||
|
||||
Omit any category with 0 files from the summary.
|
||||
|
||||
Then act on it:
|
||||
- If `total_files` is 0: stop with "No supported files found in [path]."
|
||||
- If `skipped_sensitive` is non-empty: mention file count skipped, not the file names.
|
||||
- If `total_words` > 2,000,000 OR `total_files` > 500: show the warning. Then compute the top 5 first-level subdirectories by file count:
|
||||
- Read `scan_root` from the detect JSON (always an absolute path to the resolved INPUT_PATH).
|
||||
- Concatenate all file lists across all types (`code`, `document`, `paper`, `image`, `video`).
|
||||
- Filter out any path that starts with `scan_root + "/graphify-out/"` to exclude converted sidecars.
|
||||
- For each file, strip the `scan_root` prefix and take the first path component. Files directly in `scan_root` with no subdirectory count as `(root)`.
|
||||
- If all files are in `(root)` with no subdirectories, do not ask to narrow — no subfolders exist. Instead suggest `--no-cluster` to skip the expensive clustering step and proceed.
|
||||
- Otherwise rank by count, show the top 5 with file counts, then ask which subfolder to run on. Wait for the user's answer before proceeding.
|
||||
- Otherwise: proceed directly to Step 2.5 if video files were detected, or Step 3 if not.
|
||||
|
||||
### Step 2.5 - Video and audio (only if video files detected)
|
||||
|
||||
Skip this step entirely if `detect` returned zero `video` files. When the corpus has video or audio, see `references/transcribe.md` to transcribe them to text first, then treat the transcripts as doc files in Step 3.
|
||||
|
||||
### Step 3 - Extract entities and relationships
|
||||
|
||||
**Before starting:** note whether `--mode deep` was given. You must pass `DEEP_MODE=true` to every subagent in Step B2 if it was. Track this from the original invocation - do not lose it.
|
||||
|
||||
This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens).
|
||||
|
||||
**Before dispatching subagents:** check whether `GEMINI_API_KEY` or `GOOGLE_API_KEY` is set. If neither is set, print this one-liner to the user:
|
||||
> Tip: set `GEMINI_API_KEY` or `GOOGLE_API_KEY` to use Gemini for semantic extraction (`pip install 'graphifyy[gemini]'`).
|
||||
|
||||
Print it once, then continue. If `GEMINI_API_KEY` or `GOOGLE_API_KEY` IS set, use `graphify.llm.extract_corpus_parallel(files, backend="gemini")` for semantic extraction instead of dispatching Claude subagents. The default Gemini model is `gemini-3-flash-preview`; set `GRAPHIFY_GEMINI_MODEL` or pass `--model` in headless CLI flows to override it.
|
||||
|
||||
> **No other API keys are read.** If `GEMINI_API_KEY`/`GOOGLE_API_KEY` are unset, fall straight through to Claude Code subagent dispatch (Part B below) — the host session itself is the LLM. graphify does **not** read `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or any other provider key from the environment. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written.
|
||||
|
||||
**Run Part A (AST) and Part B (semantic) in parallel. Dispatch all semantic subagents AND start AST extraction in the same message. Both can run simultaneously since they operate on different file types. Merge results in Part C as before.**
|
||||
|
||||
Note: Parallelizing AST + semantic saves 5-15s on large corpora. AST is deterministic and fast; start it while subagents are processing docs/papers.
|
||||
|
||||
#### Part A - Structural extraction for code files
|
||||
|
||||
For any code files detected, run AST extraction in parallel with Part B subagents:
|
||||
|
||||
```bash
|
||||
$(cat graphify-out/.graphify_python) -c "
|
||||
import sys, json
|
||||
from graphify.extract import collect_files, extract
|
||||
from pathlib import Path
|
||||
import json
|
||||
|
||||
code_files = []
|
||||
detect = json.loads(Path('graphify-out/.graphify_detect.json').read_text(encoding=\"utf-8\"))
|
||||
for f in detect.get('files', {}).get('code', []):
|
||||
code_files.extend(collect_files(Path(f)) if Path(f).is_dir() else [Path(f)])
|
||||
|
||||
if code_files:
|
||||
result = extract(code_files, cache_root=Path('.'))
|
||||
Path('graphify-out/.graphify_ast.json').write_text(json.dumps(result, indent=2, ensure_ascii=False), encoding=\"utf-8\")
|
||||
print(f'AST: {len(result[\"nodes\"])} nodes, {len(result[\"edges\"])} edges')
|
||||
else:
|
||||
Path('graphify-out/.graphify_ast.json').write_text(json.dumps({'nodes':[],'edges':[],'input_tokens':0,'output_tokens':0}, ensure_ascii=False), encoding=\"utf-8\")
|
||||
print('No code files - skipping AST extraction')
|
||||
"
|
||||
```
|
||||
|
||||
#### Part B - Semantic extraction (parallel subagents)
|
||||
|
||||
**Fast path:** If detection found zero docs, papers, and images (code-only corpus), skip Part B entirely and go straight to Part C. AST handles code - there is nothing for semantic subagents to do.
|
||||
|
||||
**MANDATORY: You MUST use the Agent tool here. Reading files yourself one-by-one is forbidden - it is 5-10x slower. If you do not use the Agent tool you are doing this wrong.**
|
||||
|
||||
Before dispatching subagents, print a timing estimate:
|
||||
- Load `total_words` and file counts from `graphify-out/.graphify_detect.json`
|
||||
- Estimate agents needed: `ceil(uncached_non_code_files / 22)` (chunk size is 20-25)
|
||||
- Estimate time: ~45s per agent batch (they run in parallel, so total ≈ 45s × ceil(agents/parallel_limit))
|
||||
- Print: "Semantic extraction: ~N files → X agents, estimated ~Ys"
|
||||
|
||||
**Step B0 - Check extraction cache first**
|
||||
|
||||
Before dispatching any subagents, check which files already have cached extraction results:
|
||||
|
||||
```bash
|
||||
$(cat graphify-out/.graphify_python) -c "
|
||||
import json
|
||||
from graphify.cache import check_semantic_cache
|
||||
from pathlib import Path
|
||||
|
||||
detect = json.loads(Path('graphify-out/.graphify_detect.json').read_text(encoding=\"utf-8\"))
|
||||
all_files = [f for files in detect['files'].values() for f in files]
|
||||
|
||||
cached_nodes, cached_edges, cached_hyperedges, uncached = check_semantic_cache(all_files)
|
||||
|
||||
if cached_nodes or cached_edges or cached_hyperedges:
|
||||
Path('graphify-out/.graphify_cached.json').write_text(json.dumps({'nodes': cached_nodes, 'edges': cached_edges, 'hyperedges': cached_hyperedges}, ensure_ascii=False), encoding=\"utf-8\")
|
||||
Path('graphify-out/.graphify_uncached.txt').write_text('\n'.join(uncached), encoding=\"utf-8\")
|
||||
print(f'Cache: {len(all_files)-len(uncached)} files hit, {len(uncached)} files need extraction')
|
||||
"
|
||||
```
|
||||
|
||||
Only dispatch subagents for files listed in `graphify-out/.graphify_uncached.txt`. If all files are cached, skip to Part C directly.
|
||||
|
||||
**Step B1 - Split into chunks**
|
||||
|
||||
Load files from `graphify-out/.graphify_uncached.txt`. Split into chunks of 20-25 files each. Each image gets its own chunk (vision needs separate context). When splitting, group files from the same directory together so related artifacts land in the same chunk and cross-file relationships are more likely to be extracted.
|
||||
|
||||
**Step B2 - Dispatch ALL subagents in a single message**
|
||||
|
||||
Call the Agent tool multiple times IN THE SAME RESPONSE - one call per chunk. This is the only way they run in parallel. If you make one Agent call, wait, then make another, you are doing it sequentially and defeating the purpose.
|
||||
|
||||
**IMPORTANT - subagent type:** Always use `subagent_type="general-purpose"`. Do NOT use `Explore` - it is read-only and cannot write chunk files to disk, which silently drops extraction results. General-purpose has Write and Bash access which the subagent needs.
|
||||
|
||||
Concrete example for 3 chunks:
|
||||
```
|
||||
[Agent tool call 1: files 1-15, subagent_type="general-purpose"]
|
||||
[Agent tool call 2: files 16-30, subagent_type="general-purpose"]
|
||||
[Agent tool call 3: files 31-45, subagent_type="general-purpose"]
|
||||
```
|
||||
All three in one message. Not three separate messages.
|
||||
|
||||
Each subagent receives this exact prompt (substitute FILE_LIST, CHUNK_NUM, TOTAL_CHUNKS, DEEP_MODE, and CHUNK_PATH).
|
||||
|
||||
CHUNK_PATH must be an **absolute** path — derive it before dispatching:
|
||||
```bash
|
||||
PROJECT_ROOT=$(cat graphify-out/.graphify_root)
|
||||
# Then for chunk N: CHUNK_PATH="${PROJECT_ROOT}/graphify-out/.graphify_chunk_0N.json"
|
||||
```
|
||||
|
||||
Subagent prompt template:
|
||||
|
||||
See `references/extraction-spec.md` for the exact subagent prompt (JSON schema, node-ID rules, confidence rubric, frontmatter, hyperedge, and vision rules). Load it only here, only when at least one chunk holds a doc, paper, or image; a pure-code corpus has skipped Part B and never reads it. Pass each subagent that prompt verbatim with FILE_LIST, CHUNK_NUM, TOTAL_CHUNKS, DEEP_MODE, and CHUNK_PATH substituted, and have it write the result to CHUNK_PATH.
|
||||
|
||||
**Step B3 - Collect, cache, and merge**
|
||||
|
||||
Wait for all subagents. For each result:
|
||||
- Check that `graphify-out/.graphify_chunk_NN.json` exists on disk — this is the success signal
|
||||
- If the file exists and contains valid JSON with `nodes` and `edges`, include it and save to cache
|
||||
- If the file is missing, the subagent was likely dispatched as read-only (Explore type) — print a warning: "chunk N missing from disk — subagent may have been read-only. Re-run with general-purpose agent." Do not silently skip.
|
||||
- If a subagent failed or returned invalid JSON, print a warning and skip that chunk - do not abort
|
||||
|
||||
If more than half the chunks failed or are missing, stop and tell the user to re-run and ensure `subagent_type="general-purpose"` is used.
|
||||
|
||||
Merge all chunk files into `.graphify_semantic_new.json`. **After each Agent call completes, read the real token counts from the Agent tool result's `usage` field and write them back into the chunk JSON before merging** — the chunk JSON itself always has placeholder zeros. Then run:
|
||||
```bash
|
||||
$(cat graphify-out/.graphify_python) -c "
|
||||
import json, glob
|
||||
from pathlib import Path
|
||||
|
||||
chunks = sorted(glob.glob('graphify-out/.graphify_chunk_*.json'))
|
||||
all_nodes, all_edges, all_hyperedges = [], [], []
|
||||
total_in, total_out = 0, 0
|
||||
for c in chunks:
|
||||
d = json.loads(Path(c).read_text(encoding=\"utf-8\"))
|
||||
all_nodes += d.get('nodes', [])
|
||||
all_edges += d.get('edges', [])
|
||||
all_hyperedges += d.get('hyperedges', [])
|
||||
total_in += d.get('input_tokens', 0)
|
||||
total_out += d.get('output_tokens', 0)
|
||||
Path('graphify-out/.graphify_semantic_new.json').write_text(json.dumps({
|
||||
'nodes': all_nodes, 'edges': all_edges, 'hyperedges': all_hyperedges,
|
||||
'input_tokens': total_in, 'output_tokens': total_out,
|
||||
}, indent=2, ensure_ascii=False), encoding=\"utf-8\")
|
||||
print(f'Merged {len(chunks)} chunks: {total_in:,} in / {total_out:,} out tokens')
|
||||
"
|
||||
```
|
||||
|
||||
Save new results to cache:
|
||||
```bash
|
||||
$(cat graphify-out/.graphify_python) -c "
|
||||
import json
|
||||
from graphify.cache import save_semantic_cache
|
||||
from pathlib import Path
|
||||
|
||||
new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
|
||||
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []))
|
||||
print(f'Cached {saved} files')
|
||||
"
|
||||
```
|
||||
|
||||
Merge cached + new results into `graphify-out/.graphify_semantic.json`:
|
||||
```bash
|
||||
$(cat graphify-out/.graphify_python) -c "
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
cached = json.loads(Path('graphify-out/.graphify_cached.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_cached.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
|
||||
new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
|
||||
|
||||
all_nodes = cached['nodes'] + new.get('nodes', [])
|
||||
all_edges = cached['edges'] + new.get('edges', [])
|
||||
all_hyperedges = cached.get('hyperedges', []) + new.get('hyperedges', [])
|
||||
seen = set()
|
||||
deduped = []
|
||||
for n in all_nodes:
|
||||
if n['id'] not in seen:
|
||||
seen.add(n['id'])
|
||||
deduped.append(n)
|
||||
|
||||
merged = {
|
||||
'nodes': deduped,
|
||||
'edges': all_edges,
|
||||
'hyperedges': all_hyperedges,
|
||||
'input_tokens': new.get('input_tokens', 0),
|
||||
'output_tokens': new.get('output_tokens', 0),
|
||||
}
|
||||
Path('graphify-out/.graphify_semantic.json').write_text(json.dumps(merged, indent=2, ensure_ascii=False), encoding=\"utf-8\")
|
||||
print(f'Extraction complete - {len(deduped)} nodes, {len(all_edges)} edges ({len(cached[\"nodes\"])} from cache, {len(new.get(\"nodes\",[]))} new)')
|
||||
"
|
||||
```
|
||||
Clean up temp files: `rm -f graphify-out/.graphify_cached.json graphify-out/.graphify_uncached.txt graphify-out/.graphify_semantic_new.json`
|
||||
|
||||
#### Part C - Merge AST + semantic into final extraction
|
||||
|
||||
```bash
|
||||
$(cat graphify-out/.graphify_python) -c "
|
||||
import sys, json
|
||||
from pathlib import Path
|
||||
|
||||
ast = json.loads(Path('graphify-out/.graphify_ast.json').read_text(encoding=\"utf-8\"))
|
||||
sem = json.loads(Path('graphify-out/.graphify_semantic.json').read_text(encoding=\"utf-8\"))
|
||||
|
||||
# Merge: AST nodes first, semantic nodes deduplicated by id
|
||||
seen = {n['id'] for n in ast['nodes']}
|
||||
merged_nodes = list(ast['nodes'])
|
||||
for n in sem['nodes']:
|
||||
if n['id'] not in seen:
|
||||
merged_nodes.append(n)
|
||||
seen.add(n['id'])
|
||||
|
||||
merged_edges = ast['edges'] + sem['edges']
|
||||
merged_hyperedges = sem.get('hyperedges', [])
|
||||
merged = {
|
||||
'nodes': merged_nodes,
|
||||
'edges': merged_edges,
|
||||
'hyperedges': merged_hyperedges,
|
||||
'input_tokens': sem.get('input_tokens', 0),
|
||||
'output_tokens': sem.get('output_tokens', 0),
|
||||
}
|
||||
Path('graphify-out/.graphify_extract.json').write_text(json.dumps(merged, indent=2, ensure_ascii=False), encoding=\"utf-8\")
|
||||
total = len(merged_nodes)
|
||||
edges = len(merged_edges)
|
||||
print(f'Merged: {total} nodes, {edges} edges ({len(ast[\"nodes\"])} AST + {len(sem[\"nodes\"])} semantic)')
|
||||
"
|
||||
```
|
||||
|
||||
### Step 4 - Build graph, cluster, analyze, generate outputs
|
||||
|
||||
**Before starting:** note whether `--directed` was given. If so, pass `directed=True` to `build_from_json()` in the code block below. This builds a `DiGraph` that preserves edge direction (source→target) instead of the default undirected `Graph`.
|
||||
|
||||
```bash
|
||||
mkdir -p graphify-out
|
||||
$(cat graphify-out/.graphify_python) -c "
|
||||
import sys, json
|
||||
from graphify.build import build_from_json
|
||||
from graphify.cluster import cluster, score_all
|
||||
from graphify.analyze import god_nodes, surprising_connections, suggest_questions
|
||||
from graphify.report import generate
|
||||
from graphify.export import to_json
|
||||
from pathlib import Path
|
||||
|
||||
extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
|
||||
detection = json.loads(Path('graphify-out/.graphify_detect.json').read_text(encoding=\"utf-8\"))
|
||||
|
||||
G = build_from_json(extraction)
|
||||
communities = cluster(G)
|
||||
cohesion = score_all(G, communities)
|
||||
tokens = {'input': extraction.get('input_tokens', 0), 'output': extraction.get('output_tokens', 0)}
|
||||
gods = god_nodes(G)
|
||||
surprises = surprising_connections(G, communities)
|
||||
labels = {cid: 'Community ' + str(cid) for cid in communities}
|
||||
# Placeholder questions - regenerated with real labels in Step 5
|
||||
questions = suggest_questions(G, communities, labels)
|
||||
|
||||
report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, '.', suggested_questions=questions)
|
||||
Path('graphify-out/GRAPH_REPORT.md').write_text(report, encoding=\"utf-8\")
|
||||
to_json(G, communities, 'graphify-out/graph.json')
|
||||
|
||||
analysis = {
|
||||
'communities': {str(k): v for k, v in communities.items()},
|
||||
'cohesion': {str(k): v for k, v in cohesion.items()},
|
||||
'gods': gods,
|
||||
'surprises': surprises,
|
||||
'questions': questions,
|
||||
}
|
||||
Path('graphify-out/.graphify_analysis.json').write_text(json.dumps(analysis, indent=2, ensure_ascii=False), encoding=\"utf-8\")
|
||||
if G.number_of_nodes() == 0:
|
||||
print('ERROR: Graph is empty - extraction produced no nodes.')
|
||||
print('Possible causes: all files were skipped, binary-only corpus, or extraction failed.')
|
||||
raise SystemExit(1)
|
||||
print(f'Graph: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges, {len(communities)} communities')
|
||||
"
|
||||
```
|
||||
|
||||
If this step prints `ERROR: Graph is empty`, stop and tell the user what happened - do not proceed to labeling or visualization.
|
||||
|
||||
Replace INPUT_PATH with the actual path.
|
||||
|
||||
### Step 5 - Label communities
|
||||
|
||||
Read `graphify-out/.graphify_analysis.json`. For each community key, look at its node labels and write a 2-5 word plain-language name (e.g. "Attention Mechanism", "Training Pipeline", "Data Loading").
|
||||
|
||||
Then regenerate the report and save the labels for the visualizer:
|
||||
|
||||
```bash
|
||||
$(cat graphify-out/.graphify_python) -c "
|
||||
import sys, json
|
||||
from graphify.build import build_from_json
|
||||
from graphify.cluster import score_all
|
||||
from graphify.analyze import god_nodes, surprising_connections, suggest_questions
|
||||
from graphify.report import generate
|
||||
from pathlib import Path
|
||||
|
||||
extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
|
||||
detection = json.loads(Path('graphify-out/.graphify_detect.json').read_text(encoding=\"utf-8\"))
|
||||
analysis = json.loads(Path('graphify-out/.graphify_analysis.json').read_text(encoding=\"utf-8\"))
|
||||
|
||||
G = build_from_json(extraction)
|
||||
communities = {int(k): v for k, v in analysis['communities'].items()}
|
||||
cohesion = {int(k): v for k, v in analysis['cohesion'].items()}
|
||||
tokens = {'input': extraction.get('input_tokens', 0), 'output': extraction.get('output_tokens', 0)}
|
||||
|
||||
# LABELS - replace these with the names you chose above
|
||||
labels = LABELS_DICT
|
||||
|
||||
# Regenerate questions with real community labels (labels affect question phrasing)
|
||||
questions = suggest_questions(G, communities, labels)
|
||||
|
||||
report = generate(G, communities, cohesion, labels, analysis['gods'], analysis['surprises'], detection, tokens, '.', suggested_questions=questions)
|
||||
Path('graphify-out/GRAPH_REPORT.md').write_text(report, encoding=\"utf-8\")
|
||||
Path('graphify-out/.graphify_labels.json').write_text(json.dumps({str(k): v for k, v in labels.items()}, ensure_ascii=False), encoding=\"utf-8\")
|
||||
print('Report updated with community labels')
|
||||
"
|
||||
```
|
||||
|
||||
Replace `LABELS_DICT` with the actual dict you constructed (e.g. `{0: "Attention Mechanism", 1: "Training Pipeline"}`).
|
||||
Replace INPUT_PATH with the actual path.
|
||||
|
||||
### Step 6 - Generate Obsidian vault (opt-in) + HTML
|
||||
|
||||
**Generate HTML always** (unless `--no-viz`). **Obsidian vault only if `--obsidian` was explicitly given** — skip it otherwise, it generates one file per node.
|
||||
|
||||
If `--obsidian` was given:
|
||||
|
||||
- If `--obsidian-dir <path>` was also given, pass it via `--dir`. Otherwise defaults to `graphify-out/obsidian`.
|
||||
|
||||
```bash
|
||||
graphify export obsidian
|
||||
# or with custom dir: graphify export obsidian --dir ~/vaults/my-project
|
||||
```
|
||||
|
||||
Generate the HTML graph (always, unless `--no-viz`):
|
||||
|
||||
```bash
|
||||
graphify export html # auto-aggregates to community view if graph > 5000 nodes
|
||||
# or: graphify export html --no-viz
|
||||
```
|
||||
|
||||
### Steps 6b-8 - Wiki, Neo4j, SVG, GraphML, MCP, benchmark (only on their flags)
|
||||
|
||||
These run only when their flag is present (`--wiki`, `--neo4j`/`--neo4j-push`, `--svg`, `--graphml`, `--mcp`) or, for the token-reduction benchmark, when `total_words` exceeds 5,000. A default run with no export flags skips all of them. See `references/exports.md` for each one. Run any `--wiki` export before Step 9 cleanup so `.graphify_labels.json` is still available.
|
||||
|
||||
---
|
||||
|
||||
### Step 9 - Save manifest, update cost tracker, clean up, and report
|
||||
|
||||
```bash
|
||||
$(cat graphify-out/.graphify_python) -c "
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timezone
|
||||
from graphify.detect import save_manifest
|
||||
|
||||
# Save manifest for --update
|
||||
detect = json.loads(Path('graphify-out/.graphify_detect.json').read_text(encoding=\"utf-8\"))
|
||||
# In --update mode, 'all_files' carries the full corpus; 'files' is the changed
|
||||
# subset. Full-rebuild mode populates only 'files', so the fallback handles that.
|
||||
save_manifest(detect.get('all_files') or detect['files'])
|
||||
|
||||
# Update cumulative cost tracker
|
||||
extract = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
|
||||
input_tok = extract.get('input_tokens', 0)
|
||||
output_tok = extract.get('output_tokens', 0)
|
||||
|
||||
cost_path = Path('graphify-out/cost.json')
|
||||
if cost_path.exists():
|
||||
cost = json.loads(cost_path.read_text(encoding=\"utf-8\"))
|
||||
else:
|
||||
cost = {'runs': [], 'total_input_tokens': 0, 'total_output_tokens': 0}
|
||||
|
||||
cost['runs'].append({
|
||||
'date': datetime.now(timezone.utc).isoformat(),
|
||||
'input_tokens': input_tok,
|
||||
'output_tokens': output_tok,
|
||||
'files': detect.get('total_files', 0),
|
||||
})
|
||||
cost['total_input_tokens'] += input_tok
|
||||
cost['total_output_tokens'] += output_tok
|
||||
cost_path.write_text(json.dumps(cost, indent=2, ensure_ascii=False), encoding=\"utf-8\")
|
||||
|
||||
print(f'This run: {input_tok:,} input tokens, {output_tok:,} output tokens')
|
||||
print(f'All time: {cost[\"total_input_tokens\"]:,} input, {cost[\"total_output_tokens\"]:,} output ({len(cost[\"runs\"])} runs)')
|
||||
"
|
||||
rm -f graphify-out/.graphify_detect.json graphify-out/.graphify_extract.json graphify-out/.graphify_ast.json graphify-out/.graphify_semantic.json graphify-out/.graphify_analysis.json
|
||||
find graphify-out -maxdepth 1 -name '.graphify_chunk_*.json' -delete 2>/dev/null
|
||||
rm -f graphify-out/.needs_update 2>/dev/null || true
|
||||
```
|
||||
|
||||
Tell the user (omit the obsidian line unless --obsidian was given):
|
||||
```
|
||||
Graph complete. Outputs in PATH_TO_DIR/graphify-out/
|
||||
|
||||
graph.html - interactive graph, open in browser
|
||||
GRAPH_REPORT.md - audit report
|
||||
graph.json - raw graph data
|
||||
obsidian/ - Obsidian vault (only if --obsidian was given)
|
||||
```
|
||||
|
||||
If graphify saved you time, consider supporting it: https://github.com/sponsors/safishamsi
|
||||
|
||||
Replace PATH_TO_DIR with the actual absolute path of the directory that was processed.
|
||||
|
||||
Then paste these sections from GRAPH_REPORT.md directly into the chat:
|
||||
- God Nodes
|
||||
- Surprising Connections
|
||||
- Suggested Questions
|
||||
|
||||
Do NOT paste the full report - just those three sections. Keep it concise.
|
||||
|
||||
Then immediately offer to explore. Pick the single most interesting suggested question from the report - the one that crosses the most community boundaries or has the most surprising bridge node - and ask:
|
||||
|
||||
> "The most interesting question this graph can answer: **[question]**. Want me to trace it?"
|
||||
|
||||
If the user says yes, run `/graphify query "[question]"` on the graph and walk them through the answer using the graph structure - which nodes connect, which community boundaries get crossed, what the path reveals. Keep going as long as they want to explore. Each answer should end with a natural follow-up ("this connects to X - want to go deeper?") so the session feels like navigation, not a one-shot report.
|
||||
|
||||
The graph is the map. Your job after the pipeline is to be the guide.
|
||||
|
||||
---
|
||||
|
||||
## Interpreter guard for subcommands
|
||||
|
||||
Before running any subcommand below (`--update`, `--cluster-only`, `query`, `path`, `explain`, `add`), check that `.graphify_python` exists. If it's missing (e.g. user deleted `graphify-out/`), re-resolve the interpreter first:
|
||||
|
||||
```bash
|
||||
if [ ! -f graphify-out/.graphify_python ]; then
|
||||
GRAPHIFY_BIN=$(which graphify 2>/dev/null)
|
||||
if [ -n "$GRAPHIFY_BIN" ]; then
|
||||
PYTHON=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!')
|
||||
case "$PYTHON" in *[!a-zA-Z0-9/_.-]*) PYTHON="python3" ;; esac
|
||||
else
|
||||
PYTHON="python3"
|
||||
fi
|
||||
mkdir -p graphify-out
|
||||
"$PYTHON" -c "import sys; open('graphify-out/.graphify_python', 'w', encoding='utf-8').write(sys.executable)"
|
||||
fi
|
||||
```
|
||||
|
||||
## For --update and --cluster-only
|
||||
|
||||
Both are non-default subcommands. `--update` re-extracts only new or changed files; `--cluster-only` reruns clustering on the existing graph. See `references/update.md` for both flows.
|
||||
|
||||
---
|
||||
|
||||
## For /graphify query
|
||||
|
||||
When `graphify-out/graph.json` already exists and the user asks a question about the corpus, run the query directly:
|
||||
|
||||
```bash
|
||||
graphify query "<question>"
|
||||
```
|
||||
|
||||
Answer using only what the graph output contains, and quote `source_location` when citing a specific fact. Before traversal, expand the question against the graph's own vocabulary so a wording mismatch does not collapse the answer to noise. For that vocab-expansion step, the `--dfs` / `--budget` modes, `save-result` feedback, and the `/graphify path` and `/graphify explain` flows, see `references/query.md`.
|
||||
|
||||
---
|
||||
|
||||
## For /graphify add and --watch
|
||||
|
||||
Neither is part of the default build. When the user runs `/graphify add <url>` to fetch a URL into the corpus, or passes `--watch` to auto-rebuild on file changes, see `references/add-watch.md`.
|
||||
|
||||
---
|
||||
|
||||
## For the commit hook and native CLAUDE.md integration
|
||||
|
||||
When the user asks to install the post-commit auto-rebuild hook or wire graphify into a project's CLAUDE.md, see `references/hooks.md`.
|
||||
|
||||
---
|
||||
|
||||
## Honesty Rules
|
||||
|
||||
- Never invent an edge. If unsure, use AMBIGUOUS.
|
||||
- Never skip the corpus check warning.
|
||||
- Always show token cost in the report.
|
||||
- Never hide cohesion scores behind symbols - show the raw number.
|
||||
- Never run HTML viz on a graph with more than 5,000 nodes without warning the user.
|
||||
@@ -0,0 +1,56 @@
|
||||
# graphify reference: add a URL and watch a folder
|
||||
|
||||
Load this when the user ran `/graphify add <url>` or passed `--watch`. Neither is part of the default build.
|
||||
|
||||
## For /graphify add
|
||||
|
||||
Fetch a URL and add it to the corpus, then update the graph.
|
||||
|
||||
```bash
|
||||
$(cat graphify-out/.graphify_python) -c "
|
||||
import sys
|
||||
from graphify.ingest import ingest
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
out = ingest('URL', Path('./raw'), author='AUTHOR', contributor='CONTRIBUTOR')
|
||||
print(f'Saved to {out}')
|
||||
except ValueError as e:
|
||||
print(f'error: {e}', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except RuntimeError as e:
|
||||
print(f'error: {e}', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
"
|
||||
```
|
||||
|
||||
Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph.
|
||||
|
||||
Supported URL types (auto-detected):
|
||||
- YouTube / any video URL → audio downloaded via yt-dlp, transcribed to `.txt` on next run (requires `pip install 'graphifyy[video]'`)
|
||||
- Twitter/X → fetched via oEmbed, saved as `.md` with tweet text and author
|
||||
- arXiv → abstract + metadata saved as `.md`
|
||||
- PDF → downloaded as `.pdf`
|
||||
- Images (.png/.jpg/.webp) → downloaded, Claude vision extracts on next run
|
||||
- Any webpage → converted to markdown via html2text
|
||||
|
||||
---
|
||||
|
||||
## For --watch
|
||||
|
||||
Start a background watcher that monitors a folder and auto-updates the graph when files change.
|
||||
|
||||
```bash
|
||||
python3 -m graphify.watch INPUT_PATH --debounce 3
|
||||
```
|
||||
|
||||
Replace INPUT_PATH with the folder to watch. Behavior depends on what changed:
|
||||
|
||||
- **Code files only (.py, .ts, .go, etc.):** re-runs AST extraction + rebuild + cluster immediately, no LLM needed. `graph.json` and `GRAPH_REPORT.md` are updated automatically.
|
||||
- **Docs, papers, or images:** writes a `graphify-out/needs_update` flag and prints a notification to run `/graphify --update` (LLM semantic re-extraction required).
|
||||
|
||||
Debounce (default 3s): waits until file activity stops before triggering, so a wave of parallel agent writes doesn't trigger a rebuild per file.
|
||||
|
||||
Press Ctrl+C to stop.
|
||||
|
||||
For agentic workflows: run `--watch` in a background terminal. Code changes from agent waves are picked up automatically between waves. If agents are also writing docs or notes, you'll need a manual `/graphify --update` after those waves.
|
||||
@@ -0,0 +1,71 @@
|
||||
# graphify reference: extra exports and benchmark
|
||||
|
||||
Load this when the user passed one of the export flags (`--wiki`, `--neo4j`, `--neo4j-push`, `--svg`, `--graphml`, `--mcp`), or when the corpus is large enough for the token-reduction benchmark. Each step runs only for its own flag.
|
||||
|
||||
### Step 6b - Wiki (only if --wiki flag)
|
||||
|
||||
**Only run this step if `--wiki` was explicitly given in the original command.**
|
||||
|
||||
Run this before Step 9 (cleanup) so `.graphify_labels.json` is still available.
|
||||
|
||||
```bash
|
||||
graphify export wiki
|
||||
```
|
||||
|
||||
### Step 7 - Neo4j export (only if --neo4j or --neo4j-push flag)
|
||||
|
||||
**If `--neo4j`** - generate a Cypher file for manual import:
|
||||
|
||||
```bash
|
||||
graphify export neo4j
|
||||
```
|
||||
|
||||
**If `--neo4j-push <uri>`** - push directly to a running Neo4j instance. Ask the user for credentials if not provided:
|
||||
|
||||
```bash
|
||||
graphify export neo4j --push bolt://localhost:7687 --user neo4j --password PASSWORD
|
||||
```
|
||||
|
||||
Default URI is `bolt://localhost:7687`, default user is `neo4j`. Uses MERGE - safe to re-run without creating duplicates.
|
||||
|
||||
### Step 7b - SVG export (only if --svg flag)
|
||||
|
||||
```bash
|
||||
graphify export svg
|
||||
```
|
||||
|
||||
### Step 7c - GraphML export (only if --graphml flag)
|
||||
|
||||
```bash
|
||||
graphify export graphml
|
||||
```
|
||||
|
||||
### Step 7d - MCP server (only if --mcp flag)
|
||||
|
||||
```bash
|
||||
python3 -m graphify.serve graphify-out/graph.json
|
||||
```
|
||||
|
||||
This starts a stdio MCP server that exposes tools: `query_graph`, `get_node`, `get_neighbors`, `get_community`, `god_nodes`, `graph_stats`, `shortest_path`. Add to Claude Desktop or any MCP-compatible agent orchestrator so other agents can query the graph live.
|
||||
|
||||
To configure in Claude Desktop, add to `claude_desktop_config.json`:
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"graphify": {
|
||||
"command": "python3",
|
||||
"args": ["-m", "graphify.serve", "/absolute/path/to/graphify-out/graph.json"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 8 - Token reduction benchmark (only if total_words > 5000)
|
||||
|
||||
If `total_words` from `graphify-out/.graphify_detect.json` is greater than 5,000, run:
|
||||
|
||||
```bash
|
||||
graphify benchmark
|
||||
```
|
||||
|
||||
Print the output directly in chat. If `total_words <= 5000`, skip silently - the graph value is structural clarity, not token compression, for small corpora.
|
||||
@@ -0,0 +1,68 @@
|
||||
# graphify reference: extraction subagent prompt
|
||||
|
||||
Load this in Step 3 Part B when the corpus has at least one doc, paper, or image chunk. A pure-code corpus skips Part B and never reads this file. Each semantic subagent receives the prompt below verbatim (substitute FILE_LIST, CHUNK_NUM, TOTAL_CHUNKS, DEEP_MODE, and CHUNK_PATH).
|
||||
|
||||
```
|
||||
You are a graphify extraction subagent. Read the files listed and extract a knowledge graph fragment.
|
||||
Output ONLY valid JSON matching the schema below - no explanation, no markdown fences, no preamble.
|
||||
|
||||
Files (chunk CHUNK_NUM of TOTAL_CHUNKS):
|
||||
FILE_LIST
|
||||
|
||||
Rules:
|
||||
- EXTRACTED: relationship explicit in source (import, call, citation, "see §3.2")
|
||||
- INFERRED: reasonable inference (shared data structure, implied dependency)
|
||||
- AMBIGUOUS: uncertain - flag for review, do not omit
|
||||
|
||||
Code files: focus on semantic edges AST cannot find (call relationships, shared data, arch patterns).
|
||||
Do not re-extract imports - AST already has those.
|
||||
Doc/paper files: extract named concepts, entities, citations. For rationale (WHY decisions were made, trade-offs, design intent): store as a `rationale` attribute on the relevant concept node — do NOT create a separate rationale node or fragment node. Only create a node for something that is itself a named entity or concept. Use `file_type:"rationale"` for concept-like nodes (ideas, principles, mechanisms, design patterns). `file_type` MUST be one of exactly these six values: `code`, `document`, `paper`, `image`, `rationale`, `concept`. Any other value is invalid and will be rejected.
|
||||
Code files: when adding `calls` edges, source MUST be the caller (the function/class doing the calling), target MUST be the callee. Never reverse this direction. `calls` edges MUST stay within one language: a Python function cannot `calls` a JS/TS/Go/Rust/Java symbol and vice versa — cross-language call edges are phantom artifacts, never emit them.
|
||||
Image files: use vision to understand what the image IS - do not just OCR.
|
||||
UI screenshot: layout patterns, design decisions, key elements, purpose.
|
||||
Chart: metric, trend/insight, data source.
|
||||
Tweet/post: claim as node, author, concepts mentioned.
|
||||
Diagram: components and connections.
|
||||
Research figure: what it demonstrates, method, result.
|
||||
Handwritten/whiteboard: ideas and arrows, mark uncertain readings AMBIGUOUS.
|
||||
|
||||
DEEP_MODE (if --mode deep was given): be aggressive with INFERRED edges - indirect deps,
|
||||
shared assumptions, latent couplings. Mark uncertain ones AMBIGUOUS instead of omitting.
|
||||
|
||||
Semantic similarity: if two concepts in this chunk solve the same problem or represent the same idea without any structural link (no import, no call, no citation), add a `semantically_similar_to` edge marked INFERRED with a confidence_score reflecting how similar they are (0.6-0.95). Examples:
|
||||
- Two functions that both validate user input but never call each other
|
||||
- A class in code and a concept in a paper that describe the same algorithm
|
||||
- Two error types that handle the same failure mode differently
|
||||
Only add these when the similarity is genuinely non-obvious and cross-cutting. Do not add them for trivially similar things.
|
||||
|
||||
Hyperedges: if 3 or more nodes clearly participate together in a shared concept, flow, or pattern that is not captured by pairwise edges alone, add a hyperedge to a top-level `hyperedges` array. Examples:
|
||||
- All classes that implement a common protocol or interface
|
||||
- All functions in an authentication flow (even if they don't all call each other)
|
||||
- All concepts from a paper section that form one coherent idea
|
||||
Use sparingly — only when the group relationship adds information beyond the pairwise edges. Maximum 3 hyperedges per chunk.
|
||||
|
||||
If a file has YAML frontmatter (--- ... ---), copy source_url, captured_at, author,
|
||||
contributor onto every node from that file.
|
||||
|
||||
confidence_score is REQUIRED on every edge - never omit it, never use 0.5 as a default:
|
||||
- EXTRACTED edges: confidence_score = 1.0 always
|
||||
- INFERRED edges: pick exactly ONE value from this set — never 0.5:
|
||||
0.95 direct structural evidence (shared data structure, named cross-file reference).
|
||||
0.85 strong inference (clear functional alignment, no direct symbol link).
|
||||
0.75 reasonable inference (shared problem domain + similar shape, requires interpretation).
|
||||
0.65 weak inference (thematically related, no shape evidence).
|
||||
0.55 speculative but plausible (surface-level co-occurrence only).
|
||||
Models follow discrete rubrics better than continuous ranges; the bimodal
|
||||
distribution observed in production (>50% at 0.5, >40% at 0.85+) shows the
|
||||
range guidance is being collapsed to a binary. If no value above fits, mark
|
||||
the edge AMBIGUOUS rather than picking 0.4 or below.
|
||||
- AMBIGUOUS edges: 0.1-0.3
|
||||
|
||||
Node ID format: lowercase, only `[a-z0-9_]`, no dots or slashes. Format: `{stem}_{entity}` where stem is `{parent_dir}_{filename_without_ext}` (the **immediate** parent directory name + the filename stem, both lowercased with non-alphanumeric chars replaced by `_`) and entity is the symbol name similarly normalized. Only one level of parent is used — not the full path. Examples: `src/auth/session.py` + `ValidateToken` → `auth_session_validatetoken`; `lib/utils/helpers.py` + `parse_url` → `utils_helpers_parse_url`; `tests/test_foo.py` + `_helper` → `tests_test_foo_helper`. Top-level files (no parent dir, e.g. `setup.py`) use just the filename stem: `setup_my_func`. This must match the ID the AST extractor generates — using just the filename (e.g., `session_validatetoken`) or the full path (e.g., `src_auth_session_validatetoken`) will create orphan ghost-duplicate nodes. If you are re-extracting a project that had ghost duplicates under the old format, the user should run `graphify extract --force` to rebuild cleanly. CRITICAL: never append chunk numbers, sequence numbers, or any suffix to an ID (no `_c1`, `_c2`, `_chunk2`, etc.). IDs must be deterministic from the label alone — the same entity must always produce the same ID regardless of which chunk processes it.
|
||||
|
||||
Generate the extraction JSON matching this schema exactly:
|
||||
{"nodes":[{"id":"session_validatetoken","label":"Human Readable Name","file_type":"code|document|paper|image|rationale|concept","source_file":"relative/path","source_location":null,"source_url":null,"captured_at":null,"author":null,"contributor":null}],"edges":[{"source":"node_id","target":"node_id","relation":"calls|implements|references|cites|conceptually_related_to|shares_data_with|semantically_similar_to|rationale_for","confidence":"EXTRACTED|INFERRED|AMBIGUOUS","confidence_score":1.0,"source_file":"relative/path","source_location":null,"weight":1.0}],"hyperedges":[{"id":"snake_case_id","label":"Human Readable Label","nodes":["node_id1","node_id2","node_id3"],"relation":"participate_in|implement|form","confidence":"EXTRACTED|INFERRED","confidence_score":0.75,"source_file":"relative/path"}],"input_tokens":0,"output_tokens":0}
|
||||
|
||||
Then write the JSON to disk using the Write tool at this exact absolute path (no relative paths — Write resolves relative paths against an undefined cwd and the file will be silently lost):
|
||||
CHUNK_PATH
|
||||
```
|
||||
@@ -0,0 +1,46 @@
|
||||
# graphify reference: GitHub clone and cross-repo merge
|
||||
|
||||
Load this when the user passed one or more `https://github.com/...` URLs, or named several local subfolders to merge into one graph.
|
||||
|
||||
### Step 0 - Clone GitHub repo(s) (only if a GitHub URL was given)
|
||||
|
||||
**Single repo:**
|
||||
```bash
|
||||
LOCAL_PATH=$(graphify clone <github-url> [--branch <branch>])
|
||||
# Use LOCAL_PATH as the target for all subsequent steps
|
||||
```
|
||||
|
||||
**Multiple repos (cross-repo graph):**
|
||||
```bash
|
||||
# Clone each repo, run the full pipeline on each, then merge
|
||||
graphify clone <url1> # → ~/.graphify/repos/<owner1>/<repo1>
|
||||
graphify clone <url2> # → ~/.graphify/repos/<owner2>/<repo2>
|
||||
# Run /graphify on each local path to produce their graph.json files
|
||||
# Then merge:
|
||||
graphify merge-graphs \
|
||||
~/.graphify/repos/<owner1>/<repo1>/graphify-out/graph.json \
|
||||
~/.graphify/repos/<owner2>/<repo2>/graphify-out/graph.json \
|
||||
--out graphify-out/cross-repo-graph.json
|
||||
```
|
||||
|
||||
Graphify clones into `~/.graphify/repos/<owner>/<repo>` and reuses existing clones on repeat runs. Each node in the merged graph carries a `repo` attribute so you can filter by origin.
|
||||
|
||||
**Multiple local subfolders (monorepo or multi-service layout):**
|
||||
|
||||
The skill pipeline writes all intermediate and final outputs to `graphify-out/` in the current working directory. Running the skill on each subfolder separately will clobber the same output dir. Instead, use the CLI directly for each subfolder — it places `graphify-out/` *inside* the scanned path:
|
||||
|
||||
```bash
|
||||
graphify extract ./core/ # → ./core/graphify-out/graph.json
|
||||
graphify extract ./service/ # → ./service/graphify-out/graph.json
|
||||
graphify extract ./platform/ # → ./platform/graphify-out/graph.json
|
||||
# Add --backend gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set
|
||||
|
||||
# Then merge at the project root:
|
||||
graphify merge-graphs \
|
||||
./core/graphify-out/graph.json \
|
||||
./service/graphify-out/graph.json \
|
||||
./platform/graphify-out/graph.json \
|
||||
--out graphify-out/graph.json
|
||||
```
|
||||
|
||||
Once `graphify-out/graph.json` exists, the fast path above takes over: any codebase question runs `graphify query` directly on the merged graph — no re-extraction, no size gate.
|
||||
@@ -0,0 +1,33 @@
|
||||
# graphify reference: commit hook and native CLAUDE.md integration
|
||||
|
||||
Load this when the user asked to install the post-commit hook or wire graphify into a project's CLAUDE.md.
|
||||
|
||||
## For git commit hook
|
||||
|
||||
Install a post-commit hook that auto-rebuilds the graph after every commit. No background process needed - triggers once per commit, works with any editor.
|
||||
|
||||
```bash
|
||||
graphify hook install # install
|
||||
graphify hook uninstall # remove
|
||||
graphify hook status # check
|
||||
```
|
||||
|
||||
After every `git commit`, the hook detects which code files changed (via `git diff HEAD~1`), re-runs AST extraction on those files, and rebuilds `graph.json` and `GRAPH_REPORT.md`. Doc/image changes are ignored by the hook - run `/graphify --update` manually for those.
|
||||
|
||||
If a post-commit hook already exists, graphify appends to it rather than replacing it.
|
||||
|
||||
---
|
||||
|
||||
## For native CLAUDE.md integration
|
||||
|
||||
Run once per project to make graphify always-on in Claude Code sessions:
|
||||
|
||||
```bash
|
||||
graphify claude install
|
||||
```
|
||||
|
||||
This writes a `## graphify` section to the local `CLAUDE.md` that instructs Claude to check the graph before answering codebase questions and rebuild it after code changes. No manual `/graphify` needed in future sessions.
|
||||
|
||||
```bash
|
||||
graphify claude uninstall # remove the section
|
||||
```
|
||||
@@ -0,0 +1,103 @@
|
||||
# graphify reference: query, path, explain
|
||||
|
||||
Load this when the user asks a question against an existing graph, or runs `/graphify path` or `/graphify explain`. The core's query stub points here for the full traversal flow.
|
||||
|
||||
Two traversal modes - choose based on the question:
|
||||
|
||||
| Mode | Flag | Best for |
|
||||
|------|------|----------|
|
||||
| BFS (default) | _(none)_ | "What is X connected to?" - broad context, nearest neighbors first |
|
||||
| DFS | `--dfs` | "How does X reach Y?" - trace a specific chain or dependency path |
|
||||
|
||||
### Step 0 — Constrained query expansion (REQUIRED before traversal)
|
||||
|
||||
graphify's `query` CLI matches nodes via case-folded substring + IDF — there is **no stemming, no synonyms, no cross-language match** inside the binary. If the user's question uses different language or different domain vocabulary than the graph's labels (user says "обработчик" / graph says "handler"; user says "authentication" / graph says "Guardian"), the literal matcher returns 0 hits and the answer collapses to noise.
|
||||
|
||||
Fix this **without inventing tokens** by expanding the query against the actual graph vocabulary first:
|
||||
|
||||
1. Extract the token vocabulary from node labels:
|
||||
```bash
|
||||
$(cat graphify-out/.graphify_python) -c "
|
||||
import json, re
|
||||
from pathlib import Path
|
||||
data = json.loads(Path('graphify-out/graph.json').read_text())
|
||||
vocab = set()
|
||||
for n in data['nodes']:
|
||||
for c in re.findall(r'[^\W\d_]+', n.get('label','') or '', re.UNICODE):
|
||||
parts = re.findall(r'[A-Z]+(?=[A-Z][a-z])|[A-Z]?[a-z]+|[A-Z]+', c) or [c]
|
||||
for p in parts:
|
||||
t = p.lower()
|
||||
if 3 <= len(t) <= 30:
|
||||
vocab.add(t)
|
||||
Path('graphify-out/.vocab.txt').write_text('\n'.join(sorted(vocab)))
|
||||
print(f'vocab: {len(vocab)} tokens')
|
||||
"
|
||||
```
|
||||
|
||||
2. Read `graphify-out/.vocab.txt`. Then for the user's question, select **up to 12 tokens from this exact list** that semantically match the query intent. Hard constraints:
|
||||
- You MUST pick only tokens present in the vocabulary file. Do NOT invent tokens.
|
||||
- If a query concept has no plausible token in the vocab, skip it — do not substitute a near-synonym from training memory.
|
||||
- If **no** vocab tokens match the query at all, output an empty list and tell the user the corpus has no relevant vocabulary for this question. Do not fabricate a search.
|
||||
- Translate cross-language: Russian "аутентификация" → look for `auth`, `credential`, `token`, `security` IFF present in vocab.
|
||||
- Morphology: "handlers" maps to `handler` IFF present; "todos" maps to `todo` IFF present.
|
||||
|
||||
3. Print the selection explicitly to the user before running the query, so the expansion is auditable:
|
||||
```
|
||||
Query expanded to (from graph vocab, N tokens): [token1, token2, ...]
|
||||
```
|
||||
If the list is empty, say so plainly and stop — do not proceed to traversal.
|
||||
|
||||
### Step 1 — Traversal
|
||||
|
||||
Build the **expanded query string** by joining the selected tokens with spaces. Use this string as `QUESTION` below — NOT the original user question. (The original question is preserved only for `save-result` at the end.)
|
||||
|
||||
```bash
|
||||
graphify query "QUESTION"
|
||||
# or: graphify query "QUESTION" --dfs --budget 3000
|
||||
```
|
||||
|
||||
Answer using **only** what the graph output contains. Quote `source_location` when citing a specific fact. If the graph lacks enough information, say so - do not hallucinate edges.
|
||||
|
||||
After writing the answer, save it back into the graph so it improves future queries. Include the expanded tokens inside the `--answer` text (e.g. `"Expanded from original query via vocab: [tokens]. Then traversed..."`) so the next `--update` extracts the expansion history as a graph node:
|
||||
|
||||
```bash
|
||||
$(cat graphify-out/.graphify_python) -m graphify save-result --question "ORIGINAL_QUESTION" --answer "ANSWER" --type query --nodes NODE1 NODE2
|
||||
```
|
||||
|
||||
Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph.
|
||||
|
||||
---
|
||||
|
||||
## For /graphify path
|
||||
|
||||
Find the shortest path between two named concepts in the graph.
|
||||
|
||||
```bash
|
||||
graphify path "NODE_A" "NODE_B"
|
||||
```
|
||||
|
||||
Replace `NODE_A` and `NODE_B` with the actual concept names. Then explain the path in plain language - what each hop means, why it's significant.
|
||||
|
||||
After writing the explanation, save it back:
|
||||
|
||||
```bash
|
||||
$(cat graphify-out/.graphify_python) -m graphify save-result --question "Path from NODE_A to NODE_B" --answer "ANSWER" --type path_query --nodes NODE_A NODE_B
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## For /graphify explain
|
||||
|
||||
Give a plain-language explanation of a single node - everything connected to it.
|
||||
|
||||
```bash
|
||||
graphify explain "NODE_NAME"
|
||||
```
|
||||
|
||||
Replace `NODE_NAME` with the concept the user asked about. Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations.
|
||||
|
||||
After writing the explanation, save it back:
|
||||
|
||||
```bash
|
||||
$(cat graphify-out/.graphify_python) -m graphify save-result --question "Explain NODE_NAME" --answer "ANSWER" --type explain --nodes NODE_NAME
|
||||
```
|
||||
@@ -0,0 +1,48 @@
|
||||
# graphify reference: transcribe video and audio
|
||||
|
||||
Load this only when `detect` reported one or more `video` files. A corpus with no video never reads this.
|
||||
|
||||
### Step 2.5 - Transcribe video / audio files (only if video files detected)
|
||||
|
||||
Skip this step entirely if `detect` returned zero `video` files.
|
||||
|
||||
Video and audio files cannot be read directly. Transcribe them to text first, then treat the transcripts as doc files in Step 3.
|
||||
|
||||
**Strategy:** Read the god nodes from `graphify-out/.graphify_detect.json` (or the analysis file if it exists from a previous run). You are already a language model — write a one-sentence domain hint yourself from those labels. Then pass it to Whisper as the initial prompt. No separate API call needed.
|
||||
|
||||
**However**, if the corpus has *only* video files and no other docs/code, use the generic fallback prompt: `"Use proper punctuation and paragraph breaks."`
|
||||
|
||||
**Step 1 - Write the Whisper prompt yourself.**
|
||||
|
||||
Read the top god node labels from detect output or analysis, then compose a short domain hint sentence, for example:
|
||||
|
||||
- Labels: `transformer, attention, encoder, decoder` → `"Machine learning research on transformer architectures and attention mechanisms. Use proper punctuation and paragraph breaks."`
|
||||
- Labels: `kubernetes, deployment, pod, helm` → `"DevOps discussion about Kubernetes deployments and Helm charts. Use proper punctuation and paragraph breaks."`
|
||||
|
||||
Set it as `WHISPER_PROMPT` to use in the next command.
|
||||
|
||||
**Step 2 - Transcribe:**
|
||||
|
||||
```bash
|
||||
GRAPHIFY_WHISPER_MODEL=base # or whatever --whisper-model the user passed
|
||||
$(cat graphify-out/.graphify_python) -c "
|
||||
import json, os
|
||||
from pathlib import Path
|
||||
from graphify.transcribe import transcribe_all
|
||||
|
||||
detect = json.loads(Path('graphify-out/.graphify_detect.json').read_text(encoding=\"utf-8\"))
|
||||
video_files = detect.get('files', {}).get('video', [])
|
||||
prompt = os.environ.get('GRAPHIFY_WHISPER_PROMPT', 'Use proper punctuation and paragraph breaks.')
|
||||
|
||||
transcript_paths = transcribe_all(video_files, initial_prompt=prompt)
|
||||
print(json.dumps(transcript_paths, ensure_ascii=False))
|
||||
" > graphify-out/.graphify_transcripts.json
|
||||
```
|
||||
|
||||
After transcription:
|
||||
- Read the transcript paths from `graphify-out/.graphify_transcripts.json`
|
||||
- Add them to the docs list before dispatching semantic subagents in Step 3B
|
||||
- Print how many transcripts were created: `Transcribed N video file(s) -> treating as docs`
|
||||
- If transcription fails for a file, print a warning and continue with the rest
|
||||
|
||||
**Whisper model:** Default is `base`. If the user passed `--whisper-model <name>`, set `GRAPHIFY_WHISPER_MODEL=<name>` in the environment before running the command above.
|
||||
@@ -0,0 +1,179 @@
|
||||
# graphify reference: incremental update and cluster-only
|
||||
|
||||
Load this only when the user passed `--update` or `--cluster-only`. A first-time full build never reads this file.
|
||||
|
||||
## For --update (incremental re-extraction)
|
||||
|
||||
Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time.
|
||||
|
||||
```bash
|
||||
$(cat graphify-out/.graphify_python) -c "
|
||||
import sys, json
|
||||
from graphify.detect import detect_incremental, save_manifest
|
||||
from pathlib import Path
|
||||
|
||||
result = detect_incremental(Path('INPUT_PATH'))
|
||||
new_total = result.get('new_total', 0)
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
Path('graphify-out/.graphify_incremental.json').write_text(json.dumps(result, ensure_ascii=False), encoding=\"utf-8\")
|
||||
deleted = list(result.get('deleted_files', []))
|
||||
if new_total == 0 and not deleted:
|
||||
print('No files changed since last run. Nothing to update.')
|
||||
raise SystemExit(0)
|
||||
if deleted:
|
||||
print(f'{len(deleted)} deleted file(s) to prune.')
|
||||
if new_total > 0:
|
||||
print(f'{new_total} new/changed file(s) to re-extract.')
|
||||
"
|
||||
```
|
||||
|
||||
Then populate `.graphify_detect.json` so Steps 3A–6 (which read it unconditionally) see the right state for an incremental run. `files` carries the changed subset (drives Step 3A AST + Step 3B0 cache check on only what changed); `all_files` carries the full corpus for any step that needs corpus-wide context:
|
||||
|
||||
```bash
|
||||
$(cat graphify-out/.graphify_python) -c "
|
||||
import json
|
||||
from pathlib import Path
|
||||
r = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\"))
|
||||
Path('graphify-out/.graphify_detect.json').write_text(json.dumps({
|
||||
'files': r.get('new_files', {}),
|
||||
'all_files': r.get('files', {}),
|
||||
'total_files': r.get('new_total', 0),
|
||||
'total_words': r.get('total_words', 0),
|
||||
'skipped_sensitive': r.get('skipped_sensitive', []),
|
||||
'needs_graph': True,
|
||||
}, ensure_ascii=False), encoding=\"utf-8\")
|
||||
"
|
||||
```
|
||||
|
||||
If new files exist, first check whether all changed files are code files:
|
||||
|
||||
```bash
|
||||
$(cat graphify-out/.graphify_python) -c "
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
result = json.loads(open('graphify-out/.graphify_incremental.json', encoding='utf-8').read()) if Path('graphify-out/.graphify_incremental.json').exists() else {}
|
||||
code_exts = {'.py','.ts','.js','.go','.rs','.java','.cpp','.c','.rb','.swift','.kt','.cs','.scala','.php','.cc','.cxx','.hpp','.h','.kts','.lua','.toc','.f','.F','.f90','.F90','.f95','.F95','.f03','.F03','.f08','.F08'}
|
||||
new_files = result.get('new_files', {})
|
||||
all_changed = [f for files in new_files.values() for f in files]
|
||||
code_only = all(Path(f).suffix.lower() in code_exts for f in all_changed)
|
||||
print('code_only:', code_only)
|
||||
"
|
||||
```
|
||||
|
||||
If `code_only` is True: print `[graphify update] Code-only changes detected - skipping semantic extraction (no LLM needed)`, run only Step 3A (AST) on the changed files, skip Step 3B entirely (no subagents), then go straight to merge and Steps 4–8.
|
||||
|
||||
If `code_only` is False (any changed file is a doc/paper/image): run the full Steps 3A–3C pipeline as normal.
|
||||
|
||||
|
||||
If no new files exist (only deletions), create an empty extraction so the merge step can prune:
|
||||
|
||||
```bash
|
||||
if [ ! -f graphify-out/.graphify_extract.json ]; then
|
||||
echo '[graphify update] Only deletions -- creating empty extraction for merge.'
|
||||
$(cat graphify-out/.graphify_python) -c "
|
||||
import json
|
||||
from pathlib import Path
|
||||
Path('graphify-out/.graphify_extract.json').write_text(json.dumps({'nodes':[],'edges':[],'hyperedges':[],'input_tokens':0,'output_tokens':0}), encoding='utf-8')
|
||||
"
|
||||
fi
|
||||
```
|
||||
|
||||
|
||||
Then:
|
||||
|
||||
```bash
|
||||
$(cat graphify-out/.graphify_python) -c "
|
||||
import json
|
||||
from pathlib import Path
|
||||
from graphify.build import build_merge
|
||||
from graphify.detect import save_manifest
|
||||
|
||||
# Load new extraction and incremental state
|
||||
new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
|
||||
incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\"))
|
||||
deleted = list(incremental.get('deleted_files', []))
|
||||
# Also prune old nodes for re-extracted (changed) files before inserting fresh AST.
|
||||
# Without this, build_merge's dedup pass tries to reconcile old and new versions of
|
||||
# the same file's nodes and can collapse same-named symbols across files (#1178).
|
||||
changed = [f for files in incremental.get('new_files', {}).values() for f in files]
|
||||
prune = list(dict.fromkeys(deleted + changed)) or None
|
||||
|
||||
# Use build_merge() — reads graph.json directly without NetworkX round-trip
|
||||
# so edge direction (calls, implements, imports) is always preserved (#801).
|
||||
G = build_merge(
|
||||
[new_extraction],
|
||||
graph_path='graphify-out/graph.json',
|
||||
prune_sources=prune,
|
||||
)
|
||||
print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges')
|
||||
|
||||
# Write merged result back to .graphify_extract.json so Step 4 sees the full graph
|
||||
merged_out = {
|
||||
'nodes': [{'id': n, **d} for n, d in G.nodes(data=True)],
|
||||
'edges': [
|
||||
# Explicit source/target last so they win over any stale attrs in d.
|
||||
{**{k: val for k, val in d.items() if k not in ('_src', '_tgt', 'source', 'target')},
|
||||
'source': d.get('_src', u), 'target': d.get('_tgt', v)}
|
||||
for u, v, d in G.edges(data=True)
|
||||
],
|
||||
# G.graph["hyperedges"] holds hyperedges from both existing graph.json
|
||||
# and new_extraction (build_merge combines them). Falling back to
|
||||
# new_extraction only would silently drop prior-run hyperedges (#801).
|
||||
'hyperedges': list(G.graph.get('hyperedges', [])),
|
||||
'input_tokens': new_extraction.get('input_tokens', 0),
|
||||
'output_tokens': new_extraction.get('output_tokens', 0),
|
||||
}
|
||||
Path('graphify-out/.graphify_extract.json').write_text(json.dumps(merged_out, ensure_ascii=False), encoding=\"utf-8\")
|
||||
print(f'[graphify update] Merged extraction written ({len(merged_out[\"nodes\"])} nodes, {len(merged_out[\"edges\"])} edges)')
|
||||
|
||||
# Save manifest so next --update diffs against today's state, not the
|
||||
# prior run's baseline (prevents ghost-node reports on subsequent updates).
|
||||
save_manifest(incremental['files'])
|
||||
print('[graphify update] Manifest saved.')
|
||||
"
|
||||
```
|
||||
|
||||
Then run Steps 4–8 on the merged graph as normal.
|
||||
|
||||
After Step 4, show the graph diff:
|
||||
|
||||
```bash
|
||||
$(cat graphify-out/.graphify_python) -c "
|
||||
import json
|
||||
from graphify.analyze import graph_diff
|
||||
from graphify.build import build_from_json
|
||||
from networkx.readwrite import json_graph
|
||||
import networkx as nx
|
||||
from pathlib import Path
|
||||
|
||||
# Load old graph (before update) from backup written before merge
|
||||
old_data = json.loads(Path('graphify-out/.graphify_old.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_old.json').exists() else None
|
||||
new_extract = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
|
||||
G_new = build_from_json(new_extract)
|
||||
|
||||
if old_data:
|
||||
G_old = json_graph.node_link_graph(old_data, edges='links')
|
||||
diff = graph_diff(G_old, G_new)
|
||||
print(diff['summary'])
|
||||
if diff['new_nodes']:
|
||||
print('New nodes:', ', '.join(n['label'] for n in diff['new_nodes'][:5]))
|
||||
if diff['new_edges']:
|
||||
print('New edges:', len(diff['new_edges']))
|
||||
"
|
||||
```
|
||||
|
||||
Before the merge step, save the old graph: `cp graphify-out/graph.json graphify-out/.graphify_old.json`
|
||||
Clean up after: `rm -f graphify-out/.graphify_old.json`
|
||||
|
||||
---
|
||||
|
||||
## For --cluster-only
|
||||
|
||||
Skip Steps 1–3. Re-run clustering on the existing graph:
|
||||
|
||||
```bash
|
||||
graphify cluster-only .
|
||||
```
|
||||
|
||||
Then run Steps 5–9 as normal (label communities, generate viz, benchmark, clean up, report).
|
||||
@@ -0,0 +1,130 @@
|
||||
---
|
||||
name: harness-eval
|
||||
description: "评估功能的真实运行性能。从 Wiki 读取 schema 和 metric 定义,查询 SQLite 日志,对比基线,调用 LLM judge 做语义评估,产出诊断和迭代建议。触发短语: harness eval, 评估性能, 性能评估。"
|
||||
argument-hint: "[run_id]"
|
||||
---
|
||||
|
||||
# Harness Eval
|
||||
|
||||
## Overview
|
||||
|
||||
在真实环境中运行程序后,评估性能是否达标,诊断问题,决定是否迭代。
|
||||
|
||||
**核心原则:** 不是"通过就停",而是基于真实指标判断"跑得好不好"。
|
||||
|
||||
## 触发方式
|
||||
|
||||
1. **自动**:subagent-driven-development Step 10,所有任务完成 + final review 通过后
|
||||
2. **手动**:`/harness-eval` 或 `/harness-eval <run_id>`
|
||||
|
||||
## 前置条件
|
||||
|
||||
- `research-wiki/schemas/` 中有至少一个 schema 实体
|
||||
- `research-wiki/metrics/` 中有至少一个 metric 实体
|
||||
- `results/harness.db` 存在且包含数据
|
||||
- `.env` 中有 LLM API key(用于 LlmJudge)
|
||||
|
||||
## 长时命令执行约定
|
||||
|
||||
对真实样本重跑流水线属于长时操作。超过 30s 的命令建议在 tmux 中后台运行,便于人工随时 attach 检查进度;短命令直接前台执行即可。
|
||||
|
||||
## 执行流程
|
||||
|
||||
### Phase 1: 准备
|
||||
|
||||
1. 列出 `research-wiki/schemas/` 下所有 schema 实体,获取表名和列信息
|
||||
2. 列出 `research-wiki/metrics/` 下所有 metric 实体,获取基线值和阈值
|
||||
3. 确认 `results/harness.db` 存在,查询 `_runs` 表获取最新运行状态
|
||||
4. 如果指定了 run_id,使用该 run_id;否则使用最近一次 status='completed' 的 run
|
||||
|
||||
### Phase 2: 运行
|
||||
|
||||
#### 2a: 检查已有运行记录
|
||||
|
||||
在 subagent-driven-development 流程中,某个子任务可能已经执行过全局运行并将结果写入了 `harness.db`。此时 harness-eval **不应重复运行**,直接复用已有记录即可。
|
||||
|
||||
1. 查询 `_runs` 表,查找 `status='completed'` 且 `git_sha` 与当前 `HEAD` 匹配的记录
|
||||
2. **找到匹配记录** → 使用该记录的 run_id,**跳过运行**,直接进入 Phase 3。在最终报告中注明「复用已有运行: run_id=xxx」
|
||||
3. **未找到匹配记录 + 自动触发** → 继续 2b
|
||||
4. **未找到匹配记录 + 手动触发** → 询问用户是否需要执行运行
|
||||
|
||||
#### 2b: 重跑流水线(仅无可用记录时)
|
||||
|
||||
1. `make deploy-testing` 起服务(api / worker / Postgres / Redis / 对象存储)
|
||||
2. 对真实样本重跑流水线,结果落 `results/harness.db`;属长时操作,建议按「长时命令执行约定」在 tmux 中运行
|
||||
3. 轮询检查 `_runs` 表状态,等待完成
|
||||
4. 确认 `_runs` 表中对应 run 的 status = 'completed'
|
||||
5. 如果 status = 'failed',直接进入 Phase 6 诊断
|
||||
|
||||
### Phase 3: 硬性指标检查
|
||||
|
||||
对 `research-wiki/metrics/` 中每个标记为硬性判定的指标:
|
||||
|
||||
1. 从 metric 实体中读取:指标名、基线值、阈值、对应的 schema(表名和列名)
|
||||
2. 执行 SQL 查询获取当前运行的实际值
|
||||
3. 对比:实际值 vs 阈值
|
||||
4. 记录结果:pass / fail + 具体数值
|
||||
|
||||
### Phase 4: 语义评估
|
||||
|
||||
对 `research-wiki/metrics/` 中每个标记为语义判定的指标:
|
||||
|
||||
1. 收集 evidence:相关表的数据、_events 表的事件、配置信息
|
||||
2. 调用 `LlmJudge.evaluate(EvalRequest)`(请求体封装 criteria、evidence、rubric)
|
||||
3. 记录结果:Verdict
|
||||
|
||||
### Phase 5: 综合判定
|
||||
|
||||
- 硬性指标全部 pass **且** 语义评估全部 pass → **通过**
|
||||
- 任一不通过 → **不通过**,进入 Phase 6
|
||||
|
||||
### Phase 6: 诊断与迭代决策(仅不通过时)
|
||||
|
||||
1. 收集失败信息:哪些指标不达标、实际值与期望值的差距
|
||||
2. 查询 `_events` 表获取运行过程中的关键事件
|
||||
3. 调用 `LlmJudge.diagnose(error_context, log)`
|
||||
4. 向用户报告:
|
||||
- 问题根因分析
|
||||
- 修复建议
|
||||
- 是否建议再迭代一轮
|
||||
|
||||
### Phase 7: 记录到 Wiki
|
||||
|
||||
1. 创建 finding 实体:
|
||||
```bash
|
||||
conda run -n PolyGateway python3 .claude/tools/research_wiki.py add_entity research-wiki/ --type finding --id eval-<run_id> --title "Harness 评估: <run_id>"
|
||||
```
|
||||
2. 在 finding 中记录完整评估报告(硬性指标表 + 语义评估结果 + 诊断结论)
|
||||
3. 建立 edge:
|
||||
```bash
|
||||
conda run -n PolyGateway python3 .claude/tools/research_wiki.py add_edge research-wiki/ --from "finding:eval-<run_id>" --to "metric:<id>" --type evaluates --evidence "..."
|
||||
```
|
||||
4. 如果通过且用户确认,更新 metric 实体中的基线值为当前值
|
||||
5. 重建索引
|
||||
|
||||
## 输出格式
|
||||
|
||||
评估完成后,向用户输出结构化报告:
|
||||
|
||||
| 字段 | 内容 |
|
||||
|------|------|
|
||||
| 运行信息 | run_id, git_sha, 耗时 |
|
||||
| 硬性指标表 | 指标名, 基线, 当前, 阈值, 判定 |
|
||||
| 语义评估表 | 维度, 得分, 判定, 说明 |
|
||||
| 综合判定 | 通过/不通过 |
|
||||
| 诊断(如有) | 根因, 建议 |
|
||||
|
||||
## 与 subagent-driven-development 集成
|
||||
|
||||
在 SKILL.md 的 Step 9(final whole-implementation review)之后追加 Step 10:
|
||||
|
||||
```
|
||||
Step 10: Harness 评估
|
||||
调用 /harness-eval skill
|
||||
通过 → 进入 finishing-a-development-branch
|
||||
不通过 → 根据诊断结果,回到 Step 2 迭代相关 task
|
||||
```
|
||||
|
||||
## 评估范围
|
||||
|
||||
本 skill 当前覆盖①工程评估;②提取信度/③诊断效度的全量评估待 `app/` 流水线落地后接入。
|
||||
@@ -0,0 +1,108 @@
|
||||
---
|
||||
name: idea-creator
|
||||
description: "Generate and rank research ideas. GPT-5.4 + Claude dual independent brainstorming. Trigger phrases: find ideas, brainstorm ideas, generate research ideas."
|
||||
argument-hint: [research-direction]
|
||||
---
|
||||
|
||||
# Research Idea Generator
|
||||
Research direction: $ARGUMENTS
|
||||
|
||||
## Constants
|
||||
- `OUTPUT_DIR = idea-stage/`
|
||||
|
||||
## Workflow
|
||||
|
||||
### Phase 0: Load Wiki Context
|
||||
If `research-wiki/query_pack.md` exists and was updated less than 7 days ago:
|
||||
- Read it
|
||||
- Treat failed ideas as a banlist
|
||||
- Treat gaps as seeds
|
||||
- Treat top papers as known prior work
|
||||
|
||||
### Phase 1: Domain Survey
|
||||
If there is no `query_pack`, do a quick WebSearch first to build a broad domain context.
|
||||
|
||||
### Phase 2: Dual Independent Brainstorming
|
||||
**Critical requirement: brainstorm independently on two tracks first, then synthesize.**
|
||||
|
||||
GPT brainstorming (via Codex):
|
||||
- Use `/codex:rescue --fresh --background`
|
||||
- The prompt must ask GPT to generate 8-12 ideas
|
||||
- The prompt must include: the domain overview, known gaps, and resource constraints
|
||||
- Poll with `/codex:status`
|
||||
- Use `/codex:result` to collect the output
|
||||
|
||||
Claude brainstorming (via Agent):
|
||||
- Dispatch a subagent through the Agent tool with `subagent_type="general-purpose"`
|
||||
- Provide the same domain overview context
|
||||
- Require it to independently generate 8-12 ideas
|
||||
|
||||
### Phase 3: Claude Synthesis
|
||||
- Merge both idea sets and deduplicate by similarity
|
||||
- Cross-check: Claude challenges GPT ideas, GPT challenges Claude ideas
|
||||
- Score and rank everything in a single pass
|
||||
- Label each idea with its source: `GPT` / `Claude` / `consensus`
|
||||
|
||||
### Phase 4: First Filtering Pass
|
||||
Filtering criteria:
|
||||
- Feasibility: compute, data, and implementation cost
|
||||
- Fast novelty check: run 2-3 WebSearch passes for each idea
|
||||
- Impact: answer "so what?"
|
||||
- Remove ideas that are infeasible or too vague
|
||||
|
||||
### Phase 5: Deep Verification
|
||||
For the top ideas, call the `/novelty-check` skill.
|
||||
|
||||
### Phase 7: Output Report
|
||||
Write `idea-stage/IDEA_REPORT.md` in Chinese, with the following format:
|
||||
|
||||
```md
|
||||
# Research Idea Report
|
||||
**Direction**: ...
|
||||
**Date**: YYYY-MM-DD
|
||||
**Evaluation Result**: X generated -> Y survived -> W recommended
|
||||
|
||||
## Domain Overview Summary
|
||||
## Recommended Ideas (Ranked)
|
||||
### Idea 1: <title>
|
||||
- Hypothesis / minimal experiment / novelty / feasibility / risk / source
|
||||
|
||||
## Rejected Ideas
|
||||
```
|
||||
|
||||
### Phase 8: Wiki Integration
|
||||
- Check whether `research-wiki/` exists
|
||||
- For each idea:
|
||||
- `.claude/tools/research_wiki.py add_entity research-wiki/ --type idea --id <id> --title "..."`
|
||||
- Add edges:
|
||||
- `inspired_by`
|
||||
- `addresses_gap`
|
||||
- Rebuild `query_pack` and the index
|
||||
|
||||
## Key Rules
|
||||
- If the direction is too broad, STOP and ask the user to narrow it
|
||||
- Failed ideas must also be written into the wiki
|
||||
- Empirical signals matter more than theoretical attractiveness
|
||||
- `"Apply X to Y"` is the lowest-level research idea
|
||||
- If `research-wiki/` does not exist, still finish the report output but skip all wiki writes
|
||||
- First independent, then synthesize; first evidence, then judgment; first filter, then dig deeper
|
||||
|
||||
## Output Requirements
|
||||
When the task is complete, you must provide at least:
|
||||
- A domain overview summary
|
||||
- 8-12 raw ideas
|
||||
- A deduplicated candidate list
|
||||
- Filtering results and reasons
|
||||
- Deep verification results for the top ideas
|
||||
- `idea-stage/IDEA_REPORT.md`
|
||||
- Wiki write results, if applicable
|
||||
|
||||
## Completion Criteria
|
||||
Only finish when all of the following are complete:
|
||||
1. Wiki context loading or domain survey is complete
|
||||
2. Both independent brainstorming tracks are complete
|
||||
3. Merge, deduplication, and ranking are complete
|
||||
4. The first filtering pass is complete
|
||||
5. Deep verification of top ideas is complete
|
||||
6. `idea-stage/IDEA_REPORT.md` has been written
|
||||
7. `research-wiki/` integration is complete, if applicable
|
||||
@@ -0,0 +1,141 @@
|
||||
---
|
||||
name: novelty-check
|
||||
description: "Verify the novelty of research ideas. GPT cross-validation. Trigger phrases: novelty check, has anyone done this, check novelty."
|
||||
argument-hint: [method-or-idea-description]
|
||||
---
|
||||
|
||||
# Novelty Verification
|
||||
Verify novelty of: $ARGUMENTS
|
||||
|
||||
## Goal
|
||||
Perform a strict check on whether a method, idea, or experimental setting is actually new. The default stance is skepticism, not help-seeking for supporting evidence.
|
||||
|
||||
## Working Principles
|
||||
- Brutally honest: do not relax the standard just to make something look new.
|
||||
- `Applying X to Y` is not novel by default unless the application produces an unexpected mechanism, theoretical explanation, or clearly different experimental phenomenon.
|
||||
- Check the novelty of both the `METHOD` and the `EXPERIMENTAL SETTING`.
|
||||
- If the method itself is not new, but the findings, conclusions, experimental setup, or failure analysis are new, state that distinction explicitly.
|
||||
- Always search the last 6 months of arXiv.
|
||||
- Do not rely on titles alone; read the abstract and, when necessary, the key parts of related work, intro, method, and appendix.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Phase A: Extract Core Claims
|
||||
First break the user's method description into 3-5 core technical claims. Each one should be as specific as possible.
|
||||
|
||||
For each claim, answer:
|
||||
- What is the method?
|
||||
- What problem does it solve?
|
||||
- What is the key mechanism?
|
||||
- What is the essential difference from an obvious baseline?
|
||||
|
||||
Rewrite the story-like description into searchable technical propositions and avoid vague phrasing.
|
||||
|
||||
### Phase B: Multi-source Literature Search
|
||||
Run multi-source retrieval for each claim, prioritizing recent work and similar settings.
|
||||
|
||||
For each claim, try at least 3 search-query sets, and make them complementary:
|
||||
- Direct technical terms
|
||||
- Synonyms / abbreviations / related task names
|
||||
- "Problem + mechanism" combinations
|
||||
- "Method + dataset / setting" combinations
|
||||
|
||||
#### Required Search Channels
|
||||
1. WebSearch: arXiv / Google Scholar / Semantic Scholar / conference homepages
|
||||
2. `python3 .claude/tools/arxiv_fetch.py search "QUERY" --max 10`
|
||||
3. `python3 .claude/tools/semantic_scholar_fetch.py search "QUERY" --max 10`
|
||||
4. `python3 .claude/tools/exa_search.py search "QUERY" --max 10` (if available)
|
||||
5. `python3 .claude/tools/openalex_fetch.py search "QUERY" --max 10` (if available)
|
||||
|
||||
#### Search Priorities
|
||||
- ICLR / NeurIPS / ICML 2025-2026
|
||||
- arXiv preprints from the last 6 months
|
||||
- Papers close to the method mechanism, not only papers on the same task
|
||||
- Papers close to the experimental setting, not only papers using the same method
|
||||
|
||||
#### Decision Strategy
|
||||
- Record potentially overlapping papers first; do not exclude them too early
|
||||
- Prefer reading the abstract, intro, related work, and method sections
|
||||
- If overlap looks suspicious, also read the experimental setup and appendix
|
||||
|
||||
#### Recording Requirements
|
||||
For each candidate paper, record:
|
||||
- Title
|
||||
- Year
|
||||
- Venue / status
|
||||
- Relevant point
|
||||
- The specific reason it may overlap
|
||||
- Why it might still be a different work
|
||||
|
||||
If a data source is unavailable, explicitly record the fallback reason and continue with the others; do not stop the task.
|
||||
|
||||
### Phase C: GPT Cross-Validation
|
||||
Send the method description from Phase A and all candidate papers found in Phase B to `/codex:rescue --fresh --wait` for a second review.
|
||||
|
||||
The cross-validation prompt must include:
|
||||
- proposed method description
|
||||
- the full candidate paper list
|
||||
- ask:
|
||||
- `Is this method novel?`
|
||||
- `What is the closest prior work?`
|
||||
- `What is the delta?`
|
||||
|
||||
Use high reasoning effort.
|
||||
|
||||
The goal of cross-validation is not to find even more papers. It is to force out the closest prior art, the smallest difference, and the risk of pseudo-novelty.
|
||||
|
||||
### Phase D: Output Report + Wiki Integration
|
||||
The output must be in English and follow a fixed structure.
|
||||
|
||||
#### Report Format
|
||||
```markdown
|
||||
## Novelty Check Report
|
||||
### Method Under Review
|
||||
### Core Claims
|
||||
- Claim 1: ... (novelty: high / medium / low; closest paper: ...)
|
||||
- Claim 2: ... (novelty: high / medium / low; closest paper: ...)
|
||||
|
||||
### Recent Prior Work
|
||||
| Paper | Year | Venue / Status | Overlap Point | Key Difference |
|
||||
|---|---:|---|---|---|
|
||||
|
||||
### Overall Assessment
|
||||
- score X/10
|
||||
- recommendation: continue / continue cautiously / abandon
|
||||
- key differentiator: ...
|
||||
- positioning advice: ...
|
||||
```
|
||||
|
||||
#### Evaluation Scale
|
||||
- `high`: current search shows no close prior art, and the difference is concrete and technical
|
||||
- `medium`: there is related prior work, but there is still a clear and defensible technical delta
|
||||
- `low`: mostly a reorganization of known methods, task switching, dataset switching, hyperparameter changes, or standard engineering changes
|
||||
|
||||
#### Wiki Integration
|
||||
If the project has `research-wiki/`, also ingest the knowledge there:
|
||||
- Create a `claim` entity for each core claim
|
||||
- Create a `paper` entity for each newly found paper
|
||||
- Add claim-paper / paper-paper relation edges
|
||||
- Rebuild `query_pack`
|
||||
|
||||
Prefer existing tools such as `.claude/tools/research_wiki.py`; if the wiki does not exist, skip silently and do not error.
|
||||
|
||||
Ingestion rules:
|
||||
- Only write high-confidence information
|
||||
- Claim names should be short, stable, and reusable
|
||||
- Edges must include evidence; do not create empty links
|
||||
|
||||
## Completion Criteria
|
||||
Only finish when all of the following are complete:
|
||||
1. 3-5 core claims have been extracted
|
||||
2. Multi-source search has been completed, including the last 6 months of arXiv
|
||||
3. Candidate paper abstracts have been read, and related work / method sections were read when necessary
|
||||
4. GPT cross-validation has been completed
|
||||
5. A structured English report has been produced
|
||||
6. If `research-wiki/` exists, the corresponding writes and `query_pack` rebuild have been completed
|
||||
|
||||
## Failure Handling
|
||||
- Missing tools: record the missing item and degrade gracefully
|
||||
- Too few search results: expand synonyms, abbreviations, higher-level terms, and experimental settings
|
||||
- Too many search results: prefer the most recent, most similar, and most likely overlapping work
|
||||
- Conflicting evidence: read the original abstract and method sections first; do not rely on intuition
|
||||
@@ -0,0 +1,228 @@
|
||||
---
|
||||
name: receiving-code-review
|
||||
description: Use when receiving code review feedback, before implementing suggestions, especially if feedback seems unclear or technically questionable - requires technical rigor and verification, not performative agreement or blind implementation
|
||||
---
|
||||
|
||||
# Code Review Reception
|
||||
|
||||
## Overview
|
||||
|
||||
Code review requires technical evaluation, not emotional performance.
|
||||
|
||||
**Core principle:** Verify before implementing. Ask before assuming. Technical correctness over social comfort.
|
||||
|
||||
## The Response Pattern
|
||||
|
||||
```
|
||||
WHEN receiving code review feedback:
|
||||
|
||||
1. READ: Complete feedback without reacting
|
||||
2. UNDERSTAND: Restate requirement in own words (or ask)
|
||||
3. VERIFY: Check against codebase reality
|
||||
4. EVALUATE: Technically sound for THIS codebase?
|
||||
5. RESPOND: Technical acknowledgment or reasoned pushback
|
||||
6. IMPLEMENT: One item at a time, test each
|
||||
```
|
||||
|
||||
## Forbidden Responses
|
||||
|
||||
**NEVER:**
|
||||
- "You're absolutely right!" (explicit CLAUDE.md violation)
|
||||
- "Great point!" / "Excellent feedback!" (performative)
|
||||
- "Let me implement that now" (before verification)
|
||||
|
||||
**INSTEAD:**
|
||||
- Restate the technical requirement
|
||||
- Ask clarifying questions
|
||||
- Push back with technical reasoning if wrong
|
||||
- Just start working (actions > words)
|
||||
|
||||
## Handling Unclear Feedback
|
||||
|
||||
```
|
||||
IF any item is unclear:
|
||||
STOP - do not implement anything yet
|
||||
ASK for clarification on unclear items
|
||||
|
||||
WHY: Items may be related. Partial understanding = wrong implementation.
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
your human partner: "Fix 1-6"
|
||||
You understand 1,2,3,6. Unclear on 4,5.
|
||||
|
||||
❌ WRONG: Implement 1,2,3,6 now, ask about 4,5 later
|
||||
✅ RIGHT: "I understand items 1,2,3,6. Need clarification on 4 and 5 before proceeding."
|
||||
```
|
||||
|
||||
## Source-Specific Handling
|
||||
|
||||
### From your human partner
|
||||
- **Trusted** - implement after understanding
|
||||
- **Still ask** if scope unclear
|
||||
- **No performative agreement**
|
||||
- **Skip to action** or technical acknowledgment
|
||||
|
||||
### From External Reviewers
|
||||
```
|
||||
BEFORE implementing:
|
||||
1. Check: Technically correct for THIS codebase?
|
||||
2. Check: Breaks existing functionality?
|
||||
3. Check: Reason for current implementation?
|
||||
4. Check: Works on all platforms/versions?
|
||||
5. Check: Does reviewer understand full context?
|
||||
|
||||
IF suggestion seems wrong:
|
||||
Push back with technical reasoning
|
||||
|
||||
IF can't easily verify:
|
||||
Say so: "I can't verify this without [X]. Should I [investigate/ask/proceed]?"
|
||||
|
||||
IF conflicts with your human partner's prior decisions:
|
||||
Stop and discuss with your human partner first
|
||||
```
|
||||
|
||||
**your human partner's rule:** "External feedback - be skeptical, but check carefully"
|
||||
|
||||
## YAGNI Check for "Professional" Features
|
||||
|
||||
```
|
||||
IF reviewer suggests "implementing properly":
|
||||
grep codebase for actual usage
|
||||
|
||||
IF unused: "This endpoint isn't called. Remove it (YAGNI)?"
|
||||
IF used: Then implement properly
|
||||
```
|
||||
|
||||
**your human partner's rule:** "You and reviewer both report to me. If we don't need this feature, don't add it."
|
||||
|
||||
## Implementation Order
|
||||
|
||||
```
|
||||
FOR multi-item feedback:
|
||||
1. Clarify anything unclear FIRST
|
||||
2. Then implement in this order:
|
||||
- Blocking issues (breaks, security)
|
||||
- Simple fixes (typos, imports)
|
||||
- Complex fixes (refactoring, logic)
|
||||
3. Test each fix individually
|
||||
4. Verify no regressions
|
||||
```
|
||||
|
||||
## When To Push Back
|
||||
|
||||
Push back when:
|
||||
- Suggestion breaks existing functionality
|
||||
- Reviewer lacks full context
|
||||
- Violates YAGNI (unused feature)
|
||||
- Technically incorrect for this stack
|
||||
- Legacy/compatibility reasons exist
|
||||
- Conflicts with your human partner's architectural decisions
|
||||
|
||||
**How to push back:**
|
||||
- Use technical reasoning, not defensiveness
|
||||
- Ask specific questions
|
||||
- Reference working tests/code
|
||||
- Involve your human partner if architectural
|
||||
|
||||
|
||||
## Acknowledging Correct Feedback
|
||||
|
||||
When feedback IS correct:
|
||||
```
|
||||
✅ "Fixed. [Brief description of what changed]"
|
||||
✅ "Good catch - [specific issue]. Fixed in [location]."
|
||||
✅ [Just fix it and show in the code]
|
||||
|
||||
❌ "You're absolutely right!"
|
||||
❌ "Great point!"
|
||||
❌ "Thanks for catching that!"
|
||||
❌ "Thanks for [anything]"
|
||||
❌ ANY gratitude expression
|
||||
```
|
||||
|
||||
**Why no thanks:** Actions speak. Just fix it. The code itself shows you heard the feedback.
|
||||
|
||||
**If you catch yourself about to write "Thanks":** DELETE IT. State the fix instead.
|
||||
|
||||
## Gracefully Correcting Your Pushback
|
||||
|
||||
If you pushed back and were wrong:
|
||||
```
|
||||
✅ "You were right - I checked [X] and it does [Y]. Implementing now."
|
||||
✅ "Verified this and you're correct. My initial understanding was wrong because [reason]. Fixing."
|
||||
|
||||
❌ Long apology
|
||||
❌ Defending why you pushed back
|
||||
❌ Over-explaining
|
||||
```
|
||||
|
||||
State the correction factually and move on.
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Fix |
|
||||
|---------|-----|
|
||||
| Performative agreement | State requirement or just act |
|
||||
| Blind implementation | Verify against codebase first |
|
||||
| Batch without testing | One at a time, test each |
|
||||
| Assuming reviewer is right | Check if breaks things |
|
||||
| Avoiding pushback | Technical correctness > comfort |
|
||||
| Partial implementation | Clarify all items first |
|
||||
| Can't verify, proceed anyway | State limitation, ask for direction |
|
||||
|
||||
## Real Examples
|
||||
|
||||
**Performative Agreement (Bad):**
|
||||
```
|
||||
Reviewer: "Remove legacy code"
|
||||
❌ "You're absolutely right! Let me remove that..."
|
||||
```
|
||||
|
||||
**Technical Verification (Good):**
|
||||
```
|
||||
Reviewer: "Remove legacy code"
|
||||
✅ "Checking... build target is 10.15+, this API needs 13+. Need legacy for backward compat. Current impl has wrong bundle ID - fix it or drop pre-13 support?"
|
||||
```
|
||||
|
||||
**YAGNI (Good):**
|
||||
```
|
||||
Reviewer: "Implement proper metrics tracking with database, date filters, CSV export"
|
||||
✅ "Grepped codebase - nothing calls this endpoint. Remove it (YAGNI)? Or is there usage I'm missing?"
|
||||
```
|
||||
|
||||
**Unclear Item (Good):**
|
||||
```
|
||||
your human partner: "Fix items 1-6"
|
||||
You understand 1,2,3,6. Unclear on 4,5.
|
||||
✅ "Understand 1,2,3,6. Need clarification on 4 and 5 before implementing."
|
||||
```
|
||||
|
||||
## GitHub Thread Replies
|
||||
|
||||
When replying to inline review comments on GitHub, reply in the comment thread (`gh api repos/{owner}/{repo}/pulls/{pr}/comments/{id}/replies`), not as a top-level PR comment.
|
||||
|
||||
## The Bottom Line
|
||||
|
||||
**External feedback = suggestions to evaluate, not orders to follow.**
|
||||
|
||||
Verify. Question. Then implement.
|
||||
|
||||
No performative agreement. Technical rigor always.
|
||||
|
||||
## Wiki Integration
|
||||
|
||||
**Precondition**: `research-wiki/` directory exists (skip this section entirely if it does not).
|
||||
|
||||
**Trigger**: When review feedback is received and you decide whether to accept or reject it.
|
||||
|
||||
**Type**: `review`
|
||||
|
||||
**Severity**: `approved` / `needs_changes` / `rejected`
|
||||
|
||||
**Steps**:
|
||||
1. Run `.claude/tools/research_wiki.py add_entity research-wiki/ --type review --id <slug> --title "<review response title>"` to create the review entity
|
||||
2. Append the accepted or rejected suggestions, the reason, and the follow-up actions to the generated page
|
||||
3. If the response changes a design or plan, run `.claude/tools/research_wiki.py add_edge research-wiki/ --from "review:<id>" --to "<target-type>:<id>" --type informs --evidence "..."`
|
||||
4. Run `.claude/tools/research_wiki.py rebuild_index research-wiki/`
|
||||
@@ -0,0 +1,116 @@
|
||||
---
|
||||
name: requesting-code-review
|
||||
description: Use when completing tasks, implementing major features, or before merging to verify work meets requirements
|
||||
---
|
||||
|
||||
# Requesting Code Review
|
||||
|
||||
Dispatch Codex as a read-only reviewer to catch issues before they cascade. The reviewer gets precisely crafted context for evaluation — never your session's history. This keeps the reviewer focused on the work product, not your thought process, and preserves your own context for continued work.
|
||||
|
||||
**Core principle:** Review early, review often.
|
||||
|
||||
## When to Request Review
|
||||
|
||||
**Mandatory(硬门):**
|
||||
- After completing a major feature
|
||||
- Before merge to main (subagent-driven development 场景即其合并前一次性审查)
|
||||
|
||||
**Optional but valuable:**
|
||||
- When stuck (fresh perspective)
|
||||
- Before refactoring (baseline check)
|
||||
- After fixing complex bug
|
||||
|
||||
## How to Request
|
||||
|
||||
**1. Get git SHAs:**
|
||||
```bash
|
||||
BASE_SHA=$(git rev-parse HEAD~1) # or origin/main
|
||||
HEAD_SHA=$(git rev-parse HEAD)
|
||||
```
|
||||
|
||||
**2. Dispatch Codex as reviewer:**
|
||||
|
||||
用 `/codex:rescue --fresh --wait` 把 `code-reviewer.md` 模板(含 `{BASE_SHA}`/`{HEAD_SHA}` 占位与 `git diff` 指令)交 Codex 只读审查。
|
||||
|
||||
**Placeholders:**
|
||||
- `{DESCRIPTION}` - Brief summary of what you built
|
||||
- `{PLAN_OR_REQUIREMENTS}` - What it should do
|
||||
- `{BASE_SHA}` - Starting commit
|
||||
- `{HEAD_SHA}` - Ending commit
|
||||
|
||||
**3. Act on feedback:**
|
||||
- Fix Critical issues immediately
|
||||
- Fix Important issues before proceeding
|
||||
- Note Minor issues for later
|
||||
- Push back if reviewer is wrong (with reasoning)
|
||||
|
||||
## Example
|
||||
|
||||
```
|
||||
[Just completed Task 2: Add verification function]
|
||||
|
||||
You: Let me request code review before proceeding.
|
||||
|
||||
BASE_SHA=$(git log --oneline | grep "Task 1" | head -1 | awk '{print $1}')
|
||||
HEAD_SHA=$(git rev-parse HEAD)
|
||||
|
||||
[Dispatch Codex as reviewer via /codex:rescue --fresh --wait]
|
||||
DESCRIPTION: Added verifyIndex() and repairIndex() with 4 issue types
|
||||
PLAN_OR_REQUIREMENTS: Task 2 from research-wiki/plans/deployment-plan.md
|
||||
BASE_SHA: a7981ec
|
||||
HEAD_SHA: 3df7661
|
||||
|
||||
[Codex returns]:
|
||||
Strengths: Clean architecture, real tests
|
||||
Issues:
|
||||
Important: Missing progress indicators
|
||||
Minor: Magic number (100) for reporting interval
|
||||
Assessment: Ready to proceed
|
||||
|
||||
You: [Fix progress indicators]
|
||||
[Continue to Task 3]
|
||||
```
|
||||
|
||||
## Integration with Workflows
|
||||
|
||||
**Subagent-Driven Development:**
|
||||
- 合并前一次整分支审查(见该 skill 的 merge-reviewer-prompt.md);任务级把关交给自动质量门
|
||||
|
||||
**Executing Plans:**
|
||||
- Review after each task or at natural checkpoints
|
||||
- Get feedback, apply, continue
|
||||
|
||||
**Ad-Hoc Development:**
|
||||
- Review before merge
|
||||
- Review when stuck
|
||||
|
||||
## Red Flags
|
||||
|
||||
**Never:**
|
||||
- 合并前跳过审查(小改动可自判不做中途审查,但合并/PR 前的审查是硬门)
|
||||
- Ignore Critical issues
|
||||
- Proceed with unfixed Important issues
|
||||
- Argue with valid technical feedback
|
||||
|
||||
**If reviewer wrong:**
|
||||
- Push back with technical reasoning
|
||||
- Show code/tests that prove it works
|
||||
- Request clarification
|
||||
|
||||
See template at: requesting-code-review/code-reviewer.md
|
||||
|
||||
## Wiki Integration
|
||||
|
||||
**Precondition**: `research-wiki/` directory exists (skip this section entirely if it does not).
|
||||
|
||||
**Trigger**: When requesting a code review or when a clear review result has been received.
|
||||
|
||||
**Type**: `review`
|
||||
|
||||
**Severity**: `approved` / `needs_changes` / `rejected`
|
||||
|
||||
**Steps**:
|
||||
1. Run `.claude/tools/research_wiki.py add_entity research-wiki/ --type review --id <slug> --title "<review title>"` to create the review entity
|
||||
2. Append the review conclusion, key issues, recommendations, and whether it passed to the generated page
|
||||
3. If the review recommendations change a design or plan, run `.claude/tools/research_wiki.py add_edge research-wiki/ --from "review:<id>" --to "<target-type>:<id>" --type informs --evidence "..."`
|
||||
4. Run `.claude/tools/research_wiki.py rebuild_index research-wiki/`
|
||||
@@ -0,0 +1,175 @@
|
||||
# Code Reviewer Prompt Template
|
||||
|
||||
Use this template when dispatching Codex as reviewer.
|
||||
|
||||
**Purpose:** Review completed work against requirements and code quality standards before it cascades into more work.
|
||||
|
||||
```
|
||||
/codex:rescue --fresh --wait (read-only review):
|
||||
description: "Review code changes"
|
||||
prompt: |
|
||||
You are a Senior Code Reviewer with expertise in software architecture,
|
||||
design patterns, and best practices. Your job is to review completed work
|
||||
against its plan or requirements and identify issues before they cascade.
|
||||
|
||||
## What Was Implemented
|
||||
|
||||
{DESCRIPTION}
|
||||
|
||||
## Requirements / Plan
|
||||
|
||||
{PLAN_OR_REQUIREMENTS}
|
||||
|
||||
## Git Range to Review
|
||||
|
||||
**Base:** {BASE_SHA}
|
||||
**Head:** {HEAD_SHA}
|
||||
|
||||
```bash
|
||||
git diff --stat {BASE_SHA}..{HEAD_SHA}
|
||||
git diff {BASE_SHA}..{HEAD_SHA}
|
||||
```
|
||||
|
||||
## What to Check
|
||||
|
||||
**Plan alignment:**
|
||||
- Does the implementation match the plan / requirements?
|
||||
- Are deviations justified improvements, or problematic departures?
|
||||
- Is all planned functionality present?
|
||||
|
||||
**Code quality:**
|
||||
- Clean separation of concerns?
|
||||
- Proper error handling?
|
||||
- Type safety where applicable?
|
||||
- DRY without premature abstraction?
|
||||
- Edge cases handled?
|
||||
|
||||
**Architecture:**
|
||||
- Sound design decisions?
|
||||
- Reasonable scalability and performance?
|
||||
- Security concerns?
|
||||
- Integrates cleanly with surrounding code?
|
||||
|
||||
**Testing:**
|
||||
- Tests verify real behavior, not mocks?
|
||||
- Edge cases covered?
|
||||
- Integration tests where they matter?
|
||||
- All tests passing?
|
||||
|
||||
**Production readiness:**
|
||||
- Migration strategy if schema changed?
|
||||
- Backward compatibility considered?
|
||||
- Documentation complete?
|
||||
- No obvious bugs?
|
||||
|
||||
**Runtime data & logging:**
|
||||
- Are SqliteRunStore calls present at all critical code paths?
|
||||
- Do log tables capture enough data for post-run diagnosis?
|
||||
- Query Wiki schemas/ to understand which tables this code affects
|
||||
- Query SQLite to check if metrics regressed after these changes
|
||||
- Are log_event calls placed at error boundaries and state transitions?
|
||||
|
||||
## Calibration
|
||||
|
||||
Categorize issues by actual severity. Not everything is Critical.
|
||||
Acknowledge what was done well before listing issues — accurate praise
|
||||
helps the implementer trust the rest of the feedback.
|
||||
|
||||
If you find significant deviations from the plan, flag them specifically
|
||||
so the implementer can confirm whether the deviation was intentional.
|
||||
If you find issues with the plan itself rather than the implementation,
|
||||
say so.
|
||||
|
||||
## Output Format
|
||||
|
||||
### Strengths
|
||||
[What's well done? Be specific.]
|
||||
|
||||
### Issues
|
||||
|
||||
#### Critical (Must Fix)
|
||||
[Bugs, security issues, data loss risks, broken functionality]
|
||||
|
||||
#### Important (Should Fix)
|
||||
[Architecture problems, missing features, poor error handling, test gaps]
|
||||
|
||||
#### Minor (Nice to Have)
|
||||
[Code style, optimization opportunities, documentation polish]
|
||||
|
||||
For each issue:
|
||||
- File:line reference
|
||||
- What's wrong
|
||||
- Why it matters
|
||||
- How to fix (if not obvious)
|
||||
|
||||
### Recommendations
|
||||
[Improvements for code quality, architecture, or process]
|
||||
|
||||
### Assessment
|
||||
|
||||
**Ready to merge?** [Yes | No | With fixes]
|
||||
|
||||
**Reasoning:** [1-2 sentence technical assessment]
|
||||
|
||||
## Critical Rules
|
||||
|
||||
**DO:**
|
||||
- Categorize by actual severity
|
||||
- Be specific (file:line, not vague)
|
||||
- Explain WHY each issue matters
|
||||
- Acknowledge strengths
|
||||
- Give a clear verdict
|
||||
|
||||
**DON'T:**
|
||||
- Say "looks good" without checking
|
||||
- Mark nitpicks as Critical
|
||||
- Give feedback on code you didn't actually read
|
||||
- Be vague ("improve error handling")
|
||||
- Avoid giving a clear verdict
|
||||
```
|
||||
|
||||
**Placeholders:**
|
||||
- `{DESCRIPTION}` — brief summary of what was built
|
||||
- `{PLAN_OR_REQUIREMENTS}` — what it should do (plan file path, task text, or requirements)
|
||||
- `{BASE_SHA}` — starting commit
|
||||
- `{HEAD_SHA}` — ending commit
|
||||
|
||||
**Reviewer returns:** Strengths, Issues (Critical / Important / Minor), Recommendations, Assessment
|
||||
|
||||
## Example Output
|
||||
|
||||
```
|
||||
### Strengths
|
||||
- Clean database schema with proper migrations (db.ts:15-42)
|
||||
- Comprehensive test coverage (18 tests, all edge cases)
|
||||
- Good error handling with fallbacks (summarizer.ts:85-92)
|
||||
|
||||
### Issues
|
||||
|
||||
#### Important
|
||||
1. **Missing help text in CLI wrapper**
|
||||
- File: index-conversations:1-31
|
||||
- Issue: No --help flag, users won't discover --concurrency
|
||||
- Fix: Add --help case with usage examples
|
||||
|
||||
2. **Date validation missing**
|
||||
- File: search.ts:25-27
|
||||
- Issue: Invalid dates silently return no results
|
||||
- Fix: Validate ISO format, throw error with example
|
||||
|
||||
#### Minor
|
||||
1. **Progress indicators**
|
||||
- File: indexer.ts:130
|
||||
- Issue: No "X of Y" counter for long operations
|
||||
- Impact: Users don't know how long to wait
|
||||
|
||||
### Recommendations
|
||||
- Add progress reporting for user experience
|
||||
- Consider config file for excluded projects (portability)
|
||||
|
||||
### Assessment
|
||||
|
||||
**Ready to merge: With fixes**
|
||||
|
||||
**Reasoning:** Core implementation is solid with good architecture and tests. Important issues (help text, date validation) are easily fixed and don't affect core functionality.
|
||||
```
|
||||
@@ -0,0 +1,213 @@
|
||||
---
|
||||
name: research-lit
|
||||
description: "Search and analyze research papers, literature reviews, and related work. Trigger phrases: find papers, literature review, related work, literature review."
|
||||
argument-hint: [research-topic]
|
||||
---
|
||||
|
||||
# Literature Review
|
||||
Research topic: $ARGUMENTS
|
||||
|
||||
## Constants
|
||||
- `PAPER_LIBRARY = references/` (local PDF directory)
|
||||
- `MAX_LOCAL_PAPERS = 20`
|
||||
|
||||
## Data Sources (all enabled by default)
|
||||
|
||||
| Data source | How to determine availability | What it provides | Fallback behavior |
|
||||
|---|---|---|---|
|
||||
| Local PDF | `references/` exists and `references/**/*.pdf` is non-empty | Papers, reports, appendices, and drafts collected in the repo | Read only locally; if there are no PDFs, continue with online search |
|
||||
| Web search | Network is available and general search results are accessible | Google Scholar / paper pages / arXiv / conference homepages / survey blogs | If search fails, skip that source and keep results from others |
|
||||
| arXiv API | `python3 .claude/tools/arxiv_fetch.py` is runnable and the network is available | arXiv metadata, abstracts, IDs, categories, versions | Skip if unavailable; prefer arXiv records from other sources |
|
||||
| Semantic Scholar | `python3 .claude/tools/semantic_scholar_fetch.py` is runnable and the network is available | Paper metadata, venue, citation, author, and citation relationships | Skip if unavailable; if an arXiv paper has S2 venue metadata, prefer S2 |
|
||||
| Exa | `python3 .claude/tools/exa_search.py` is runnable, the network is available, and API config exists | Semantic search, page highlight excerpts, related paper page clues | Skip if unavailable; use results only as supplemental clues |
|
||||
| OpenAlex | `python3 .claude/tools/openalex_fetch.py` is runnable and the network is available | Open scholarly graph, DOI, venue, year, and citation relationships | Skip if unavailable; use it to fill in DOI / venue / author information |
|
||||
| DeepXiv | `python3 .claude/tools/deepxiv_fetch.py` is runnable and the network is available | Semantic search and paper aggregation results focused on arXiv | Skip if unavailable; cross-check against arXiv / S2 |
|
||||
|
||||
### Coverage Control
|
||||
All data sources are enabled by default. If the user includes the instruction:
|
||||
|
||||
`— sources: <list>`
|
||||
|
||||
then only the sources listed in `<list>` are used. Parsing rules:
|
||||
|
||||
- `<list>` is comma-separated and may use Chinese or English names, such as `local PDF, arXiv, Semantic Scholar`
|
||||
- Keep only recognizable names; ignore unknown items
|
||||
- If parsing yields nothing, fall back to the default of enabling everything
|
||||
- Either local-only reading or online-only searching is allowed; do not stop because some sources are unavailable
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 0: Scan local PDFs
|
||||
|
||||
Scan the local library first, then decide where to focus online search.
|
||||
|
||||
1. Glob: `references/**/*.pdf`
|
||||
2. Filter by relevance to the research topic, prioritizing PDFs whose title, abstract, first chapter, or conclusion matches the topic
|
||||
3. For up to `MAX_LOCAL_PAPERS` relevant PDFs, read the first 3 pages
|
||||
4. Record the title, authors, year, venue, method keywords, and relevance for each paper
|
||||
|
||||
Recommended reading command:
|
||||
|
||||
```bash
|
||||
python3 tools/read_pdf_pages.py references/path/to/paper.pdf --pages 1-3
|
||||
```
|
||||
|
||||
If the repository does not include that script, use any available PDF-reading tool or existing command. The rule is to read only the first 3 pages and avoid blind full-document reading.
|
||||
|
||||
### Step 1: Online search
|
||||
|
||||
Run retrieval for each enabled data source. Commands should be structured as closely as possible to the following forms. `QUERY` should be replaced with a search string built around the research topic, including the task noun, core method, aliases, synonyms, and common abbreviations.
|
||||
|
||||
- `python3 .claude/tools/arxiv_fetch.py search "QUERY" --max 10`
|
||||
- `python3 .claude/tools/semantic_scholar_fetch.py search "QUERY" --max 10`
|
||||
- `python3 .claude/tools/exa_search.py search "QUERY" --max 10 --category "research paper" --content highlights`
|
||||
- `python3 .claude/tools/openalex_fetch.py search "QUERY" --max 10 --year "2022-"`
|
||||
- `python3 .claude/tools/deepxiv_fetch.py search "QUERY" --max 10`
|
||||
- WebSearch for Google Scholar / the general web
|
||||
|
||||
Search strategy:
|
||||
|
||||
1. Start with broad queries to identify the main direction
|
||||
2. Use narrower queries to find recent work, SOTA, benchmarks, surveys, and ablations from the last two years
|
||||
3. Add controversy points, failure cases, negative results, and competing methods
|
||||
4. Record raw results from each source; do not drop borderline candidates too early
|
||||
|
||||
### Step 2: Cross-source deduplication
|
||||
|
||||
Deduplicate in this order:
|
||||
|
||||
1. `arXiv ID`
|
||||
2. `DOI`
|
||||
3. normalized title
|
||||
|
||||
Rules:
|
||||
|
||||
- Normalize titles by lowercasing, removing punctuation, removing extra spaces, and removing version suffixes
|
||||
- If the same paper has different metadata across sources, keep the record with the most complete information
|
||||
- If `S2` (Semantic Scholar) provides venue, year, author, or citation metadata for an arXiv paper, prefer those fields from `S2`
|
||||
- Distinguish between preprints and formally published versions; if both correspond to the same research, record the relationship but ultimately prefer the more authoritative published version
|
||||
|
||||
### Step 3: Analyze each paper
|
||||
|
||||
For each retained paper, extract:
|
||||
|
||||
- `problem/gap`: what problem is being solved and what existing methods are missing
|
||||
- `method`: core idea, model, training / inference flow, and key tricks
|
||||
- `key results`: main experimental findings, metrics, baselines, and datasets
|
||||
- `relevance to our work`: the direct connection to the current research topic and what can be borrowed
|
||||
- `source`: which source or sources the paper came from and whether metadata conflicts exist
|
||||
|
||||
Requirements:
|
||||
|
||||
- Every paper must include author, year, and venue
|
||||
- Explicitly mark `preprint`, `conference paper`, `journal paper`, `workshop`, and similar statuses
|
||||
- If author / year / venue is uncertain, state the source of uncertainty and do not fabricate it
|
||||
|
||||
### Step 4: Synthesis
|
||||
|
||||
Cluster papers by method route or theme instead of sorting only by time.
|
||||
|
||||
The synthesis must answer:
|
||||
|
||||
- Which method routes have become the consensus
|
||||
- Where the important disagreements are
|
||||
- Which conclusions hold only for specific datasets or settings
|
||||
- What gaps remain unsolved
|
||||
- Which results matter most for our research, and why
|
||||
|
||||
Prioritize:
|
||||
|
||||
- Work from the last 2 years
|
||||
- Representative methods and accepted baselines for the direction
|
||||
- Ablations, diagnostics, and failure analyses that explain performance differences
|
||||
|
||||
If the topic is foundational or classical, trace back to the original work, but keep the newest work as the main line.
|
||||
|
||||
### Step 5: Output
|
||||
|
||||
The final output must include both:
|
||||
|
||||
1. A structured literature table
|
||||
2. A 3-5 paragraph narrative summary
|
||||
|
||||
Suggested table columns:
|
||||
|
||||
- Paper
|
||||
- Authors / Year / Venue
|
||||
- Source
|
||||
- Problem / Gap
|
||||
- Method
|
||||
- Key Results
|
||||
- Relevance to Our Work
|
||||
|
||||
The narrative should:
|
||||
|
||||
- Summarize the sub-branches of the topic first, then the consensus and disagreements
|
||||
- Clearly call out the 3-5 papers worth following next
|
||||
- Clearly identify 1-3 gaps that can become future research entry points
|
||||
|
||||
### Step 6: Wiki Integration
|
||||
|
||||
First check whether `research-wiki/` exists; if it does not, skip all writes without error.
|
||||
|
||||
If it does exist, then:
|
||||
|
||||
1. Ingest the top 8-12 papers into the wiki
|
||||
|
||||
```bash
|
||||
.claude/tools/research_wiki.py ingest_paper research-wiki/ --arxiv-id <id> [--title "..." --authors "..." --year ...]
|
||||
```
|
||||
|
||||
2. Create entities for identified gaps
|
||||
|
||||
```bash
|
||||
.claude/tools/research_wiki.py add_entity research-wiki/ --type gap --id <slug> --title "..."
|
||||
```
|
||||
|
||||
3. Add paper relationship edges
|
||||
|
||||
```bash
|
||||
.claude/tools/research_wiki.py add_edge research-wiki/ --from "paper:X" --to "paper:Y" --type extends --evidence "..."
|
||||
```
|
||||
|
||||
4. Rebuild the query pack and index
|
||||
|
||||
```bash
|
||||
.claude/tools/research_wiki.py rebuild_query_pack research-wiki/ && .claude/tools/research_wiki.py rebuild_index research-wiki/
|
||||
```
|
||||
|
||||
Integration rules:
|
||||
|
||||
- Only write papers with high confidence and highest topic relevance into the wiki
|
||||
- Gap node names should be short and stable so they can be reused later
|
||||
- Relationship edges must include evidence; do not create vague connections
|
||||
|
||||
## Key Rules
|
||||
|
||||
- Always cite papers: author, year, and venue are required
|
||||
- Distinguish peer-reviewed papers from preprints
|
||||
- Missing tools, missing APIs, or missing network access must degrade gracefully; never stop because one source fails
|
||||
- Focus on the last 2 years by default; only trace back further when the task is about foundational work
|
||||
- Do not just list papers; summarize the thread, disagreements, and gaps
|
||||
- If the query is too narrow, first expand with synonyms, abbreviations, and higher-level terms, then narrow again to a precise sub-direction
|
||||
- If there are too many results, keep representative, highly cited, newer, stronger-experiment, and closest-to-topic papers first
|
||||
- If there are too few results, broaden the query and rescan relevant PDFs in the local library
|
||||
|
||||
## Execution Standard
|
||||
|
||||
- Local first, then online
|
||||
- Deduplicate first, then analyze
|
||||
- Evidence first, then conclusions
|
||||
- Cluster first, then narrate
|
||||
- Integrate the wiki first, then finish
|
||||
|
||||
## Completion Criteria
|
||||
|
||||
Only finish the task when all of the following are complete:
|
||||
|
||||
1. Local PDF scanning is complete
|
||||
2. At least the default-enabled data sources were covered, or fallback reasons were recorded
|
||||
3. Deduplication is complete
|
||||
4. Every core paper has been analyzed
|
||||
5. A table and narrative summary were produced
|
||||
6. If `research-wiki/` exists, the corresponding writes and rebuilds were completed
|
||||
@@ -0,0 +1,89 @@
|
||||
---
|
||||
name: research-wiki
|
||||
description: "Manage the project research knowledge base. Initialization, querying, statistics, and health checks. Trigger phrases: knowledge base, research wiki, wiki query, check the knowledge base."
|
||||
argument-hint: [subcommand: init|query|stats|lint|ingest]
|
||||
---
|
||||
|
||||
# Research Wiki Management
|
||||
|
||||
Manage the project research knowledge base: $ARGUMENTS
|
||||
|
||||
## Overview
|
||||
|
||||
Research Wiki is the project's single source of truth for knowledge. All research and development knowledge produced by skills is stored here.
|
||||
|
||||
### Entity Types (12)
|
||||
|
||||
| Entity | Directory | node_id Format | Primary Source Skill |
|
||||
|---|---|---|---|
|
||||
| paper | `papers/` | `paper:<slug>` | `research-lit` |
|
||||
| idea | `ideas/` | `idea:<id>` | `idea-creator` |
|
||||
| experiment | `experiments/` | `exp:<id>` | `run-experiment` (future) |
|
||||
| claim | `claims/` | `claim:<id>` | `novelty-check` |
|
||||
| gap | `gaps/` | `gap:<id>` | `research-lit` / `idea-creator` |
|
||||
| design | `designs/` | `design:<id>` | `brainstorming` |
|
||||
| finding | `findings/` | `finding:<id>` | `systematic-debugging` / `verification-before-completion` |
|
||||
| adr | `adrs/` | `adr:<id>` | 架构决策记录 |
|
||||
| plan | `plans/` | `plan:<id>` | `writing-plans` |
|
||||
| review | `reviews/` | `review:<id>` | `requesting-code-review` / `receiving-code-review` |
|
||||
| schema | `schemas/` | `schema:<id>` | `structured-logging` |
|
||||
| metric | `metrics/` | `metric:<id>` | `structured-logging` / `harness-eval` |
|
||||
|
||||
### Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `query_pack.md` | Compressed summary |
|
||||
| `index.md` | Category index |
|
||||
| `log.md` | Audit log |
|
||||
| `graph/edges.json` | Relationship graph |
|
||||
|
||||
## Subcommands
|
||||
|
||||
### `/research-wiki init`
|
||||
|
||||
Initialize the wiki:
|
||||
|
||||
```bash
|
||||
.claude/tools/research_wiki.py init research-wiki/
|
||||
```
|
||||
|
||||
### `/research-wiki query "<topic>"`
|
||||
|
||||
Rebuild `query_pack.md`:
|
||||
|
||||
```bash
|
||||
.claude/tools/research_wiki.py rebuild_query_pack research-wiki/
|
||||
```
|
||||
|
||||
Then show the content to the user.
|
||||
|
||||
### `/research-wiki stats`
|
||||
|
||||
Show statistics:
|
||||
|
||||
```bash
|
||||
.claude/tools/research_wiki.py stats research-wiki/
|
||||
```
|
||||
|
||||
### `/research-wiki lint`
|
||||
|
||||
Run health checks:
|
||||
|
||||
```bash
|
||||
.claude/tools/research_wiki.py lint research-wiki/
|
||||
```
|
||||
|
||||
### `/research-wiki ingest "<title>" — arxiv: <id>`
|
||||
|
||||
Import a paper:
|
||||
|
||||
```bash
|
||||
.claude/tools/research_wiki.py ingest_paper research-wiki/ --arxiv-id <id>
|
||||
```
|
||||
|
||||
## Key Rules
|
||||
|
||||
- Other skills call `.claude/tools/research_wiki.py` directly; they do not go through this skill.
|
||||
- If the wiki does not exist (`research-wiki/` directory is missing), all write operations are skipped silently.
|
||||
- Failed ideas are always preserved in `query_pack`.
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
name: structured-logging
|
||||
description: "设计结构化日志/遥测方案。当功能会产生运行时数据时,在 brainstorming 产出 design 之后、writing-plans 之前调用:确定记录什么、记到哪、如何评估,并注册到 Wiki。纯内部重构、不产生运行时数据的改动不需要。"
|
||||
argument-hint: "[功能描述]"
|
||||
---
|
||||
|
||||
# Structured Logging
|
||||
|
||||
## Overview
|
||||
|
||||
为即将开发的功能设计结构化日志/遥测方案:记录什么数据、落到哪张表、如何评估。
|
||||
|
||||
**边界**: 会产生运行时数据的功能,编码前必须有日志方案——埋点是"当前需要"(CLAUDE.md P1),事后补埋点意味着丢失基线数据。不产生运行时数据的改动直接跳过本 skill。
|
||||
|
||||
本项目背景:PolyGateway 自带遥测子系统(`telemetry/`,SQLite 后端,每次调用必录,见 CLAUDE.md 库铁律"遥测必录")。本 skill 设计的是**具体功能的埋点方案**,必须与遥测子系统对齐,不另起炉灶。
|
||||
|
||||
## 设计要回答的问题
|
||||
|
||||
1. **这个功能产生什么运行时数据?** 网关领域的典型维度:
|
||||
- 每次调用的时延 / TTFT / token 用量与成本
|
||||
- 错误分类计数(Transient/SourceDead/RequestRejected/ResultInvalid)与重试次数
|
||||
- 熔断状态迁移、限流等待/拒绝、缓存命中率
|
||||
- 阶段事件(开始/结束/错误/降级)
|
||||
2. **新建表还是复用现有表?** 现有表能覆盖就复用;需要新维度才新建。先对照 `research-wiki/schemas/` 已登记的 schema 与实际库中的表,标记不一致。
|
||||
3. **每张表的 schema**: 列名、类型、说明、主键、哪些列服务于诊断查询。
|
||||
4. **埋点位置**: 具体模块与函数;必须走库的 `TelemetryRecorder` 端口/统一 helper,禁止散落的 ad-hoc 写库(三项目遥测调用被复制 4 次的教训)。
|
||||
5. **评估基线**: 可量化指标 + 阈值 + 判定方式;基线来源(对比哪次历史运行;首次则标"待首次运行后建立")。
|
||||
|
||||
## 注册到 Wiki(留痕,不可省略)
|
||||
|
||||
```bash
|
||||
.claude/tools/research_wiki.py add_entity research-wiki/ --type schema --id <table-name> --title "表结构: <table-name>"
|
||||
.claude/tools/research_wiki.py add_entity research-wiki/ --type metric --id <metric-name> --title "<指标描述>"
|
||||
.claude/tools/research_wiki.py add_edge research-wiki/ --from "metric:<id>" --to "schema:<id>" --type measures --evidence "..."
|
||||
.claude/tools/research_wiki.py add_edge research-wiki/ --from "schema:<id>" --to "design:<id>" --type implements --evidence "..."
|
||||
.claude/tools/research_wiki.py rebuild_index research-wiki/
|
||||
```
|
||||
|
||||
在生成的 md 中填入完整列定义、埋点位置、基线值与阈值。
|
||||
|
||||
## 产出(交给 writing-plans)
|
||||
|
||||
埋点清单:哪些文件、哪些函数、在什么位置记什么。这些埋点必须成为 plan 中的显式步骤,不得遗漏。
|
||||
|
||||
| 产出 | 位置 |
|
||||
|------|------|
|
||||
| schema 实体 | `research-wiki/schemas/<name>.md` |
|
||||
| metric 实体 | `research-wiki/metrics/<name>.md` |
|
||||
| edge 关系 | `research-wiki/graph/edges.json` |
|
||||
| 埋点清单 | 传递给 writing-plans |
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
name: subagent-driven-development
|
||||
description: "Optional executor for large approved plans (many mostly-independent tasks): delegate each task to a fresh Claude subagent, run an automated quality gate per task, and one independent Codex review before merge. For small or tightly-coupled plans, implement directly instead."
|
||||
---
|
||||
|
||||
# Subagent-Driven Development
|
||||
|
||||
## 何时使用
|
||||
|
||||
- **适用**: 已有批准的 plan、任务多(≥3)且大体相互独立、值得为每个任务开独立上下文。
|
||||
- **不适用**: 小计划、任务强耦合、探索性工作——直接实现更省更好。
|
||||
|
||||
结构:Claude 主会话做控制器;每个任务派一个**全新** Claude subagent 实现;任务完成跑**自动质量门**;全部任务完成后、合并前做**一次** Codex 独立审查(跨模型,消除自评盲区)。
|
||||
|
||||
## 流程
|
||||
|
||||
### 1. 读计划,建任务清单
|
||||
|
||||
读一遍 plan,把每个任务的**全文**与上下文提取出来,进 TodoWrite。之后不再让 subagent 去读 plan 文件——派发时把任务全文直接贴进 prompt。
|
||||
|
||||
若项目已建 graphify 知识图谱(`graphify-out/` 存在),先 `/graphify . --update` 刷新;未建图则跳过,不阻断。
|
||||
|
||||
### 2. 派发实现 subagent(每任务一个,全新上下文)
|
||||
|
||||
用 `Agent` 工具(`subagent_type=general-purpose`),prompt 用 `./claude-implementer-prompt.md` 模板填充(任务全文、上下文、绝对路径)。**记下 agentId**。
|
||||
|
||||
- 同一任务的返修一律 `SendMessage(to: agentId)` 发回原 subagent(保留其上下文);只有下一个任务才开新 subagent。
|
||||
- subagent 以结构化 `STATUS` 块收尾:`DONE` → 进质量门;`NEEDS_CONTEXT` → 补上下文继续;`BLOCKED` → 评估(补上下文/拆任务/计划有误则上报人类);缺失 STATUS 块按 `BLOCKED` 处理。
|
||||
|
||||
### 3. 自动质量门(每任务,机器判定)
|
||||
|
||||
```bash
|
||||
conda run -n PolyGateway ruff format --check <changed_files>
|
||||
conda run -n PolyGateway ruff check <changed_files>
|
||||
conda run -n PolyGateway radon cc <changed_files> -n C -s # C 级及以下复杂度即失败
|
||||
conda run -n PolyGateway pytest tests/ -x -q
|
||||
# 结构检查: 无裸 except / except Exception: pass;核心文件不超 200 行
|
||||
```
|
||||
|
||||
失败 → 汇总工具输出,`SendMessage` 发回原 subagent 修,最多 2 轮,仍失败则上报人类。仅格式问题可由控制器直接 `ruff format` 修掉。测试超 30s 的在 tmux 里跑(`tmux new-session -d -s sdd-task<N> "<cmd>"`),便于人类 attach。
|
||||
|
||||
**不信任 subagent 自述**: 标记任务完成前,控制器亲自看 `git diff` 确认变更真实存在。
|
||||
|
||||
### 4. 合并前一次 Codex 独立审查(整分支)
|
||||
|
||||
全部任务完成、质量门全绿后,用 `/codex:rescue --fresh --wait` 按 `./merge-reviewer-prompt.md` 做**一次**只读审查,范围是整条分支(所有 SHA + plan 全文),一次覆盖:spec 符合性(缺失/多余/误解)、跨任务集成问题、功能质量、明显的过度设计。
|
||||
|
||||
- Critical/Important 问题 → 发回对应 subagent(或自己)修复,复审至清零。
|
||||
- 前置:Codex 插件可用(`/codex:setup` 报 ready);审查模型用 `.codex/config.toml` 的默认强配置。Codex 不可用时,降级为派一个全新上下文的 Claude verifier subagent 按同一 prompt 审(见 `verification-before-completion`)。
|
||||
|
||||
### 5. 收尾
|
||||
|
||||
`/graphify . --update`(若在用),然后交给 `finishing-a-development-branch`。
|
||||
|
||||
## 纪律(约束点)
|
||||
|
||||
- 连续执行,任务间不停下来找人类确认;只有 BLOCKED 无解、真歧义、全部完成三种停法。
|
||||
- 每任务提交留痕;严禁把多个任务squash成一坨再审。
|
||||
- 审查不接受"差不多就行":Critical/Important 清零才合并。
|
||||
|
||||
## Companion files
|
||||
|
||||
- `./claude-implementer-prompt.md` — 实现 subagent 的 prompt 模板。
|
||||
- `./merge-reviewer-prompt.md` — 合并前一次性 Codex 审查的 prompt 模板。
|
||||
|
||||
## Wiki 留痕(`research-wiki/` 存在时)
|
||||
|
||||
产生可复用实现知识或有价值审查意见的任务,记 plan/review 实体并连边(工具 `.claude/tools/research_wiki.py`,类型 `implements`/`informs`),纯机械改动跳过。
|
||||
@@ -0,0 +1,59 @@
|
||||
# Claude Subagent Implementer Prompt Template
|
||||
|
||||
派发实现 subagent(`Agent` 工具,`subagent_type=general-purpose`)时,以下面模板为 prompt 主体,填好方括号内容。新任务开新 subagent;同一任务的返修用 `SendMessage(to: agentId)`。
|
||||
|
||||
---
|
||||
|
||||
```
|
||||
You are implementing Task N: [task name] in the PolyGateway repository.
|
||||
|
||||
A controller spawned you, will read your output, verify your diff itself, and run an independent
|
||||
cross-model review before merge. Reviewers read actual code — optimize for being right, not for
|
||||
sounding right.
|
||||
|
||||
## Task Description
|
||||
|
||||
[任务全文,从 plan 逐字粘贴。不要给文件路径让 subagent 自己去找。]
|
||||
|
||||
## Context
|
||||
|
||||
[背景:该任务在整体设计中的位置、依赖、既有文件与约定。给足上下文,省一次 NEEDS_CONTEXT 往返。]
|
||||
|
||||
## Working Directory
|
||||
|
||||
[worktree 绝对路径]
|
||||
|
||||
## Project Conventions (non-negotiable, gates enforce these)
|
||||
|
||||
- 先读根目录 `CLAUDE.md`(库铁律、代码规范、目录规则)与 `research-wiki/ARCHITECTURE.md` 相关章节。
|
||||
- **目录**: 库代码只进 `src/polygateway/`(内核 ports/types/errors + middleware/transports/backends/telemetry/structured);测试进 `tests/{unit,integration,e2e}/`;`scripts/` 只放 `.sh`;根目录不得出现 `.py`;禁止 `helpers/ common/ shared/ misc/ lib/` 目录名。
|
||||
- **`reference/` 只读**,绝不修改(有 hook 硬拦截)。它是迁移蓝本:涉及迁移的任务必须逐段比对参考实现,不得简化核心逻辑。
|
||||
- **conda 环境**: 一切 Python 命令经 `conda run -n PolyGateway <cmd>`,不裸调 pytest/ruff。
|
||||
- **中文 Docstring**(模块/类/公共函数)、loguru 而非 print、公共函数完整类型注解、严禁裸 except 与 except Exception: pass、敏感信息只走 .env。
|
||||
- **测试证据**: 每个行为变更须有先失败后通过的测试证据(结果门);用真实样本或其二次构造。
|
||||
- 超过 30 秒的命令在 tmux 中运行(`tmux new-session -d -s sdd-taskN-<name> "<cmd>"`)。
|
||||
- 不做任务外的重构/抽象;文件长得不健康就在 NOTES 里标记,让控制器决定。
|
||||
|
||||
## Escalation
|
||||
|
||||
你无法与控制器交互式提问。真被卡住时不要猜:以 `STATUS: NEEDS_CONTEXT`(缺信息)或
|
||||
`STATUS: BLOCKED`(无法完成)结束,并具体说明卡点、已尝试什么、需要什么帮助。
|
||||
非阻塞的判断题(如 helper 放哪),选最站得住脚的做法实现,并在 NOTES 里写明供审查者挑战。
|
||||
|
||||
## Your Job
|
||||
|
||||
1. 精确实现任务所述——不多不少。
|
||||
2. 写测试并留下红→绿证据;跑 `conda run -n PolyGateway pytest` 与 ruff/radon,全绿。
|
||||
3. 按语义分段提交,常规提交信息格式(见 commit skill 规则,无 AI 签名)。
|
||||
4. 交付前自查:spec 每条都实现了吗?有没有 spec 外的东西?报告里的每句话对得上 diff 吗?
|
||||
|
||||
## Report Format (required — last thing in your output)
|
||||
|
||||
---
|
||||
STATUS: <DONE | DONE_WITH_CONCERNS | NEEDS_CONTEXT | BLOCKED>
|
||||
COMMIT_SHAS: <short SHAs, or "none">
|
||||
FILES_CHANGED: <paths, or "none">
|
||||
TESTS: <"all passing: X/Y" | "failing: <details>" | "not applicable">
|
||||
NOTES: <STATUS != DONE 时必填;顾虑、判断题、具体卡点写这里>
|
||||
---
|
||||
```
|
||||
@@ -0,0 +1,61 @@
|
||||
# Merge Reviewer Prompt Template(合并前一次性独立审查)
|
||||
|
||||
全部任务完成、自动质量门全绿后,用 `/codex:rescue --fresh --wait` 以本模板做**一次**只读审查,范围是整条分支。Codex 不可用时,同一模板派给全新上下文的 Claude verifier subagent(只读)。
|
||||
|
||||
---
|
||||
|
||||
```
|
||||
Codex review (read-only) — pass this as the /codex:rescue --fresh --wait prompt body:
|
||||
|
||||
description: "Pre-merge review for <branch>"
|
||||
prompt: |
|
||||
You are the independent pre-merge reviewer for work implemented by Claude subagents in the
|
||||
PolyGateway repository. You are read-only: read diffs and report, do not edit code.
|
||||
|
||||
## Plan / Requirements
|
||||
|
||||
[plan 全文或其需求部分,逐字粘贴]
|
||||
|
||||
## Scope
|
||||
|
||||
Working directory: [绝对路径]
|
||||
Commits to review: [分支上全部 SHA,或 base..head 区间]
|
||||
|
||||
## Do Not Trust Reports
|
||||
|
||||
实现方的自述可能不完整或过于乐观。一切以 `git show <sha>`、`git diff <base>..<head>` 与直接
|
||||
读文件为准,逐条独立核验。
|
||||
|
||||
## Review Dimensions (one pass, all of them)
|
||||
|
||||
1. **Spec 符合性**: plan 每条需求能否指到具体实现行?有无缺失、多余(未要求的功能/flag/依赖)、
|
||||
误解(接口/位置/签名与 plan 不符)?测试是在测 spec 要求的行为,还是在测"碰巧写出的代码"?
|
||||
有没有被删除或弱化的既有测试?
|
||||
2. **跨任务集成**: 任务间接口漂移、跨文件重复逻辑、早期任务留下的死代码、日志/配置/错误路径
|
||||
的跨任务一致性——这些是单任务审查抓不到的。
|
||||
3. **功能质量**: 职责划分、错误处理(吞异常/静默回退?)、并发与取消安全(CancelledError 穿透?)、
|
||||
降级方向是否符合 CLAUDE.md 库铁律、测试是否覆盖边界而非只有 happy path。
|
||||
4. **迁移保真**(若涉及 reference/ 蓝本迁移): 对照参考实现,核心逻辑(分支条件、Lua 语义、
|
||||
状态机、退避公式)是否被简化或改变语义而未在设计中声明。
|
||||
5. **明显过度设计**: 无人使用的参数/扩展点、可以是函数的类、不必要的间接层(YAGNI)。
|
||||
|
||||
## Calibration
|
||||
|
||||
只报会造成真实问题的项。措辞偏好与格式吹毛求疵不报(自动工具已管)。
|
||||
- Critical — 真实 bug、数据损坏、安全问题、破坏下游迁移承诺。必修。
|
||||
- Important — 维护痛点、脆弱代码、spec 缺口。应修。
|
||||
- Minor — 顺手可改,不阻塞。
|
||||
|
||||
## Report Format
|
||||
|
||||
Verdict: <APPROVED | CHANGES_REQUESTED>
|
||||
Spec gaps: <bullets 或 "none">
|
||||
Integration: <bullets 或 "none">
|
||||
Issues:
|
||||
Critical: <file:line + 问题,或 "none">
|
||||
Important: <file:line + 问题,或 "none">
|
||||
Minor: <file:line + 问题,或 "none">
|
||||
Assessment: <一段话:整体质量、能否合并、跨切面问题>
|
||||
|
||||
APPROVED = Critical 与 Important 均为 none。否则控制器将把发现发回修复并复审。
|
||||
```
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
name: systematic-debugging
|
||||
description: Use when encountering any bug, test failure, or unexpected behavior, before proposing fixes - find the root cause first; symptom patches are failure
|
||||
---
|
||||
|
||||
# Systematic Debugging
|
||||
|
||||
## 边界声明
|
||||
|
||||
```
|
||||
先定位根因,再动手修。没有根因假设与证据,不提出修复。
|
||||
```
|
||||
|
||||
理由:乱试修复浪费时间且制造新 bug;症状修补会掩盖真实问题,在库场景下同时击穿所有下游项目。系统化定位对简单 bug 同样更快——简单 bug 也有根因。
|
||||
|
||||
## 定位要素(按需取用,不是仪式)
|
||||
|
||||
1. **完整读错误信息与堆栈**——它经常直接给出答案。记下行号、文件、错误码。
|
||||
2. **稳定复现**——不能复现就先收集数据,不要猜。
|
||||
3. **查最近变更**——`git diff`、新依赖、配置与环境差异。已建 graphify 图谱时,可用 `conda run -n PolyGateway graphify path/affected/explain` 追调用链与影响面,免于盲读。
|
||||
4. **多组件系统先取证再归因**——在组件边界加日志(进/出数据、配置传播),跑一次拿到"断在哪一层"的证据,再深入该层。深层调用栈用回溯法(见 `root-cause-tracing.md`):坏值从哪来,一路向上追到源头,在源头修。
|
||||
5. **查结构化运行日志**——若项目有遥测/结构化日志(如 SQLite 遥测表、`research-wiki/schemas/` 登记的表),查事件流与指标趋势,让假设有数据支撑而非纯读码猜测。
|
||||
6. **对照可工作的样例**——同库相似可用代码、`reference/` 参考实现。逐项列差异,不要跳过"这不可能有影响"的差异;参考实现要读完整,不要按印象改编。
|
||||
|
||||
## 假设与修复(纪律)
|
||||
|
||||
- 一次一个假设,写清"我认为根因是 X,因为 Y";用**最小改动**验证,一次只动一个变量。
|
||||
- 修复前先有失败的复现测试(接 `test-driven-development` 结果门);修复只针对根因,禁止"顺手"重构与打包多个改动。
|
||||
- 修完验证:该测试通过、其余测试不回归、原症状确实消失。
|
||||
|
||||
**3 次修复失败 = 停下质疑架构。** 若每次修复都在别处暴露新问题、或修复需要"大动干戈"才能实施,这不是假设错了,是架构错了——停止继续修,与人类讨论架构后再动。
|
||||
|
||||
若彻查后确属环境/时序/外部问题:记录调查过程,实现恰当的处理(重试/超时/报错),加监控埋点。但 95% 的"查无根因"是调查没做完。
|
||||
|
||||
## 红线(出现即停,回到定位)
|
||||
|
||||
- "先快速修一下,回头再查" / "改改 X 试试看" / "大概是 X,先修了再说"
|
||||
- 一次提交多个猜测性修改;注释掉测试或跳过校验让报错消失
|
||||
- 已经失败 2 次还想"再试一个修复"
|
||||
|
||||
## 附属技术
|
||||
|
||||
- `root-cause-tracing.md` — 沿调用栈回溯到源头
|
||||
- `defense-in-depth.md` — 找到根因后的多层校验
|
||||
- `condition-based-waiting.md` — 用条件轮询替代拍脑袋超时
|
||||
|
||||
## Wiki 留痕(`research-wiki/` 存在时)
|
||||
|
||||
得出值得留存的结论时(尤其影响后续任务的),记 finding:
|
||||
|
||||
```bash
|
||||
.claude/tools/research_wiki.py add_entity research-wiki/ --type finding --id <slug> --title "<问题>"
|
||||
.claude/tools/research_wiki.py rebuild_index research-wiki/
|
||||
```
|
||||
|
||||
页内记录:症状、根因、验证方法、修复、影响面;若暴露 design/plan 缺陷,加 `reveals` 边。
|
||||
@@ -0,0 +1,158 @@
|
||||
// Complete implementation of condition-based waiting utilities
|
||||
// From: Lace test infrastructure improvements (2025-10-03)
|
||||
// Context: Fixed 15 flaky tests by replacing arbitrary timeouts
|
||||
|
||||
import type { ThreadManager } from '~/threads/thread-manager';
|
||||
import type { LaceEvent, LaceEventType } from '~/threads/types';
|
||||
|
||||
/**
|
||||
* Wait for a specific event type to appear in thread
|
||||
*
|
||||
* @param threadManager - The thread manager to query
|
||||
* @param threadId - Thread to check for events
|
||||
* @param eventType - Type of event to wait for
|
||||
* @param timeoutMs - Maximum time to wait (default 5000ms)
|
||||
* @returns Promise resolving to the first matching event
|
||||
*
|
||||
* Example:
|
||||
* await waitForEvent(threadManager, agentThreadId, 'TOOL_RESULT');
|
||||
*/
|
||||
export function waitForEvent(
|
||||
threadManager: ThreadManager,
|
||||
threadId: string,
|
||||
eventType: LaceEventType,
|
||||
timeoutMs = 5000
|
||||
): Promise<LaceEvent> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
const check = () => {
|
||||
const events = threadManager.getEvents(threadId);
|
||||
const event = events.find((e) => e.type === eventType);
|
||||
|
||||
if (event) {
|
||||
resolve(event);
|
||||
} else if (Date.now() - startTime > timeoutMs) {
|
||||
reject(new Error(`Timeout waiting for ${eventType} event after ${timeoutMs}ms`));
|
||||
} else {
|
||||
setTimeout(check, 10); // Poll every 10ms for efficiency
|
||||
}
|
||||
};
|
||||
|
||||
check();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a specific number of events of a given type
|
||||
*
|
||||
* @param threadManager - The thread manager to query
|
||||
* @param threadId - Thread to check for events
|
||||
* @param eventType - Type of event to wait for
|
||||
* @param count - Number of events to wait for
|
||||
* @param timeoutMs - Maximum time to wait (default 5000ms)
|
||||
* @returns Promise resolving to all matching events once count is reached
|
||||
*
|
||||
* Example:
|
||||
* // Wait for 2 AGENT_MESSAGE events (initial response + continuation)
|
||||
* await waitForEventCount(threadManager, agentThreadId, 'AGENT_MESSAGE', 2);
|
||||
*/
|
||||
export function waitForEventCount(
|
||||
threadManager: ThreadManager,
|
||||
threadId: string,
|
||||
eventType: LaceEventType,
|
||||
count: number,
|
||||
timeoutMs = 5000
|
||||
): Promise<LaceEvent[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
const check = () => {
|
||||
const events = threadManager.getEvents(threadId);
|
||||
const matchingEvents = events.filter((e) => e.type === eventType);
|
||||
|
||||
if (matchingEvents.length >= count) {
|
||||
resolve(matchingEvents);
|
||||
} else if (Date.now() - startTime > timeoutMs) {
|
||||
reject(
|
||||
new Error(
|
||||
`Timeout waiting for ${count} ${eventType} events after ${timeoutMs}ms (got ${matchingEvents.length})`
|
||||
)
|
||||
);
|
||||
} else {
|
||||
setTimeout(check, 10);
|
||||
}
|
||||
};
|
||||
|
||||
check();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for an event matching a custom predicate
|
||||
* Useful when you need to check event data, not just type
|
||||
*
|
||||
* @param threadManager - The thread manager to query
|
||||
* @param threadId - Thread to check for events
|
||||
* @param predicate - Function that returns true when event matches
|
||||
* @param description - Human-readable description for error messages
|
||||
* @param timeoutMs - Maximum time to wait (default 5000ms)
|
||||
* @returns Promise resolving to the first matching event
|
||||
*
|
||||
* Example:
|
||||
* // Wait for TOOL_RESULT with specific ID
|
||||
* await waitForEventMatch(
|
||||
* threadManager,
|
||||
* agentThreadId,
|
||||
* (e) => e.type === 'TOOL_RESULT' && e.data.id === 'call_123',
|
||||
* 'TOOL_RESULT with id=call_123'
|
||||
* );
|
||||
*/
|
||||
export function waitForEventMatch(
|
||||
threadManager: ThreadManager,
|
||||
threadId: string,
|
||||
predicate: (event: LaceEvent) => boolean,
|
||||
description: string,
|
||||
timeoutMs = 5000
|
||||
): Promise<LaceEvent> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
const check = () => {
|
||||
const events = threadManager.getEvents(threadId);
|
||||
const event = events.find(predicate);
|
||||
|
||||
if (event) {
|
||||
resolve(event);
|
||||
} else if (Date.now() - startTime > timeoutMs) {
|
||||
reject(new Error(`Timeout waiting for ${description} after ${timeoutMs}ms`));
|
||||
} else {
|
||||
setTimeout(check, 10);
|
||||
}
|
||||
};
|
||||
|
||||
check();
|
||||
});
|
||||
}
|
||||
|
||||
// Usage example from actual debugging session:
|
||||
//
|
||||
// BEFORE (flaky):
|
||||
// ---------------
|
||||
// const messagePromise = agent.sendMessage('Execute tools');
|
||||
// await new Promise(r => setTimeout(r, 300)); // Hope tools start in 300ms
|
||||
// agent.abort();
|
||||
// await messagePromise;
|
||||
// await new Promise(r => setTimeout(r, 50)); // Hope results arrive in 50ms
|
||||
// expect(toolResults.length).toBe(2); // Fails randomly
|
||||
//
|
||||
// AFTER (reliable):
|
||||
// ----------------
|
||||
// const messagePromise = agent.sendMessage('Execute tools');
|
||||
// await waitForEventCount(threadManager, threadId, 'TOOL_CALL', 2); // Wait for tools to start
|
||||
// agent.abort();
|
||||
// await messagePromise;
|
||||
// await waitForEventCount(threadManager, threadId, 'TOOL_RESULT', 2); // Wait for results
|
||||
// expect(toolResults.length).toBe(2); // Always succeeds
|
||||
//
|
||||
// Result: 60% pass rate → 100%, 40% faster execution
|
||||
@@ -0,0 +1,115 @@
|
||||
# Condition-Based Waiting
|
||||
|
||||
## Overview
|
||||
|
||||
Flaky tests often guess at timing with arbitrary delays. This creates race conditions where tests pass on fast machines but fail under load or in CI.
|
||||
|
||||
**Core principle:** Wait for the actual condition you care about, not a guess about how long it takes.
|
||||
|
||||
## When to Use
|
||||
|
||||
```dot
|
||||
digraph when_to_use {
|
||||
"Test uses setTimeout/sleep?" [shape=diamond];
|
||||
"Testing timing behavior?" [shape=diamond];
|
||||
"Document WHY timeout needed" [shape=box];
|
||||
"Use condition-based waiting" [shape=box];
|
||||
|
||||
"Test uses setTimeout/sleep?" -> "Testing timing behavior?" [label="yes"];
|
||||
"Testing timing behavior?" -> "Document WHY timeout needed" [label="yes"];
|
||||
"Testing timing behavior?" -> "Use condition-based waiting" [label="no"];
|
||||
}
|
||||
```
|
||||
|
||||
**Use when:**
|
||||
- Tests have arbitrary delays (`setTimeout`, `sleep`, `time.sleep()`)
|
||||
- Tests are flaky (pass sometimes, fail under load)
|
||||
- Tests timeout when run in parallel
|
||||
- Waiting for async operations to complete
|
||||
|
||||
**Don't use when:**
|
||||
- Testing actual timing behavior (debounce, throttle intervals)
|
||||
- Always document WHY if using arbitrary timeout
|
||||
|
||||
## Core Pattern
|
||||
|
||||
```typescript
|
||||
// ❌ BEFORE: Guessing at timing
|
||||
await new Promise(r => setTimeout(r, 50));
|
||||
const result = getResult();
|
||||
expect(result).toBeDefined();
|
||||
|
||||
// ✅ AFTER: Waiting for condition
|
||||
await waitFor(() => getResult() !== undefined);
|
||||
const result = getResult();
|
||||
expect(result).toBeDefined();
|
||||
```
|
||||
|
||||
## Quick Patterns
|
||||
|
||||
| Scenario | Pattern |
|
||||
|----------|---------|
|
||||
| Wait for event | `waitFor(() => events.find(e => e.type === 'DONE'))` |
|
||||
| Wait for state | `waitFor(() => machine.state === 'ready')` |
|
||||
| Wait for count | `waitFor(() => items.length >= 5)` |
|
||||
| Wait for file | `waitFor(() => fs.existsSync(path))` |
|
||||
| Complex condition | `waitFor(() => obj.ready && obj.value > 10)` |
|
||||
|
||||
## Implementation
|
||||
|
||||
Generic polling function:
|
||||
```typescript
|
||||
async function waitFor<T>(
|
||||
condition: () => T | undefined | null | false,
|
||||
description: string,
|
||||
timeoutMs = 5000
|
||||
): Promise<T> {
|
||||
const startTime = Date.now();
|
||||
|
||||
while (true) {
|
||||
const result = condition();
|
||||
if (result) return result;
|
||||
|
||||
if (Date.now() - startTime > timeoutMs) {
|
||||
throw new Error(`Timeout waiting for ${description} after ${timeoutMs}ms`);
|
||||
}
|
||||
|
||||
await new Promise(r => setTimeout(r, 10)); // Poll every 10ms
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See `condition-based-waiting-example.ts` in this directory for complete implementation with domain-specific helpers (`waitForEvent`, `waitForEventCount`, `waitForEventMatch`) from actual debugging session.
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
**❌ Polling too fast:** `setTimeout(check, 1)` - wastes CPU
|
||||
**✅ Fix:** Poll every 10ms
|
||||
|
||||
**❌ No timeout:** Loop forever if condition never met
|
||||
**✅ Fix:** Always include timeout with clear error
|
||||
|
||||
**❌ Stale data:** Cache state before loop
|
||||
**✅ Fix:** Call getter inside loop for fresh data
|
||||
|
||||
## When Arbitrary Timeout IS Correct
|
||||
|
||||
```typescript
|
||||
// Tool ticks every 100ms - need 2 ticks to verify partial output
|
||||
await waitForEvent(manager, 'TOOL_STARTED'); // First: wait for condition
|
||||
await new Promise(r => setTimeout(r, 200)); // Then: wait for timed behavior
|
||||
// 200ms = 2 ticks at 100ms intervals - documented and justified
|
||||
```
|
||||
|
||||
**Requirements:**
|
||||
1. First wait for triggering condition
|
||||
2. Based on known timing (not guessing)
|
||||
3. Comment explaining WHY
|
||||
|
||||
## Real-World Impact
|
||||
|
||||
From debugging session (2025-10-03):
|
||||
- Fixed 15 flaky tests across 3 files
|
||||
- Pass rate: 60% → 100%
|
||||
- Execution time: 40% faster
|
||||
- No more race conditions
|
||||
@@ -0,0 +1,122 @@
|
||||
# Defense-in-Depth Validation
|
||||
|
||||
## Overview
|
||||
|
||||
When you fix a bug caused by invalid data, adding validation at one place feels sufficient. But that single check can be bypassed by different code paths, refactoring, or mocks.
|
||||
|
||||
**Core principle:** Validate at EVERY layer data passes through. Make the bug structurally impossible.
|
||||
|
||||
## Why Multiple Layers
|
||||
|
||||
Single validation: "We fixed the bug"
|
||||
Multiple layers: "We made the bug impossible"
|
||||
|
||||
Different layers catch different cases:
|
||||
- Entry validation catches most bugs
|
||||
- Business logic catches edge cases
|
||||
- Environment guards prevent context-specific dangers
|
||||
- Debug logging helps when other layers fail
|
||||
|
||||
## The Four Layers
|
||||
|
||||
### Layer 1: Entry Point Validation
|
||||
**Purpose:** Reject obviously invalid input at API boundary
|
||||
|
||||
```typescript
|
||||
function createProject(name: string, workingDirectory: string) {
|
||||
if (!workingDirectory || workingDirectory.trim() === '') {
|
||||
throw new Error('workingDirectory cannot be empty');
|
||||
}
|
||||
if (!existsSync(workingDirectory)) {
|
||||
throw new Error(`workingDirectory does not exist: ${workingDirectory}`);
|
||||
}
|
||||
if (!statSync(workingDirectory).isDirectory()) {
|
||||
throw new Error(`workingDirectory is not a directory: ${workingDirectory}`);
|
||||
}
|
||||
// ... proceed
|
||||
}
|
||||
```
|
||||
|
||||
### Layer 2: Business Logic Validation
|
||||
**Purpose:** Ensure data makes sense for this operation
|
||||
|
||||
```typescript
|
||||
function initializeWorkspace(projectDir: string, sessionId: string) {
|
||||
if (!projectDir) {
|
||||
throw new Error('projectDir required for workspace initialization');
|
||||
}
|
||||
// ... proceed
|
||||
}
|
||||
```
|
||||
|
||||
### Layer 3: Environment Guards
|
||||
**Purpose:** Prevent dangerous operations in specific contexts
|
||||
|
||||
```typescript
|
||||
async function gitInit(directory: string) {
|
||||
// In tests, refuse git init outside temp directories
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
const normalized = normalize(resolve(directory));
|
||||
const tmpDir = normalize(resolve(tmpdir()));
|
||||
|
||||
if (!normalized.startsWith(tmpDir)) {
|
||||
throw new Error(
|
||||
`Refusing git init outside temp dir during tests: ${directory}`
|
||||
);
|
||||
}
|
||||
}
|
||||
// ... proceed
|
||||
}
|
||||
```
|
||||
|
||||
### Layer 4: Debug Instrumentation
|
||||
**Purpose:** Capture context for forensics
|
||||
|
||||
```typescript
|
||||
async function gitInit(directory: string) {
|
||||
const stack = new Error().stack;
|
||||
logger.debug('About to git init', {
|
||||
directory,
|
||||
cwd: process.cwd(),
|
||||
stack,
|
||||
});
|
||||
// ... proceed
|
||||
}
|
||||
```
|
||||
|
||||
## Applying the Pattern
|
||||
|
||||
When you find a bug:
|
||||
|
||||
1. **Trace the data flow** - Where does bad value originate? Where used?
|
||||
2. **Map all checkpoints** - List every point data passes through
|
||||
3. **Add validation at each layer** - Entry, business, environment, debug
|
||||
4. **Test each layer** - Try to bypass layer 1, verify layer 2 catches it
|
||||
|
||||
## Example from Session
|
||||
|
||||
Bug: Empty `projectDir` caused `git init` in source code
|
||||
|
||||
**Data flow:**
|
||||
1. Test setup → empty string
|
||||
2. `Project.create(name, '')`
|
||||
3. `WorkspaceManager.createWorkspace('')`
|
||||
4. `git init` runs in `process.cwd()`
|
||||
|
||||
**Four layers added:**
|
||||
- Layer 1: `Project.create()` validates not empty/exists/writable
|
||||
- Layer 2: `WorkspaceManager` validates projectDir not empty
|
||||
- Layer 3: `WorktreeManager` refuses git init outside tmpdir in tests
|
||||
- Layer 4: Stack trace logging before git init
|
||||
|
||||
**Result:** All 1847 tests passed, bug impossible to reproduce
|
||||
|
||||
## Key Insight
|
||||
|
||||
All four layers were necessary. During testing, each layer caught bugs the others missed:
|
||||
- Different code paths bypassed entry validation
|
||||
- Mocks bypassed business logic checks
|
||||
- Edge cases on different platforms needed environment guards
|
||||
- Debug logging identified structural misuse
|
||||
|
||||
**Don't stop at one validation point.** Add checks at every layer.
|
||||
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env bash
|
||||
# Bisection script to find which test creates unwanted files/state
|
||||
# Usage: ./find-polluter.sh <file_or_dir_to_check> <test_pattern>
|
||||
# Example: ./find-polluter.sh '.git' 'src/**/*.test.ts'
|
||||
|
||||
set -e
|
||||
|
||||
if [ $# -ne 2 ]; then
|
||||
echo "Usage: $0 <file_to_check> <test_pattern>"
|
||||
echo "Example: $0 '.git' 'src/**/*.test.ts'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
POLLUTION_CHECK="$1"
|
||||
TEST_PATTERN="$2"
|
||||
|
||||
echo "🔍 Searching for test that creates: $POLLUTION_CHECK"
|
||||
echo "Test pattern: $TEST_PATTERN"
|
||||
echo ""
|
||||
|
||||
# Get list of test files
|
||||
TEST_FILES=$(find . -path "$TEST_PATTERN" | sort)
|
||||
TOTAL=$(echo "$TEST_FILES" | wc -l | tr -d ' ')
|
||||
|
||||
echo "Found $TOTAL test files"
|
||||
echo ""
|
||||
|
||||
COUNT=0
|
||||
for TEST_FILE in $TEST_FILES; do
|
||||
COUNT=$((COUNT + 1))
|
||||
|
||||
# Skip if pollution already exists
|
||||
if [ -e "$POLLUTION_CHECK" ]; then
|
||||
echo "⚠️ Pollution already exists before test $COUNT/$TOTAL"
|
||||
echo " Skipping: $TEST_FILE"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "[$COUNT/$TOTAL] Testing: $TEST_FILE"
|
||||
|
||||
# Run the test
|
||||
npm test "$TEST_FILE" > /dev/null 2>&1 || true
|
||||
|
||||
# Check if pollution appeared
|
||||
if [ -e "$POLLUTION_CHECK" ]; then
|
||||
echo ""
|
||||
echo "🎯 FOUND POLLUTER!"
|
||||
echo " Test: $TEST_FILE"
|
||||
echo " Created: $POLLUTION_CHECK"
|
||||
echo ""
|
||||
echo "Pollution details:"
|
||||
ls -la "$POLLUTION_CHECK"
|
||||
echo ""
|
||||
echo "To investigate:"
|
||||
echo " npm test $TEST_FILE # Run just this test"
|
||||
echo " cat $TEST_FILE # Review test code"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "✅ No polluter found - all tests clean!"
|
||||
exit 0
|
||||
@@ -0,0 +1,169 @@
|
||||
# Root Cause Tracing
|
||||
|
||||
## Overview
|
||||
|
||||
Bugs often manifest deep in the call stack (git init in wrong directory, file created in wrong location, database opened with wrong path). Your instinct is to fix where the error appears, but that's treating a symptom.
|
||||
|
||||
**Core principle:** Trace backward through the call chain until you find the original trigger, then fix at the source.
|
||||
|
||||
## When to Use
|
||||
|
||||
```dot
|
||||
digraph when_to_use {
|
||||
"Bug appears deep in stack?" [shape=diamond];
|
||||
"Can trace backwards?" [shape=diamond];
|
||||
"Fix at symptom point" [shape=box];
|
||||
"Trace to original trigger" [shape=box];
|
||||
"BETTER: Also add defense-in-depth" [shape=box];
|
||||
|
||||
"Bug appears deep in stack?" -> "Can trace backwards?" [label="yes"];
|
||||
"Can trace backwards?" -> "Trace to original trigger" [label="yes"];
|
||||
"Can trace backwards?" -> "Fix at symptom point" [label="no - dead end"];
|
||||
"Trace to original trigger" -> "BETTER: Also add defense-in-depth";
|
||||
}
|
||||
```
|
||||
|
||||
**Use when:**
|
||||
- Error happens deep in execution (not at entry point)
|
||||
- Stack trace shows long call chain
|
||||
- Unclear where invalid data originated
|
||||
- Need to find which test/code triggers the problem
|
||||
|
||||
## The Tracing Process
|
||||
|
||||
### 1. Observe the Symptom
|
||||
```
|
||||
Error: git init failed in ~/project/packages/core
|
||||
```
|
||||
|
||||
### 2. Find Immediate Cause
|
||||
**What code directly causes this?**
|
||||
```typescript
|
||||
await execFileAsync('git', ['init'], { cwd: projectDir });
|
||||
```
|
||||
|
||||
### 3. Ask: What Called This?
|
||||
```typescript
|
||||
WorktreeManager.createSessionWorktree(projectDir, sessionId)
|
||||
→ called by Session.initializeWorkspace()
|
||||
→ called by Session.create()
|
||||
→ called by test at Project.create()
|
||||
```
|
||||
|
||||
### 4. Keep Tracing Up
|
||||
**What value was passed?**
|
||||
- `projectDir = ''` (empty string!)
|
||||
- Empty string as `cwd` resolves to `process.cwd()`
|
||||
- That's the source code directory!
|
||||
|
||||
### 5. Find Original Trigger
|
||||
**Where did empty string come from?**
|
||||
```typescript
|
||||
const context = setupCoreTest(); // Returns { tempDir: '' }
|
||||
Project.create('name', context.tempDir); // Accessed before beforeEach!
|
||||
```
|
||||
|
||||
## Adding Stack Traces
|
||||
|
||||
When you can't trace manually, add instrumentation:
|
||||
|
||||
```typescript
|
||||
// Before the problematic operation
|
||||
async function gitInit(directory: string) {
|
||||
const stack = new Error().stack;
|
||||
console.error('DEBUG git init:', {
|
||||
directory,
|
||||
cwd: process.cwd(),
|
||||
nodeEnv: process.env.NODE_ENV,
|
||||
stack,
|
||||
});
|
||||
|
||||
await execFileAsync('git', ['init'], { cwd: directory });
|
||||
}
|
||||
```
|
||||
|
||||
**Critical:** Use `console.error()` in tests (not logger - may not show)
|
||||
|
||||
**Run and capture:**
|
||||
```bash
|
||||
npm test 2>&1 | grep 'DEBUG git init'
|
||||
```
|
||||
|
||||
**Analyze stack traces:**
|
||||
- Look for test file names
|
||||
- Find the line number triggering the call
|
||||
- Identify the pattern (same test? same parameter?)
|
||||
|
||||
## Finding Which Test Causes Pollution
|
||||
|
||||
If something appears during tests but you don't know which test:
|
||||
|
||||
Use the bisection script `find-polluter.sh` in this directory:
|
||||
|
||||
```bash
|
||||
./find-polluter.sh '.git' 'src/**/*.test.ts'
|
||||
```
|
||||
|
||||
Runs tests one-by-one, stops at first polluter. See script for usage.
|
||||
|
||||
## Real Example: Empty projectDir
|
||||
|
||||
**Symptom:** `.git` created in `packages/core/` (source code)
|
||||
|
||||
**Trace chain:**
|
||||
1. `git init` runs in `process.cwd()` ← empty cwd parameter
|
||||
2. WorktreeManager called with empty projectDir
|
||||
3. Session.create() passed empty string
|
||||
4. Test accessed `context.tempDir` before beforeEach
|
||||
5. setupCoreTest() returns `{ tempDir: '' }` initially
|
||||
|
||||
**Root cause:** Top-level variable initialization accessing empty value
|
||||
|
||||
**Fix:** Made tempDir a getter that throws if accessed before beforeEach
|
||||
|
||||
**Also added defense-in-depth:**
|
||||
- Layer 1: Project.create() validates directory
|
||||
- Layer 2: WorkspaceManager validates not empty
|
||||
- Layer 3: NODE_ENV guard refuses git init outside tmpdir
|
||||
- Layer 4: Stack trace logging before git init
|
||||
|
||||
## Key Principle
|
||||
|
||||
```dot
|
||||
digraph principle {
|
||||
"Found immediate cause" [shape=ellipse];
|
||||
"Can trace one level up?" [shape=diamond];
|
||||
"Trace backwards" [shape=box];
|
||||
"Is this the source?" [shape=diamond];
|
||||
"Fix at source" [shape=box];
|
||||
"Add validation at each layer" [shape=box];
|
||||
"Bug impossible" [shape=doublecircle];
|
||||
"NEVER fix just the symptom" [shape=octagon, style=filled, fillcolor=red, fontcolor=white];
|
||||
|
||||
"Found immediate cause" -> "Can trace one level up?";
|
||||
"Can trace one level up?" -> "Trace backwards" [label="yes"];
|
||||
"Can trace one level up?" -> "NEVER fix just the symptom" [label="no"];
|
||||
"Trace backwards" -> "Is this the source?";
|
||||
"Is this the source?" -> "Trace backwards" [label="no - keeps going"];
|
||||
"Is this the source?" -> "Fix at source" [label="yes"];
|
||||
"Fix at source" -> "Add validation at each layer";
|
||||
"Add validation at each layer" -> "Bug impossible";
|
||||
}
|
||||
```
|
||||
|
||||
**NEVER fix just where the error appears.** Trace back to find the original trigger.
|
||||
|
||||
## Stack Trace Tips
|
||||
|
||||
**In tests:** Use `console.error()` not logger - logger may be suppressed
|
||||
**Before operation:** Log before the dangerous operation, not after it fails
|
||||
**Include context:** Directory, cwd, environment variables, timestamps
|
||||
**Capture stack:** `new Error().stack` shows complete call chain
|
||||
|
||||
## Real-World Impact
|
||||
|
||||
From debugging session (2025-10-03):
|
||||
- Found root cause through 5-level trace
|
||||
- Fixed at source (getter validation)
|
||||
- Added 4 layers of defense
|
||||
- 1847 tests passed, zero pollution
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: test-driven-development
|
||||
description: "Testing outcome gate for features and bugfixes: before any merge, every behavior change must be backed by test evidence that failed before the change and passes after it. Defines what counts as evidence and what test quality means; does not prescribe your coding order."
|
||||
---
|
||||
|
||||
# 测试结果门(Test Evidence Gate)
|
||||
|
||||
## 结果门(合并前的硬性验收)
|
||||
|
||||
```
|
||||
每个行为变更,合并前必须有"先失败、后通过"的测试证据。
|
||||
```
|
||||
|
||||
具体要求:
|
||||
|
||||
1. **红→绿证据**: 对每个新行为/修复,存在一个测试,你能出示它在变更前失败(或对着被还原的代码失败)、在变更后通过的**本会话工具输出**。测试从未失败过 = 无法证明它在测真东西。
|
||||
2. **Bug 修复必带回归测试**: 先有复现该 bug 的失败测试,再谈修复。修完后该测试通过、其余测试不回归。
|
||||
3. **全绿交付**: 合并前全部测试通过,输出干净(无报错、无告警噪音)。
|
||||
|
||||
为什么要求"先失败":变更后才补写的测试天然通过,它验证的是"代码做了什么"而不是"代码应该做什么"——你没见过它抓到问题,就不知道它能不能抓到问题。事后补测的情况下,用"还原变更→测试转红→恢复变更→测试转绿"的方式补出证据。
|
||||
|
||||
写代码的顺序(先测后码、先码后测、边探索边写)不做规定;探索性代码写完后,仍要按上述方式补出红绿证据才能合并。
|
||||
|
||||
**例外**(与人类确认后): 一次性原型、生成代码、纯配置文件。
|
||||
|
||||
## 测试质量(证据必须是真证据)
|
||||
|
||||
| 要求 | 说明 |
|
||||
|---|---|
|
||||
| 测行为,不测 mock | 测试对象是真实代码路径;mock 只用于不可控外部依赖(真实网关、计费 API)。断言 mock 被调用了几次 ≠ 测试了行为 |
|
||||
| 一个测试一个行为 | 名字里需要"和"就拆开;名字描述行为而非编号 |
|
||||
| 真实样本优先 | 用录制的真实网关响应或其二次构造,不凭空捏造响应结构(CLAUDE.md §4.6) |
|
||||
| 并发/韧性是一等对象 | 重试穿透取消、熔断开路半开、限流结算退款、降级方向都要有测试 |
|
||||
|
||||
添加 mock 或测试工具前,读 `testing-anti-patterns.md`(测 mock 行为、给生产类加测试专用方法等常见坑)。
|
||||
|
||||
## 常见自欺(仍然不算证据)
|
||||
|
||||
- "太简单不用测" —— 简单代码也会坏;库的 bug 击穿所有下游。
|
||||
- "我手动验证过了" —— 无记录、不可重跑,不是证据。
|
||||
- "测试一次通过,应该没问题" —— 从未失败过的测试,不知道它测的是什么。
|
||||
|
||||
运行命令统一 `conda run -n PolyGateway pytest ...`。
|
||||
@@ -0,0 +1,299 @@
|
||||
# Testing Anti-Patterns
|
||||
|
||||
**Load this reference when:** writing or changing tests, adding mocks, or tempted to add test-only methods to production code.
|
||||
|
||||
## Overview
|
||||
|
||||
Tests must verify real behavior, not mock behavior. Mocks are a means to isolate, not the thing being tested.
|
||||
|
||||
**Core principle:** Test what the code does, not what the mocks do.
|
||||
|
||||
**Following strict TDD prevents these anti-patterns.**
|
||||
|
||||
## The Iron Laws
|
||||
|
||||
```
|
||||
1. NEVER test mock behavior
|
||||
2. NEVER add test-only methods to production classes
|
||||
3. NEVER mock without understanding dependencies
|
||||
```
|
||||
|
||||
## Anti-Pattern 1: Testing Mock Behavior
|
||||
|
||||
**The violation:**
|
||||
```typescript
|
||||
// ❌ BAD: Testing that the mock exists
|
||||
test('renders sidebar', () => {
|
||||
render(<Page />);
|
||||
expect(screen.getByTestId('sidebar-mock')).toBeInTheDocument();
|
||||
});
|
||||
```
|
||||
|
||||
**Why this is wrong:**
|
||||
- You're verifying the mock works, not that the component works
|
||||
- Test passes when mock is present, fails when it's not
|
||||
- Tells you nothing about real behavior
|
||||
|
||||
**your human partner's correction:** "Are we testing the behavior of a mock?"
|
||||
|
||||
**The fix:**
|
||||
```typescript
|
||||
// ✅ GOOD: Test real component or don't mock it
|
||||
test('renders sidebar', () => {
|
||||
render(<Page />); // Don't mock sidebar
|
||||
expect(screen.getByRole('navigation')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// OR if sidebar must be mocked for isolation:
|
||||
// Don't assert on the mock - test Page's behavior with sidebar present
|
||||
```
|
||||
|
||||
### Gate Function
|
||||
|
||||
```
|
||||
BEFORE asserting on any mock element:
|
||||
Ask: "Am I testing real component behavior or just mock existence?"
|
||||
|
||||
IF testing mock existence:
|
||||
STOP - Delete the assertion or unmock the component
|
||||
|
||||
Test real behavior instead
|
||||
```
|
||||
|
||||
## Anti-Pattern 2: Test-Only Methods in Production
|
||||
|
||||
**The violation:**
|
||||
```typescript
|
||||
// ❌ BAD: destroy() only used in tests
|
||||
class Session {
|
||||
async destroy() { // Looks like production API!
|
||||
await this._workspaceManager?.destroyWorkspace(this.id);
|
||||
// ... cleanup
|
||||
}
|
||||
}
|
||||
|
||||
// In tests
|
||||
afterEach(() => session.destroy());
|
||||
```
|
||||
|
||||
**Why this is wrong:**
|
||||
- Production class polluted with test-only code
|
||||
- Dangerous if accidentally called in production
|
||||
- Violates YAGNI and separation of concerns
|
||||
- Confuses object lifecycle with entity lifecycle
|
||||
|
||||
**The fix:**
|
||||
```typescript
|
||||
// ✅ GOOD: Test utilities handle test cleanup
|
||||
// Session has no destroy() - it's stateless in production
|
||||
|
||||
// In test-utils/
|
||||
export async function cleanupSession(session: Session) {
|
||||
const workspace = session.getWorkspaceInfo();
|
||||
if (workspace) {
|
||||
await workspaceManager.destroyWorkspace(workspace.id);
|
||||
}
|
||||
}
|
||||
|
||||
// In tests
|
||||
afterEach(() => cleanupSession(session));
|
||||
```
|
||||
|
||||
### Gate Function
|
||||
|
||||
```
|
||||
BEFORE adding any method to production class:
|
||||
Ask: "Is this only used by tests?"
|
||||
|
||||
IF yes:
|
||||
STOP - Don't add it
|
||||
Put it in test utilities instead
|
||||
|
||||
Ask: "Does this class own this resource's lifecycle?"
|
||||
|
||||
IF no:
|
||||
STOP - Wrong class for this method
|
||||
```
|
||||
|
||||
## Anti-Pattern 3: Mocking Without Understanding
|
||||
|
||||
**The violation:**
|
||||
```typescript
|
||||
// ❌ BAD: Mock breaks test logic
|
||||
test('detects duplicate server', () => {
|
||||
// Mock prevents config write that test depends on!
|
||||
vi.mock('ToolCatalog', () => ({
|
||||
discoverAndCacheTools: vi.fn().mockResolvedValue(undefined)
|
||||
}));
|
||||
|
||||
await addServer(config);
|
||||
await addServer(config); // Should throw - but won't!
|
||||
});
|
||||
```
|
||||
|
||||
**Why this is wrong:**
|
||||
- Mocked method had side effect test depended on (writing config)
|
||||
- Over-mocking to "be safe" breaks actual behavior
|
||||
- Test passes for wrong reason or fails mysteriously
|
||||
|
||||
**The fix:**
|
||||
```typescript
|
||||
// ✅ GOOD: Mock at correct level
|
||||
test('detects duplicate server', () => {
|
||||
// Mock the slow part, preserve behavior test needs
|
||||
vi.mock('MCPServerManager'); // Just mock slow server startup
|
||||
|
||||
await addServer(config); // Config written
|
||||
await addServer(config); // Duplicate detected ✓
|
||||
});
|
||||
```
|
||||
|
||||
### Gate Function
|
||||
|
||||
```
|
||||
BEFORE mocking any method:
|
||||
STOP - Don't mock yet
|
||||
|
||||
1. Ask: "What side effects does the real method have?"
|
||||
2. Ask: "Does this test depend on any of those side effects?"
|
||||
3. Ask: "Do I fully understand what this test needs?"
|
||||
|
||||
IF depends on side effects:
|
||||
Mock at lower level (the actual slow/external operation)
|
||||
OR use test doubles that preserve necessary behavior
|
||||
NOT the high-level method the test depends on
|
||||
|
||||
IF unsure what test depends on:
|
||||
Run test with real implementation FIRST
|
||||
Observe what actually needs to happen
|
||||
THEN add minimal mocking at the right level
|
||||
|
||||
Red flags:
|
||||
- "I'll mock this to be safe"
|
||||
- "This might be slow, better mock it"
|
||||
- Mocking without understanding the dependency chain
|
||||
```
|
||||
|
||||
## Anti-Pattern 4: Incomplete Mocks
|
||||
|
||||
**The violation:**
|
||||
```typescript
|
||||
// ❌ BAD: Partial mock - only fields you think you need
|
||||
const mockResponse = {
|
||||
status: 'success',
|
||||
data: { userId: '123', name: 'Alice' }
|
||||
// Missing: metadata that downstream code uses
|
||||
};
|
||||
|
||||
// Later: breaks when code accesses response.metadata.requestId
|
||||
```
|
||||
|
||||
**Why this is wrong:**
|
||||
- **Partial mocks hide structural assumptions** - You only mocked fields you know about
|
||||
- **Downstream code may depend on fields you didn't include** - Silent failures
|
||||
- **Tests pass but integration fails** - Mock incomplete, real API complete
|
||||
- **False confidence** - Test proves nothing about real behavior
|
||||
|
||||
**The Iron Rule:** Mock the COMPLETE data structure as it exists in reality, not just fields your immediate test uses.
|
||||
|
||||
**The fix:**
|
||||
```typescript
|
||||
// ✅ GOOD: Mirror real API completeness
|
||||
const mockResponse = {
|
||||
status: 'success',
|
||||
data: { userId: '123', name: 'Alice' },
|
||||
metadata: { requestId: 'req-789', timestamp: 1234567890 }
|
||||
// All fields real API returns
|
||||
};
|
||||
```
|
||||
|
||||
### Gate Function
|
||||
|
||||
```
|
||||
BEFORE creating mock responses:
|
||||
Check: "What fields does the real API response contain?"
|
||||
|
||||
Actions:
|
||||
1. Examine actual API response from docs/examples
|
||||
2. Include ALL fields system might consume downstream
|
||||
3. Verify mock matches real response schema completely
|
||||
|
||||
Critical:
|
||||
If you're creating a mock, you must understand the ENTIRE structure
|
||||
Partial mocks fail silently when code depends on omitted fields
|
||||
|
||||
If uncertain: Include all documented fields
|
||||
```
|
||||
|
||||
## Anti-Pattern 5: Integration Tests as Afterthought
|
||||
|
||||
**The violation:**
|
||||
```
|
||||
✅ Implementation complete
|
||||
❌ No tests written
|
||||
"Ready for testing"
|
||||
```
|
||||
|
||||
**Why this is wrong:**
|
||||
- Testing is part of implementation, not optional follow-up
|
||||
- TDD would have caught this
|
||||
- Can't claim complete without tests
|
||||
|
||||
**The fix:**
|
||||
```
|
||||
TDD cycle:
|
||||
1. Write failing test
|
||||
2. Implement to pass
|
||||
3. Refactor
|
||||
4. THEN claim complete
|
||||
```
|
||||
|
||||
## When Mocks Become Too Complex
|
||||
|
||||
**Warning signs:**
|
||||
- Mock setup longer than test logic
|
||||
- Mocking everything to make test pass
|
||||
- Mocks missing methods real components have
|
||||
- Test breaks when mock changes
|
||||
|
||||
**your human partner's question:** "Do we need to be using a mock here?"
|
||||
|
||||
**Consider:** Integration tests with real components often simpler than complex mocks
|
||||
|
||||
## TDD Prevents These Anti-Patterns
|
||||
|
||||
**Why TDD helps:**
|
||||
1. **Write test first** → Forces you to think about what you're actually testing
|
||||
2. **Watch it fail** → Confirms test tests real behavior, not mocks
|
||||
3. **Minimal implementation** → No test-only methods creep in
|
||||
4. **Real dependencies** → You see what the test actually needs before mocking
|
||||
|
||||
**If you're testing mock behavior, you violated TDD** - you added mocks without watching test fail against real code first.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Anti-Pattern | Fix |
|
||||
|--------------|-----|
|
||||
| Assert on mock elements | Test real component or unmock it |
|
||||
| Test-only methods in production | Move to test utilities |
|
||||
| Mock without understanding | Understand dependencies first, mock minimally |
|
||||
| Incomplete mocks | Mirror real API completely |
|
||||
| Tests as afterthought | TDD - tests first |
|
||||
| Over-complex mocks | Consider integration tests |
|
||||
|
||||
## Red Flags
|
||||
|
||||
- Assertion checks for `*-mock` test IDs
|
||||
- Methods only called in test files
|
||||
- Mock setup is >50% of test
|
||||
- Test fails when you remove mock
|
||||
- Can't explain why mock is needed
|
||||
- Mocking "just to be safe"
|
||||
|
||||
## The Bottom Line
|
||||
|
||||
**Mocks are tools to isolate, not things to test.**
|
||||
|
||||
If TDD reveals you're testing mock behavior, you've gone wrong.
|
||||
|
||||
Fix: Test real behavior or question why you're mocking at all.
|
||||
@@ -0,0 +1,214 @@
|
||||
---
|
||||
name: using-git-worktrees
|
||||
description: Use when starting feature work that needs isolation from current workspace or before executing implementation plans - ensures an isolated workspace exists via native tools or git worktree fallback
|
||||
---
|
||||
|
||||
# Using Git Worktrees
|
||||
|
||||
## Overview
|
||||
|
||||
Ensure work happens in an isolated workspace. Prefer your platform's native worktree tools. Fall back to manual git worktrees only when no native tool is available.
|
||||
|
||||
**Core principle:** Detect existing isolation first. Then use native tools. Then fall back to git. Never fight the harness.
|
||||
|
||||
**Announce at start:** "I'm using the using-git-worktrees skill to set up an isolated workspace."
|
||||
|
||||
## Step 0: Detect Existing Isolation
|
||||
|
||||
**Before creating anything, check if you are already in an isolated workspace.**
|
||||
|
||||
```bash
|
||||
GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P)
|
||||
GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P)
|
||||
BRANCH=$(git branch --show-current)
|
||||
```
|
||||
|
||||
**Submodule guard:** `GIT_DIR != GIT_COMMON` is also true inside git submodules. Before concluding "already in a worktree," verify you are not in a submodule:
|
||||
|
||||
```bash
|
||||
# If this returns a path, you're in a submodule, not a worktree — treat as normal repo
|
||||
git rev-parse --show-superproject-working-tree 2>/dev/null
|
||||
```
|
||||
|
||||
**If `GIT_DIR != GIT_COMMON` (and not a submodule):** You are already in a linked worktree. Skip to Step 3 (Project Setup). Do NOT create another worktree.
|
||||
|
||||
Report with branch state:
|
||||
- On a branch: "Already in isolated workspace at `<path>` on branch `<name>`."
|
||||
- Detached HEAD: "Already in isolated workspace at `<path>` (detached HEAD, externally managed). Branch creation needed at finish time."
|
||||
|
||||
**If `GIT_DIR == GIT_COMMON` (or in a submodule):** You are in a normal repo checkout.
|
||||
|
||||
Has the user already indicated their worktree preference in your instructions? If not, ask for consent before creating a worktree:
|
||||
|
||||
> "Would you like me to set up an isolated worktree? It protects your current branch from changes."
|
||||
|
||||
Honor any existing declared preference without asking. If the user declines consent, work in place and skip to Step 3.
|
||||
|
||||
## Step 1: Create Isolated Workspace
|
||||
|
||||
**You have two mechanisms. Try them in this order.**
|
||||
|
||||
### 1a. Native Worktree Tools (preferred)
|
||||
|
||||
The user has asked for an isolated workspace (Step 0 consent). Do you already have a way to create a worktree? It might be a tool with a name like `EnterWorktree`, `WorktreeCreate`, a `/worktree` command, or a `--worktree` flag. If you do, use it and skip to Step 3.
|
||||
|
||||
Native tools handle directory placement, branch creation, and cleanup automatically. Using `git worktree add` when you have a native tool creates phantom state your harness can't see or manage.
|
||||
|
||||
Only proceed to Step 1b if you have no native worktree tool available.
|
||||
|
||||
### 1b. Git Worktree Fallback
|
||||
|
||||
**Only use this if Step 1a does not apply** — you have no native worktree tool available. Create a worktree manually using git.
|
||||
|
||||
#### Directory Selection
|
||||
|
||||
Follow this priority order. Explicit user preference always beats observed filesystem state.
|
||||
|
||||
1. **Check your instructions for a declared worktree directory preference.** If the user has already specified one, use it without asking.
|
||||
|
||||
2. **Check for an existing project-local worktree directory:**
|
||||
```bash
|
||||
ls -d .worktrees 2>/dev/null # Preferred (hidden)
|
||||
ls -d worktrees 2>/dev/null # Alternative
|
||||
```
|
||||
If found, use it. If both exist, `.worktrees` wins.
|
||||
|
||||
3. **Check for an existing global directory:**
|
||||
```bash
|
||||
project=$(basename "$(git rev-parse --show-toplevel)")
|
||||
ls -d ~/.config/superpowers/worktrees/$project 2>/dev/null
|
||||
```
|
||||
If found, use it (backward compatibility with legacy global path).
|
||||
|
||||
4. **If there is no other guidance available**, default to `.worktrees/` at the project root.
|
||||
|
||||
#### Safety Verification (project-local directories only)
|
||||
|
||||
**MUST verify directory is ignored before creating worktree:**
|
||||
|
||||
```bash
|
||||
git check-ignore -q .worktrees 2>/dev/null || git check-ignore -q worktrees 2>/dev/null
|
||||
```
|
||||
|
||||
**If NOT ignored:** Add to .gitignore, commit the change, then proceed.
|
||||
|
||||
**Why critical:** Prevents accidentally committing worktree contents to repository.
|
||||
|
||||
Global directories (`~/.config/superpowers/worktrees/`) need no verification.
|
||||
|
||||
#### Create the Worktree
|
||||
|
||||
```bash
|
||||
project=$(basename "$(git rev-parse --show-toplevel)")
|
||||
|
||||
# Determine path based on chosen location
|
||||
# For project-local: path="$LOCATION/$BRANCH_NAME"
|
||||
# For global: path="~/.config/superpowers/worktrees/$project/$BRANCH_NAME"
|
||||
|
||||
git worktree add "$path" -b "$BRANCH_NAME"
|
||||
cd "$path"
|
||||
```
|
||||
|
||||
**Sandbox fallback:** If `git worktree add` fails with a permission error (sandbox denial), tell the user the sandbox blocked worktree creation and you're working in the current directory instead. Then run setup and baseline tests in place.
|
||||
|
||||
## Step 3: Project Setup
|
||||
|
||||
Auto-detect and run appropriate setup:
|
||||
|
||||
```bash
|
||||
# Node.js
|
||||
if [ -f package.json ]; then npm install; fi
|
||||
|
||||
# Rust
|
||||
if [ -f Cargo.toml ]; then cargo build; fi
|
||||
|
||||
# Python (PolyGateway conda env)
|
||||
if [ -f requirements.txt ]; then conda run -n PolyGateway pip install -r requirements.txt; fi
|
||||
|
||||
# Go
|
||||
if [ -f go.mod ]; then go mod download; fi
|
||||
```
|
||||
|
||||
## Step 4: Verify Clean Baseline
|
||||
|
||||
Run tests to ensure workspace starts clean:
|
||||
|
||||
```bash
|
||||
# Use project-appropriate command
|
||||
npm test / cargo test / conda run -n PolyGateway pytest / go test ./...
|
||||
```
|
||||
|
||||
**If tests fail:** Report failures, ask whether to proceed or investigate.
|
||||
|
||||
**If tests pass:** Report ready.
|
||||
|
||||
### Report
|
||||
|
||||
```
|
||||
Worktree ready at <full-path>
|
||||
Tests passing (<N> tests, 0 failures)
|
||||
Ready to implement <feature-name>
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Situation | Action |
|
||||
|-----------|--------|
|
||||
| Already in linked worktree | Skip creation (Step 0) |
|
||||
| In a submodule | Treat as normal repo (Step 0 guard) |
|
||||
| Native worktree tool available | Use it (Step 1a) |
|
||||
| No native tool | Git worktree fallback (Step 1b) |
|
||||
| `.worktrees/` exists | Use it (verify ignored) |
|
||||
| `worktrees/` exists | Use it (verify ignored) |
|
||||
| Both exist | Use `.worktrees/` |
|
||||
| Neither exists | Check instruction file, then default `.worktrees/` |
|
||||
| Global path exists | Use it (backward compat) |
|
||||
| Directory not ignored | Add to .gitignore + commit |
|
||||
| Permission error on create | Sandbox fallback, work in place |
|
||||
| Tests fail during baseline | Report failures + ask |
|
||||
| No package.json/Cargo.toml | Skip dependency install |
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
### Fighting the harness
|
||||
|
||||
- **Problem:** Using `git worktree add` when the platform already provides isolation
|
||||
- **Fix:** Step 0 detects existing isolation. Step 1a defers to native tools.
|
||||
|
||||
### Skipping detection
|
||||
|
||||
- **Problem:** Creating a nested worktree inside an existing one
|
||||
- **Fix:** Always run Step 0 before creating anything
|
||||
|
||||
### Skipping ignore verification
|
||||
|
||||
- **Problem:** Worktree contents get tracked, pollute git status
|
||||
- **Fix:** Always use `git check-ignore` before creating project-local worktree
|
||||
|
||||
### Assuming directory location
|
||||
|
||||
- **Problem:** Creates inconsistency, violates project conventions
|
||||
- **Fix:** Follow priority: existing > global legacy > instruction file > default
|
||||
|
||||
### Proceeding with failing tests
|
||||
|
||||
- **Problem:** Can't distinguish new bugs from pre-existing issues
|
||||
- **Fix:** Report failures, get explicit permission to proceed
|
||||
|
||||
## Red Flags
|
||||
|
||||
**Never:**
|
||||
- Create a worktree when Step 0 detects existing isolation
|
||||
- Use `git worktree add` when you have a native worktree tool (e.g., `EnterWorktree`). This is the #1 mistake — if you have it, use it.
|
||||
- Skip Step 1a by jumping straight to Step 1b's git commands
|
||||
- Create worktree without verifying it's ignored (project-local)
|
||||
- Skip baseline test verification
|
||||
- Proceed with failing tests without asking
|
||||
|
||||
**Always:**
|
||||
- Run Step 0 detection first
|
||||
- Prefer native tools over git fallback
|
||||
- Follow directory priority: existing > global legacy > instruction file > default
|
||||
- Verify directory is ignored for project-local
|
||||
- Auto-detect and run project setup
|
||||
- Verify clean test baseline
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
name: verification-before-completion
|
||||
description: "Use before claiming work is complete, fixed, or passing, and before committing or creating PRs. Every completion claim must map to tool output produced in this session; for milestone-scale or multi-file work, dispatch a fresh-context verifier subagent instead of self-review."
|
||||
---
|
||||
|
||||
# Verification Before Completion
|
||||
|
||||
## 边界声明
|
||||
|
||||
```
|
||||
没有本会话的新鲜验证证据,就不得声称完成。
|
||||
```
|
||||
|
||||
"应该可以了"、"看起来没问题"、"我有信心"都不是证据。证据 = 本会话中实际运行的命令及其输出。
|
||||
|
||||
## 证据化声明
|
||||
|
||||
最终报告里的每一条"已完成 / 已修复 / 已通过",都必须能对应到本会话的一次工具结果:
|
||||
|
||||
| 声明 | 所需证据 |
|
||||
|---|---|
|
||||
| 测试通过 | 本会话 `conda run -n PolyGateway pytest` 输出:0 failures |
|
||||
| lint/格式干净 | `make lint` / `ruff check` 输出:0 errors |
|
||||
| bug 已修复 | 复现原症状的测试:变更前失败、变更后通过(见 `test-driven-development` 结果门) |
|
||||
| 需求已满足 | 对照 plan/spec 逐条核对的清单,每条指向 diff 或测试 |
|
||||
| subagent 完成了任务 | 你亲自查看的 `git diff` / 文件内容,而非 subagent 的自述 |
|
||||
| 无残留半成品 | grep TODO/NotImplementedError/占位 mock:0 命中;工作区无临时脚本与调试产物 |
|
||||
|
||||
无法出示证据的声明,改为陈述实际状态("X 已实现,Y 尚未验证,因为…")。
|
||||
|
||||
## 独立验证(里程碑级/多文件工作必做)
|
||||
|
||||
自我检查会继承自己的盲区。达到以下任一条件时,**必须派一个全新上下文的 verifier subagent**(只读)做独立核验,而不是自己过检查清单:
|
||||
|
||||
- 里程碑级任务或跨多文件的功能完成时;
|
||||
- 提 PR / 合并回主线前;
|
||||
- 执行完一份 plan 时。
|
||||
|
||||
做法:用 `Agent` 工具派一个 read-only subagent(如 `Explore` 或 general-purpose 限只读),prompt 只给它:任务需求原文(spec/plan 相关部分)、变更范围(分支/SHA 区间)、验证命令。**不给它你的实现思路与自评**——它的价值就在于没有你的上下文。要求它:
|
||||
|
||||
1. 亲自读 diff 与相关文件,逐条核对需求覆盖(缺失/多余/误解);
|
||||
2. 亲自跑测试与 lint,报告真实输出;
|
||||
3. 按 Critical/Important/Minor 返回问题清单。
|
||||
|
||||
对其结论逐条核验后修复;Critical/Important 未清零不得声称完成。小改动(单文件、明确修复)不必派 subagent,但证据化声明的要求不变。
|
||||
|
||||
## 留痕(`research-wiki/` 存在时)
|
||||
|
||||
验证**发现问题**时(全过则不写),记一条 finding:
|
||||
|
||||
```bash
|
||||
.claude/tools/research_wiki.py add_entity research-wiki/ --type finding --id <slug> --title "<验证问题>"
|
||||
.claude/tools/research_wiki.py rebuild_index research-wiki/
|
||||
```
|
||||
|
||||
页内记录:验证命令、实际输出、失败原因、修复后结果。
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
name: writing-plans
|
||||
description: "Use for milestone-scale or multi-file feature work: write an implementation plan before coding. MANDATORY for work spanning multiple modules or implementing an approved design. For small, well-bounded changes (single file, clear fix), plan inline and skip this skill."
|
||||
---
|
||||
|
||||
# Writing Plans
|
||||
|
||||
## 触发边界
|
||||
|
||||
- **强制**: 里程碑级任务、跨多文件的新功能、实现一份已批准设计的工作。
|
||||
- **自判**: 单文件小改动、明确的 bug 修复、纯文档/配置变更——不值得为它们写计划文档。
|
||||
|
||||
计划保存至 `research-wiki/plans/YYYY-MM-DD-<feature-name>.md`(≤1000 行)。
|
||||
|
||||
## 计划要写给谁
|
||||
|
||||
假设执行者(可能是 subagent、可能是未来的你)对本代码库零上下文。计划要交代:每个任务动哪些文件(精确路径)、验收标准是什么、怎么验证。DRY、YAGNI、频繁提交。不写计划外的重构与抽象。
|
||||
|
||||
## 计划必备内容
|
||||
|
||||
**头部**: 目标(一句话)、方案概述(2-3 句)、涉及技术。
|
||||
|
||||
**文件结构**: 开始拆任务前,先列出将创建/修改的文件及各自职责——分解决策在这里锁定。遵循既有架构(`ports/types/errors` 内核 + middleware/transports/backends/telemetry,见 ARCHITECTURE.md §8)。
|
||||
|
||||
**任务清单**: 每个任务包含:
|
||||
- 精确文件路径(创建/修改/测试);
|
||||
- 要实现的行为与验收标准;
|
||||
- 测试要求:该任务合并前必须能出示"先失败后通过"的测试证据(见 `test-driven-development` 的结果门);
|
||||
- 验证命令及预期输出(如 `conda run -n PolyGateway pytest tests/... -v` → PASS);
|
||||
- 提交点(checkbox `- [ ]` 语法便于追踪)。
|
||||
|
||||
任务粒度以"一次提交、独立可验证"为准,不必把每个动作拆成几分钟一格的脚本步骤——执行者会自己安排动作顺序。
|
||||
|
||||
## No Placeholders(计划失败模式,禁止出现)
|
||||
|
||||
- "TBD" / "TODO" / "以后补充" / "实现细节略"
|
||||
- "添加适当的错误处理 / 校验 / 边界处理"(不说清楚是什么)
|
||||
- "为上文写测试"(没有说明测什么行为)
|
||||
- "同 Task N"(执行者可能乱序读,关键内容要重复或明确引用)
|
||||
- 引用了任何任务中都未定义的类型/函数/方法
|
||||
|
||||
关键接口(跨任务消费的类型、函数签名)必须在计划中写出实际代码;其余代码执行时再写。
|
||||
|
||||
## 保真校验(迁移类计划必做)
|
||||
|
||||
本库的主体工作是把 `reference/` 三项目的治理代码迁移进库。若计划涉及 ARCHITECTURE.md §1.4"关键资产索引"中列出的任何移植蓝本(治理主循环、流式看门狗、Redis+Lua 限流、跨进程熔断、错误分类、遥测等):
|
||||
|
||||
1. 在对应任务中标注参考文件路径,要求实现时**逐段比对参考实现**;
|
||||
2. 为该任务加"保真校验"检查点:核心逻辑(条件分支、Lua 脚本语义、状态机转换、退避公式)不得被简化或悄悄改变行为——重构结构可以,改变语义必须在设计中声明过。
|
||||
|
||||
计划不涉及迁移时,注明"本计划不涉及参考实现迁移,保真校验不适用"。
|
||||
|
||||
## 审核门(保留;plan 无需人类批准)
|
||||
|
||||
1. **自审**: 对照 spec 逐节检查覆盖(每条需求能指到任务)、扫 placeholder、检查跨任务类型/签名一致性。发现问题就地修。
|
||||
2. **Codex 审**: 用 `/codex:rescue --fresh --wait` 只读审查(设计覆盖、每步可执行性、测试证据要求是否齐全),模板见 `plan-document-reviewer-prompt.md`。意见仅供参考,逐条核验后自行决定采纳,就地修订。
|
||||
3. 通过后**直接进入执行**,无需人类批准(与 design 的人类门不同)。执行方式自选:任务多且相互独立的大计划可用 `subagent-driven-development`;中小计划直接按计划实现。
|
||||
|
||||
## Wiki 注册(`research-wiki/` 存在时)
|
||||
|
||||
```bash
|
||||
.claude/tools/research_wiki.py add_entity research-wiki/ --type plan --id <slug> --title "<title>"
|
||||
.claude/tools/research_wiki.py add_edge research-wiki/ --from "plan:<id>" --to "design:<id>" --type implements --evidence "..."
|
||||
.claude/tools/research_wiki.py rebuild_index research-wiki/
|
||||
```
|
||||
@@ -0,0 +1,49 @@
|
||||
# Plan Document Reviewer Prompt Template
|
||||
|
||||
Use this template when dispatching a plan document reviewer subagent.
|
||||
|
||||
**Purpose:** Verify the plan is complete, matches the spec, and has proper task decomposition.
|
||||
|
||||
**Dispatch after:** The complete plan is written.
|
||||
|
||||
```
|
||||
Task tool (general-purpose):
|
||||
description: "Review plan document"
|
||||
prompt: |
|
||||
You are a plan document reviewer. Verify this plan is complete and ready for implementation.
|
||||
|
||||
**Plan to review:** [PLAN_FILE_PATH]
|
||||
**Spec for reference:** [SPEC_FILE_PATH]
|
||||
|
||||
## What to Check
|
||||
|
||||
| Category | What to Look For |
|
||||
|----------|------------------|
|
||||
| Completeness | TODOs, placeholders, incomplete tasks, missing steps |
|
||||
| Spec Alignment | Plan covers spec requirements, no major scope creep |
|
||||
| Task Decomposition | Tasks have clear boundaries, steps are actionable |
|
||||
| Buildability | Could an engineer follow this plan without getting stuck? |
|
||||
|
||||
## Calibration
|
||||
|
||||
**Only flag issues that would cause real problems during implementation.**
|
||||
An implementer building the wrong thing or getting stuck is an issue.
|
||||
Minor wording, stylistic preferences, and "nice to have" suggestions are not.
|
||||
|
||||
Approve unless there are serious gaps — missing requirements from the spec,
|
||||
contradictory steps, placeholder content, or tasks so vague they can't be acted on.
|
||||
|
||||
## Output Format
|
||||
|
||||
## Plan Review
|
||||
|
||||
**Status:** Approved | Issues Found
|
||||
|
||||
**Issues (if any):**
|
||||
- [Task X, Step Y]: [specific issue] - [why it matters for implementation]
|
||||
|
||||
**Recommendations (advisory, do not block approval):**
|
||||
- [suggestions for improvement]
|
||||
```
|
||||
|
||||
**Reviewer returns:** Status, Issues (if any), Recommendations
|
||||
Reference in New Issue
Block a user