mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
chore: move ccw-skill-hub to standalone repository
Migrated ccw-skill-hub to D:/ccw-skill-hub as independent git project. Removed nested git repos (ccw/frontend/ccw-skill-hub, skill-hub-repo, skill-hub-temp).
This commit is contained in:
1
.claude/skills/.gitignore
vendored
Normal file
1
.claude/skills/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.ace-tool/
|
||||
@@ -113,7 +113,6 @@ mcp__ccw-tools__team_msg({ summary: `[${role}] ...` })
|
||||
const TEAM_CONFIG = {
|
||||
name: "planex",
|
||||
sessionDir: ".workflow/.team/PEX-{slug}-{date}/",
|
||||
msgDir: ".workflow/.team-msg/planex/",
|
||||
issueDataDir: ".workflow/issues/"
|
||||
}
|
||||
```
|
||||
|
||||
131
.claude/skills/team-review/SKILL.md
Normal file
131
.claude/skills/team-review/SKILL.md
Normal file
@@ -0,0 +1,131 @@
|
||||
---
|
||||
name: team-review
|
||||
description: "Unified team skill for code scanning, vulnerability review, optimization suggestions, and automated fix. 4-role team: coordinator, scanner, reviewer, fixer. Triggers on team-review."
|
||||
allowed-tools: Task, AskUserQuestion, TaskCreate, TaskUpdate, TaskList, TaskGet, Read, Write, Edit, Bash, Glob, Grep, Skill, mcp__ace-tool__search_context
|
||||
---
|
||||
|
||||
# Team Review — Role Router
|
||||
|
||||
Single entry point for code scanning, review, and fix. Parses `$ARGUMENTS`, extracts role, and dispatches to the corresponding `role.md`.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ SKILL.md (Role Router) │
|
||||
│ Parse $ARGUMENTS → Extract --role → Dispatch to role.md │
|
||||
│ No --role → Dispatch to coordinator │
|
||||
└──────────────────────────┬──────────────────────────────────┘
|
||||
│
|
||||
┌───────────┬───────────┼───────────┐
|
||||
↓ ↓ ↓ ↓
|
||||
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
|
||||
│ coord │ │scanner │ │reviewer│ │ fixer │
|
||||
│ (RC-*) │ │(SCAN-*)│ │(REV-*) │ │(FIX-*) │
|
||||
└────────┘ └────────┘ └────────┘ └────────┘
|
||||
```
|
||||
|
||||
## Pipeline (CP-1 Linear)
|
||||
|
||||
```
|
||||
coordinator dispatch
|
||||
→ SCAN-* (scanner: toolchain + LLM scan)
|
||||
→ REV-* (reviewer: deep analysis + report)
|
||||
→ [user confirm]
|
||||
→ FIX-* (fixer: plan + execute + verify)
|
||||
```
|
||||
|
||||
## Available Roles
|
||||
|
||||
| Role | Prefix | Type | File |
|
||||
|------|--------|------|------|
|
||||
| coordinator | RC | orchestration | roles/coordinator/role.md |
|
||||
| scanner | SCAN | read-only-analysis | roles/scanner/role.md |
|
||||
| reviewer | REV | read-only-analysis | roles/reviewer/role.md |
|
||||
| fixer | FIX | code-generation | roles/fixer/role.md |
|
||||
|
||||
## Role Router
|
||||
|
||||
```javascript
|
||||
const VALID_ROLES = {
|
||||
"coordinator": "roles/coordinator/role.md",
|
||||
"scanner": "roles/scanner/role.md",
|
||||
"reviewer": "roles/reviewer/role.md",
|
||||
"fixer": "roles/fixer/role.md"
|
||||
}
|
||||
|
||||
// 1. Auto mode detection
|
||||
const autoYes = /\b(-y|--yes)\b/.test($ARGUMENTS)
|
||||
|
||||
// 2. Extract role
|
||||
const roleMatch = $ARGUMENTS.match(/--role[=\s]+(\w+)/)
|
||||
const role = roleMatch ? roleMatch[1] : null
|
||||
|
||||
if (role && VALID_ROLES[role]) {
|
||||
// Explicit role → dispatch directly
|
||||
Read(VALID_ROLES[role]) → Execute with $ARGUMENTS
|
||||
} else if (!role) {
|
||||
// No --role → coordinator handles all routing
|
||||
Read("roles/coordinator/role.md") → Execute with $ARGUMENTS
|
||||
} else {
|
||||
Error(`Unknown role "${role}". Available: ${Object.keys(VALID_ROLES).join(", ")}`)
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Via coordinator (auto pipeline)
|
||||
Skill(skill="team-review", args="src/auth/**") # scan + review
|
||||
Skill(skill="team-review", args="--full src/auth/**") # scan + review + fix
|
||||
Skill(skill="team-review", args="--fix .review/review-*.json") # fix only
|
||||
Skill(skill="team-review", args="-q src/auth/**") # quick scan only
|
||||
|
||||
# Direct role invocation
|
||||
Skill(skill="team-review", args="--role=scanner src/auth/**")
|
||||
Skill(skill="team-review", args="--role=reviewer --input scan-result.json")
|
||||
Skill(skill="team-review", args="--role=fixer --input fix-manifest.json")
|
||||
|
||||
# Flags (all modes)
|
||||
--dimensions=sec,cor,perf,maint # custom dimensions (default: all 4)
|
||||
-y / --yes # skip confirmations
|
||||
-q / --quick # quick scan mode
|
||||
--full # full pipeline (scan → review → fix)
|
||||
--fix # fix mode only
|
||||
```
|
||||
|
||||
## Coordinator Spawn Template
|
||||
|
||||
```javascript
|
||||
// Coordinator spawns worker roles via Skill
|
||||
Skill(skill="team-review", args="--role=scanner ${target} ${flags}")
|
||||
Skill(skill="team-review", args="--role=reviewer --input ${scan_output} ${flags}")
|
||||
Skill(skill="team-review", args="--role=fixer --input ${fix_manifest} ${flags}")
|
||||
```
|
||||
|
||||
## Shared Infrastructure
|
||||
|
||||
| Component | Location |
|
||||
|-----------|----------|
|
||||
| Session directory | `.workflow/.team-review/{workflow_id}/` |
|
||||
| Shared memory | `shared-memory.json` in session dir |
|
||||
| Team config | `specs/team-config.json` |
|
||||
| Finding schema | `specs/finding-schema.json` |
|
||||
| Dimensions | `specs/dimensions.md` |
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Error | Action |
|
||||
|-------|--------|
|
||||
| Unknown --role value | Error with available roles list |
|
||||
| Role file not found | Error with expected file path |
|
||||
| Invalid flags | Warn and continue with defaults |
|
||||
| No target specified (no --role) | AskUserQuestion to clarify |
|
||||
|
||||
## Execution Rules
|
||||
|
||||
1. **Parse first**: Extract --role and flags from $ARGUMENTS before anything else
|
||||
2. **Progressive loading**: Read ONLY the matched role.md, not all four
|
||||
3. **Full delegation**: Role.md owns entire execution — do not add logic here
|
||||
4. **Self-contained**: Each role.md includes its own message bus, task lifecycle, toolbox
|
||||
5. **DO NOT STOP**: Continuous execution until role completes all 5 phases
|
||||
@@ -0,0 +1,145 @@
|
||||
# Command: dispatch
|
||||
|
||||
> Task chain creation based on pipeline mode. Creates SCAN/REV/FIX tasks with dependencies.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Phase 3 of Coordinator
|
||||
- Pipeline mode detected, need to create task chain
|
||||
- Session initialized
|
||||
|
||||
**Trigger conditions**:
|
||||
- Coordinator Phase 2 complete
|
||||
- Mode switch requires chain rebuild
|
||||
|
||||
## Strategy
|
||||
|
||||
### Delegation Mode
|
||||
|
||||
**Mode**: Direct (coordinator operates TaskCreate/TaskUpdate directly)
|
||||
|
||||
### Decision Logic
|
||||
|
||||
```javascript
|
||||
// Build pipeline based on mode
|
||||
function buildPipeline(pipelineMode) {
|
||||
const pipelines = {
|
||||
'default': [
|
||||
{ prefix: 'SCAN', suffix: '001', owner: 'scanner', desc: 'Multi-dimension code scan', blockedBy: [], meta: {} },
|
||||
{ prefix: 'REV', suffix: '001', owner: 'reviewer', desc: 'Deep finding analysis and review', blockedBy: ['SCAN-001'], meta: {} }
|
||||
],
|
||||
'full': [
|
||||
{ prefix: 'SCAN', suffix: '001', owner: 'scanner', desc: 'Multi-dimension code scan', blockedBy: [], meta: {} },
|
||||
{ prefix: 'REV', suffix: '001', owner: 'reviewer', desc: 'Deep finding analysis and review', blockedBy: ['SCAN-001'], meta: {} },
|
||||
{ prefix: 'FIX', suffix: '001', owner: 'fixer', desc: 'Plan and execute fixes', blockedBy: ['REV-001'], meta: {} }
|
||||
],
|
||||
'fix-only': [
|
||||
{ prefix: 'FIX', suffix: '001', owner: 'fixer', desc: 'Execute fixes from manifest', blockedBy: [], meta: {} }
|
||||
],
|
||||
'quick': [
|
||||
{ prefix: 'SCAN', suffix: '001', owner: 'scanner', desc: 'Quick scan (fast mode)', blockedBy: [], meta: { quick: true } }
|
||||
]
|
||||
}
|
||||
return pipelines[pipelineMode] || pipelines['default']
|
||||
}
|
||||
```
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### Step 1: Session Initialization
|
||||
|
||||
```javascript
|
||||
// Session directory already created in Phase 2
|
||||
// Write pipeline config to shared memory
|
||||
const sharedMemory = JSON.parse(Read(`${sessionFolder}/shared-memory.json`))
|
||||
sharedMemory.pipeline_mode = pipelineMode
|
||||
sharedMemory.pipeline_stages = buildPipeline(pipelineMode).map(s => `${s.prefix}-${s.suffix}`)
|
||||
Write(`${sessionFolder}/shared-memory.json`, JSON.stringify(sharedMemory, null, 2))
|
||||
```
|
||||
|
||||
### Step 2: Create Task Chain
|
||||
|
||||
```javascript
|
||||
const pipeline = buildPipeline(pipelineMode)
|
||||
const taskIds = {}
|
||||
|
||||
for (const stage of pipeline) {
|
||||
const taskSubject = `${stage.prefix}-${stage.suffix}: ${stage.desc}`
|
||||
|
||||
// Build task description with session context
|
||||
const fullDesc = [
|
||||
stage.desc,
|
||||
`\nsession: ${sessionFolder}`,
|
||||
`\ntarget: ${target}`,
|
||||
`\ndimensions: ${dimensions.join(',')}`,
|
||||
stage.meta?.quick ? `\nquick: true` : '',
|
||||
`\n\nGoal: ${taskDescription || target}`
|
||||
].join('')
|
||||
|
||||
// Create task
|
||||
TaskCreate({
|
||||
subject: taskSubject,
|
||||
description: fullDesc,
|
||||
activeForm: `${stage.desc} in progress`
|
||||
})
|
||||
|
||||
// Record task ID
|
||||
const allTasks = TaskList()
|
||||
const newTask = allTasks.find(t => t.subject.startsWith(`${stage.prefix}-${stage.suffix}`))
|
||||
taskIds[`${stage.prefix}-${stage.suffix}`] = newTask.id
|
||||
|
||||
// Set owner and dependencies
|
||||
const blockedByIds = stage.blockedBy
|
||||
.map(dep => taskIds[dep])
|
||||
.filter(Boolean)
|
||||
|
||||
TaskUpdate({
|
||||
taskId: newTask.id,
|
||||
owner: stage.owner,
|
||||
addBlockedBy: blockedByIds
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Verify Chain
|
||||
|
||||
```javascript
|
||||
const allTasks = TaskList()
|
||||
const chainTasks = pipeline.map(s => taskIds[`${s.prefix}-${s.suffix}`]).filter(Boolean)
|
||||
const chainValid = chainTasks.length === pipeline.length
|
||||
|
||||
if (!chainValid) {
|
||||
mcp__ccw-tools__team_msg({
|
||||
operation: "log", team: teamName, from: "coordinator",
|
||||
to: "user", type: "error",
|
||||
summary: `[coordinator] Task chain incomplete: ${chainTasks.length}/${pipeline.length}`
|
||||
})
|
||||
}
|
||||
|
||||
mcp__ccw-tools__team_msg({
|
||||
operation: "log", team: teamName, from: "coordinator",
|
||||
to: "all", type: "dispatch_ready",
|
||||
summary: `[coordinator] Task chain created: ${pipeline.map(s => `${s.prefix}-${s.suffix}`).join(' -> ')} (mode: ${pipelineMode})`
|
||||
})
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
```
|
||||
## Task Chain Created
|
||||
|
||||
### Mode: [default|full|fix-only|quick]
|
||||
### Pipeline Stages: [count]
|
||||
- [prefix]-[suffix]: [description] (owner: [role], blocked by: [deps])
|
||||
|
||||
### Verification: PASS/FAIL
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Scenario | Resolution |
|
||||
|----------|------------|
|
||||
| Task creation fails | Retry once, then report to user |
|
||||
| Dependency cycle | Flatten dependencies, warn coordinator |
|
||||
| Invalid pipelineMode | Default to 'default' mode |
|
||||
| Missing session folder | Re-create, log warning |
|
||||
218
.claude/skills/team-review/roles/coordinator/commands/monitor.md
Normal file
218
.claude/skills/team-review/roles/coordinator/commands/monitor.md
Normal file
@@ -0,0 +1,218 @@
|
||||
# Command: monitor
|
||||
|
||||
> Stop-Wait stage execution. Spawns each worker via Skill(), blocks until return, drives transitions.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Phase 4 of Coordinator, after dispatch complete
|
||||
|
||||
## Strategy
|
||||
|
||||
**Mode**: Stop-Wait (synchronous Skill call, not polling)
|
||||
|
||||
> **No polling. Synchronous Skill() call IS the wait mechanism.**
|
||||
>
|
||||
> - FORBIDDEN: `while` + `sleep` + check status
|
||||
> - REQUIRED: `Skill()` blocking call = worker return = stage done
|
||||
|
||||
### Stage-Worker Map
|
||||
|
||||
```javascript
|
||||
const STAGE_WORKER_MAP = {
|
||||
'SCAN': { role: 'scanner', skillArgs: '--role=scanner' },
|
||||
'REV': { role: 'reviewer', skillArgs: '--role=reviewer' },
|
||||
'FIX': { role: 'fixer', skillArgs: '--role=fixer' }
|
||||
}
|
||||
```
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### Step 1: Context Preparation
|
||||
|
||||
```javascript
|
||||
const sharedMemory = JSON.parse(Read(`${sessionFolder}/shared-memory.json`))
|
||||
|
||||
// Get pipeline tasks in creation order (= dependency order)
|
||||
const allTasks = TaskList()
|
||||
const pipelineTasks = allTasks
|
||||
.filter(t => t.owner && t.owner !== 'coordinator')
|
||||
.sort((a, b) => Number(a.id) - Number(b.id))
|
||||
|
||||
// Auto mode detection
|
||||
const autoYes = /\b(-y|--yes)\b/.test(args)
|
||||
```
|
||||
|
||||
### Step 2: Sequential Stage Execution (Stop-Wait)
|
||||
|
||||
> **Core**: Spawn one worker per stage, block until return.
|
||||
> Worker return = stage complete. No sleep, no polling.
|
||||
|
||||
```javascript
|
||||
for (const stageTask of pipelineTasks) {
|
||||
// 1. Extract stage prefix -> determine worker role
|
||||
const stagePrefix = stageTask.subject.match(/^(\w+)-/)?.[1]
|
||||
const workerConfig = STAGE_WORKER_MAP[stagePrefix]
|
||||
|
||||
if (!workerConfig) {
|
||||
mcp__ccw-tools__team_msg({
|
||||
operation: "log", team: teamName, from: "coordinator",
|
||||
to: "user", type: "error",
|
||||
summary: `[coordinator] Unknown stage prefix: ${stagePrefix}, skipping`
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// 2. Mark task in progress
|
||||
TaskUpdate({ taskId: stageTask.id, status: 'in_progress' })
|
||||
|
||||
mcp__ccw-tools__team_msg({
|
||||
operation: "log", team: teamName, from: "coordinator",
|
||||
to: workerConfig.role, type: "stage_transition",
|
||||
summary: `[coordinator] Starting stage: ${stageTask.subject} -> ${workerConfig.role}`
|
||||
})
|
||||
|
||||
// 3. Build worker arguments
|
||||
const workerArgs = buildWorkerArgs(stageTask, workerConfig)
|
||||
|
||||
// 4. Spawn worker via Skill — blocks until return (Stop-Wait core)
|
||||
Skill(skill="team-review", args=workerArgs)
|
||||
|
||||
// 5. Worker returned — check result
|
||||
const taskState = TaskGet({ taskId: stageTask.id })
|
||||
|
||||
if (taskState.status !== 'completed') {
|
||||
const action = handleStageFailure(stageTask, taskState, workerConfig, autoYes)
|
||||
if (action === 'abort') break
|
||||
if (action === 'skip') continue
|
||||
} else {
|
||||
mcp__ccw-tools__team_msg({
|
||||
operation: "log", team: teamName, from: "coordinator",
|
||||
to: "user", type: "stage_transition",
|
||||
summary: `[coordinator] Stage complete: ${stageTask.subject}`
|
||||
})
|
||||
}
|
||||
|
||||
// 6. Post-stage: After SCAN check findings
|
||||
if (stagePrefix === 'SCAN') {
|
||||
const mem = JSON.parse(Read(`${sessionFolder}/shared-memory.json`))
|
||||
if ((mem.findings_count || 0) === 0) {
|
||||
mcp__ccw-tools__team_msg({ operation: "log", team: teamName, from: "coordinator",
|
||||
to: "user", type: "pipeline_complete",
|
||||
summary: `[coordinator] 0 findings. Code is clean. Skipping review/fix.` })
|
||||
for (const r of pipelineTasks.slice(pipelineTasks.indexOf(stageTask) + 1))
|
||||
TaskUpdate({ taskId: r.id, status: 'deleted' })
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Post-stage: After REV confirm fix scope
|
||||
if (stagePrefix === 'REV' && pipelineMode === 'full') {
|
||||
const mem = JSON.parse(Read(`${sessionFolder}/shared-memory.json`))
|
||||
|
||||
if (!autoYes) {
|
||||
const conf = AskUserQuestion({ questions: [{
|
||||
question: `${mem.findings_count || 0} findings reviewed. Proceed with fix?`,
|
||||
header: "Fix Confirmation", multiSelect: false,
|
||||
options: [
|
||||
{ label: "Fix all", description: "All actionable findings" },
|
||||
{ label: "Fix critical/high only", description: "Severity filter" },
|
||||
{ label: "Skip fix", description: "No code changes" }
|
||||
]
|
||||
}] })
|
||||
|
||||
if (conf["Fix Confirmation"] === "Skip fix") {
|
||||
pipelineTasks.filter(t => t.subject.startsWith('FIX-'))
|
||||
.forEach(ft => TaskUpdate({ taskId: ft.id, status: 'deleted' }))
|
||||
break
|
||||
}
|
||||
mem.fix_scope = conf["Fix Confirmation"] === "Fix critical/high only" ? 'critical,high' : 'all'
|
||||
Write(`${sessionFolder}/shared-memory.json`, JSON.stringify(mem, null, 2))
|
||||
}
|
||||
|
||||
Write(`${sessionFolder}/fix/fix-manifest.json`, JSON.stringify({
|
||||
source: `${sessionFolder}/review/review-report.json`,
|
||||
scope: mem.fix_scope || 'all', session: sessionFolder
|
||||
}, null, 2))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2.1: Worker Argument Builder
|
||||
|
||||
```javascript
|
||||
function buildWorkerArgs(stageTask, workerConfig) {
|
||||
const stagePrefix = stageTask.subject.match(/^(\w+)-/)?.[1]
|
||||
let workerArgs = `${workerConfig.skillArgs} --session ${sessionFolder}`
|
||||
|
||||
if (stagePrefix === 'SCAN') {
|
||||
workerArgs += ` ${target} --dimensions ${dimensions.join(',')}`
|
||||
if (stageTask.description?.includes('quick: true')) workerArgs += ' -q'
|
||||
} else if (stagePrefix === 'REV') {
|
||||
workerArgs += ` --input ${sessionFolder}/scan/scan-results.json --dimensions ${dimensions.join(',')}`
|
||||
} else if (stagePrefix === 'FIX') {
|
||||
workerArgs += ` --input ${sessionFolder}/fix/fix-manifest.json`
|
||||
}
|
||||
|
||||
if (autoYes) workerArgs += ' -y'
|
||||
return workerArgs
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2.2: Stage Failure Handler
|
||||
|
||||
```javascript
|
||||
function handleStageFailure(stageTask, taskState, workerConfig, autoYes) {
|
||||
if (autoYes) {
|
||||
mcp__ccw-tools__team_msg({ operation: "log", team: teamName, from: "coordinator",
|
||||
to: "user", type: "error",
|
||||
summary: `[coordinator] [auto] ${stageTask.subject} incomplete, skipping` })
|
||||
TaskUpdate({ taskId: stageTask.id, status: 'deleted' })
|
||||
return 'skip'
|
||||
}
|
||||
|
||||
const decision = AskUserQuestion({ questions: [{
|
||||
question: `Stage "${stageTask.subject}" incomplete (${taskState.status}). Action?`,
|
||||
header: "Stage Failure", multiSelect: false,
|
||||
options: [
|
||||
{ label: "Retry", description: "Re-spawn worker" },
|
||||
{ label: "Skip", description: "Continue pipeline" },
|
||||
{ label: "Abort", description: "Stop pipeline" }
|
||||
]
|
||||
}] })
|
||||
|
||||
const answer = decision["Stage Failure"]
|
||||
if (answer === "Retry") {
|
||||
TaskUpdate({ taskId: stageTask.id, status: 'in_progress' })
|
||||
Skill(skill="team-review", args=buildWorkerArgs(stageTask, workerConfig))
|
||||
if (TaskGet({ taskId: stageTask.id }).status !== 'completed')
|
||||
TaskUpdate({ taskId: stageTask.id, status: 'deleted' })
|
||||
return 'retried'
|
||||
} else if (answer === "Skip") {
|
||||
TaskUpdate({ taskId: stageTask.id, status: 'deleted' })
|
||||
return 'skip'
|
||||
} else {
|
||||
mcp__ccw-tools__team_msg({ operation: "log", team: teamName, from: "coordinator",
|
||||
to: "user", type: "error",
|
||||
summary: `[coordinator] User aborted at: ${stageTask.subject}` })
|
||||
return 'abort'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Finalize
|
||||
|
||||
```javascript
|
||||
const finalMemory = JSON.parse(Read(`${sessionFolder}/shared-memory.json`))
|
||||
finalMemory.pipeline_status = 'complete'
|
||||
finalMemory.completed_at = new Date().toISOString()
|
||||
Write(`${sessionFolder}/shared-memory.json`, JSON.stringify(finalMemory, null, 2))
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Scenario | Resolution |
|
||||
|----------|------------|
|
||||
| Worker incomplete (interactive) | AskUser: Retry / Skip / Abort |
|
||||
| Worker incomplete (auto) | Auto-skip, log warning |
|
||||
| 0 findings after scan | Skip remaining stages |
|
||||
| User declines fix | Delete FIX tasks, report review-only |
|
||||
218
.claude/skills/team-review/roles/coordinator/role.md
Normal file
218
.claude/skills/team-review/roles/coordinator/role.md
Normal file
@@ -0,0 +1,218 @@
|
||||
# Role: coordinator
|
||||
|
||||
Code review team coordinator. Orchestrates the scan-review-fix pipeline (CP-1 Linear): parse target, detect mode, dispatch task chain, drive sequential stage execution via Stop-Wait, aggregate results.
|
||||
|
||||
## Role Identity
|
||||
|
||||
- **Name**: `coordinator`
|
||||
- **Task Prefix**: RC (coordinator creates tasks, doesn't receive them)
|
||||
- **Responsibility**: Orchestration
|
||||
- **Communication**: SendMessage to all teammates
|
||||
- **Output Tag**: `[coordinator]`
|
||||
|
||||
## Role Boundaries
|
||||
|
||||
### MUST
|
||||
|
||||
- All output (SendMessage, team_msg, logs) prefixed with `[coordinator]`
|
||||
- Only: target parsing, mode detection, task creation/dispatch, stage monitoring, result aggregation
|
||||
- Create tasks via TaskCreate and assign to worker roles
|
||||
- Drive pipeline stages via Stop-Wait (synchronous Skill() calls)
|
||||
|
||||
### MUST NOT
|
||||
|
||||
- Run analysis tools directly (semgrep, eslint, tsc, etc.)
|
||||
- Modify source code files
|
||||
- Perform code review analysis
|
||||
- Bypass worker roles to do delegated work
|
||||
- Omit `[coordinator]` prefix on any output
|
||||
|
||||
> **Core principle**: coordinator is the orchestrator, not the executor. All actual work delegated to scanner/reviewer/fixer via task chain.
|
||||
|
||||
## Message Types
|
||||
|
||||
| Type | Direction | Trigger | Description |
|
||||
|------|-----------|---------|-------------|
|
||||
| `dispatch_ready` | coordinator -> all | Phase 3 done | Task chain created, pipeline ready |
|
||||
| `stage_transition` | coordinator -> worker | Stage unblocked | Next stage starting |
|
||||
| `pipeline_complete` | coordinator -> user | All stages done | Pipeline finished, summary ready |
|
||||
| `error` | coordinator -> user | Stage failure | Blocking issue requiring attention |
|
||||
|
||||
## Message Bus
|
||||
|
||||
Before every SendMessage, call `mcp__ccw-tools__team_msg` to log:
|
||||
|
||||
```javascript
|
||||
mcp__ccw-tools__team_msg({
|
||||
operation: "log", team: "team-review", from: "coordinator",
|
||||
to: "user", type: "dispatch_ready",
|
||||
summary: "[coordinator] Task chain created, pipeline ready"
|
||||
})
|
||||
```
|
||||
|
||||
**CLI Fallback**: If unavailable, `Bash(echo JSON >> "${sessionFolder}/message-log.jsonl")`
|
||||
|
||||
## Toolbox
|
||||
|
||||
| Command | File | Phase | Description |
|
||||
|---------|------|-------|-------------|
|
||||
| `dispatch` | [commands/dispatch.md](commands/dispatch.md) | Phase 3 | Task chain creation based on mode |
|
||||
| `monitor` | [commands/monitor.md](commands/monitor.md) | Phase 4 | Stop-Wait stage execution loop |
|
||||
|
||||
## Execution (5-Phase)
|
||||
|
||||
### Phase 1: Parse Arguments & Detect Mode
|
||||
|
||||
```javascript
|
||||
const args = "$ARGUMENTS"
|
||||
|
||||
// Extract task description (strip all flags)
|
||||
const taskDescription = args
|
||||
.replace(/--\w+[=\s]+\S+/g, '').replace(/\b(-y|--yes|-q|--quick|--full|--fix)\b/g, '').trim()
|
||||
|
||||
// Mode detection
|
||||
function detectMode(args) {
|
||||
if (/\b--fix\b/.test(args)) return 'fix-only'
|
||||
if (/\b--full\b/.test(args)) return 'full'
|
||||
if (/\b(-q|--quick)\b/.test(args)) return 'quick'
|
||||
return 'default' // scan + review
|
||||
}
|
||||
|
||||
const pipelineMode = detectMode(args)
|
||||
|
||||
// Auto mode (skip confirmations)
|
||||
const autoYes = /\b(-y|--yes)\b/.test(args)
|
||||
|
||||
// Dimension filter (default: all 4)
|
||||
const dimMatch = args.match(/--dimensions[=\s]+([\w,]+)/)
|
||||
const dimensions = dimMatch ? dimMatch[1].split(',') : ['sec', 'cor', 'perf', 'maint']
|
||||
|
||||
// Target extraction (file patterns or git changes)
|
||||
const target = taskDescription || '.'
|
||||
|
||||
// Check for existing RC-* tasks (when invoked by another coordinator)
|
||||
const existingTasks = TaskList()
|
||||
|
||||
if (!autoYes && !taskDescription) {
|
||||
AskUserQuestion({
|
||||
questions: [{
|
||||
question: "What code should be reviewed?",
|
||||
header: "Review Target",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "Custom", description: "Enter file patterns or paths" },
|
||||
{ label: "Uncommitted changes", description: "Review git diff" },
|
||||
{ label: "Full project scan", description: "Scan entire project" }
|
||||
]
|
||||
}]
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 2: Initialize Session
|
||||
|
||||
```javascript
|
||||
const teamName = "team-review"
|
||||
const sessionSlug = target.slice(0, 30).replace(/[^a-zA-Z0-9]/g, '-').replace(/-+/g, '-')
|
||||
const sessionDate = new Date().toISOString().slice(0, 10)
|
||||
const workflowId = `RC-${sessionSlug}-${sessionDate}`
|
||||
const sessionFolder = `.workflow/.team-review/${workflowId}`
|
||||
|
||||
Bash(`mkdir -p "${sessionFolder}/scan" "${sessionFolder}/review" "${sessionFolder}/fix"`)
|
||||
|
||||
// Initialize shared memory
|
||||
Write(`${sessionFolder}/shared-memory.json`, JSON.stringify({
|
||||
workflow_id: workflowId, mode: pipelineMode, target, dimensions, auto: autoYes,
|
||||
scan_results: null, review_results: null, fix_results: null,
|
||||
findings_count: 0, fixed_count: 0
|
||||
}, null, 2))
|
||||
|
||||
// Workers spawned per-stage in Phase 4 via Stop-Wait Skill()
|
||||
goto Phase3
|
||||
```
|
||||
|
||||
### Phase 3: Create Task Chain
|
||||
|
||||
```javascript
|
||||
Output("[coordinator] Phase 3: Task Dispatching")
|
||||
Read("commands/dispatch.md") // Full task chain creation logic
|
||||
goto Phase4
|
||||
```
|
||||
|
||||
**Default** (scan+review): `SCAN-001 -> REV-001`
|
||||
**Full** (scan+review+fix): `SCAN-001 -> REV-001 -> FIX-001`
|
||||
**Fix-Only**: `FIX-001`
|
||||
**Quick**: `SCAN-001 (quick=true)`
|
||||
|
||||
### Phase 4: Sequential Stage Execution (Stop-Wait)
|
||||
|
||||
```javascript
|
||||
// Read commands/monitor.md for full implementation
|
||||
Read("commands/monitor.md")
|
||||
```
|
||||
|
||||
> **Strategy**: Spawn workers sequentially via Skill(), synchronous blocking until return. Worker return = stage complete. No polling.
|
||||
>
|
||||
> - FORBIDDEN: `while` loop + `sleep` + check status
|
||||
> - REQUIRED: Synchronous `Skill()` call = natural callback
|
||||
|
||||
**Stage Flow**:
|
||||
|
||||
| Stage | Worker | On Complete |
|
||||
|-------|--------|-------------|
|
||||
| SCAN-001 | scanner | Check findings count -> start REV |
|
||||
| REV-001 | reviewer | Generate review report -> [user confirm] -> start FIX |
|
||||
| FIX-001 | fixer | Execute fixes -> verify |
|
||||
|
||||
### Phase 5: Aggregate Results & Report
|
||||
|
||||
```javascript
|
||||
const memory = JSON.parse(Read(`${sessionFolder}/shared-memory.json`))
|
||||
const fixRate = memory.findings_count > 0
|
||||
? Math.round((memory.fixed_count / memory.findings_count) * 100) : 0
|
||||
|
||||
const report = {
|
||||
mode: pipelineMode, target, dimensions,
|
||||
findings_total: memory.findings_count || 0,
|
||||
by_severity: memory.review_results?.by_severity || {},
|
||||
by_dimension: memory.review_results?.by_dimension || {},
|
||||
fixed_count: memory.fixed_count || 0,
|
||||
fix_rate: fixRate
|
||||
}
|
||||
|
||||
mcp__ccw-tools__team_msg({
|
||||
operation: "log", team: teamName, from: "coordinator",
|
||||
to: "user", type: "pipeline_complete",
|
||||
summary: `[coordinator] Complete: ${report.findings_total} findings, ${report.fixed_count} fixed (${fixRate}%)`
|
||||
})
|
||||
|
||||
SendMessage({
|
||||
content: `## [coordinator] Review Report\n\n${JSON.stringify(report, null, 2)}`,
|
||||
summary: `[coordinator] ${report.findings_total} findings, ${report.fixed_count} fixed`
|
||||
})
|
||||
|
||||
if (!autoYes) {
|
||||
AskUserQuestion({
|
||||
questions: [{
|
||||
question: "Pipeline complete. Next:",
|
||||
header: "Next",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "New target", description: "Review different files" },
|
||||
{ label: "Deep review", description: "Re-review stricter" },
|
||||
{ label: "Done", description: "Close session" }
|
||||
]
|
||||
}]
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Scenario | Resolution |
|
||||
|----------|------------|
|
||||
| Scanner finds 0 findings | Report clean, skip review + fix stages |
|
||||
| Worker returns incomplete | Ask user: retry / skip / abort |
|
||||
| Fix verification fails | Log warning, report partial results |
|
||||
| Session folder missing | Re-create and log warning |
|
||||
| Target path invalid | AskUserQuestion for corrected path |
|
||||
@@ -0,0 +1,186 @@
|
||||
# Command: semantic-scan
|
||||
|
||||
> LLM-based semantic analysis via CLI. Supplements toolchain findings with issues that static tools cannot detect: business logic flaws, architectural problems, complex security patterns.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Phase 3 of Scanner, Standard mode, Step B
|
||||
- Runs AFTER toolchain-scan completes (needs its output to avoid duplication)
|
||||
- Quick mode does NOT use this command
|
||||
|
||||
**Trigger conditions**:
|
||||
- SCAN-* task in Phase 3 with `quickMode === false`
|
||||
- toolchain-scan.md has completed (toolchain-findings.json exists or empty)
|
||||
|
||||
## Strategy
|
||||
|
||||
### Delegation Mode
|
||||
|
||||
**Mode**: CLI Fan-out (single gemini agent, analysis only)
|
||||
|
||||
### Tool Fallback Chain
|
||||
|
||||
```
|
||||
gemini (primary) -> qwen (fallback) -> codex (fallback)
|
||||
```
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### Step 1: Prepare Context
|
||||
|
||||
Build the CLI prompt with target files and a summary of toolchain findings to avoid duplication.
|
||||
|
||||
```javascript
|
||||
// Read toolchain findings for dedup context
|
||||
let toolFindings = []
|
||||
try {
|
||||
toolFindings = JSON.parse(Read(`${sessionFolder}/scan/toolchain-findings.json`))
|
||||
} catch { /* no toolchain findings */ }
|
||||
|
||||
// Build toolchain summary for dedup (compact: file:line:rule per line)
|
||||
const toolSummary = toolFindings.length > 0
|
||||
? toolFindings.slice(0, 50).map(f =>
|
||||
`${f.location?.file}:${f.location?.line} [${f.source}] ${f.title}`
|
||||
).join('\n')
|
||||
: '(no toolchain findings)'
|
||||
|
||||
// Build target file list for CLI context
|
||||
// Limit to reasonable size for CLI prompt
|
||||
const fileList = targetFiles.slice(0, 100)
|
||||
const targetPattern = fileList.length <= 20
|
||||
? fileList.join(' ')
|
||||
: `${target}/**/*.{ts,tsx,js,jsx,py,go,java,rs}`
|
||||
|
||||
// Map requested dimensions to scan focus areas
|
||||
const DIM_FOCUS = {
|
||||
sec: 'Security: business logic vulnerabilities, privilege escalation, sensitive data flow, auth bypass, injection beyond simple patterns',
|
||||
cor: 'Correctness: logic errors, unhandled exception paths, state management bugs, race conditions, incorrect algorithm implementation',
|
||||
perf: 'Performance: algorithm complexity (O(n^2)+), N+1 queries, unnecessary sync operations, memory leaks, missing caching opportunities',
|
||||
maint: 'Maintainability: architectural coupling, abstraction leaks, project convention violations, dead code paths, excessive complexity'
|
||||
}
|
||||
|
||||
const focusAreas = dimensions
|
||||
.map(d => DIM_FOCUS[d])
|
||||
.filter(Boolean)
|
||||
.map((desc, i) => `${i + 1}. ${desc}`)
|
||||
.join('\n')
|
||||
```
|
||||
|
||||
### Step 2: Execute CLI Scan
|
||||
|
||||
```javascript
|
||||
const maxPerDimension = 5
|
||||
const minSeverity = 'medium'
|
||||
|
||||
const cliPrompt = `PURPOSE: Supplement toolchain scan with semantic analysis that static tools cannot detect. Find logic errors, architectural issues, and complex vulnerability patterns.
|
||||
TASK:
|
||||
${focusAreas}
|
||||
MODE: analysis
|
||||
CONTEXT: @${targetPattern}
|
||||
Toolchain already detected these issues (DO NOT repeat them):
|
||||
${toolSummary}
|
||||
EXPECTED: Respond with ONLY a JSON array (no markdown, no explanation). Each element:
|
||||
{"dimension":"security|correctness|performance|maintainability","category":"<sub-category>","severity":"critical|high|medium","title":"<concise title>","description":"<detailed explanation>","location":{"file":"<path>","line":<number>,"end_line":<number>,"code_snippet":"<relevant code>"},"source":"llm","suggested_fix":"<how to fix>","effort":"low|medium|high","confidence":"high|medium|low"}
|
||||
CONSTRAINTS: Max ${maxPerDimension} findings per dimension | Only ${minSeverity} severity and above | Do not duplicate toolchain findings | Focus on issues tools CANNOT detect | Return raw JSON array only`
|
||||
|
||||
let cliOutput = null
|
||||
let cliTool = 'gemini'
|
||||
|
||||
// Try primary tool
|
||||
try {
|
||||
cliOutput = Bash(
|
||||
`ccw cli -p "${cliPrompt.replace(/"/g, '\\"')}" --tool gemini --mode analysis --rule analysis-review-code-quality`,
|
||||
{ timeout: 300000 }
|
||||
)
|
||||
} catch {
|
||||
// Fallback to qwen
|
||||
try {
|
||||
cliTool = 'qwen'
|
||||
cliOutput = Bash(
|
||||
`ccw cli -p "${cliPrompt.replace(/"/g, '\\"')}" --tool qwen --mode analysis`,
|
||||
{ timeout: 300000 }
|
||||
)
|
||||
} catch {
|
||||
// Fallback to codex
|
||||
try {
|
||||
cliTool = 'codex'
|
||||
cliOutput = Bash(
|
||||
`ccw cli -p "${cliPrompt.replace(/"/g, '\\"')}" --tool codex --mode analysis`,
|
||||
{ timeout: 300000 }
|
||||
)
|
||||
} catch {
|
||||
// All CLI tools failed
|
||||
cliOutput = null
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Parse & Validate Output
|
||||
|
||||
```javascript
|
||||
let semanticFindings = []
|
||||
|
||||
if (cliOutput) {
|
||||
try {
|
||||
// Extract JSON array from CLI output (may have surrounding text)
|
||||
const jsonMatch = cliOutput.match(/\[[\s\S]*\]/)
|
||||
if (jsonMatch) {
|
||||
const parsed = JSON.parse(jsonMatch[0])
|
||||
|
||||
// Validate each finding against schema
|
||||
semanticFindings = parsed.filter(f => {
|
||||
// Required fields check
|
||||
if (!f.dimension || !f.title || !f.location?.file) return false
|
||||
// Dimension must be valid
|
||||
if (!['security', 'correctness', 'performance', 'maintainability'].includes(f.dimension)) return false
|
||||
// Severity must be valid and meet minimum
|
||||
const validSev = ['critical', 'high', 'medium']
|
||||
if (!validSev.includes(f.severity)) return false
|
||||
return true
|
||||
}).map(f => ({
|
||||
dimension: f.dimension,
|
||||
category: f.category || 'general',
|
||||
severity: f.severity,
|
||||
title: f.title,
|
||||
description: f.description || f.title,
|
||||
location: {
|
||||
file: f.location.file,
|
||||
line: f.location.line || 1,
|
||||
end_line: f.location.end_line || f.location.line || 1,
|
||||
code_snippet: f.location.code_snippet || ''
|
||||
},
|
||||
source: 'llm',
|
||||
tool_rule: null,
|
||||
suggested_fix: f.suggested_fix || '',
|
||||
effort: ['low', 'medium', 'high'].includes(f.effort) ? f.effort : 'medium',
|
||||
confidence: ['high', 'medium', 'low'].includes(f.confidence) ? f.confidence : 'medium'
|
||||
}))
|
||||
}
|
||||
} catch {
|
||||
// JSON parse failed - log and continue with empty
|
||||
}
|
||||
}
|
||||
|
||||
// Enforce per-dimension limits
|
||||
const dimCounts = {}
|
||||
semanticFindings = semanticFindings.filter(f => {
|
||||
dimCounts[f.dimension] = (dimCounts[f.dimension] || 0) + 1
|
||||
return dimCounts[f.dimension] <= maxPerDimension
|
||||
})
|
||||
|
||||
// Write output
|
||||
Write(`${sessionFolder}/scan/semantic-findings.json`,
|
||||
JSON.stringify(semanticFindings, null, 2))
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Scenario | Resolution |
|
||||
|----------|------------|
|
||||
| gemini CLI fails | Fallback to qwen, then codex |
|
||||
| All CLI tools fail | Log warning, write empty findings array (toolchain results still valid) |
|
||||
| CLI output not valid JSON | Attempt regex extraction, else empty findings |
|
||||
| Findings exceed per-dimension limit | Truncate to max per dimension |
|
||||
| Invalid dimension/severity in output | Filter out invalid entries |
|
||||
| CLI timeout (>5 min) | Kill, log warning, return empty findings |
|
||||
@@ -0,0 +1,187 @@
|
||||
# Command: toolchain-scan
|
||||
|
||||
> Parallel static analysis tool execution. Detects available tools, runs concurrently, normalizes output into standardized findings.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Phase 3 of Scanner, Standard mode, Step A
|
||||
- At least one tool detected in Phase 2
|
||||
- Quick mode does NOT use this command
|
||||
|
||||
## Strategy
|
||||
|
||||
### Delegation Mode
|
||||
|
||||
**Mode**: Direct (Bash parallel execution)
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### Step 1: Build Tool Commands
|
||||
|
||||
```javascript
|
||||
if (!Object.values(toolchain).some(Boolean)) {
|
||||
Write(`${sessionFolder}/scan/toolchain-findings.json`, '[]')
|
||||
return
|
||||
}
|
||||
|
||||
const tmpDir = `${sessionFolder}/scan/tmp`
|
||||
Bash(`mkdir -p "${tmpDir}"`)
|
||||
|
||||
const cmds = []
|
||||
|
||||
if (toolchain.tsc)
|
||||
cmds.push(`(cd "${projectRoot}" && npx tsc --noEmit --pretty false 2>&1 | head -500 > "${tmpDir}/tsc.txt") &`)
|
||||
if (toolchain.eslint)
|
||||
cmds.push(`(cd "${projectRoot}" && npx eslint "${target}" --format json --no-error-on-unmatched-pattern 2>/dev/null | head -5000 > "${tmpDir}/eslint.json") &`)
|
||||
if (toolchain.semgrep)
|
||||
cmds.push(`(cd "${projectRoot}" && semgrep --config auto --json "${target}" 2>/dev/null | head -5000 > "${tmpDir}/semgrep.json") &`)
|
||||
if (toolchain.ruff)
|
||||
cmds.push(`(cd "${projectRoot}" && ruff check "${target}" --output-format json 2>/dev/null | head -5000 > "${tmpDir}/ruff.json") &`)
|
||||
if (toolchain.mypy)
|
||||
cmds.push(`(cd "${projectRoot}" && mypy "${target}" --output json 2>/dev/null | head -2000 > "${tmpDir}/mypy.txt") &`)
|
||||
if (toolchain.npmAudit)
|
||||
cmds.push(`(cd "${projectRoot}" && npm audit --json 2>/dev/null | head -5000 > "${tmpDir}/audit.json") &`)
|
||||
```
|
||||
|
||||
### Step 2: Parallel Execution
|
||||
|
||||
```javascript
|
||||
Bash(cmds.join('\n') + '\nwait', { timeout: 300000 })
|
||||
```
|
||||
|
||||
### Step 3: Parse Tool Outputs
|
||||
|
||||
Each parser normalizes to: `{ dimension, category, severity, title, description, location:{file,line,end_line,code_snippet}, source, tool_rule, suggested_fix, effort, confidence }`
|
||||
|
||||
```javascript
|
||||
const findings = []
|
||||
|
||||
// --- tsc: file(line,col): error TSxxxx: message ---
|
||||
if (toolchain.tsc) {
|
||||
try {
|
||||
const out = Read(`${tmpDir}/tsc.txt`)
|
||||
const re = /^(.+)\((\d+),\d+\):\s+(error|warning)\s+(TS\d+):\s+(.+)$/gm
|
||||
let m; while ((m = re.exec(out)) !== null) {
|
||||
findings.push({
|
||||
dimension: 'correctness', category: 'type-safety',
|
||||
severity: m[3] === 'error' ? 'high' : 'medium',
|
||||
title: `tsc ${m[4]}: ${m[5].slice(0,80)}`, description: m[5],
|
||||
location: { file: m[1], line: +m[2] },
|
||||
source: 'tool:tsc', tool_rule: m[4], suggested_fix: '',
|
||||
effort: 'low', confidence: 'high'
|
||||
})
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// --- eslint: JSON array of {filePath, messages[{severity,ruleId,message,line}]} ---
|
||||
if (toolchain.eslint) {
|
||||
try {
|
||||
const data = JSON.parse(Read(`${tmpDir}/eslint.json`))
|
||||
for (const f of data) for (const msg of (f.messages || [])) {
|
||||
const isErr = msg.severity === 2
|
||||
findings.push({
|
||||
dimension: isErr ? 'correctness' : 'maintainability',
|
||||
category: isErr ? 'bug' : 'code-smell',
|
||||
severity: isErr ? 'high' : 'medium',
|
||||
title: `eslint ${msg.ruleId || '?'}: ${(msg.message||'').slice(0,80)}`,
|
||||
description: msg.message || '',
|
||||
location: { file: f.filePath, line: msg.line || 1, end_line: msg.endLine, code_snippet: msg.source || '' },
|
||||
source: 'tool:eslint', tool_rule: msg.ruleId || null,
|
||||
suggested_fix: msg.fix ? 'Auto-fixable' : '', effort: msg.fix ? 'low' : 'medium', confidence: 'high'
|
||||
})
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// --- semgrep: {results[{path,start:{line},end:{line},check_id,extra:{severity,message,fix,lines}}]} ---
|
||||
if (toolchain.semgrep) {
|
||||
try {
|
||||
const data = JSON.parse(Read(`${tmpDir}/semgrep.json`))
|
||||
const smap = { ERROR:'high', WARNING:'medium', INFO:'low' }
|
||||
for (const r of (data.results || [])) {
|
||||
findings.push({
|
||||
dimension: 'security', category: r.check_id?.split('.').pop() || 'generic',
|
||||
severity: smap[r.extra?.severity] || 'medium',
|
||||
title: `semgrep: ${(r.extra?.message || r.check_id || '').slice(0,80)}`,
|
||||
description: r.extra?.message || '', location: { file: r.path, line: r.start?.line || 1, end_line: r.end?.line, code_snippet: r.extra?.lines || '' },
|
||||
source: 'tool:semgrep', tool_rule: r.check_id || null,
|
||||
suggested_fix: r.extra?.fix || '', effort: 'medium', confidence: smap[r.extra?.severity] === 'high' ? 'high' : 'medium'
|
||||
})
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// --- ruff: [{code,message,filename,location:{row},end_location:{row},fix}] ---
|
||||
if (toolchain.ruff) {
|
||||
try {
|
||||
const data = JSON.parse(Read(`${tmpDir}/ruff.json`))
|
||||
for (const item of data) {
|
||||
const code = item.code || ''
|
||||
const dim = code.startsWith('S') ? 'security' : (code.startsWith('F') || code.startsWith('B')) ? 'correctness' : 'maintainability'
|
||||
findings.push({
|
||||
dimension: dim, category: dim === 'security' ? 'input-validation' : dim === 'correctness' ? 'bug' : 'code-smell',
|
||||
severity: (code.startsWith('S') || code.startsWith('F')) ? 'high' : 'medium',
|
||||
title: `ruff ${code}: ${(item.message||'').slice(0,80)}`, description: item.message || '',
|
||||
location: { file: item.filename, line: item.location?.row || 1, end_line: item.end_location?.row },
|
||||
source: 'tool:ruff', tool_rule: code, suggested_fix: item.fix?.message || '',
|
||||
effort: item.fix ? 'low' : 'medium', confidence: 'high'
|
||||
})
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// --- npm audit: {vulnerabilities:{name:{severity,title,fixAvailable,via}}} ---
|
||||
if (toolchain.npmAudit) {
|
||||
try {
|
||||
const data = JSON.parse(Read(`${tmpDir}/audit.json`))
|
||||
const smap = { critical:'critical', high:'high', moderate:'medium', low:'low', info:'info' }
|
||||
for (const [,v] of Object.entries(data.vulnerabilities || {})) {
|
||||
findings.push({
|
||||
dimension: 'security', category: 'dependency', severity: smap[v.severity] || 'medium',
|
||||
title: `npm audit: ${v.name} - ${(v.title || '').slice(0,80)}`,
|
||||
description: v.title || `Vulnerable: ${v.name}`,
|
||||
location: { file: 'package.json', line: 1 },
|
||||
source: 'tool:npm-audit', tool_rule: null,
|
||||
suggested_fix: v.fixAvailable ? 'npm audit fix' : 'Manual resolution',
|
||||
effort: v.fixAvailable ? 'low' : 'high', confidence: 'high'
|
||||
})
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// --- mypy: file:line: error: message [code] ---
|
||||
if (toolchain.mypy) {
|
||||
try {
|
||||
const out = Read(`${tmpDir}/mypy.txt`)
|
||||
const re = /^(.+):(\d+):\s+(error|warning):\s+(.+?)(?:\s+\[(\w[\w-]*)\])?$/gm
|
||||
let m; while ((m = re.exec(out)) !== null) {
|
||||
if (m[3] === 'note') continue
|
||||
findings.push({
|
||||
dimension: 'correctness', category: 'type-safety',
|
||||
severity: m[3] === 'error' ? 'high' : 'medium',
|
||||
title: `mypy${m[5] ? ` [${m[5]}]` : ''}: ${m[4].slice(0,80)}`, description: m[4],
|
||||
location: { file: m[1], line: +m[2] },
|
||||
source: 'tool:mypy', tool_rule: m[5] || null, suggested_fix: '',
|
||||
effort: 'low', confidence: 'high'
|
||||
})
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Write Output
|
||||
|
||||
```javascript
|
||||
Write(`${sessionFolder}/scan/toolchain-findings.json`, JSON.stringify(findings, null, 2))
|
||||
Bash(`rm -rf "${tmpDir}"`)
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Scenario | Resolution |
|
||||
|----------|------------|
|
||||
| Tool not found at runtime | Skip gracefully, continue with others |
|
||||
| Tool times out (>5 min) | Killed by `wait` timeout, partial output used |
|
||||
| Tool output unparseable | try/catch skips that tool's findings |
|
||||
| All tools fail | Empty array written, semantic-scan covers all dimensions |
|
||||
160
.claude/skills/team-review/roles/scanner/role.md
Normal file
160
.claude/skills/team-review/roles/scanner/role.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# Role: scanner
|
||||
|
||||
Toolchain + LLM semantic scan producing structured findings. Static analysis tools in parallel, then LLM for issues tools miss.
|
||||
|
||||
## Role Identity
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Name | `scanner` |
|
||||
| Task Prefix | `SCAN-*` |
|
||||
| Type | read-only-analysis |
|
||||
| Output Tag | `[scanner]` |
|
||||
| Communication | coordinator only |
|
||||
|
||||
## Role Boundaries
|
||||
|
||||
**MUST**: Only `SCAN-*` tasks. All output `[scanner]`-prefixed. Write only to session scan dir. IDs: SEC-001, COR-001, PRF-001, MNT-001.
|
||||
|
||||
**MUST NOT**: Modify source files. Fix issues. Create tasks for other roles. Contact reviewer/fixer directly.
|
||||
|
||||
## Messages: `scan_progress` (milestone), `scan_complete` (Phase 5), `error`
|
||||
|
||||
## Message Bus
|
||||
|
||||
```javascript
|
||||
mcp__ccw-tools__team_msg({ operation:"log", team:"team-review", from:"scanner", to:"coordinator", type:"scan_complete", summary:"[scanner] ..." })
|
||||
// Fallback: Bash(echo JSON >> "${sessionFolder}/message-log.jsonl")
|
||||
```
|
||||
|
||||
## Toolbox
|
||||
|
||||
| Command | File | Phase |
|
||||
|---------|------|-------|
|
||||
| `toolchain-scan` | [commands/toolchain-scan.md](commands/toolchain-scan.md) | 3A: Parallel static analysis |
|
||||
| `semantic-scan` | [commands/semantic-scan.md](commands/semantic-scan.md) | 3B: LLM analysis via CLI (gemini/qwen/codex fallback) |
|
||||
|
||||
## Execution (5-Phase)
|
||||
|
||||
### Phase 1: Task Discovery
|
||||
|
||||
```javascript
|
||||
const tasks = TaskList()
|
||||
const myTasks = tasks.filter(t =>
|
||||
t.subject.startsWith('SCAN-') &&
|
||||
t.status !== 'completed' &&
|
||||
(t.blockedBy || []).length === 0
|
||||
)
|
||||
if (myTasks.length === 0) return
|
||||
|
||||
const task = TaskGet({ taskId: myTasks[0].id })
|
||||
TaskUpdate({ taskId: task.id, status: 'in_progress' })
|
||||
|
||||
// Extract from task description
|
||||
const target = task.description.match(/target:\s*(.+)/)?.[1]?.trim() || '.'
|
||||
const dimStr = task.description.match(/dimensions:\s*(.+)/)?.[1]?.trim() || 'sec,cor,perf,maint'
|
||||
const dimensions = dimStr.split(',').map(d => d.trim())
|
||||
const quickMode = /quick:\s*true/.test(task.description)
|
||||
const sessionFolder = task.description.match(/session:\s*(.+)/)?.[1]?.trim()
|
||||
```
|
||||
|
||||
### Phase 2: Context Resolution
|
||||
|
||||
```javascript
|
||||
const targetFiles = Glob(target.includes('*') ? target : `${target}/**/*`)
|
||||
.filter(f => /\.(ts|tsx|js|jsx|py|go|java|rs)$/.test(f))
|
||||
if (targetFiles.length === 0) { /* report error, complete task */ return }
|
||||
|
||||
// Detect toolchain: check config files + tool availability
|
||||
const projectRoot = Bash('git rev-parse --show-toplevel 2>/dev/null || pwd').trim()
|
||||
const chk = (c) => Bash(c).trim() === 'y'
|
||||
const toolchain = {
|
||||
tsc: chk(`test -f "${projectRoot}/tsconfig.json" && echo y || echo n`),
|
||||
eslint: chk(`(ls "${projectRoot}"/.eslintrc* "${projectRoot}"/eslint.config.* 2>/dev/null | head -1 >/dev/null && echo y) || (grep -q eslint "${projectRoot}/package.json" 2>/dev/null && echo y) || echo n`),
|
||||
semgrep: chk(`test -f "${projectRoot}/.semgrep.yml" && echo y || echo n`),
|
||||
ruff: chk(`test -f "${projectRoot}/pyproject.toml" && command -v ruff >/dev/null 2>&1 && echo y || echo n`),
|
||||
mypy: chk(`command -v mypy >/dev/null 2>&1 && test -f "${projectRoot}/pyproject.toml" && echo y || echo n`),
|
||||
npmAudit: chk(`test -f "${projectRoot}/package-lock.json" && echo y || echo n`)
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 3: Scan Execution
|
||||
|
||||
```javascript
|
||||
let toolchainFindings = [], semanticFindings = []
|
||||
|
||||
if (quickMode) {
|
||||
// Quick Mode: Single inline CLI, max 20 findings
|
||||
const qr = Bash(`ccw cli -p "Quick scan ${target}. Dims: ${dimensions.join(',')}. Return JSON array max 20 critical/high findings. Schema: {dimension,category,severity,title,description,location:{file,line},source:'llm',suggested_fix,effort,confidence}" --tool gemini --mode analysis --rule analysis-review-code-quality`, { timeout: 300000 })
|
||||
try { const m = qr.match(/\[[\s\S]*\]/); if (m) semanticFindings = JSON.parse(m[0]) } catch {}
|
||||
} else {
|
||||
// Standard Mode: Sequential A -> B
|
||||
Read("commands/toolchain-scan.md") // writes toolchain-findings.json
|
||||
try { toolchainFindings = JSON.parse(Read(`${sessionFolder}/scan/toolchain-findings.json`)) } catch {}
|
||||
Read("commands/semantic-scan.md") // writes semantic-findings.json (uses toolchain output for dedup)
|
||||
try { semanticFindings = JSON.parse(Read(`${sessionFolder}/scan/semantic-findings.json`)) } catch {}
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 4: Aggregate & Deduplicate
|
||||
|
||||
```javascript
|
||||
// Dedup: same file + line + dimension = duplicate
|
||||
const seen = new Set()
|
||||
const unique = [...toolchainFindings, ...semanticFindings].filter(f => {
|
||||
const key = `${f.location?.file}:${f.location?.line}:${f.dimension}`
|
||||
return !seen.has(key) && seen.add(key)
|
||||
})
|
||||
|
||||
// Assign dimension-prefixed IDs (SEC-001, COR-001, PRF-001, MNT-001)
|
||||
const DIM_PREFIX = { security:'SEC', correctness:'COR', performance:'PRF', maintainability:'MNT' }
|
||||
const dimCounters = { SEC:0, COR:0, PRF:0, MNT:0 }
|
||||
const findings = unique.map(f => {
|
||||
const pfx = DIM_PREFIX[f.dimension] || 'MNT'; dimCounters[pfx]++
|
||||
return { ...f, id: `${pfx}-${String(dimCounters[pfx]).padStart(3,'0')}`,
|
||||
severity: f.severity||'medium', confidence: f.confidence||'medium', effort: f.effort||'medium', source: f.source||'llm',
|
||||
root_cause:null, impact:null, optimization:null, fix_strategy:null, fix_complexity:null, fix_dependencies:[] }
|
||||
})
|
||||
|
||||
// Write scan-results.json (schema: scan_date, target, total_findings, by_severity, by_dimension, findings[])
|
||||
const scanResult = { scan_date: new Date().toISOString(), target, dimensions, quick_mode: quickMode,
|
||||
total_findings: findings.length,
|
||||
by_severity: findings.reduce((a,f) => ({...a,[f.severity]:(a[f.severity]||0)+1}), {}),
|
||||
by_dimension: Object.fromEntries(Object.entries(DIM_PREFIX).map(([k,v]) => [k, dimCounters[v]])),
|
||||
findings }
|
||||
Write(`${sessionFolder}/scan/scan-results.json`, JSON.stringify(scanResult, null, 2))
|
||||
```
|
||||
|
||||
### Phase 5: Update Shared Memory & Report
|
||||
|
||||
```javascript
|
||||
let sharedMemory = {}
|
||||
try { sharedMemory = JSON.parse(Read(`${sessionFolder}/shared-memory.json`)) } catch {}
|
||||
sharedMemory.scan_results = { file: `${sessionFolder}/scan/scan-results.json`, total: findings.length, by_severity: scanResult.by_severity, by_dimension: scanResult.by_dimension }
|
||||
sharedMemory.findings_count = findings.length
|
||||
Write(`${sessionFolder}/shared-memory.json`, JSON.stringify(sharedMemory, null, 2))
|
||||
|
||||
const dimSum = Object.entries(dimCounters).filter(([,v]) => v > 0).map(([k,v]) => `${k}:${v}`).join(' ')
|
||||
const top = findings.filter(f => f.severity==='critical'||f.severity==='high').slice(0,10)
|
||||
.map(f => `- **[${f.id}]** [${f.severity}] ${f.location.file}:${f.location.line} - ${f.title}`).join('\n')
|
||||
|
||||
mcp__ccw-tools__team_msg({ operation:"log", team:"team-review", from:"scanner", to:"coordinator", type:"scan_complete",
|
||||
summary:`[scanner] Scan complete: ${findings.length} findings (${dimSum})`, ref:`${sessionFolder}/scan/scan-results.json` })
|
||||
|
||||
SendMessage({ type:"message", recipient:"coordinator",
|
||||
content:`## [scanner] Scan Results\n**Target**: ${target} | **Mode**: ${quickMode?'quick':'standard'}\n### ${findings.length} findings (${dimSum})\n${top||'(clean)'}\nOutput: ${sessionFolder}/scan/scan-results.json`,
|
||||
summary:`[scanner] SCAN complete: ${findings.length} findings` })
|
||||
|
||||
TaskUpdate({ taskId: task.id, status: 'completed' })
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Scenario | Resolution |
|
||||
|----------|------------|
|
||||
| No source files match target | Report empty, complete task cleanly |
|
||||
| All toolchain tools unavailable | Skip toolchain, run semantic-only |
|
||||
| CLI semantic scan fails | Log warning, use toolchain results only |
|
||||
| Quick mode CLI timeout | Return partial or empty findings |
|
||||
| Toolchain tool crashes | Skip that tool, continue with others |
|
||||
| Session folder missing | Re-create scan subdirectory |
|
||||
82
.claude/skills/team-review/specs/dimensions.md
Normal file
82
.claude/skills/team-review/specs/dimensions.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# Review Dimensions (4-Dimension System)
|
||||
|
||||
## Security (SEC)
|
||||
|
||||
Vulnerabilities, attack surfaces, and data protection issues.
|
||||
|
||||
**Categories**: injection, authentication, authorization, data-exposure, encryption, input-validation, access-control
|
||||
|
||||
**Tool Support**: Semgrep (`--config auto`), npm audit, tsc strict mode
|
||||
**LLM Focus**: Business logic vulnerabilities, privilege escalation paths, sensitive data flows
|
||||
|
||||
**Severity Mapping**:
|
||||
- Critical: RCE, SQL injection, auth bypass, data breach
|
||||
- High: XSS, CSRF, insecure deserialization, weak crypto
|
||||
- Medium: Missing input validation, overly permissive CORS
|
||||
- Low: Informational headers, minor config issues
|
||||
|
||||
---
|
||||
|
||||
## Correctness (COR)
|
||||
|
||||
Bugs, logic errors, and type safety issues.
|
||||
|
||||
**Categories**: bug, error-handling, edge-case, type-safety, race-condition, null-reference
|
||||
|
||||
**Tool Support**: tsc `--noEmit`, ESLint error-level rules
|
||||
**LLM Focus**: Logic errors, unhandled exception paths, state management bugs, race conditions
|
||||
|
||||
**Severity Mapping**:
|
||||
- Critical: Data corruption, crash in production path
|
||||
- High: Incorrect business logic, unhandled error in common path
|
||||
- Medium: Edge case not handled, missing null check
|
||||
- Low: Minor type inconsistency, unused variable
|
||||
|
||||
---
|
||||
|
||||
## Performance (PRF)
|
||||
|
||||
Inefficiencies, resource waste, and scalability issues.
|
||||
|
||||
**Categories**: n-plus-one, memory-leak, blocking-operation, complexity, resource-usage, caching
|
||||
|
||||
**Tool Support**: None (LLM-only dimension)
|
||||
**LLM Focus**: Algorithm complexity, N+1 queries, unnecessary sync operations, memory leaks, missing caching
|
||||
|
||||
**Severity Mapping**:
|
||||
- Critical: Memory leak in long-running process, O(n³) on user data
|
||||
- High: N+1 query in hot path, blocking I/O in async context
|
||||
- Medium: Suboptimal algorithm, missing obvious cache
|
||||
- Low: Minor inefficiency, premature optimization opportunity
|
||||
|
||||
---
|
||||
|
||||
## Maintainability (MNT)
|
||||
|
||||
Code quality, readability, and structural health.
|
||||
|
||||
**Categories**: code-smell, naming, complexity, duplication, dead-code, pattern-violation, coupling
|
||||
|
||||
**Tool Support**: ESLint warning-level rules, complexity metrics
|
||||
**LLM Focus**: Architectural coupling, abstraction leaks, project convention violations
|
||||
|
||||
**Severity Mapping**:
|
||||
- High: God class, circular dependency, copy-paste across modules
|
||||
- Medium: Long method, magic numbers, unclear naming
|
||||
- Low: Minor style inconsistency, commented-out code
|
||||
- Info: Pattern observation, refactoring suggestion
|
||||
|
||||
---
|
||||
|
||||
## Why 4 Dimensions (Not 7)
|
||||
|
||||
The original review-cycle used 7 dimensions with significant overlap:
|
||||
|
||||
| Original | Problem | Merged Into |
|
||||
|----------|---------|-------------|
|
||||
| Quality | Overlaps Maintainability + Best-Practices | **Maintainability** |
|
||||
| Best-Practices | Overlaps Quality + Maintainability | **Maintainability** |
|
||||
| Architecture | Overlaps Maintainability (coupling/layering) | **Maintainability** (structure) + **Security** (security architecture) |
|
||||
| Action-Items | Not a dimension — it's a report format | Standard field on every finding |
|
||||
|
||||
4 dimensions = clear ownership, no overlap, each maps to distinct tooling.
|
||||
82
.claude/skills/team-review/specs/finding-schema.json
Normal file
82
.claude/skills/team-review/specs/finding-schema.json
Normal file
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "Finding",
|
||||
"description": "Standardized finding format for team-review pipeline",
|
||||
"type": "object",
|
||||
"required": ["id", "dimension", "category", "severity", "title", "description", "location", "source", "effort", "confidence"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^(SEC|COR|PRF|MNT)-\\d{3}$",
|
||||
"description": "{DIM_PREFIX}-{SEQ}"
|
||||
},
|
||||
"dimension": {
|
||||
"type": "string",
|
||||
"enum": ["security", "correctness", "performance", "maintainability"]
|
||||
},
|
||||
"category": {
|
||||
"type": "string",
|
||||
"description": "Sub-category within the dimension"
|
||||
},
|
||||
"severity": {
|
||||
"type": "string",
|
||||
"enum": ["critical", "high", "medium", "low", "info"]
|
||||
},
|
||||
"title": { "type": "string" },
|
||||
"description": { "type": "string" },
|
||||
"location": {
|
||||
"type": "object",
|
||||
"required": ["file", "line"],
|
||||
"properties": {
|
||||
"file": { "type": "string" },
|
||||
"line": { "type": "integer" },
|
||||
"end_line": { "type": "integer" },
|
||||
"code_snippet": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"source": {
|
||||
"type": "string",
|
||||
"description": "tool:eslint | tool:tsc | tool:semgrep | llm | tool+llm"
|
||||
},
|
||||
"tool_rule": { "type": ["string", "null"] },
|
||||
"suggested_fix": { "type": "string" },
|
||||
"references": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"effort": { "type": "string", "enum": ["low", "medium", "high"] },
|
||||
"confidence": { "type": "string", "enum": ["high", "medium", "low"] },
|
||||
"root_cause": {
|
||||
"type": ["object", "null"],
|
||||
"description": "Populated by reviewer role",
|
||||
"properties": {
|
||||
"description": { "type": "string" },
|
||||
"related_findings": { "type": "array", "items": { "type": "string" } },
|
||||
"is_symptom": { "type": "boolean" }
|
||||
}
|
||||
},
|
||||
"impact": {
|
||||
"type": ["object", "null"],
|
||||
"properties": {
|
||||
"scope": { "type": "string", "enum": ["low", "medium", "high"] },
|
||||
"affected_files": { "type": "array", "items": { "type": "string" } },
|
||||
"blast_radius": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"optimization": {
|
||||
"type": ["object", "null"],
|
||||
"properties": {
|
||||
"approach": { "type": "string" },
|
||||
"alternative": { "type": "string" },
|
||||
"tradeoff": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"fix_strategy": { "type": ["string", "null"], "enum": ["minimal", "refactor", "skip", null] },
|
||||
"fix_complexity": { "type": ["string", "null"], "enum": ["low", "medium", "high", null] },
|
||||
"fix_dependencies": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"default": []
|
||||
}
|
||||
}
|
||||
}
|
||||
27
.claude/skills/team-review/specs/team-config.json
Normal file
27
.claude/skills/team-review/specs/team-config.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "team-review",
|
||||
"description": "Code scanning, vulnerability review, optimization suggestions, and automated fix",
|
||||
"sessionDir": ".workflow/.team-review/",
|
||||
"msgDir": ".workflow/.team-msg/team-review/",
|
||||
"roles": {
|
||||
"coordinator": { "prefix": "RC", "type": "orchestration", "file": "roles/coordinator/role.md" },
|
||||
"scanner": { "prefix": "SCAN", "type": "read-only-analysis", "file": "roles/scanner/role.md" },
|
||||
"reviewer": { "prefix": "REV", "type": "read-only-analysis", "file": "roles/reviewer/role.md" },
|
||||
"fixer": { "prefix": "FIX", "type": "code-generation", "file": "roles/fixer/role.md" }
|
||||
},
|
||||
"collaboration_pattern": "CP-1",
|
||||
"pipeline": ["scanner", "reviewer", "fixer"],
|
||||
"dimensions": {
|
||||
"security": { "prefix": "SEC", "tools": ["semgrep", "npm-audit"] },
|
||||
"correctness": { "prefix": "COR", "tools": ["tsc", "eslint-error"] },
|
||||
"performance": { "prefix": "PRF", "tools": [] },
|
||||
"maintainability": { "prefix": "MNT", "tools": ["eslint-warning"] }
|
||||
},
|
||||
"severity_levels": ["critical", "high", "medium", "low", "info"],
|
||||
"defaults": {
|
||||
"max_deep_analysis": 15,
|
||||
"max_quick_findings": 20,
|
||||
"max_parallel_fixers": 3,
|
||||
"quick_fix_threshold": 5
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"updated_at": "2026-02-23T15:30:00Z",
|
||||
"skills": [
|
||||
{
|
||||
"id": "code-review-helper",
|
||||
"name": "Code Review Helper",
|
||||
"description": "Assists with code review by analyzing patterns and suggesting improvements",
|
||||
"version": "1.2.0",
|
||||
"author": "CCW Team",
|
||||
"category": "Code Quality",
|
||||
"tags": ["review", "quality", "analysis", "best-practices"],
|
||||
"downloadUrl": "https://raw.githubusercontent.com/catlog22/skill-hub/main/skills/code-review-helper/skill.tar.gz",
|
||||
"readmeUrl": "https://raw.githubusercontent.com/catlog22/skill-hub/main/skills/code-review-helper/README.md"
|
||||
},
|
||||
{
|
||||
"id": "test-skill",
|
||||
"name": "Test Skill",
|
||||
"description": "A test skill to verify the Skill Hub functionality",
|
||||
"version": "1.0.0",
|
||||
"author": "CCW Team",
|
||||
"category": "Development",
|
||||
"tags": ["test", "demo", "utility"],
|
||||
"downloadUrl": "https://raw.githubusercontent.com/catlog22/skill-hub/main/skills/test-skill/skill.tar.gz",
|
||||
"readmeUrl": "https://raw.githubusercontent.com/catlog22/skill-hub/main/skills/test-skill/README.md"
|
||||
}
|
||||
]
|
||||
}
|
||||
353
ccw/frontend/src/components/shared/SkillHubDetailPanel.tsx
Normal file
353
ccw/frontend/src/components/shared/SkillHubDetailPanel.tsx
Normal file
@@ -0,0 +1,353 @@
|
||||
// ========================================
|
||||
// SkillHubDetailPanel Component
|
||||
// ========================================
|
||||
// Right-side slide-out panel for viewing skill hub skill details
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import {
|
||||
X,
|
||||
FileText,
|
||||
Tag,
|
||||
User,
|
||||
Globe,
|
||||
Folder,
|
||||
ExternalLink,
|
||||
Download,
|
||||
RefreshCw,
|
||||
Check,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { CliModeToggle, type CliMode } from '@/components/mcp/CliModeToggle';
|
||||
import type { RemoteSkill, LocalSkill, InstalledSkill, CliType, SkillSource } from '@/hooks/useSkillHub';
|
||||
|
||||
export interface SkillHubDetailPanelProps {
|
||||
skill: RemoteSkill | LocalSkill | null;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
source: SkillSource;
|
||||
installedInfo?: InstalledSkill;
|
||||
onInstall?: (skill: RemoteSkill | LocalSkill, cliType: CliType) => Promise<void>;
|
||||
onUninstall?: (skill: RemoteSkill | LocalSkill, cliType: CliType) => Promise<void>;
|
||||
isInstalling?: boolean;
|
||||
}
|
||||
|
||||
export function SkillHubDetailPanel({
|
||||
skill,
|
||||
isOpen,
|
||||
onClose,
|
||||
source,
|
||||
installedInfo,
|
||||
onInstall,
|
||||
onUninstall,
|
||||
isInstalling = false,
|
||||
}: SkillHubDetailPanelProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
const [cliMode, setCliMode] = useState<CliMode>('claude');
|
||||
const [localInstalling, setLocalInstalling] = useState(false);
|
||||
|
||||
// Prevent body scroll when panel is open
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
return () => {
|
||||
document.body.style.overflow = '';
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
const isLoading = isInstalling || localInstalling;
|
||||
const isInstalled = !!installedInfo;
|
||||
const isRemote = source === 'remote';
|
||||
|
||||
const handleInstall = async () => {
|
||||
if (!skill) return;
|
||||
setLocalInstalling(true);
|
||||
try {
|
||||
await onInstall?.(skill, cliMode);
|
||||
} finally {
|
||||
setLocalInstalling(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUninstall = async () => {
|
||||
if (!skill) return;
|
||||
await onUninstall?.(skill, installedInfo?.installedTo || cliMode);
|
||||
};
|
||||
|
||||
if (!isOpen || !skill) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Overlay */}
|
||||
<div
|
||||
className="fixed inset-0 bg-black/50 z-50 transition-opacity"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* Panel */}
|
||||
<div className="fixed top-0 right-0 w-full sm:w-[480px] md:w-[560px] lg:w-[640px] h-full bg-background border-l border-border shadow-xl z-50 flex flex-col transition-transform">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-border">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<div className="p-2 rounded-lg flex-shrink-0 bg-primary/10">
|
||||
<Download className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-lg font-semibold text-foreground truncate">{skill.name}</h3>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
{skill.version && <span>v{skill.version}</span>}
|
||||
{skill.author && (
|
||||
<>
|
||||
<span>·</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<User className="w-3 h-3" />
|
||||
{skill.author}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-8 h-8 flex items-center justify-center text-muted-foreground hover:text-foreground hover:bg-muted rounded-lg transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
<div className="space-y-6">
|
||||
{/* Status Badges */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge variant={isRemote ? 'default' : 'secondary'} className="gap-1">
|
||||
{isRemote ? <Globe className="w-3 h-3" /> : <Folder className="w-3 h-3" />}
|
||||
{isRemote
|
||||
? formatMessage({ id: 'skillHub.source.remote' })
|
||||
: formatMessage({ id: 'skillHub.source.local' })}
|
||||
</Badge>
|
||||
{skill.category && (
|
||||
<Badge variant="outline">{skill.category}</Badge>
|
||||
)}
|
||||
{isInstalled && (
|
||||
installedInfo?.updatesAvailable ? (
|
||||
<Badge variant="outline" className="gap-1 text-amber-500 border-amber-500">
|
||||
<RefreshCw className="w-3 h-3" />
|
||||
{formatMessage({ id: 'skillHub.status.updateAvailable' })}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="outline" className="gap-1 text-success border-success">
|
||||
<Check className="w-3 h-3" />
|
||||
{formatMessage({ id: 'skillHub.status.installed' })}
|
||||
</Badge>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<section>
|
||||
<h4 className="text-sm font-semibold text-foreground mb-2 flex items-center gap-2">
|
||||
<FileText className="w-4 h-4 text-muted-foreground" />
|
||||
{formatMessage({ id: 'skills.card.description' })}
|
||||
</h4>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
{skill.description || formatMessage({ id: 'skills.noDescription' })}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* Tags */}
|
||||
{skill.tags && skill.tags.length > 0 && (
|
||||
<section>
|
||||
<h4 className="text-sm font-semibold text-foreground mb-2 flex items-center gap-2">
|
||||
<Tag className="w-4 h-4 text-muted-foreground" />
|
||||
{formatMessage({ id: 'skillHub.card.tags' })}
|
||||
</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{skill.tags.map((tag) => (
|
||||
<Badge key={tag} variant="secondary" className="text-sm">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Metadata */}
|
||||
<section>
|
||||
<h4 className="text-sm font-semibold text-foreground mb-3">
|
||||
{formatMessage({ id: 'skills.metadata' })}
|
||||
</h4>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{skill.version && (
|
||||
<Card className="p-3 bg-muted/50">
|
||||
<span className="text-xs text-muted-foreground block mb-1">
|
||||
{formatMessage({ id: 'skills.card.version' })}
|
||||
</span>
|
||||
<p className="text-sm font-medium text-foreground">v{skill.version}</p>
|
||||
</Card>
|
||||
)}
|
||||
{skill.author && (
|
||||
<Card className="p-3 bg-muted/50">
|
||||
<span className="text-xs text-muted-foreground block mb-1">
|
||||
{formatMessage({ id: 'skills.card.author' })}
|
||||
</span>
|
||||
<p className="text-sm font-medium text-foreground">{skill.author}</p>
|
||||
</Card>
|
||||
)}
|
||||
{skill.category && (
|
||||
<Card className="p-3 bg-muted/50">
|
||||
<span className="text-xs text-muted-foreground block mb-1">
|
||||
{formatMessage({ id: 'skills.card.category' })}
|
||||
</span>
|
||||
<p className="text-sm font-medium text-foreground">{skill.category}</p>
|
||||
</Card>
|
||||
)}
|
||||
{isRemote && (skill as RemoteSkill).updatedAt && (
|
||||
<Card className="p-3 bg-muted/50">
|
||||
<span className="text-xs text-muted-foreground block mb-1">
|
||||
{formatMessage({ id: 'skillHub.card.updated' }, { date: '' }).trim()}
|
||||
</span>
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
{new Date((skill as RemoteSkill).updatedAt as string).toLocaleDateString()}
|
||||
</p>
|
||||
</Card>
|
||||
)}
|
||||
{!isRemote && (skill as LocalSkill).path && (
|
||||
<Card className="p-3 bg-muted/50 col-span-2">
|
||||
<span className="text-xs text-muted-foreground block mb-1">
|
||||
{formatMessage({ id: 'skills.path' })}
|
||||
</span>
|
||||
<p className="text-sm font-mono text-foreground break-all">
|
||||
{(skill as LocalSkill).path}
|
||||
</p>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Links (for remote skills) */}
|
||||
{isRemote && (
|
||||
(skill as RemoteSkill).readmeUrl ||
|
||||
(skill as RemoteSkill).homepage ||
|
||||
(skill as RemoteSkill).license
|
||||
) && (
|
||||
<section>
|
||||
<h4 className="text-sm font-semibold text-foreground mb-3">
|
||||
{formatMessage({ id: 'skillHub.links' })}
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{(skill as RemoteSkill).readmeUrl && (
|
||||
<a
|
||||
href={(skill as RemoteSkill).readmeUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 text-sm text-primary hover:underline"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
README
|
||||
</a>
|
||||
)}
|
||||
{(skill as RemoteSkill).homepage && (
|
||||
<a
|
||||
href={(skill as RemoteSkill).homepage}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 text-sm text-primary hover:underline"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
{formatMessage({ id: 'skillHub.homepage' })}
|
||||
</a>
|
||||
)}
|
||||
{(skill as RemoteSkill).license && (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{formatMessage({ id: 'skillHub.license' })}: {(skill as RemoteSkill).license}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Installation Info */}
|
||||
{isInstalled && installedInfo && (
|
||||
<section>
|
||||
<h4 className="text-sm font-semibold text-foreground mb-3">
|
||||
{formatMessage({ id: 'skillHub.installationInfo' })}
|
||||
</h4>
|
||||
<Card className="p-3 bg-muted/50">
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">{formatMessage({ id: 'skillHub.installedTo' })}</span>
|
||||
<span className="font-medium">{installedInfo.installedTo}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">{formatMessage({ id: 'skillHub.installedAt' })}</span>
|
||||
<span className="font-medium">
|
||||
{new Date(installedInfo.installedAt).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
{installedInfo.updatesAvailable && installedInfo.latestVersion && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">{formatMessage({ id: 'skillHub.latestVersion' })}</span>
|
||||
<span className="font-medium text-amber-500">v{installedInfo.latestVersion}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer Actions */}
|
||||
<div className="px-6 py-4 border-t border-border">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<CliModeToggle currentMode={cliMode} onModeChange={setCliMode} />
|
||||
<div className="flex gap-2">
|
||||
{isInstalled && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleUninstall}
|
||||
className="text-destructive hover:text-destructive"
|
||||
>
|
||||
{formatMessage({ id: 'skillHub.actions.uninstall' })}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant={isInstalled ? 'outline' : 'default'}
|
||||
onClick={handleInstall}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<RefreshCw className="w-4 h-4 mr-2 animate-spin" />
|
||||
{formatMessage({ id: 'skillHub.actions.installing' })}
|
||||
</>
|
||||
) : isInstalled ? (
|
||||
<>
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
{formatMessage({ id: 'skillHub.actions.update' })}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
{formatMessage({ id: 'skillHub.actions.install' })}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default SkillHubDetailPanel;
|
||||
@@ -25,6 +25,9 @@ export type { SkillCreateDialogProps } from './SkillCreateDialog';
|
||||
export { SkillHubCard } from './SkillHubCard';
|
||||
export type { SkillHubCardProps } from './SkillHubCard';
|
||||
|
||||
export { SkillHubDetailPanel } from './SkillHubDetailPanel';
|
||||
export type { SkillHubDetailPanelProps } from './SkillHubDetailPanel';
|
||||
|
||||
export { StatCard, StatCardSkeleton } from './StatCard';
|
||||
export type { StatCardProps } from './StatCard';
|
||||
|
||||
|
||||
@@ -547,7 +547,7 @@ export function useUpdateCodexLensConfig(): UseUpdateCodexLensConfigReturn {
|
||||
mutationFn: updateCodexLensConfig,
|
||||
onMutate: () => {
|
||||
info(
|
||||
formatMessage({ id: 'status.inProgress' }),
|
||||
formatMessage({ id: 'common.status.inProgress' }),
|
||||
formatMessage({ id: 'common.feedback.codexLensConfigUpdate.success' })
|
||||
);
|
||||
},
|
||||
@@ -780,7 +780,7 @@ export function useDeleteModel(): UseDeleteModelReturn {
|
||||
},
|
||||
onMutate: () => {
|
||||
info(
|
||||
formatMessage({ id: 'status.deleting' }),
|
||||
formatMessage({ id: 'common.actions.deleting' }),
|
||||
formatMessage({ id: 'common.feedback.codexLensDeleteModel.success' })
|
||||
);
|
||||
},
|
||||
@@ -825,7 +825,7 @@ export function useUpdateCodexLensEnv(): UseUpdateCodexLensEnvReturn {
|
||||
mutationFn: (request: CodexLensUpdateEnvRequest) => updateCodexLensEnv(request),
|
||||
onMutate: () => {
|
||||
info(
|
||||
formatMessage({ id: 'status.inProgress' }),
|
||||
formatMessage({ id: 'common.status.inProgress' }),
|
||||
formatMessage({ id: 'common.feedback.codexLensUpdateEnv.success' })
|
||||
);
|
||||
},
|
||||
@@ -872,7 +872,7 @@ export function useSelectGpu(): UseSelectGpuReturn {
|
||||
mutationFn: (deviceId: string | number) => selectCodexLensGpu(deviceId),
|
||||
onMutate: () => {
|
||||
info(
|
||||
formatMessage({ id: 'status.inProgress' }),
|
||||
formatMessage({ id: 'common.status.inProgress' }),
|
||||
formatMessage({ id: 'common.feedback.codexLensSelectGpu.success' })
|
||||
);
|
||||
},
|
||||
@@ -895,7 +895,7 @@ export function useSelectGpu(): UseSelectGpuReturn {
|
||||
mutationFn: () => resetCodexLensGpu(),
|
||||
onMutate: () => {
|
||||
info(
|
||||
formatMessage({ id: 'status.inProgress' }),
|
||||
formatMessage({ id: 'common.status.inProgress' }),
|
||||
formatMessage({ id: 'common.feedback.codexLensResetGpu.success' })
|
||||
);
|
||||
},
|
||||
@@ -941,7 +941,7 @@ export function useUpdateIgnorePatterns(): UseUpdateIgnorePatternsReturn {
|
||||
mutationFn: updateCodexLensIgnorePatterns,
|
||||
onMutate: () => {
|
||||
info(
|
||||
formatMessage({ id: 'status.inProgress' }),
|
||||
formatMessage({ id: 'common.status.inProgress' }),
|
||||
formatMessage({ id: 'common.feedback.codexLensUpdatePatterns.success' })
|
||||
);
|
||||
},
|
||||
@@ -1070,7 +1070,7 @@ export function useRebuildIndex(): UseRebuildIndexReturn {
|
||||
}) => rebuildCodexLensIndex(projectPath, options),
|
||||
onMutate: () => {
|
||||
info(
|
||||
formatMessage({ id: 'status.inProgress' }),
|
||||
formatMessage({ id: 'common.status.inProgress' }),
|
||||
formatMessage({ id: 'common.feedback.codexLensRebuildIndex.success' })
|
||||
);
|
||||
},
|
||||
@@ -1132,7 +1132,7 @@ export function useUpdateIndex(): UseUpdateIndexReturn {
|
||||
}) => updateCodexLensIndex(projectPath, options),
|
||||
onMutate: () => {
|
||||
info(
|
||||
formatMessage({ id: 'status.inProgress' }),
|
||||
formatMessage({ id: 'common.status.inProgress' }),
|
||||
formatMessage({ id: 'common.feedback.codexLensUpdateIndex.success' })
|
||||
);
|
||||
},
|
||||
@@ -1178,7 +1178,7 @@ export function useCancelIndexing(): UseCancelIndexingReturn {
|
||||
mutationFn: cancelCodexLensIndexing,
|
||||
onMutate: () => {
|
||||
info(
|
||||
formatMessage({ id: 'status.inProgress' }),
|
||||
formatMessage({ id: 'common.status.inProgress' }),
|
||||
formatMessage({ id: 'common.feedback.codexLensCancelIndexing.success' })
|
||||
);
|
||||
},
|
||||
|
||||
@@ -177,6 +177,9 @@
|
||||
"helpQuotes": "Values with spaces should use quotes",
|
||||
"helpRestart": "Restart service after changes to take effect"
|
||||
},
|
||||
"downloadedModels": "Downloaded Models",
|
||||
"noConfiguredModels": "No models configured",
|
||||
"noLocalModels": "No models downloaded",
|
||||
"models": {
|
||||
"title": "Model Management",
|
||||
"searchPlaceholder": "Search models...",
|
||||
|
||||
@@ -1,42 +1,83 @@
|
||||
{
|
||||
"skillHub.title": "Skill Hub",
|
||||
"skillHub.description": "Discover and install shared skills from the community",
|
||||
"skillHub.source.remote": "Remote",
|
||||
"skillHub.source.local": "Local",
|
||||
"skillHub.status.installed": "Installed",
|
||||
"skillHub.status.updateAvailable": "Update Available",
|
||||
"skillHub.tabs.remote": "Remote",
|
||||
"skillHub.tabs.local": "Local",
|
||||
"skillHub.tabs.installed": "Installed",
|
||||
"skillHub.stats.remote": "Remote Skills",
|
||||
"skillHub.stats.remoteDesc": "Available from community",
|
||||
"skillHub.stats.local": "Local Skills",
|
||||
"skillHub.stats.localDesc": "Shared locally",
|
||||
"skillHub.stats.installed": "Installed",
|
||||
"skillHub.stats.installedDesc": "Skills in use",
|
||||
"skillHub.stats.updates": "Updates",
|
||||
"skillHub.stats.updatesDesc": "New versions available",
|
||||
"skillHub.search.placeholder": "Search skills...",
|
||||
"skillHub.filter.allCategories": "All Categories",
|
||||
"skillHub.actions.refresh": "Refresh",
|
||||
"skillHub.actions.install": "Install",
|
||||
"skillHub.actions.installing": "Installing...",
|
||||
"skillHub.actions.update": "Update",
|
||||
"skillHub.actions.uninstall": "Uninstall",
|
||||
"skillHub.actions.viewDetails": "View Details",
|
||||
"skillHub.card.tags": "Tags",
|
||||
"skillHub.card.updated": "Updated: {date}",
|
||||
"skillHub.install.success": "Skill '{name}' installed successfully",
|
||||
"skillHub.install.error": "Failed to install skill: {error}",
|
||||
"skillHub.uninstall.success": "Skill '{name}' uninstalled",
|
||||
"skillHub.uninstall.error": "Failed to uninstall skill: {error}",
|
||||
"skillHub.refresh.success": "Skill list refreshed",
|
||||
"skillHub.details.comingSoon": "Details view coming soon",
|
||||
"skillHub.error.loadFailed": "Failed to load skills. Check network connection.",
|
||||
"skillHub.empty.remote.title": "No Remote Skills",
|
||||
"skillHub.empty.remote.description": "Remote skill repository is empty or unreachable.",
|
||||
"skillHub.empty.local.title": "No Local Skills",
|
||||
"skillHub.empty.local.description": "Add skills to ~/.ccw/skill-hub/local/ to share them.",
|
||||
"skillHub.empty.installed.title": "No Installed Skills",
|
||||
"skillHub.empty.installed.description": "Install skills from Remote or Local tabs to use them."
|
||||
"title": "Skill Hub",
|
||||
"description": "Discover and install shared skills from the community",
|
||||
"links": "Links",
|
||||
"homepage": "Homepage",
|
||||
"license": "License",
|
||||
"source": {
|
||||
"remote": "Remote",
|
||||
"local": "Local"
|
||||
},
|
||||
"status": {
|
||||
"installed": "Installed",
|
||||
"updateAvailable": "Update Available"
|
||||
},
|
||||
"tabs": {
|
||||
"remote": "Remote",
|
||||
"local": "Local",
|
||||
"installed": "Installed"
|
||||
},
|
||||
"stats": {
|
||||
"remote": "Remote Skills",
|
||||
"remoteDesc": "Available from community",
|
||||
"local": "Local Skills",
|
||||
"localDesc": "Shared locally",
|
||||
"installed": "Installed",
|
||||
"installedDesc": "Skills in use",
|
||||
"updates": "Updates",
|
||||
"updatesDesc": "New versions available"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Search skills..."
|
||||
},
|
||||
"filter": {
|
||||
"allCategories": "All Categories"
|
||||
},
|
||||
"actions": {
|
||||
"refresh": "Refresh",
|
||||
"install": "Install",
|
||||
"installing": "Installing...",
|
||||
"update": "Update",
|
||||
"uninstall": "Uninstall",
|
||||
"viewDetails": "View Details"
|
||||
},
|
||||
"card": {
|
||||
"tags": "Tags",
|
||||
"updated": "Updated: {date}"
|
||||
},
|
||||
"install": {
|
||||
"success": "Skill '{name}' installed successfully",
|
||||
"error": "Failed to install skill: {error}"
|
||||
},
|
||||
"uninstall": {
|
||||
"success": "Skill '{name}' uninstalled",
|
||||
"error": "Failed to uninstall skill: {error}"
|
||||
},
|
||||
"refresh": {
|
||||
"success": "Skill list refreshed"
|
||||
},
|
||||
"details": {
|
||||
"comingSoon": "Details view coming soon"
|
||||
},
|
||||
"error": {
|
||||
"loadFailed": "Failed to load skills. Check network connection."
|
||||
},
|
||||
"empty": {
|
||||
"remote": {
|
||||
"title": "No Remote Skills",
|
||||
"description": "Remote skill repository is empty or unreachable."
|
||||
},
|
||||
"local": {
|
||||
"title": "No Local Skills",
|
||||
"description": "Add skills to ~/.ccw/skill-hub/local/ to share them."
|
||||
},
|
||||
"installed": {
|
||||
"title": "No Installed Skills",
|
||||
"description": "Install skills from Remote or Local tabs to use them."
|
||||
}
|
||||
},
|
||||
"installationInfo": "Installation Info",
|
||||
"installedTo": "Installed To",
|
||||
"installedAt": "Installed At",
|
||||
"latestVersion": "Latest Version"
|
||||
}
|
||||
|
||||
@@ -177,6 +177,9 @@
|
||||
"helpQuotes": "包含空格的值建议使用引号",
|
||||
"helpRestart": "修改后需要重启服务才能生效"
|
||||
},
|
||||
"downloadedModels": "已下载模型",
|
||||
"noConfiguredModels": "无已配置模型",
|
||||
"noLocalModels": "无已下载模型",
|
||||
"models": {
|
||||
"title": "模型管理",
|
||||
"searchPlaceholder": "搜索模型...",
|
||||
|
||||
@@ -1,42 +1,83 @@
|
||||
{
|
||||
"skillHub.title": "技能中心",
|
||||
"skillHub.description": "发现并安装社区共享的技能",
|
||||
"skillHub.source.remote": "远程",
|
||||
"skillHub.source.local": "本地",
|
||||
"skillHub.status.installed": "已安装",
|
||||
"skillHub.status.updateAvailable": "有更新",
|
||||
"skillHub.tabs.remote": "远程",
|
||||
"skillHub.tabs.local": "本地",
|
||||
"skillHub.tabs.installed": "已安装",
|
||||
"skillHub.stats.remote": "远程技能",
|
||||
"skillHub.stats.remoteDesc": "来自社区",
|
||||
"skillHub.stats.local": "本地技能",
|
||||
"skillHub.stats.localDesc": "本地共享",
|
||||
"skillHub.stats.installed": "已安装",
|
||||
"skillHub.stats.installedDesc": "使用中的技能",
|
||||
"skillHub.stats.updates": "更新",
|
||||
"skillHub.stats.updatesDesc": "有新版本可用",
|
||||
"skillHub.search.placeholder": "搜索技能...",
|
||||
"skillHub.filter.allCategories": "全部分类",
|
||||
"skillHub.actions.refresh": "刷新",
|
||||
"skillHub.actions.install": "安装",
|
||||
"skillHub.actions.installing": "安装中...",
|
||||
"skillHub.actions.update": "更新",
|
||||
"skillHub.actions.uninstall": "卸载",
|
||||
"skillHub.actions.viewDetails": "查看详情",
|
||||
"skillHub.card.tags": "标签",
|
||||
"skillHub.card.updated": "更新于: {date}",
|
||||
"skillHub.install.success": "技能 '{name}' 安装成功",
|
||||
"skillHub.install.error": "安装技能失败: {error}",
|
||||
"skillHub.uninstall.success": "技能 '{name}' 已卸载",
|
||||
"skillHub.uninstall.error": "卸载技能失败: {error}",
|
||||
"skillHub.refresh.success": "技能列表已刷新",
|
||||
"skillHub.details.comingSoon": "详情视图即将推出",
|
||||
"skillHub.error.loadFailed": "加载技能失败。请检查网络连接。",
|
||||
"skillHub.empty.remote.title": "暂无远程技能",
|
||||
"skillHub.empty.remote.description": "远程技能仓库为空或无法访问。",
|
||||
"skillHub.empty.local.title": "暂无本地技能",
|
||||
"skillHub.empty.local.description": "将技能添加到 ~/.ccw/skill-hub/local/ 即可共享。",
|
||||
"skillHub.empty.installed.title": "暂无已安装技能",
|
||||
"skillHub.empty.installed.description": "从远程或本地标签页安装技能以使用它们。"
|
||||
"title": "技能中心",
|
||||
"description": "发现并安装社区共享的技能",
|
||||
"links": "链接",
|
||||
"homepage": "主页",
|
||||
"license": "许可证",
|
||||
"source": {
|
||||
"remote": "远程",
|
||||
"local": "本地"
|
||||
},
|
||||
"status": {
|
||||
"installed": "已安装",
|
||||
"updateAvailable": "有更新"
|
||||
},
|
||||
"tabs": {
|
||||
"remote": "远程",
|
||||
"local": "本地",
|
||||
"installed": "已安装"
|
||||
},
|
||||
"stats": {
|
||||
"remote": "远程技能",
|
||||
"remoteDesc": "来自社区",
|
||||
"local": "本地技能",
|
||||
"localDesc": "本地共享",
|
||||
"installed": "已安装",
|
||||
"installedDesc": "使用中的技能",
|
||||
"updates": "更新",
|
||||
"updatesDesc": "有新版本可用"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "搜索技能..."
|
||||
},
|
||||
"filter": {
|
||||
"allCategories": "全部分类"
|
||||
},
|
||||
"actions": {
|
||||
"refresh": "刷新",
|
||||
"install": "安装",
|
||||
"installing": "安装中...",
|
||||
"update": "更新",
|
||||
"uninstall": "卸载",
|
||||
"viewDetails": "查看详情"
|
||||
},
|
||||
"card": {
|
||||
"tags": "标签",
|
||||
"updated": "更新于: {date}"
|
||||
},
|
||||
"install": {
|
||||
"success": "技能 '{name}' 安装成功",
|
||||
"error": "安装技能失败: {error}"
|
||||
},
|
||||
"uninstall": {
|
||||
"success": "技能 '{name}' 已卸载",
|
||||
"error": "卸载技能失败: {error}"
|
||||
},
|
||||
"refresh": {
|
||||
"success": "技能列表已刷新"
|
||||
},
|
||||
"details": {
|
||||
"comingSoon": "详情视图即将推出"
|
||||
},
|
||||
"error": {
|
||||
"loadFailed": "加载技能失败。请检查网络连接。"
|
||||
},
|
||||
"empty": {
|
||||
"remote": {
|
||||
"title": "暂无远程技能",
|
||||
"description": "远程技能仓库为空或无法访问。"
|
||||
},
|
||||
"local": {
|
||||
"title": "暂无本地技能",
|
||||
"description": "将技能添加到 ~/.ccw/skill-hub/local/ 即可共享。"
|
||||
},
|
||||
"installed": {
|
||||
"title": "暂无已安装技能",
|
||||
"description": "从远程或本地标签页安装技能以使用它们。"
|
||||
}
|
||||
},
|
||||
"installationInfo": "安装信息",
|
||||
"installedTo": "安装到",
|
||||
"installedAt": "安装时间",
|
||||
"latestVersion": "最新版本"
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import { Input } from '@/components/ui/Input';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/Tabs';
|
||||
import { StatCard } from '@/components/shared';
|
||||
import { StatCard, SkillHubDetailPanel } from '@/components/shared';
|
||||
import { SkillHubCard } from '@/components/shared/SkillHubCard';
|
||||
import {
|
||||
useSkillHub,
|
||||
@@ -136,6 +136,8 @@ export function SkillHubPage() {
|
||||
const [activeTab, setActiveTab] = useState<TabValue>('remote');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [categoryFilter, setCategoryFilter] = useState<string | null>(null);
|
||||
const [selectedSkill, setSelectedSkill] = useState<{ skill: RemoteSkill | LocalSkill; source: SkillSource } | null>(null);
|
||||
const [isDetailPanelOpen, setIsDetailPanelOpen] = useState(false);
|
||||
|
||||
// Fetch data
|
||||
const {
|
||||
@@ -221,8 +223,15 @@ export function SkillHubPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewDetails = () => {
|
||||
toast.info(formatMessage({ id: 'skillHub.details.comingSoon' }));
|
||||
const handleViewDetails = (skill: RemoteSkill | LocalSkill) => {
|
||||
const source: SkillSource = 'downloadUrl' in skill ? 'remote' : 'local';
|
||||
setSelectedSkill({ skill, source });
|
||||
setIsDetailPanelOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseDetailPanel = () => {
|
||||
setIsDetailPanelOpen(false);
|
||||
setSelectedSkill(null);
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
@@ -378,7 +387,7 @@ export function SkillHubPage() {
|
||||
source={skillSource}
|
||||
onInstall={handleInstall}
|
||||
onUninstall={handleUninstall}
|
||||
onViewDetails={handleViewDetails}
|
||||
onViewDetails={() => handleViewDetails(skill as RemoteSkill | LocalSkill)}
|
||||
isInstalling={installMutation.isPending}
|
||||
/>
|
||||
);
|
||||
@@ -386,6 +395,20 @@ export function SkillHubPage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Detail Panel */}
|
||||
{selectedSkill && (
|
||||
<SkillHubDetailPanel
|
||||
skill={selectedSkill.skill}
|
||||
isOpen={isDetailPanelOpen}
|
||||
onClose={handleCloseDetailPanel}
|
||||
source={selectedSkill.source}
|
||||
installedInfo={installedMap.get(selectedSkill.skill.id)}
|
||||
onInstall={handleInstall}
|
||||
onUninstall={handleUninstall}
|
||||
isInstalling={installMutation.isPending}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { existsSync, mkdirSync, readdirSync, statSync, copyFileSync, readFileSync, writeFileSync, unlinkSync, rmdirSync, appendFileSync, renameSync } from 'fs';
|
||||
import { existsSync, mkdirSync, readdirSync, statSync, copyFileSync, readFileSync, writeFileSync, unlinkSync, rmdirSync, appendFileSync, renameSync, cpSync } from 'fs';
|
||||
import { join, dirname, basename } from 'path';
|
||||
import { homedir, platform } from 'os';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
@@ -565,7 +565,7 @@ export async function handleCodexLensConfigRoutes(ctx: RouteContext): Promise<bo
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: CodexLens Model List (list available embedding models)
|
||||
// API: CodexLens Model List (list available embedding AND reranker models)
|
||||
if (pathname === '/api/codexlens/models' && req.method === 'GET') {
|
||||
try {
|
||||
// Check if CodexLens is installed first (without auto-installing)
|
||||
@@ -575,20 +575,46 @@ export async function handleCodexLensConfigRoutes(ctx: RouteContext): Promise<bo
|
||||
res.end(JSON.stringify({ success: false, error: 'CodexLens not installed' }));
|
||||
return true;
|
||||
}
|
||||
const result = await executeCodexLens(['model-list', '--json']);
|
||||
if (result.success) {
|
||||
|
||||
// Fetch both embedding and reranker models in parallel
|
||||
const [embeddingResult, rerankerResult] = await Promise.all([
|
||||
executeCodexLens(['model-list', '--json']),
|
||||
executeCodexLens(['reranker-model-list', '--json'])
|
||||
]);
|
||||
|
||||
const allModels: any[] = [];
|
||||
|
||||
// Parse embedding models and add type field
|
||||
if (embeddingResult.success) {
|
||||
try {
|
||||
const parsed = extractJSON(result.output ?? '');
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(parsed));
|
||||
const parsed = extractJSON(embeddingResult.output ?? '');
|
||||
const models = parsed?.result?.models ?? parsed?.models ?? [];
|
||||
for (const model of models) {
|
||||
allModels.push({ ...model, type: 'embedding' });
|
||||
}
|
||||
} catch {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true, result: { models: [] }, output: result.output }));
|
||||
// Ignore parsing errors for embedding models
|
||||
}
|
||||
} else {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: false, error: result.error }));
|
||||
}
|
||||
|
||||
// Parse reranker models and add type field
|
||||
if (rerankerResult.success) {
|
||||
try {
|
||||
const parsed = extractJSON(rerankerResult.output ?? '');
|
||||
const models = parsed?.result?.models ?? parsed?.models ?? [];
|
||||
for (const model of models) {
|
||||
allModels.push({ ...model, type: 'reranker' });
|
||||
}
|
||||
} catch {
|
||||
// Ignore parsing errors for reranker models
|
||||
}
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
success: true,
|
||||
result: { models: allModels }
|
||||
}));
|
||||
} catch (err: unknown) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) }));
|
||||
@@ -596,27 +622,65 @@ export async function handleCodexLensConfigRoutes(ctx: RouteContext): Promise<bo
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: CodexLens Model Download (download embedding model by profile)
|
||||
// API: CodexLens Model Download (download embedding or reranker model by profile)
|
||||
if (pathname === '/api/codexlens/models/download' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
const { profile } = body as { profile?: unknown };
|
||||
const { profile, model_type } = body as { profile?: unknown; model_type?: unknown };
|
||||
const resolvedProfile = typeof profile === 'string' && profile.trim().length > 0 ? profile.trim() : undefined;
|
||||
const resolvedModelType = typeof model_type === 'string' ? model_type.trim() : undefined;
|
||||
|
||||
if (!resolvedProfile) {
|
||||
return { success: false, error: 'profile is required', status: 400 };
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await executeCodexLens(['model-download', resolvedProfile, '--json'], { timeout: 600000 }); // 10 min for download
|
||||
if (result.success) {
|
||||
// If model_type is specified, use it directly
|
||||
if (resolvedModelType === 'reranker') {
|
||||
const result = await executeCodexLens(['reranker-model-download', resolvedProfile, '--json'], { timeout: 600000 });
|
||||
if (result.success) {
|
||||
try {
|
||||
const parsed = extractJSON(result.output ?? '');
|
||||
return { success: true, ...parsed };
|
||||
} catch {
|
||||
return { success: true, output: result.output };
|
||||
}
|
||||
} else {
|
||||
return { success: false, error: result.error, status: 500 };
|
||||
}
|
||||
}
|
||||
|
||||
// Try embedding model first, then reranker if profile not found
|
||||
const embeddingResult = await executeCodexLens(['model-download', resolvedProfile, '--json'], { timeout: 600000 });
|
||||
|
||||
// Check if the error indicates unknown profile
|
||||
const outputStr = embeddingResult.output ?? '';
|
||||
const isUnknownProfile = !embeddingResult.success &&
|
||||
(outputStr.includes('Unknown profile') || outputStr.includes('unknown profile'));
|
||||
|
||||
if (isUnknownProfile) {
|
||||
// Try reranker model
|
||||
const rerankerResult = await executeCodexLens(['reranker-model-download', resolvedProfile, '--json'], { timeout: 600000 });
|
||||
if (rerankerResult.success) {
|
||||
try {
|
||||
const parsed = extractJSON(rerankerResult.output ?? '');
|
||||
return { success: true, ...parsed };
|
||||
} catch {
|
||||
return { success: true, output: rerankerResult.output };
|
||||
}
|
||||
} else {
|
||||
return { success: false, error: rerankerResult.error, status: 500 };
|
||||
}
|
||||
}
|
||||
|
||||
if (embeddingResult.success) {
|
||||
try {
|
||||
const parsed = extractJSON(result.output ?? '');
|
||||
const parsed = extractJSON(embeddingResult.output ?? '');
|
||||
return { success: true, ...parsed };
|
||||
} catch {
|
||||
return { success: true, output: result.output };
|
||||
return { success: true, output: embeddingResult.output };
|
||||
}
|
||||
} else {
|
||||
return { success: false, error: result.error, status: 500 };
|
||||
return { success: false, error: embeddingResult.error, status: 500 };
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
return { success: false, error: err instanceof Error ? err.message : String(err), status: 500 };
|
||||
@@ -665,27 +729,65 @@ export async function handleCodexLensConfigRoutes(ctx: RouteContext): Promise<bo
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: CodexLens Model Delete (delete embedding model by profile)
|
||||
// API: CodexLens Model Delete (delete embedding or reranker model by profile)
|
||||
if (pathname === '/api/codexlens/models/delete' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
const { profile } = body as { profile?: unknown };
|
||||
const { profile, model_type } = body as { profile?: unknown; model_type?: unknown };
|
||||
const resolvedProfile = typeof profile === 'string' && profile.trim().length > 0 ? profile.trim() : undefined;
|
||||
const resolvedModelType = typeof model_type === 'string' ? model_type.trim() : undefined;
|
||||
|
||||
if (!resolvedProfile) {
|
||||
return { success: false, error: 'profile is required', status: 400 };
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await executeCodexLens(['model-delete', resolvedProfile, '--json']);
|
||||
if (result.success) {
|
||||
// If model_type is specified, use it directly
|
||||
if (resolvedModelType === 'reranker') {
|
||||
const result = await executeCodexLens(['reranker-model-delete', resolvedProfile, '--json']);
|
||||
if (result.success) {
|
||||
try {
|
||||
const parsed = extractJSON(result.output ?? '');
|
||||
return { success: true, ...parsed };
|
||||
} catch {
|
||||
return { success: true, output: result.output };
|
||||
}
|
||||
} else {
|
||||
return { success: false, error: result.error, status: 500 };
|
||||
}
|
||||
}
|
||||
|
||||
// Try embedding model first, then reranker if profile not found
|
||||
const embeddingResult = await executeCodexLens(['model-delete', resolvedProfile, '--json']);
|
||||
|
||||
// Check if the error indicates unknown profile
|
||||
const outputStr = embeddingResult.output ?? '';
|
||||
const isUnknownProfile = !embeddingResult.success &&
|
||||
(outputStr.includes('Unknown profile') || outputStr.includes('unknown profile'));
|
||||
|
||||
if (isUnknownProfile) {
|
||||
// Try reranker model
|
||||
const rerankerResult = await executeCodexLens(['reranker-model-delete', resolvedProfile, '--json']);
|
||||
if (rerankerResult.success) {
|
||||
try {
|
||||
const parsed = extractJSON(rerankerResult.output ?? '');
|
||||
return { success: true, ...parsed };
|
||||
} catch {
|
||||
return { success: true, output: rerankerResult.output };
|
||||
}
|
||||
} else {
|
||||
return { success: false, error: rerankerResult.error, status: 500 };
|
||||
}
|
||||
}
|
||||
|
||||
if (embeddingResult.success) {
|
||||
try {
|
||||
const parsed = extractJSON(result.output ?? '');
|
||||
const parsed = extractJSON(embeddingResult.output ?? '');
|
||||
return { success: true, ...parsed };
|
||||
} catch {
|
||||
return { success: true, output: result.output };
|
||||
return { success: true, output: embeddingResult.output };
|
||||
}
|
||||
} else {
|
||||
return { success: false, error: result.error, status: 500 };
|
||||
return { success: false, error: embeddingResult.error, status: 500 };
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
return { success: false, error: err instanceof Error ? err.message : String(err), status: 500 };
|
||||
|
||||
@@ -171,10 +171,10 @@ interface SkillCacheRequest {
|
||||
* GitHub repository configuration for remote skills
|
||||
*/
|
||||
const GITHUB_CONFIG = {
|
||||
owner: 'anthropics',
|
||||
repo: 'claude-code-workflow',
|
||||
owner: 'catlog22',
|
||||
repo: 'skill-hub',
|
||||
branch: 'main',
|
||||
skillIndexPath: 'skill-hub/index.json'
|
||||
skillIndexPath: 'index.json'
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
70
codex-lens/.github/workflows/security.yml
vendored
Normal file
70
codex-lens/.github/workflows/security.yml
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
# Security scanning workflow for codex-lens
|
||||
# Runs pip-audit to check for known vulnerabilities in dependencies
|
||||
|
||||
name: Security Scan
|
||||
|
||||
on:
|
||||
# Run on push to main branch
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
# Run weekly on Sundays at 00:00 UTC
|
||||
schedule:
|
||||
- cron: '0 0 * * 0'
|
||||
# Allow manual trigger
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
security-audit:
|
||||
name: Dependency Vulnerability Scan
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.10'
|
||||
cache: 'pip'
|
||||
|
||||
- name: Install pip-audit
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install pip-audit
|
||||
|
||||
- name: Run pip-audit on requirements.in
|
||||
run: pip-audit --requirement requirements.in
|
||||
continue-on-error: false
|
||||
|
||||
- name: Run pip-audit on pyproject.toml dependencies
|
||||
run: pip-audit --project-path .
|
||||
continue-on-error: false
|
||||
|
||||
- name: Check for safety issues
|
||||
run: |
|
||||
pip install safety
|
||||
safety check --json || true
|
||||
continue-on-error: true
|
||||
|
||||
bandit-security:
|
||||
name: Code Security Linting
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Install bandit
|
||||
run: pip install bandit[toml]
|
||||
|
||||
- name: Run bandit security linter
|
||||
run: bandit -r src/ -ll -i
|
||||
continue-on-error: true
|
||||
38
codex-lens/DEPENDENCIES.md
Normal file
38
codex-lens/DEPENDENCIES.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Dependency Management
|
||||
|
||||
This project uses setuptools with `pyproject.toml` for dependency management.
|
||||
|
||||
## Locking Dependencies
|
||||
|
||||
To generate a fully pinned `requirements.txt` from `requirements.in`:
|
||||
|
||||
```bash
|
||||
# Install pip-tools
|
||||
pip install pip-tools
|
||||
|
||||
# Compile requirements
|
||||
pip-compile requirements.in --output-file=requirements.txt
|
||||
|
||||
# To upgrade dependencies
|
||||
pip-compile --upgrade requirements.in --output-file=requirements.txt
|
||||
```
|
||||
|
||||
## Version Constraints
|
||||
|
||||
This project uses **pessimistic versioning** (`~=`) for dependency specifications per PEP 440:
|
||||
|
||||
- `typer~=0.9.0` means: `>=0.9.0, ==0.9.*`
|
||||
- Allows bugfix updates (0.9.0, 0.9.1, 0.9.2) but not feature/minor updates (0.10.0)
|
||||
|
||||
This provides stability while allowing automatic patch updates.
|
||||
|
||||
## Security Scanning
|
||||
|
||||
The project includes automated security scanning via GitHub Actions:
|
||||
- Runs on every push to main branch
|
||||
- Runs weekly (Sundays at 00:00 UTC)
|
||||
- Can be triggered manually
|
||||
|
||||
The scan uses:
|
||||
- `pip-audit`: Checks for known vulnerabilities in dependencies
|
||||
- `bandit`: Security linter for Python code
|
||||
@@ -13,95 +13,95 @@ authors = [
|
||||
{ name = "CodexLens contributors" }
|
||||
]
|
||||
dependencies = [
|
||||
"typer>=0.9",
|
||||
"rich>=13",
|
||||
"pydantic>=2.0",
|
||||
"tree-sitter>=0.20",
|
||||
"tree-sitter-python>=0.25",
|
||||
"tree-sitter-javascript>=0.25",
|
||||
"tree-sitter-typescript>=0.23",
|
||||
"pathspec>=0.11",
|
||||
"watchdog>=3.0",
|
||||
"typer~=0.9.0",
|
||||
"rich~=13.0.0",
|
||||
"pydantic~=2.0.0",
|
||||
"tree-sitter~=0.20.0",
|
||||
"tree-sitter-python~=0.25.0",
|
||||
"tree-sitter-javascript~=0.25.0",
|
||||
"tree-sitter-typescript~=0.23.0",
|
||||
"pathspec~=0.11.0",
|
||||
"watchdog~=3.0.0",
|
||||
# ast-grep for pattern-based AST matching (PyO3 bindings)
|
||||
# ast-grep-py 0.40+ supports Python 3.13
|
||||
"ast-grep-py>=0.40.0",
|
||||
"ast-grep-py~=0.40.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
# Semantic search using fastembed (ONNX-based, lightweight ~200MB)
|
||||
semantic = [
|
||||
"numpy>=1.24",
|
||||
"fastembed>=0.2",
|
||||
"hnswlib>=0.8.0",
|
||||
"numpy~=1.24.0",
|
||||
"fastembed~=0.2.0",
|
||||
"hnswlib~=0.8.0",
|
||||
]
|
||||
|
||||
# GPU acceleration for semantic search (NVIDIA CUDA)
|
||||
# Install with: pip install codexlens[semantic-gpu]
|
||||
semantic-gpu = [
|
||||
"numpy>=1.24",
|
||||
"fastembed>=0.2",
|
||||
"hnswlib>=0.8.0",
|
||||
"onnxruntime-gpu>=1.15.0", # CUDA support
|
||||
"numpy~=1.24.0",
|
||||
"fastembed~=0.2.0",
|
||||
"hnswlib~=0.8.0",
|
||||
"onnxruntime-gpu~=1.15.0", # CUDA support
|
||||
]
|
||||
|
||||
# GPU acceleration for Windows (DirectML - supports NVIDIA/AMD/Intel)
|
||||
# Install with: pip install codexlens[semantic-directml]
|
||||
semantic-directml = [
|
||||
"numpy>=1.24",
|
||||
"fastembed>=0.2",
|
||||
"hnswlib>=0.8.0",
|
||||
"onnxruntime-directml>=1.15.0", # DirectML support
|
||||
"numpy~=1.24.0",
|
||||
"fastembed~=0.2.0",
|
||||
"hnswlib~=0.8.0",
|
||||
"onnxruntime-directml~=1.15.0", # DirectML support
|
||||
]
|
||||
|
||||
# Cross-encoder reranking (second-stage, optional)
|
||||
# Install with: pip install codexlens[reranker] (default: ONNX backend)
|
||||
reranker-onnx = [
|
||||
"optimum>=1.16",
|
||||
"onnxruntime>=1.15",
|
||||
"transformers>=4.36",
|
||||
"optimum~=1.16.0",
|
||||
"onnxruntime~=1.15.0",
|
||||
"transformers~=4.36.0",
|
||||
]
|
||||
|
||||
# Remote reranking via HTTP API
|
||||
reranker-api = [
|
||||
"httpx>=0.25",
|
||||
"httpx~=0.25.0",
|
||||
]
|
||||
|
||||
# LLM-based reranking via ccw-litellm
|
||||
reranker-litellm = [
|
||||
"ccw-litellm>=0.1",
|
||||
"ccw-litellm~=0.1.0",
|
||||
]
|
||||
|
||||
# Legacy sentence-transformers CrossEncoder reranker
|
||||
reranker-legacy = [
|
||||
"sentence-transformers>=2.2",
|
||||
"sentence-transformers~=2.2.0",
|
||||
]
|
||||
|
||||
# Backward-compatible alias for default reranker backend
|
||||
reranker = [
|
||||
"optimum>=1.16",
|
||||
"onnxruntime>=1.15",
|
||||
"transformers>=4.36",
|
||||
"optimum~=1.16.0",
|
||||
"onnxruntime~=1.15.0",
|
||||
"transformers~=4.36.0",
|
||||
]
|
||||
|
||||
# Encoding detection for non-UTF8 files
|
||||
encoding = [
|
||||
"chardet>=5.0",
|
||||
"chardet~=5.0.0",
|
||||
]
|
||||
|
||||
# Clustering for staged hybrid search (HDBSCAN + sklearn)
|
||||
clustering = [
|
||||
"hdbscan>=0.8.1",
|
||||
"scikit-learn>=1.3.0",
|
||||
"hdbscan~=0.8.1",
|
||||
"scikit-learn~=1.3.0",
|
||||
]
|
||||
|
||||
# Full features including tiktoken for accurate token counting
|
||||
full = [
|
||||
"tiktoken>=0.5.0",
|
||||
"tiktoken~=0.5.0",
|
||||
]
|
||||
|
||||
# Language Server Protocol support
|
||||
lsp = [
|
||||
"pygls>=1.3.0",
|
||||
"pygls~=1.3.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
22
codex-lens/requirements.in
Normal file
22
codex-lens/requirements.in
Normal file
@@ -0,0 +1,22 @@
|
||||
# Core dependencies for codex-lens
|
||||
# This file tracks direct dependencies only
|
||||
# Run: pip-compile requirements.in --output-file=requirements.txt
|
||||
|
||||
typer~=0.9.0
|
||||
rich~=13.0.0
|
||||
pydantic~=2.0.0
|
||||
tree-sitter~=0.20.0
|
||||
tree-sitter-python~=0.25.0
|
||||
tree-sitter-javascript~=0.25.0
|
||||
tree-sitter-typescript~=0.23.0
|
||||
pathspec~=0.11.0
|
||||
watchdog~=3.0.0
|
||||
ast-grep-py~=0.40.0
|
||||
|
||||
# Semantic search dependencies
|
||||
numpy~=1.24.0
|
||||
fastembed~=0.2.0
|
||||
hnswlib~=0.8.0
|
||||
|
||||
# LSP support
|
||||
pygls~=1.3.0
|
||||
1
codex-lens/src/.gitignore
vendored
Normal file
1
codex-lens/src/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.ace-tool/
|
||||
@@ -1,4 +1,42 @@
|
||||
"""Embedding Manager - Manage semantic embeddings for code indexes."""
|
||||
"""Embedding Manager - Manage semantic embeddings for code indexes.
|
||||
|
||||
This module provides functions for generating and managing semantic embeddings
|
||||
for code indexes, supporting both fastembed and litellm backends.
|
||||
|
||||
Example Usage:
|
||||
Generate embeddings for a single index:
|
||||
|
||||
>>> from pathlib import Path
|
||||
>>> from codexlens.cli.embedding_manager import generate_embeddings
|
||||
>>> result = generate_embeddings(
|
||||
... index_path=Path("path/to/_index.db"),
|
||||
... force=True
|
||||
... )
|
||||
>>> if result["success"]:
|
||||
... print(f"Generated {result['total_chunks_created']} embeddings")
|
||||
|
||||
Generate embeddings for an entire project with centralized index:
|
||||
|
||||
>>> from codexlens.cli.embedding_manager import generate_dense_embeddings_centralized
|
||||
>>> result = generate_dense_embeddings_centralized(
|
||||
... index_root=Path("path/to/project"),
|
||||
... force=True,
|
||||
... progress_callback=lambda msg: print(msg)
|
||||
... )
|
||||
|
||||
Check if embeddings exist:
|
||||
|
||||
>>> from codexlens.cli.embedding_manager import check_index_embeddings
|
||||
>>> status = check_index_embeddings(Path("path/to/_index.db"))
|
||||
>>> print(status["result"]["has_embeddings"])
|
||||
|
||||
Backward Compatibility:
|
||||
The deprecated `discover_all_index_dbs()` function is maintained for compatibility.
|
||||
`generate_embeddings_recursive()` is deprecated but functional; use
|
||||
`generate_dense_embeddings_centralized()` instead.
|
||||
The `EMBEDDING_BATCH_SIZE` constant is kept as a reference but actual batch size
|
||||
is calculated dynamically via `calculate_dynamic_batch_size()`.
|
||||
"""
|
||||
|
||||
import gc
|
||||
import json
|
||||
@@ -53,11 +91,11 @@ def calculate_dynamic_batch_size(config, embedder) -> int:
|
||||
- Utilization factor (default 80% to leave headroom)
|
||||
|
||||
Args:
|
||||
config: Config object with api_batch_size_* settings
|
||||
embedder: Embedding model object with max_tokens property
|
||||
config: Config object with api_batch_size_* settings.
|
||||
embedder: Embedding model object with max_tokens property.
|
||||
|
||||
Returns:
|
||||
Calculated batch size, clamped to [1, api_batch_size_max]
|
||||
int: Calculated batch size, clamped to [1, api_batch_size_max].
|
||||
"""
|
||||
# If dynamic calculation is disabled, return static value
|
||||
if not getattr(config, 'api_batch_size_dynamic', False):
|
||||
@@ -147,8 +185,12 @@ def _cleanup_fastembed_resources() -> None:
|
||||
try:
|
||||
from codexlens.semantic.embedder import clear_embedder_cache
|
||||
clear_embedder_cache()
|
||||
except Exception:
|
||||
except (ImportError, AttributeError):
|
||||
# Expected when semantic module unavailable or cache function doesn't exist
|
||||
pass
|
||||
except Exception as exc:
|
||||
# Log unexpected errors but don't fail cleanup
|
||||
logger.debug(f"Unexpected error during fastembed cleanup: {exc}")
|
||||
|
||||
|
||||
def _generate_chunks_from_cursor(
|
||||
@@ -201,9 +243,18 @@ def _generate_chunks_from_cursor(
|
||||
total_files += 1
|
||||
for chunk in chunks:
|
||||
yield (chunk, file_path)
|
||||
except (OSError, UnicodeDecodeError) as e:
|
||||
# File access or encoding errors
|
||||
logger.error(f"Failed to read file {file_path}: {e}")
|
||||
failed_files.append((file_path, f"File read error: {e}"))
|
||||
except ValueError as e:
|
||||
# Chunking configuration errors
|
||||
logger.error(f"Chunking config error for {file_path}: {e}")
|
||||
failed_files.append((file_path, f"Chunking error: {e}"))
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to chunk {file_path}: {e}")
|
||||
failed_files.append((file_path, str(e)))
|
||||
# Other unexpected errors
|
||||
logger.error(f"Unexpected error processing {file_path}: {e}")
|
||||
failed_files.append((file_path, f"Unexpected error: {e}"))
|
||||
|
||||
|
||||
def _create_token_aware_batches(
|
||||
@@ -371,8 +422,153 @@ def _get_embedding_defaults() -> tuple[str, str, bool, List, str, float]:
|
||||
config.embedding_strategy,
|
||||
config.embedding_cooldown,
|
||||
)
|
||||
except Exception:
|
||||
except (ImportError, AttributeError, OSError, ValueError) as exc:
|
||||
# Config not available or malformed - use defaults
|
||||
logger.debug(f"Using default embedding config (config load failed): {exc}")
|
||||
return "fastembed", "code", True, [], "latency_aware", 60.0
|
||||
except Exception as exc:
|
||||
# Unexpected error - still use defaults but log
|
||||
logger.warning(f"Unexpected error loading embedding config: {exc}")
|
||||
return "fastembed", "code", True, [], "latency_aware", 60.0
|
||||
|
||||
|
||||
def _apply_embedding_config_defaults(
|
||||
embedding_backend: Optional[str],
|
||||
model_profile: Optional[str],
|
||||
use_gpu: Optional[bool],
|
||||
endpoints: Optional[List],
|
||||
strategy: Optional[str],
|
||||
cooldown: Optional[float],
|
||||
) -> tuple[str, str, bool, List, str, float]:
|
||||
"""Apply config defaults to embedding parameters.
|
||||
|
||||
This helper function reduces code duplication across embedding generation
|
||||
functions by centralizing the default value application logic.
|
||||
|
||||
Args:
|
||||
embedding_backend: Embedding backend (fastembed/litellm) or None for default
|
||||
model_profile: Model profile/name or None for default
|
||||
use_gpu: GPU flag or None for default
|
||||
endpoints: API endpoints list or None for default
|
||||
strategy: Selection strategy or None for default
|
||||
cooldown: Cooldown seconds or None for default
|
||||
|
||||
Returns:
|
||||
Tuple of (backend, model, use_gpu, endpoints, strategy, cooldown) with
|
||||
defaults applied where None was passed.
|
||||
"""
|
||||
(default_backend, default_model, default_gpu,
|
||||
default_endpoints, default_strategy, default_cooldown) = _get_embedding_defaults()
|
||||
|
||||
backend = embedding_backend if embedding_backend is not None else default_backend
|
||||
model = model_profile if model_profile is not None else default_model
|
||||
gpu = use_gpu if use_gpu is not None else default_gpu
|
||||
eps = endpoints if endpoints is not None else default_endpoints
|
||||
strat = strategy if strategy is not None else default_strategy
|
||||
cool = cooldown if cooldown is not None else default_cooldown
|
||||
|
||||
return backend, model, gpu, eps, strat, cool
|
||||
|
||||
|
||||
def _calculate_max_workers(
|
||||
embedding_backend: str,
|
||||
endpoints: Optional[List],
|
||||
max_workers: Optional[int],
|
||||
) -> int:
|
||||
"""Calculate optimal max_workers based on backend and endpoint count.
|
||||
|
||||
Args:
|
||||
embedding_backend: The embedding backend being used
|
||||
endpoints: List of API endpoints (for litellm multi-endpoint mode)
|
||||
max_workers: Explicitly specified max_workers or None for auto-calculation
|
||||
|
||||
Returns:
|
||||
Calculated or specified max_workers value
|
||||
"""
|
||||
if max_workers is not None:
|
||||
return max_workers
|
||||
|
||||
endpoint_count = len(endpoints) if endpoints else 1
|
||||
|
||||
# Set dynamic max_workers default based on backend type and endpoint count
|
||||
# - FastEmbed: CPU-bound, sequential is optimal (1 worker)
|
||||
# - LiteLLM single endpoint: 4 workers default
|
||||
# - LiteLLM multi-endpoint: workers = endpoint_count * 2 (to saturate all APIs)
|
||||
if embedding_backend == "litellm":
|
||||
if endpoint_count > 1:
|
||||
return endpoint_count * 2 # No cap, scale with endpoints
|
||||
else:
|
||||
return 4
|
||||
else:
|
||||
return 1
|
||||
|
||||
|
||||
def _initialize_embedder_and_chunker(
|
||||
embedding_backend: str,
|
||||
model_profile: str,
|
||||
use_gpu: bool,
|
||||
endpoints: Optional[List],
|
||||
strategy: str,
|
||||
cooldown: float,
|
||||
chunk_size: int,
|
||||
overlap: int,
|
||||
) -> tuple:
|
||||
"""Initialize embedder and chunker for embedding generation.
|
||||
|
||||
This helper function reduces code duplication by centralizing embedder
|
||||
and chunker initialization logic.
|
||||
|
||||
Args:
|
||||
embedding_backend: The embedding backend (fastembed/litellm)
|
||||
model_profile: Model profile or name
|
||||
use_gpu: Whether to use GPU acceleration
|
||||
endpoints: Optional API endpoints for load balancing
|
||||
strategy: Selection strategy for multi-endpoint mode
|
||||
cooldown: Cooldown seconds for rate-limited endpoints
|
||||
chunk_size: Maximum chunk size in characters
|
||||
overlap: Overlap size in characters
|
||||
|
||||
Returns:
|
||||
Tuple of (embedder, chunker, endpoint_count)
|
||||
|
||||
Raises:
|
||||
ValueError: If embedding_backend is invalid
|
||||
"""
|
||||
from codexlens.semantic.factory import get_embedder as get_embedder_factory
|
||||
from codexlens.semantic.chunker import Chunker, ChunkConfig
|
||||
from codexlens.config import Config
|
||||
|
||||
# Initialize embedder using factory (supports fastembed, litellm, and rotational)
|
||||
# For fastembed: model_profile is a profile name (fast/code/multilingual/balanced)
|
||||
# For litellm: model_profile is a model name (e.g., qwen3-embedding)
|
||||
# For multi-endpoint: endpoints list enables load balancing
|
||||
if embedding_backend == "fastembed":
|
||||
embedder = get_embedder_factory(backend="fastembed", profile=model_profile, use_gpu=use_gpu)
|
||||
elif embedding_backend == "litellm":
|
||||
embedder = get_embedder_factory(
|
||||
backend="litellm",
|
||||
model=model_profile,
|
||||
endpoints=endpoints if endpoints else None,
|
||||
strategy=strategy,
|
||||
cooldown=cooldown,
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Invalid embedding backend: {embedding_backend}. Must be 'fastembed' or 'litellm'.")
|
||||
|
||||
# skip_token_count=True: Use fast estimation (len/4) instead of expensive tiktoken
|
||||
# This significantly reduces CPU usage with minimal impact on metadata accuracy
|
||||
# Load chunk stripping config from settings
|
||||
chunk_cfg = Config.load()
|
||||
chunker = Chunker(config=ChunkConfig(
|
||||
max_chunk_size=chunk_size,
|
||||
overlap=overlap,
|
||||
skip_token_count=True,
|
||||
strip_comments=getattr(chunk_cfg, 'chunk_strip_comments', True),
|
||||
strip_docstrings=getattr(chunk_cfg, 'chunk_strip_docstrings', True),
|
||||
))
|
||||
|
||||
endpoint_count = len(endpoints) if endpoints else 1
|
||||
return embedder, chunker, endpoint_count
|
||||
|
||||
|
||||
def generate_embeddings(
|
||||
@@ -397,16 +593,16 @@ def generate_embeddings(
|
||||
LiteLLM backend to improve throughput.
|
||||
|
||||
Args:
|
||||
index_path: Path to _index.db file
|
||||
index_path: Path to _index.db file.
|
||||
embedding_backend: Embedding backend to use (fastembed or litellm).
|
||||
Defaults to config setting.
|
||||
model_profile: Model profile for fastembed (fast, code, multilingual, balanced)
|
||||
or model name for litellm (e.g., qwen3-embedding).
|
||||
Defaults to config setting.
|
||||
force: If True, regenerate even if embeddings exist
|
||||
chunk_size: Maximum chunk size in characters
|
||||
overlap: Overlap size in characters for sliding window chunking (default: 200)
|
||||
progress_callback: Optional callback for progress updates
|
||||
force: If True, regenerate even if embeddings exist.
|
||||
chunk_size: Maximum chunk size in characters.
|
||||
overlap: Overlap size in characters for sliding window chunking (default: 200).
|
||||
progress_callback: Optional callback for progress updates.
|
||||
use_gpu: Whether to use GPU acceleration (fastembed only).
|
||||
Defaults to config setting.
|
||||
max_tokens_per_batch: Maximum tokens per batch for token-aware batching.
|
||||
@@ -420,40 +616,22 @@ def generate_embeddings(
|
||||
cooldown: Default cooldown seconds for rate-limited endpoints.
|
||||
|
||||
Returns:
|
||||
Result dictionary with generation statistics
|
||||
Dict[str, any]: Result dictionary with generation statistics.
|
||||
Contains keys: success, error (if failed), files_processed,
|
||||
total_chunks_created, execution_time, etc.
|
||||
|
||||
Raises:
|
||||
ValueError: If embedding_backend is invalid.
|
||||
ImportError: If semantic module is not available.
|
||||
"""
|
||||
# Get defaults from config if not specified
|
||||
(default_backend, default_model, default_gpu,
|
||||
default_endpoints, default_strategy, default_cooldown) = _get_embedding_defaults()
|
||||
# Apply config defaults
|
||||
embedding_backend, model_profile, use_gpu, endpoints, strategy, cooldown = \
|
||||
_apply_embedding_config_defaults(
|
||||
embedding_backend, model_profile, use_gpu, endpoints, strategy, cooldown
|
||||
)
|
||||
|
||||
if embedding_backend is None:
|
||||
embedding_backend = default_backend
|
||||
if model_profile is None:
|
||||
model_profile = default_model
|
||||
if use_gpu is None:
|
||||
use_gpu = default_gpu
|
||||
if endpoints is None:
|
||||
endpoints = default_endpoints
|
||||
if strategy is None:
|
||||
strategy = default_strategy
|
||||
if cooldown is None:
|
||||
cooldown = default_cooldown
|
||||
|
||||
# Calculate endpoint count for worker scaling
|
||||
endpoint_count = len(endpoints) if endpoints else 1
|
||||
|
||||
# Set dynamic max_workers default based on backend type and endpoint count
|
||||
# - FastEmbed: CPU-bound, sequential is optimal (1 worker)
|
||||
# - LiteLLM single endpoint: 4 workers default
|
||||
# - LiteLLM multi-endpoint: workers = endpoint_count * 2 (to saturate all APIs)
|
||||
if max_workers is None:
|
||||
if embedding_backend == "litellm":
|
||||
if endpoint_count > 1:
|
||||
max_workers = endpoint_count * 2 # No cap, scale with endpoints
|
||||
else:
|
||||
max_workers = 4
|
||||
else:
|
||||
max_workers = 1
|
||||
# Calculate max_workers
|
||||
max_workers = _calculate_max_workers(embedding_backend, endpoints, max_workers)
|
||||
|
||||
backend_available, backend_error = is_embedding_backend_available(embedding_backend)
|
||||
if not backend_available:
|
||||
@@ -487,51 +665,23 @@ def generate_embeddings(
|
||||
with sqlite3.connect(index_path) as conn:
|
||||
conn.execute("DELETE FROM semantic_chunks")
|
||||
conn.commit()
|
||||
except sqlite3.DatabaseError as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Database error clearing chunks: {str(e)}",
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Failed to clear existing chunks: {str(e)}",
|
||||
}
|
||||
|
||||
# Initialize components
|
||||
# Initialize embedder and chunker using helper
|
||||
try:
|
||||
# Import factory function to support both backends
|
||||
from codexlens.semantic.factory import get_embedder as get_embedder_factory
|
||||
from codexlens.semantic.vector_store import VectorStore
|
||||
from codexlens.semantic.chunker import Chunker, ChunkConfig
|
||||
|
||||
# Initialize embedder using factory (supports fastembed, litellm, and rotational)
|
||||
# For fastembed: model_profile is a profile name (fast/code/multilingual/balanced)
|
||||
# For litellm: model_profile is a model name (e.g., qwen3-embedding)
|
||||
# For multi-endpoint: endpoints list enables load balancing
|
||||
if embedding_backend == "fastembed":
|
||||
embedder = get_embedder_factory(backend="fastembed", profile=model_profile, use_gpu=use_gpu)
|
||||
elif embedding_backend == "litellm":
|
||||
embedder = get_embedder_factory(
|
||||
backend="litellm",
|
||||
model=model_profile,
|
||||
endpoints=endpoints if endpoints else None,
|
||||
strategy=strategy,
|
||||
cooldown=cooldown,
|
||||
)
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Invalid embedding backend: {embedding_backend}. Must be 'fastembed' or 'litellm'.",
|
||||
}
|
||||
|
||||
# skip_token_count=True: Use fast estimation (len/4) instead of expensive tiktoken
|
||||
# This significantly reduces CPU usage with minimal impact on metadata accuracy
|
||||
# Load chunk stripping config from settings
|
||||
from codexlens.config import Config
|
||||
chunk_cfg = Config.load()
|
||||
chunker = Chunker(config=ChunkConfig(
|
||||
max_chunk_size=chunk_size,
|
||||
overlap=overlap,
|
||||
skip_token_count=True,
|
||||
strip_comments=getattr(chunk_cfg, 'chunk_strip_comments', True),
|
||||
strip_docstrings=getattr(chunk_cfg, 'chunk_strip_docstrings', True),
|
||||
))
|
||||
embedder, chunker, endpoint_count = _initialize_embedder_and_chunker(
|
||||
embedding_backend, model_profile, use_gpu, endpoints, strategy, cooldown,
|
||||
chunk_size, overlap
|
||||
)
|
||||
|
||||
# Log embedder info with endpoint count for multi-endpoint mode
|
||||
if progress_callback:
|
||||
@@ -547,10 +697,17 @@ def generate_embeddings(
|
||||
if progress_callback and batch_config.api_batch_size_dynamic:
|
||||
progress_callback(f"Dynamic batch size: {effective_batch_size} (model max_tokens={getattr(embedder, 'max_tokens', 8192)})")
|
||||
|
||||
except Exception as e:
|
||||
except (ImportError, ValueError) as e:
|
||||
# Missing dependency or invalid configuration
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Failed to initialize components: {str(e)}",
|
||||
"error": f"Failed to initialize embedding components: {str(e)}",
|
||||
}
|
||||
except Exception as e:
|
||||
# Other unexpected errors
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Unexpected error initializing components: {str(e)}",
|
||||
}
|
||||
|
||||
# --- STREAMING PROCESSING ---
|
||||
@@ -814,8 +971,8 @@ def generate_embeddings(
|
||||
try:
|
||||
_cleanup_fastembed_resources()
|
||||
gc.collect()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as cleanup_exc:
|
||||
logger.debug(f"Cleanup error during exception handling: {cleanup_exc}")
|
||||
return {"success": False, "error": f"Failed to read or process files: {str(e)}"}
|
||||
|
||||
elapsed_time = time.time() - start_time
|
||||
@@ -825,8 +982,8 @@ def generate_embeddings(
|
||||
try:
|
||||
_cleanup_fastembed_resources()
|
||||
gc.collect()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as cleanup_exc:
|
||||
logger.debug(f"Cleanup error during finalization: {cleanup_exc}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
@@ -922,7 +1079,8 @@ def build_centralized_binary_vectors_from_existing(
|
||||
}
|
||||
|
||||
# We count per-dim later after selecting a target dim.
|
||||
except Exception:
|
||||
except (sqlite3.DatabaseError, ValueError, TypeError):
|
||||
# Skip corrupted or malformed indexes
|
||||
continue
|
||||
|
||||
if not dims_seen:
|
||||
@@ -971,7 +1129,8 @@ def build_centralized_binary_vectors_from_existing(
|
||||
"SELECT COUNT(*) FROM semantic_chunks WHERE embedding IS NOT NULL AND length(embedding) > 0"
|
||||
).fetchone()
|
||||
total_chunks += int(row[0] if row else 0)
|
||||
except Exception:
|
||||
except (sqlite3.DatabaseError, ValueError, TypeError):
|
||||
# Skip corrupted or malformed indexes
|
||||
continue
|
||||
|
||||
if not total_chunks:
|
||||
@@ -987,7 +1146,7 @@ def build_centralized_binary_vectors_from_existing(
|
||||
# Prepare output files / DB.
|
||||
try:
|
||||
import numpy as np
|
||||
except Exception as exc:
|
||||
except ImportError as exc:
|
||||
return {"success": False, "error": f"numpy required to build binary vectors: {exc}"}
|
||||
|
||||
store = VectorMetadataStore(vectors_meta_path)
|
||||
@@ -1243,35 +1402,14 @@ def generate_embeddings_recursive(
|
||||
stacklevel=2
|
||||
)
|
||||
|
||||
# Get defaults from config if not specified
|
||||
(default_backend, default_model, default_gpu,
|
||||
default_endpoints, default_strategy, default_cooldown) = _get_embedding_defaults()
|
||||
# Apply config defaults
|
||||
embedding_backend, model_profile, use_gpu, endpoints, strategy, cooldown = \
|
||||
_apply_embedding_config_defaults(
|
||||
embedding_backend, model_profile, use_gpu, endpoints, strategy, cooldown
|
||||
)
|
||||
|
||||
if embedding_backend is None:
|
||||
embedding_backend = default_backend
|
||||
if model_profile is None:
|
||||
model_profile = default_model
|
||||
if use_gpu is None:
|
||||
use_gpu = default_gpu
|
||||
if endpoints is None:
|
||||
endpoints = default_endpoints
|
||||
if strategy is None:
|
||||
strategy = default_strategy
|
||||
if cooldown is None:
|
||||
cooldown = default_cooldown
|
||||
|
||||
# Calculate endpoint count for worker scaling
|
||||
endpoint_count = len(endpoints) if endpoints else 1
|
||||
|
||||
# Set dynamic max_workers default based on backend type and endpoint count
|
||||
if max_workers is None:
|
||||
if embedding_backend == "litellm":
|
||||
if endpoint_count > 1:
|
||||
max_workers = endpoint_count * 2 # No cap, scale with endpoints
|
||||
else:
|
||||
max_workers = 4
|
||||
else:
|
||||
max_workers = 1
|
||||
# Calculate max_workers
|
||||
max_workers = _calculate_max_workers(embedding_backend, endpoints, max_workers)
|
||||
|
||||
# Discover all _index.db files (using internal helper to avoid double deprecation warning)
|
||||
index_files = _discover_index_dbs_internal(index_root)
|
||||
@@ -1401,34 +1539,14 @@ def generate_dense_embeddings_centralized(
|
||||
"""
|
||||
from codexlens.config import VECTORS_HNSW_NAME
|
||||
|
||||
# Get defaults from config if not specified
|
||||
(default_backend, default_model, default_gpu,
|
||||
default_endpoints, default_strategy, default_cooldown) = _get_embedding_defaults()
|
||||
# Apply config defaults
|
||||
embedding_backend, model_profile, use_gpu, endpoints, strategy, cooldown = \
|
||||
_apply_embedding_config_defaults(
|
||||
embedding_backend, model_profile, use_gpu, endpoints, strategy, cooldown
|
||||
)
|
||||
|
||||
if embedding_backend is None:
|
||||
embedding_backend = default_backend
|
||||
if model_profile is None:
|
||||
model_profile = default_model
|
||||
if use_gpu is None:
|
||||
use_gpu = default_gpu
|
||||
if endpoints is None:
|
||||
endpoints = default_endpoints
|
||||
if strategy is None:
|
||||
strategy = default_strategy
|
||||
if cooldown is None:
|
||||
cooldown = default_cooldown
|
||||
|
||||
# Calculate endpoint count for worker scaling
|
||||
endpoint_count = len(endpoints) if endpoints else 1
|
||||
|
||||
if max_workers is None:
|
||||
if embedding_backend == "litellm":
|
||||
if endpoint_count > 1:
|
||||
max_workers = endpoint_count * 2
|
||||
else:
|
||||
max_workers = 4
|
||||
else:
|
||||
max_workers = 1
|
||||
# Calculate max_workers
|
||||
max_workers = _calculate_max_workers(embedding_backend, endpoints, max_workers)
|
||||
|
||||
backend_available, backend_error = is_embedding_backend_available(embedding_backend)
|
||||
if not backend_available:
|
||||
@@ -1470,38 +1588,18 @@ def generate_dense_embeddings_centralized(
|
||||
"error": f"Centralized vector index already exists at {central_hnsw_path}. Use --force to regenerate.",
|
||||
}
|
||||
|
||||
# Initialize embedder
|
||||
# Initialize embedder and chunker using helper
|
||||
try:
|
||||
from codexlens.semantic.factory import get_embedder as get_embedder_factory
|
||||
from codexlens.semantic.chunker import Chunker, ChunkConfig
|
||||
from codexlens.semantic.ann_index import ANNIndex
|
||||
|
||||
if embedding_backend == "fastembed":
|
||||
embedder = get_embedder_factory(backend="fastembed", profile=model_profile, use_gpu=use_gpu)
|
||||
elif embedding_backend == "litellm":
|
||||
embedder = get_embedder_factory(
|
||||
backend="litellm",
|
||||
model=model_profile,
|
||||
endpoints=endpoints if endpoints else None,
|
||||
strategy=strategy,
|
||||
cooldown=cooldown,
|
||||
)
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Invalid embedding backend: {embedding_backend}",
|
||||
}
|
||||
embedder, chunker, endpoint_count = _initialize_embedder_and_chunker(
|
||||
embedding_backend, model_profile, use_gpu, endpoints, strategy, cooldown,
|
||||
chunk_size, overlap
|
||||
)
|
||||
|
||||
# Load chunk stripping config from settings
|
||||
# Load chunk stripping config for batch size calculation
|
||||
from codexlens.config import Config
|
||||
chunk_cfg = Config.load()
|
||||
chunker = Chunker(config=ChunkConfig(
|
||||
max_chunk_size=chunk_size,
|
||||
overlap=overlap,
|
||||
skip_token_count=True,
|
||||
strip_comments=getattr(chunk_cfg, 'chunk_strip_comments', True),
|
||||
strip_docstrings=getattr(chunk_cfg, 'chunk_strip_docstrings', True),
|
||||
))
|
||||
batch_config = Config.load()
|
||||
|
||||
if progress_callback:
|
||||
if endpoint_count > 1:
|
||||
@@ -1509,7 +1607,6 @@ def generate_dense_embeddings_centralized(
|
||||
progress_callback(f"Using model: {embedder.model_name} ({embedder.embedding_dim} dimensions)")
|
||||
|
||||
# Calculate dynamic batch size based on model capacity
|
||||
batch_config = chunk_cfg # Reuse already loaded config
|
||||
effective_batch_size = calculate_dynamic_batch_size(batch_config, embedder)
|
||||
|
||||
if progress_callback and batch_config.api_batch_size_dynamic:
|
||||
|
||||
@@ -120,8 +120,12 @@ def load_env_file(env_path: Path) -> Dict[str, str]:
|
||||
if result:
|
||||
key, value = result
|
||||
env_vars[key] = value
|
||||
except Exception as exc:
|
||||
except (OSError, UnicodeDecodeError) as exc:
|
||||
# File access errors or encoding issues are expected and logged
|
||||
log.warning("Failed to load .env file %s: %s", env_path, exc)
|
||||
except Exception as exc:
|
||||
# Other unexpected errors are also logged but indicate a code issue
|
||||
log.warning("Unexpected error loading .env file %s: %s", env_path, exc)
|
||||
|
||||
return env_vars
|
||||
|
||||
|
||||
278
codex-lens/tests/conftest.py
Normal file
278
codex-lens/tests/conftest.py
Normal file
@@ -0,0 +1,278 @@
|
||||
"""Pytest configuration and shared fixtures for codex-lens tests.
|
||||
|
||||
This module provides common fixtures and test utilities to reduce code duplication
|
||||
across the test suite. Using fixtures ensures consistent test setup and makes tests
|
||||
more maintainable.
|
||||
|
||||
Common Fixtures:
|
||||
- temp_dir: Temporary directory for test files
|
||||
- sample_index_db: Sample index database with test data
|
||||
- mock_config: Mock configuration object
|
||||
- sample_code_files: Factory for creating sample code files
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import tempfile
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
import sqlite3
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_dir():
|
||||
"""Create a temporary directory for test files.
|
||||
|
||||
The directory is automatically cleaned up after the test.
|
||||
|
||||
Yields:
|
||||
Path: Path to the temporary directory.
|
||||
"""
|
||||
temp_path = Path(tempfile.mkdtemp())
|
||||
yield temp_path
|
||||
# Cleanup
|
||||
if temp_path.exists():
|
||||
shutil.rmtree(temp_path)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_index_db(temp_dir):
|
||||
"""Create a sample index database with test data.
|
||||
|
||||
The database has a basic schema with files and chunks tables
|
||||
populated with sample data.
|
||||
|
||||
Args:
|
||||
temp_dir: Temporary directory fixture.
|
||||
|
||||
Yields:
|
||||
Path: Path to the sample index database.
|
||||
"""
|
||||
db_path = temp_dir / "_index.db"
|
||||
|
||||
# Create database with basic schema
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Files table
|
||||
cursor.execute("""
|
||||
CREATE TABLE files (
|
||||
id INTEGER PRIMARY KEY,
|
||||
path TEXT NOT NULL UNIQUE,
|
||||
content TEXT,
|
||||
language TEXT,
|
||||
hash TEXT,
|
||||
indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
|
||||
# Insert sample files
|
||||
sample_files = [
|
||||
("test.py", "def hello():\n print('world')", "python", "hash1"),
|
||||
("test.js", "function hello() { console.log('world'); }", "javascript", "hash2"),
|
||||
("README.md", "# Test Project", "markdown", "hash3"),
|
||||
]
|
||||
cursor.executemany(
|
||||
"INSERT INTO files (path, content, language, hash) VALUES (?, ?, ?, ?)",
|
||||
sample_files
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
yield db_path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config():
|
||||
"""Create a mock configuration object with default values.
|
||||
|
||||
Returns:
|
||||
Mock: Mock object with common config attributes.
|
||||
"""
|
||||
from unittest.mock import Mock
|
||||
|
||||
config = Mock()
|
||||
config.index_path = Path("/tmp/test_index")
|
||||
config.chunk_size = 2000
|
||||
config.overlap = 200
|
||||
config.embedding_backend = "fastembed"
|
||||
config.embedding_model = "code"
|
||||
config.max_results = 10
|
||||
|
||||
return config
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_code_factory(temp_dir):
|
||||
"""Factory for creating sample code files.
|
||||
|
||||
Args:
|
||||
temp_dir: Temporary directory fixture.
|
||||
|
||||
Returns:
|
||||
callable: Function that creates sample code files.
|
||||
"""
|
||||
def _create_file(filename: str, content: str, language: str = "python") -> Path:
|
||||
"""Create a sample code file.
|
||||
|
||||
Args:
|
||||
filename: Name of the file to create.
|
||||
content: Content of the file.
|
||||
language: Programming language (default: python).
|
||||
|
||||
Returns:
|
||||
Path: Path to the created file.
|
||||
"""
|
||||
file_path = temp_dir / filename
|
||||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
file_path.write_text(content)
|
||||
return file_path
|
||||
|
||||
return _create_file
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_python_code():
|
||||
"""Sample Python code for testing.
|
||||
|
||||
Returns:
|
||||
str: Sample Python code snippet.
|
||||
"""
|
||||
return '''
|
||||
def calculate_sum(a: int, b: int) -> int:
|
||||
"""Calculate the sum of two integers."""
|
||||
return a + b
|
||||
|
||||
class Calculator:
|
||||
"""A simple calculator class."""
|
||||
|
||||
def __init__(self):
|
||||
self.value = 0
|
||||
|
||||
def add(self, x: int) -> None:
|
||||
"""Add a value to the calculator."""
|
||||
self.value += x
|
||||
|
||||
if __name__ == "__main__":
|
||||
calc = Calculator()
|
||||
calc.add(5)
|
||||
print(f"Result: {calc.value}")
|
||||
'''
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_javascript_code():
|
||||
"""Sample JavaScript code for testing.
|
||||
|
||||
Returns:
|
||||
str: Sample JavaScript code snippet.
|
||||
"""
|
||||
return '''
|
||||
// Simple utility functions
|
||||
function add(a, b) {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
const Calculator = class {
|
||||
constructor() {
|
||||
this.value = 0;
|
||||
}
|
||||
|
||||
add(x) {
|
||||
this.value += x;
|
||||
}
|
||||
};
|
||||
|
||||
// Example usage
|
||||
const calc = new Calculator();
|
||||
calc.add(5);
|
||||
console.log(`Result: ${calc.value}`);
|
||||
'''
|
||||
|
||||
|
||||
class CodeSampleFactory:
|
||||
"""Factory class for generating various code samples.
|
||||
|
||||
This class provides methods to generate code samples in different
|
||||
languages with various patterns (classes, functions, imports, etc.).
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def python_function(name: str = "example", docstring: bool = True) -> str:
|
||||
"""Generate a Python function sample.
|
||||
|
||||
Args:
|
||||
name: Function name.
|
||||
docstring: Whether to include docstring.
|
||||
|
||||
Returns:
|
||||
str: Python function code.
|
||||
"""
|
||||
doc = f' """Example function."""\n' if docstring else ''
|
||||
return f'''
|
||||
def {name}(param1: str, param2: int = 10) -> str:
|
||||
{doc} return param1 * param2
|
||||
'''.strip()
|
||||
|
||||
@staticmethod
|
||||
def python_class(name: str = "Example") -> str:
|
||||
"""Generate a Python class sample.
|
||||
|
||||
Args:
|
||||
name: Class name.
|
||||
|
||||
Returns:
|
||||
str: Python class code.
|
||||
"""
|
||||
return f'''
|
||||
class {name}:
|
||||
"""Example class."""
|
||||
|
||||
def __init__(self, value: int = 0):
|
||||
self.value = value
|
||||
|
||||
def increment(self) -> None:
|
||||
"""Increment the value."""
|
||||
self.value += 1
|
||||
'''.strip()
|
||||
|
||||
@staticmethod
|
||||
def javascript_function(name: str = "example") -> str:
|
||||
"""Generate a JavaScript function sample.
|
||||
|
||||
Args:
|
||||
name: Function name.
|
||||
|
||||
Returns:
|
||||
str: JavaScript function code.
|
||||
"""
|
||||
return f'''function {name}(param1, param2 = 10) {{
|
||||
return param1 * param2;
|
||||
}}'''.strip()
|
||||
|
||||
@staticmethod
|
||||
def typescript_interface(name: str = "Example") -> str:
|
||||
"""Generate a TypeScript interface sample.
|
||||
|
||||
Args:
|
||||
name: Interface name.
|
||||
|
||||
Returns:
|
||||
str: TypeScript interface code.
|
||||
"""
|
||||
return f'''interface {name} {{
|
||||
id: number;
|
||||
name: string;
|
||||
getValue(): number;
|
||||
}}'''.strip()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def code_sample_factory():
|
||||
"""Create a code sample factory instance.
|
||||
|
||||
Returns:
|
||||
CodeSampleFactory: Factory for generating code samples.
|
||||
"""
|
||||
return CodeSampleFactory()
|
||||
101
codex-lens/tests/lsp/test_lsp_edge_cases.py
Normal file
101
codex-lens/tests/lsp/test_lsp_edge_cases.py
Normal file
@@ -0,0 +1,101 @@
|
||||
"""LSP Edge Case Tests.
|
||||
|
||||
This module tests edge cases and error conditions in LSP (Language Server Protocol)
|
||||
operations, including timeout handling, protocol errors, and connection failures.
|
||||
|
||||
Test Coverage:
|
||||
- Timeout scenarios for LSP operations
|
||||
- Protocol errors and malformed responses
|
||||
- Connection failures and recovery
|
||||
- Concurrent request handling
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
import time
|
||||
|
||||
|
||||
class TestLSPTimeouts:
|
||||
"""Test timeout handling in LSP operations."""
|
||||
|
||||
def test_hover_request_timeout(self):
|
||||
"""Test that hover requests timeout appropriately after configured duration."""
|
||||
# This is a placeholder for actual timeout testing
|
||||
# Implementation requires mocking LSP client with delayed response
|
||||
pytest.skip("Requires LSP server fixture setup")
|
||||
|
||||
def test_definition_request_timeout(self):
|
||||
"""Test that go-to-definition requests timeout appropriately."""
|
||||
pytest.skip("Requires LSP server fixture setup")
|
||||
|
||||
def test_references_request_timeout(self):
|
||||
"""Test that find-references requests timeout appropriately."""
|
||||
pytest.skip("Requires LSP server fixture setup")
|
||||
|
||||
def test_concurrent_requests_with_timeout(self):
|
||||
"""Test behavior when multiple requests exceed timeout threshold."""
|
||||
pytest.skip("Requires LSP server fixture setup")
|
||||
|
||||
|
||||
class TestLSPProtocolErrors:
|
||||
"""Test handling of LSP protocol errors."""
|
||||
|
||||
def test_malformed_json_response(self):
|
||||
"""Test handling of malformed JSON in LSP responses."""
|
||||
pytest.skip("Requires LSP client fixture")
|
||||
|
||||
def test_invalid_method_error(self):
|
||||
"""Test handling of unknown/invalid method calls."""
|
||||
pytest.skip("Requires LSP client fixture")
|
||||
|
||||
def test_missing_required_params(self):
|
||||
"""Test handling of responses with missing required parameters."""
|
||||
pytest.skip("Requires LSP client fixture")
|
||||
|
||||
def test_null_result_handling(self):
|
||||
"""Test that null results from LSP are handled gracefully."""
|
||||
pytest.skip("Requires LSP client fixture")
|
||||
|
||||
|
||||
class TestLSPConnectionFailures:
|
||||
"""Test LSP connection failure scenarios."""
|
||||
|
||||
def test_server_not_found(self):
|
||||
"""Test behavior when LSP server is not available."""
|
||||
pytest.skip("Requires LSP client fixture")
|
||||
|
||||
def test_connection_dropped_mid_request(self):
|
||||
"""Test handling of dropped connections during active requests."""
|
||||
pytest.skip("Requires LSP client fixture")
|
||||
|
||||
def test_connection_retry_logic(self):
|
||||
"""Test that connection retry logic works as expected."""
|
||||
pytest.skip("Requires LSP client fixture")
|
||||
|
||||
def test_server_startup_failure(self):
|
||||
"""Test handling of LSP server startup failures."""
|
||||
pytest.skip("Requires LSP server fixture")
|
||||
|
||||
|
||||
class TestLSPResourceLimits:
|
||||
"""Test LSP behavior under resource constraints."""
|
||||
|
||||
def test_large_file_handling(self):
|
||||
"""Test LSP operations on very large source files."""
|
||||
pytest.skip("Requires test file fixtures")
|
||||
|
||||
def test_memory_pressure(self):
|
||||
"""Test LSP behavior under memory pressure."""
|
||||
pytest.skip("Requires memory simulation")
|
||||
|
||||
def test_concurrent_request_limits(self):
|
||||
"""Test handling of too many concurrent LSP requests."""
|
||||
pytest.skip("Requires LSP client fixture")
|
||||
|
||||
|
||||
# TODO: Implement actual tests using pytest fixtures and LSP mock objects
|
||||
# The test infrastructure needs to be set up with:
|
||||
# - LSP server fixture (maybe using pygls test server)
|
||||
# - LSP client fixture with configurable delays/errors
|
||||
# - Test file fixtures with various code patterns
|
||||
125
codex-lens/tests/test_incremental_indexer.py
Normal file
125
codex-lens/tests/test_incremental_indexer.py
Normal file
@@ -0,0 +1,125 @@
|
||||
"""Incremental Indexer File Event Processing Tests.
|
||||
|
||||
This module tests the file event processing in the incremental indexer,
|
||||
covering all file system event types (CREATED, MODIFIED, DELETED, MOVED).
|
||||
|
||||
Test Coverage:
|
||||
- CREATED events: New files being indexed
|
||||
- MODIFIED events: Changed files being re-indexed
|
||||
- DELETED events: Removed files being handled
|
||||
- MOVED events: File renames being tracked
|
||||
- Batch processing of multiple events
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
import tempfile
|
||||
import shutil
|
||||
|
||||
|
||||
class TestCreatedEvents:
|
||||
"""Test handling of CREATED file events."""
|
||||
|
||||
def test_new_file_indexed(self):
|
||||
"""Test that newly created files are properly indexed."""
|
||||
pytest.skip("Requires incremental indexer fixture")
|
||||
|
||||
def test_created_in_subdirectory(self):
|
||||
"""Test that files created in subdirectories are indexed."""
|
||||
pytest.skip("Requires incremental indexer fixture")
|
||||
|
||||
def test_batch_created_events(self):
|
||||
"""Test handling multiple files created simultaneously."""
|
||||
pytest.skip("Requires incremental indexer fixture")
|
||||
|
||||
|
||||
class TestModifiedEvents:
|
||||
"""Test handling of MODIFIED file events."""
|
||||
|
||||
def test_file_content_updated(self):
|
||||
"""Test that file content changes trigger re-indexing."""
|
||||
pytest.skip("Requires incremental indexer fixture")
|
||||
|
||||
def test_metadata_only_change(self):
|
||||
"""Test handling of metadata-only changes (permissions, etc)."""
|
||||
pytest.skip("Requires incremental indexer fixture")
|
||||
|
||||
def test_rapid_modifications(self):
|
||||
"""Test handling of rapid successive modifications to same file."""
|
||||
pytest.skip("Requires incremental indexer fixture")
|
||||
|
||||
|
||||
class TestDeletedEvents:
|
||||
"""Test handling of DELETED file events."""
|
||||
|
||||
def test_file_removed_from_index(self):
|
||||
"""Test that deleted files are removed from the index."""
|
||||
pytest.skip("Requires incremental indexer fixture")
|
||||
|
||||
def test_directory_deleted(self):
|
||||
"""Test handling of directory deletion events."""
|
||||
pytest.skip("Requires incremental indexer fixture")
|
||||
|
||||
def test_delete_non_indexed_file(self):
|
||||
"""Test handling deletion of files that were never indexed."""
|
||||
pytest.skip("Requires incremental indexer fixture")
|
||||
|
||||
|
||||
class TestMovedEvents:
|
||||
"""Test handling of MOVED/RENAMED file events."""
|
||||
|
||||
def test_file_renamed(self):
|
||||
"""Test that renamed files are tracked in the index."""
|
||||
pytest.skip("Requires incremental indexer fixture")
|
||||
|
||||
def test_file_moved_to_subdirectory(self):
|
||||
"""Test that files moved to subdirectories are tracked."""
|
||||
pytest.skip("Requires incremental indexer fixture")
|
||||
|
||||
def test_file_moved_out_of_watch_root(self):
|
||||
"""Test handling of files moved outside the watch directory."""
|
||||
pytest.skip("Requires incremental indexer fixture")
|
||||
|
||||
def test_directory_renamed(self):
|
||||
"""Test handling of directory rename events."""
|
||||
pytest.skip("Requires incremental indexer fixture")
|
||||
|
||||
|
||||
class TestEventBatching:
|
||||
"""Test batching and deduplication of file events."""
|
||||
|
||||
def test_duplicate_events_deduplicated(self):
|
||||
"""Test that duplicate events for the same file are deduplicated."""
|
||||
pytest.skip("Requires incremental indexer fixture")
|
||||
|
||||
def test_event_ordering_preserved(self):
|
||||
"""Test that events are processed in the correct order."""
|
||||
pytest.skip("Requires incremental indexer fixture")
|
||||
|
||||
def test_mixed_event_types_batch(self):
|
||||
"""Test handling a batch with mixed event types."""
|
||||
pytest.skip("Requires incremental indexer fixture")
|
||||
|
||||
|
||||
class TestErrorHandling:
|
||||
"""Test error handling in file event processing."""
|
||||
|
||||
def test_unreadable_file_skipped(self):
|
||||
"""Test that unreadable files are handled gracefully."""
|
||||
pytest.skip("Requires incremental indexer fixture")
|
||||
|
||||
def test_corrupted_event_continues(self):
|
||||
"""Test that processing continues after a corrupted event."""
|
||||
pytest.skip("Requires incremental indexer fixture")
|
||||
|
||||
def test_indexer_error_recovery(self):
|
||||
"""Test recovery from indexer errors during event processing."""
|
||||
pytest.skip("Requires incremental indexer fixture")
|
||||
|
||||
|
||||
# TODO: Implement actual tests using pytest fixtures and the incremental indexer
|
||||
# The test infrastructure needs:
|
||||
# - IncrementalIndexer fixture with mock filesystem watcher
|
||||
# - Temporary directory fixtures for test files
|
||||
# - Mock event queue for controlled event injection
|
||||
114
codex-lens/tests/test_migrations.py
Normal file
114
codex-lens/tests/test_migrations.py
Normal file
@@ -0,0 +1,114 @@
|
||||
"""Database Migration Tests.
|
||||
|
||||
This module tests the database migration system for the codex-lens index,
|
||||
ensuring that forward and backward compatibility is maintained across schema versions.
|
||||
|
||||
Test Coverage:
|
||||
- Forward migrations: Old schema to new schema
|
||||
- Backward compatibility: New code can read old schemas
|
||||
- Migration rollback capabilities
|
||||
- Data integrity during migrations
|
||||
- Edge cases (empty databases, corrupted data, etc.)
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import json
|
||||
|
||||
|
||||
class TestForwardMigrations:
|
||||
"""Test upgrading from older schema versions to newer ones."""
|
||||
|
||||
def test_v0_to_v1_migration(self):
|
||||
"""Test migration from schema v0 to v1."""
|
||||
pytest.skip("Requires migration infrastructure setup")
|
||||
|
||||
def test_v1_to_v2_migration(self):
|
||||
"""Test migration from schema v1 to v2."""
|
||||
pytest.skip("Requires migration infrastructure setup")
|
||||
|
||||
def test_migration_preserves_data(self):
|
||||
"""Test that migration preserves existing data."""
|
||||
pytest.skip("Requires migration infrastructure setup")
|
||||
|
||||
def test_migration_adds_new_columns(self):
|
||||
"""Test that new columns are added with correct defaults."""
|
||||
pytest.skip("Requires migration infrastructure setup")
|
||||
|
||||
|
||||
class TestBackwardCompatibility:
|
||||
"""Test that newer code can read and work with older database schemas."""
|
||||
|
||||
def test_new_code_reads_old_schema(self):
|
||||
"""Test that current code can read old schema databases."""
|
||||
pytest.skip("Requires old schema fixture")
|
||||
|
||||
def test_new_code_writes_to_old_schema(self):
|
||||
"""Test that current code handles writes to old schema gracefully."""
|
||||
pytest.skip("Requires old schema fixture")
|
||||
|
||||
def test_old_code_rejects_new_schema(self):
|
||||
"""Test that old code fails appropriately on new schemas."""
|
||||
pytest.skip("Requires old code fixture")
|
||||
|
||||
|
||||
class TestMigrationRollback:
|
||||
"""Test rollback capabilities for failed migrations."""
|
||||
|
||||
def test_failed_migration_rolls_back(self):
|
||||
"""Test that failed migrations are rolled back completely."""
|
||||
pytest.skip("Requires migration infrastructure setup")
|
||||
|
||||
def test_partial_migration_recovery(self):
|
||||
"""Test recovery from partially completed migrations."""
|
||||
pytest.skip("Requires migration infrastructure setup")
|
||||
|
||||
def test_rollback_preserves_original_data(self):
|
||||
"""Test that rollback restores original state."""
|
||||
pytest.skip("Requires migration infrastructure setup")
|
||||
|
||||
|
||||
class TestMigrationEdgeCases:
|
||||
"""Test migration behavior in edge cases."""
|
||||
|
||||
def test_empty_database_migration(self):
|
||||
"""Test migration of an empty database."""
|
||||
pytest.skip("Requires migration infrastructure setup")
|
||||
|
||||
def test_large_database_migration(self):
|
||||
"""Test migration of a large database."""
|
||||
pytest.skip("Requires migration infrastructure setup")
|
||||
|
||||
def test_corrupted_database_handling(self):
|
||||
"""Test handling of corrupted databases during migration."""
|
||||
pytest.skip("Requires migration infrastructure setup")
|
||||
|
||||
def test_concurrent_migration_protection(self):
|
||||
"""Test that concurrent migrations are prevented."""
|
||||
pytest.skip("Requires migration infrastructure setup")
|
||||
|
||||
|
||||
class TestSchemaVersionTracking:
|
||||
"""Test schema version tracking and detection."""
|
||||
|
||||
def test_version_table_exists(self):
|
||||
"""Test that version tracking table exists and is populated."""
|
||||
pytest.skip("Requires migration infrastructure setup")
|
||||
|
||||
def test_version_auto_detection(self):
|
||||
"""Test that schema version is auto-detected from database."""
|
||||
pytest.skip("Requires migration infrastructure setup")
|
||||
|
||||
def test_version_update_after_migration(self):
|
||||
"""Test that version is updated correctly after migration."""
|
||||
pytest.skip("Requires migration infrastructure setup")
|
||||
|
||||
|
||||
# TODO: Implement actual tests using pytest fixtures
|
||||
# The test infrastructure needs:
|
||||
# - Migration runner fixture that can apply and rollback migrations
|
||||
# - Old schema fixtures (pre-built databases with known schemas)
|
||||
# - Temporary database fixtures for isolated testing
|
||||
# - Mock data generators for various schema versions
|
||||
Reference in New Issue
Block a user