mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-06 01:54:11 +08:00
Compare commits
128 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c675ee4db | ||
|
|
f6dfe28e08 | ||
|
|
e8e8746cc6 | ||
|
|
603bc00bca | ||
|
|
ae76926d5a | ||
|
|
fd48045fe3 | ||
|
|
6ec6643448 | ||
|
|
945fda2d14 | ||
|
|
7d71f603fe | ||
|
|
bd11a538a7 | ||
|
|
b9b4da6d8c | ||
|
|
70f8b14eaa | ||
|
|
0c8b2f2ec9 | ||
|
|
d532b3fd02 | ||
|
|
c56104c082 | ||
|
|
66ae1972ae | ||
|
|
7f4433e449 | ||
|
|
e1f2fc72d9 | ||
|
|
aa093f9468 | ||
|
|
a27f76abcb | ||
|
|
df34ef38d9 | ||
|
|
60fbb4177c | ||
|
|
3289562be7 | ||
|
|
73fc68a187 | ||
|
|
bce6fa7a91 | ||
|
|
88724a4df9 | ||
|
|
5914b1c5fc | ||
|
|
d8be23fa83 | ||
|
|
ffbc4a4b76 | ||
|
|
dd62a7ac13 | ||
|
|
3f29dfd4cf | ||
|
|
3fdd52742b | ||
|
|
76ab4d67fe | ||
|
|
c859af1abf | ||
|
|
6a73d3c379 | ||
|
|
5d5652c2c5 | ||
|
|
b958a1ea96 | ||
|
|
bc385a32fd | ||
|
|
9a45732a39 | ||
|
|
015b46e58b | ||
|
|
042a99dbe3 | ||
|
|
99291053f5 | ||
|
|
99eeeff6f7 | ||
|
|
883b9f0672 | ||
|
|
3c07e743e1 | ||
|
|
823e1dc487 | ||
|
|
3537c0fc74 | ||
|
|
f3e23f0a57 | ||
|
|
3df1eac2fc | ||
|
|
141472117d | ||
|
|
e2dbeca080 | ||
|
|
70063f4045 | ||
|
|
8578d2d426 | ||
|
|
5d31bfd9fa | ||
|
|
c7291ba532 | ||
|
|
1396010437 | ||
|
|
1654b121bc | ||
|
|
d5130fc4da | ||
|
|
c4f3afd8eb | ||
|
|
fb2f80ee3a | ||
|
|
dda6af130c | ||
|
|
0d82c9fa03 | ||
|
|
7c389d5028 | ||
|
|
d5704f8344 | ||
|
|
ec4018a930 | ||
|
|
673cb03a2e | ||
|
|
4d7bf5b245 | ||
|
|
267426e332 | ||
|
|
d68401fa1a | ||
|
|
eb4ba89693 | ||
|
|
ef3b6b9f6e | ||
|
|
2d1be7cd4f | ||
|
|
bf0a2bde34 | ||
|
|
d85ab2a12c | ||
|
|
33a2bdb9f0 | ||
|
|
11a7dcb6c8 | ||
|
|
d8e389df00 | ||
|
|
203b51527b | ||
|
|
bf05886770 | ||
|
|
079ecdad3e | ||
|
|
075a8357cd | ||
|
|
c99ad377c6 | ||
|
|
382d330525 | ||
|
|
e2f4241b2e | ||
|
|
32cea006b9 | ||
|
|
6ffac8810b | ||
|
|
84d06f4273 | ||
|
|
18cc536f65 | ||
|
|
af2ff54cb7 | ||
|
|
6486c56850 | ||
|
|
93dcdd2293 | ||
|
|
58caccb250 | ||
|
|
598eed92cb | ||
|
|
d3e7ecca21 | ||
|
|
847abcefce | ||
|
|
c24ad501b5 | ||
|
|
35c7fe28bb | ||
|
|
a33cacfd75 | ||
|
|
338c3d612c | ||
|
|
8b17fad723 | ||
|
|
169f218f7a | ||
|
|
3ef1e54412 | ||
|
|
4419c50942 | ||
|
|
7aa1cda367 | ||
|
|
a2c88ba885 | ||
|
|
e16950ef1e | ||
|
|
5b973b00ea | ||
|
|
3a1ebf8684 | ||
|
|
2eaefb61ab | ||
|
|
4c6b28030f | ||
|
|
2c42cefa5a | ||
|
|
35ffd3419e | ||
|
|
e3223edbb1 | ||
|
|
a061fc1428 | ||
|
|
0992d27523 | ||
|
|
5aa0c9610d | ||
|
|
7620ff703d | ||
|
|
d705a3e7d9 | ||
|
|
726151bfea | ||
|
|
b58589ddad | ||
|
|
2e493277a1 | ||
|
|
8b19edd2de | ||
|
|
3e54b5f7d8 | ||
|
|
4da06864f8 | ||
|
|
8f310339df | ||
|
|
0157e36344 | ||
|
|
cdf4833977 | ||
|
|
c8a914aeca |
@@ -2,7 +2,7 @@
|
||||
|
||||
- **CLI Tools Usage**: @~/.claude/workflows/cli-tools-usage.md
|
||||
- **Coding Philosophy**: @~/.claude/workflows/coding-philosophy.md
|
||||
- **Context Requirements**: @~/.claude/workflows/context-tools-ace.md
|
||||
- **Context Requirements**: @~/.claude/workflows/context-tools.md
|
||||
- **File Modification**: @~/.claude/workflows/file-modification.md
|
||||
- **CLI Endpoints Config**: @.claude/cli-tools.json
|
||||
|
||||
|
||||
302
.claude/agents/issue-plan-agent.md
Normal file
302
.claude/agents/issue-plan-agent.md
Normal file
@@ -0,0 +1,302 @@
|
||||
---
|
||||
name: issue-plan-agent
|
||||
description: |
|
||||
Closed-loop issue planning agent combining ACE exploration and solution generation.
|
||||
Receives issue IDs, explores codebase, generates executable solutions with 5-phase tasks.
|
||||
color: green
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
**Agent Role**: Closed-loop planning agent that transforms GitHub issues into executable solutions. Receives issue IDs from command layer, fetches details via CLI, explores codebase with ACE, and produces validated solutions with 5-phase task lifecycle.
|
||||
|
||||
**Core Capabilities**:
|
||||
- ACE semantic search for intelligent code discovery
|
||||
- Batch processing (1-3 issues per invocation)
|
||||
- 5-phase task lifecycle (analyze → implement → test → optimize → commit)
|
||||
- Conflict-aware planning (isolate file modifications across issues)
|
||||
- Dependency DAG validation
|
||||
- Auto-bind for single solution, return for selection on multiple
|
||||
|
||||
**Key Principle**: Generate tasks conforming to schema with quantified acceptance criteria.
|
||||
|
||||
---
|
||||
|
||||
## 1. Input & Execution
|
||||
|
||||
### 1.1 Input Context
|
||||
|
||||
```javascript
|
||||
{
|
||||
issue_ids: string[], // Issue IDs only (e.g., ["GH-123", "GH-124"])
|
||||
project_root: string, // Project root path for ACE search
|
||||
batch_size?: number, // Max issues per batch (default: 3)
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: Agent receives IDs only. Fetch details via `ccw issue status <id> --json`.
|
||||
|
||||
### 1.2 Execution Flow
|
||||
|
||||
```
|
||||
Phase 1: Issue Understanding (10%)
|
||||
↓ Fetch details, extract requirements, determine complexity
|
||||
Phase 2: ACE Exploration (30%)
|
||||
↓ Semantic search, pattern discovery, dependency mapping
|
||||
Phase 3: Solution Planning (45%)
|
||||
↓ Task decomposition, 5-phase lifecycle, acceptance criteria
|
||||
Phase 4: Validation & Output (15%)
|
||||
↓ DAG validation, solution registration, binding
|
||||
```
|
||||
|
||||
#### Phase 1: Issue Understanding
|
||||
|
||||
**Step 1**: Fetch issue details via CLI
|
||||
```bash
|
||||
ccw issue status <issue-id> --json
|
||||
```
|
||||
|
||||
**Step 2**: Analyze and classify
|
||||
```javascript
|
||||
function analyzeIssue(issue) {
|
||||
return {
|
||||
issue_id: issue.id,
|
||||
requirements: extractRequirements(issue.context),
|
||||
scope: inferScope(issue.title, issue.context),
|
||||
complexity: determineComplexity(issue) // Low | Medium | High
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Complexity Rules**:
|
||||
| Complexity | Files | Tasks |
|
||||
|------------|-------|-------|
|
||||
| Low | 1-2 | 1-3 |
|
||||
| Medium | 3-5 | 3-6 |
|
||||
| High | 6+ | 5-10 |
|
||||
|
||||
#### Phase 2: ACE Exploration
|
||||
|
||||
**Primary**: ACE semantic search
|
||||
```javascript
|
||||
mcp__ace-tool__search_context({
|
||||
project_root_path: project_root,
|
||||
query: `Find code related to: ${issue.title}. Keywords: ${extractKeywords(issue)}`
|
||||
})
|
||||
```
|
||||
|
||||
**Exploration Checklist**:
|
||||
- [ ] Identify relevant files (direct matches)
|
||||
- [ ] Find related patterns (similar implementations)
|
||||
- [ ] Map integration points
|
||||
- [ ] Discover dependencies
|
||||
- [ ] Locate test patterns
|
||||
|
||||
**Fallback Chain**: ACE → smart_search → Grep → rg → Glob
|
||||
|
||||
| Tool | When to Use |
|
||||
|------|-------------|
|
||||
| `mcp__ace-tool__search_context` | Semantic search (primary) |
|
||||
| `mcp__ccw-tools__smart_search` | Symbol/pattern search |
|
||||
| `Grep` | Exact regex matching |
|
||||
| `rg` / `grep` | CLI fallback |
|
||||
| `Glob` | File path discovery |
|
||||
|
||||
#### Phase 3: Solution Planning
|
||||
|
||||
**Multi-Solution Generation**:
|
||||
|
||||
Generate multiple candidate solutions when:
|
||||
- Issue complexity is HIGH
|
||||
- Multiple valid implementation approaches exist
|
||||
- Trade-offs between approaches (performance vs simplicity, etc.)
|
||||
|
||||
| Condition | Solutions |
|
||||
|-----------|-----------|
|
||||
| Low complexity, single approach | 1 solution, auto-bind |
|
||||
| Medium complexity, clear path | 1-2 solutions |
|
||||
| High complexity, multiple approaches | 2-3 solutions, user selection |
|
||||
|
||||
**Solution Evaluation** (for each candidate):
|
||||
```javascript
|
||||
{
|
||||
analysis: {
|
||||
risk: "low|medium|high", // Implementation risk
|
||||
impact: "low|medium|high", // Scope of changes
|
||||
complexity: "low|medium|high" // Technical complexity
|
||||
},
|
||||
score: 0.0-1.0 // Overall quality score (higher = recommended)
|
||||
}
|
||||
```
|
||||
|
||||
**Selection Flow**:
|
||||
1. Generate all candidate solutions
|
||||
2. Evaluate and score each
|
||||
3. Single solution → auto-bind
|
||||
4. Multiple solutions → return `pending_selection` for user choice
|
||||
|
||||
**Task Decomposition** following schema:
|
||||
```javascript
|
||||
function decomposeTasks(issue, exploration) {
|
||||
return groups.map(group => ({
|
||||
id: `T${taskId++}`, // Pattern: ^T[0-9]+$
|
||||
title: group.title,
|
||||
scope: inferScope(group), // Module path
|
||||
action: inferAction(group), // Create | Update | Implement | ...
|
||||
description: group.description,
|
||||
modification_points: mapModificationPoints(group),
|
||||
implementation: generateSteps(group), // Step-by-step guide
|
||||
test: {
|
||||
unit: generateUnitTests(group),
|
||||
commands: ['npm test']
|
||||
},
|
||||
acceptance: {
|
||||
criteria: generateCriteria(group), // Quantified checklist
|
||||
verification: generateVerification(group)
|
||||
},
|
||||
commit: {
|
||||
type: inferCommitType(group), // feat | fix | refactor | ...
|
||||
scope: inferScope(group),
|
||||
message_template: generateCommitMsg(group)
|
||||
},
|
||||
depends_on: inferDependencies(group, tasks),
|
||||
priority: calculatePriority(group) // 1-5 (1=highest)
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
#### Phase 4: Validation & Output
|
||||
|
||||
**Validation**:
|
||||
- DAG validation (no circular dependencies)
|
||||
- Task validation (all 5 phases present)
|
||||
- File isolation check (ensure minimal overlap across issues in batch)
|
||||
|
||||
**Solution Registration** (via file write):
|
||||
|
||||
**Step 1: Create solution files**
|
||||
|
||||
Write solution JSON to JSONL file (one line per solution):
|
||||
|
||||
```
|
||||
.workflow/issues/solutions/{issue-id}.jsonl
|
||||
```
|
||||
|
||||
**File Format** (JSONL - each line is a complete solution):
|
||||
```
|
||||
{"id":"SOL-GH-123-1","description":"...","approach":"...","analysis":{...},"score":0.85,"tasks":[...]}
|
||||
{"id":"SOL-GH-123-2","description":"...","approach":"...","analysis":{...},"score":0.75,"tasks":[...]}
|
||||
```
|
||||
|
||||
**Solution Schema** (must match CLI `Solution` interface):
|
||||
```typescript
|
||||
{
|
||||
id: string; // Format: SOL-{issue-id}-{N}
|
||||
description?: string;
|
||||
approach?: string;
|
||||
tasks: SolutionTask[];
|
||||
analysis?: { risk, impact, complexity };
|
||||
score?: number;
|
||||
// Note: is_bound, created_at are added by CLI on read
|
||||
}
|
||||
```
|
||||
|
||||
**Write Operation**:
|
||||
```javascript
|
||||
// Append solution to JSONL file (one line per solution)
|
||||
const solutionId = `SOL-${issueId}-${seq}`;
|
||||
const solutionLine = JSON.stringify({ id: solutionId, ...solution });
|
||||
|
||||
// Read existing, append new line, write back
|
||||
const filePath = `.workflow/issues/solutions/${issueId}.jsonl`;
|
||||
const existing = existsSync(filePath) ? readFileSync(filePath) : '';
|
||||
const newContent = existing.trimEnd() + (existing ? '\n' : '') + solutionLine + '\n';
|
||||
Write({ file_path: filePath, content: newContent })
|
||||
```
|
||||
|
||||
**Step 2: Bind decision**
|
||||
- **Single solution** → Auto-bind: `ccw issue bind <issue-id> <solution-id>`
|
||||
- **Multiple solutions** → Return for user selection (no bind)
|
||||
|
||||
---
|
||||
|
||||
## 2. Output Requirements
|
||||
|
||||
### 2.1 Generate Files (Primary)
|
||||
|
||||
**Solution file per issue**:
|
||||
```
|
||||
.workflow/issues/solutions/{issue-id}.jsonl
|
||||
```
|
||||
|
||||
Each line is a solution JSON containing tasks. Schema: `cat .claude/workflows/cli-templates/schemas/solution-schema.json`
|
||||
|
||||
### 2.2 Binding
|
||||
|
||||
| Scenario | Action |
|
||||
|----------|--------|
|
||||
| Single solution | `ccw issue bind <issue-id> <solution-id>` (auto) |
|
||||
| Multiple solutions | Register only, return for selection |
|
||||
|
||||
### 2.3 Return Summary
|
||||
|
||||
```json
|
||||
{
|
||||
"bound": [{ "issue_id": "...", "solution_id": "...", "task_count": N }],
|
||||
"pending_selection": [{ "issue_id": "GH-123", "solutions": [{ "id": "SOL-GH-123-1", "description": "...", "task_count": N }] }]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Quality Standards
|
||||
|
||||
### 3.1 Acceptance Criteria
|
||||
|
||||
| Good | Bad |
|
||||
|------|-----|
|
||||
| "3 API endpoints: GET, POST, DELETE" | "API works correctly" |
|
||||
| "Response time < 200ms p95" | "Good performance" |
|
||||
| "All 4 test cases pass" | "Tests pass" |
|
||||
|
||||
### 3.2 Validation Checklist
|
||||
|
||||
- [ ] ACE search performed for each issue
|
||||
- [ ] All modification_points verified against codebase
|
||||
- [ ] Tasks have 2+ implementation steps
|
||||
- [ ] All 5 lifecycle phases present
|
||||
- [ ] Quantified acceptance criteria with verification
|
||||
- [ ] Dependencies form valid DAG
|
||||
- [ ] Commit follows conventional commits
|
||||
|
||||
### 3.3 Guidelines
|
||||
|
||||
**ALWAYS**:
|
||||
1. Read schema first: `cat .claude/workflows/cli-templates/schemas/solution-schema.json`
|
||||
2. Use ACE semantic search as PRIMARY exploration tool
|
||||
3. Fetch issue details via `ccw issue status <id> --json`
|
||||
4. Quantify acceptance.criteria with testable conditions
|
||||
5. Validate DAG before output
|
||||
6. Evaluate each solution with `analysis` and `score`
|
||||
7. Write solutions to `.workflow/issues/solutions/{issue-id}.jsonl` (append mode)
|
||||
8. For HIGH complexity: generate 2-3 candidate solutions
|
||||
9. **Solution ID format**: `SOL-{issue-id}-{N}` (e.g., `SOL-GH-123-1`, `SOL-GH-123-2`)
|
||||
|
||||
**CONFLICT AVOIDANCE** (for batch processing of similar issues):
|
||||
1. **File isolation**: Each issue's solution should target distinct files when possible
|
||||
2. **Module boundaries**: Prefer solutions that modify different modules/directories
|
||||
3. **Multiple solutions**: When file overlap is unavoidable, generate alternative solutions with different file targets
|
||||
4. **Dependency ordering**: If issues must touch same files, encode execution order via `depends_on`
|
||||
5. **Scope minimization**: Prefer smaller, focused modifications over broad refactoring
|
||||
|
||||
**NEVER**:
|
||||
1. Execute implementation (return plan only)
|
||||
2. Use vague criteria ("works correctly", "good performance")
|
||||
3. Create circular dependencies
|
||||
4. Generate more than 10 tasks per issue
|
||||
5. **Bind when multiple solutions exist** - MUST check `solutions.length === 1` before calling `ccw issue bind`
|
||||
|
||||
**OUTPUT**:
|
||||
1. Write solutions to `.workflow/issues/solutions/{issue-id}.jsonl` (JSONL format)
|
||||
2. Single solution → `ccw issue bind <issue-id> <solution-id>`; Multiple → return only
|
||||
3. Return JSON with `bound`, `pending_selection`
|
||||
307
.claude/agents/issue-queue-agent.md
Normal file
307
.claude/agents/issue-queue-agent.md
Normal file
@@ -0,0 +1,307 @@
|
||||
---
|
||||
name: issue-queue-agent
|
||||
description: |
|
||||
Solution ordering agent for queue formation with Gemini CLI conflict analysis.
|
||||
Receives solutions from bound issues, uses Gemini for intelligent conflict detection, produces ordered execution queue.
|
||||
color: orange
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
**Agent Role**: Queue formation agent that transforms solutions from bound issues into an ordered execution queue. Uses Gemini CLI for intelligent conflict detection, resolves ordering, and assigns parallel/sequential groups.
|
||||
|
||||
**Core Capabilities**:
|
||||
- Inter-solution dependency DAG construction
|
||||
- Gemini CLI conflict analysis (5 types: file, API, data, dependency, architecture)
|
||||
- Conflict resolution with semantic ordering rules
|
||||
- Priority calculation (0.0-1.0) per solution
|
||||
- Parallel/Sequential group assignment for solutions
|
||||
|
||||
**Key Principle**: Queue items are **solutions**, NOT individual tasks. Each executor receives a complete solution with all its tasks.
|
||||
|
||||
---
|
||||
|
||||
## 1. Input & Execution
|
||||
|
||||
### 1.1 Input Context
|
||||
|
||||
```javascript
|
||||
{
|
||||
solutions: [{
|
||||
issue_id: string, // e.g., "ISS-20251227-001"
|
||||
solution_id: string, // e.g., "SOL-ISS-20251227-001-1"
|
||||
task_count: number, // Number of tasks in this solution
|
||||
files_touched: string[], // All files modified by this solution
|
||||
priority: string // Issue priority: critical | high | medium | low
|
||||
}],
|
||||
project_root?: string,
|
||||
rebuild?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: Agent generates unique `item_id` (pattern: `S-{N}`) for queue output.
|
||||
|
||||
### 1.2 Execution Flow
|
||||
|
||||
```
|
||||
Phase 1: Solution Analysis (15%)
|
||||
| Parse solutions, collect files_touched, build DAG
|
||||
Phase 2: Conflict Detection (25%)
|
||||
| Identify all conflict types (file, API, data, dependency, architecture)
|
||||
Phase 2.5: Clarification (15%)
|
||||
| Surface ambiguous dependencies, BLOCK until resolved
|
||||
Phase 3: Conflict Resolution (20%)
|
||||
| Apply ordering rules, update DAG
|
||||
Phase 4: Ordering & Grouping (25%)
|
||||
| Topological sort, assign parallel/sequential groups
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Processing Logic
|
||||
|
||||
### 2.1 Dependency Graph
|
||||
|
||||
**Build DAG from solutions**:
|
||||
1. Create node for each solution with `inDegree: 0` and `outEdges: []`
|
||||
2. Build file→solutions mapping from `files_touched`
|
||||
3. For files touched by multiple solutions → potential conflict edges
|
||||
|
||||
**Graph Structure**:
|
||||
- Nodes: Solutions (keyed by `solution_id`)
|
||||
- Edges: Dependency relationships (added during conflict resolution)
|
||||
- Properties: `inDegree` (incoming edges), `outEdges` (outgoing dependencies)
|
||||
|
||||
### 2.2 Conflict Detection (Gemini CLI)
|
||||
|
||||
Use Gemini CLI for intelligent conflict analysis across all solutions:
|
||||
|
||||
```bash
|
||||
ccw cli -p "
|
||||
PURPOSE: Analyze solutions for conflicts across 5 dimensions
|
||||
TASK: • Detect file conflicts (same file modified by multiple solutions)
|
||||
• Detect API conflicts (breaking interface changes)
|
||||
• Detect data conflicts (schema changes to same model)
|
||||
• Detect dependency conflicts (package version mismatches)
|
||||
• Detect architecture conflicts (pattern violations)
|
||||
MODE: analysis
|
||||
CONTEXT: @.workflow/issues/solutions/**/*.jsonl | Solution data: \${SOLUTIONS_JSON}
|
||||
EXPECTED: JSON array of conflicts with type, severity, solutions, recommended_order
|
||||
RULES: $(cat ~/.claude/workflows/cli-templates/protocols/analysis-protocol.md) | Severity: high (API/data) > medium (file/dependency) > low (architecture)
|
||||
" --tool gemini --mode analysis --cd .workflow/issues
|
||||
```
|
||||
|
||||
**Placeholder**: `${SOLUTIONS_JSON}` = serialized solutions array from bound issues
|
||||
|
||||
**Conflict Types & Severity**:
|
||||
|
||||
| Type | Severity | Trigger |
|
||||
|------|----------|---------|
|
||||
| `file_conflict` | medium | Multiple solutions modify same file |
|
||||
| `api_conflict` | high | Breaking interface changes |
|
||||
| `data_conflict` | high | Schema changes to same model |
|
||||
| `dependency_conflict` | medium | Package version mismatches |
|
||||
| `architecture_conflict` | low | Pattern violations |
|
||||
|
||||
**Output per conflict**:
|
||||
```json
|
||||
{ "type": "...", "severity": "...", "solutions": [...], "recommended_order": [...], "rationale": "..." }
|
||||
```
|
||||
|
||||
### 2.2.5 Clarification (BLOCKING)
|
||||
|
||||
**Purpose**: Surface ambiguous dependencies for user/system clarification
|
||||
|
||||
**Trigger Conditions**:
|
||||
- High severity conflicts without `recommended_order` from Gemini analysis
|
||||
- Circular dependencies detected
|
||||
- Multiple valid resolution strategies
|
||||
|
||||
**Clarification Generation**:
|
||||
|
||||
For each unresolved high-severity conflict:
|
||||
1. Generate conflict ID: `CFT-{N}`
|
||||
2. Build question: `"{type}: Which solution should execute first?"`
|
||||
3. List options with solution summaries (issue title + task count)
|
||||
4. Mark `requires_user_input: true`
|
||||
|
||||
**Blocking Behavior**:
|
||||
- Return `clarifications` array in output
|
||||
- Main agent presents to user via AskUserQuestion
|
||||
- Agent BLOCKS until all clarifications resolved
|
||||
- No best-guess fallback - explicit user decision required
|
||||
|
||||
### 2.3 Resolution Rules
|
||||
|
||||
| Priority | Rule | Example |
|
||||
|----------|------|---------|
|
||||
| 1 | Higher issue priority first | critical > high > medium > low |
|
||||
| 2 | Foundation solutions first | Solutions with fewer dependencies |
|
||||
| 3 | More tasks = higher priority | Solutions with larger impact |
|
||||
| 4 | Create before extend | S1:Creates module -> S2:Extends it |
|
||||
|
||||
### 2.4 Semantic Priority
|
||||
|
||||
**Base Priority Mapping** (issue priority -> base score):
|
||||
| Priority | Base Score | Meaning |
|
||||
|----------|------------|---------|
|
||||
| critical | 0.9 | Highest |
|
||||
| high | 0.7 | High |
|
||||
| medium | 0.5 | Medium |
|
||||
| low | 0.3 | Low |
|
||||
|
||||
**Task-count Boost** (applied to base score):
|
||||
| Factor | Boost |
|
||||
|--------|-------|
|
||||
| task_count >= 5 | +0.1 |
|
||||
| task_count >= 3 | +0.05 |
|
||||
| Foundation scope | +0.1 |
|
||||
| Fewer dependencies | +0.05 |
|
||||
|
||||
**Formula**: `semantic_priority = clamp(baseScore + sum(boosts), 0.0, 1.0)`
|
||||
|
||||
### 2.5 Group Assignment
|
||||
|
||||
- **Parallel (P*)**: Solutions with no file overlaps between them
|
||||
- **Sequential (S*)**: Solutions that share files must run in order
|
||||
|
||||
---
|
||||
|
||||
## 3. Output Requirements
|
||||
|
||||
### 3.1 Generate Files (Primary)
|
||||
|
||||
**Queue files**:
|
||||
```
|
||||
.workflow/issues/queues/{queue-id}.json # Full queue with solutions, conflicts, groups
|
||||
.workflow/issues/queues/index.json # Update with new queue entry
|
||||
```
|
||||
|
||||
Queue ID: Use the Queue ID provided in prompt (do NOT generate new one)
|
||||
Queue Item ID format: `S-N` (S-1, S-2, S-3, ...)
|
||||
|
||||
### 3.2 Queue File Schema
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "QUE-20251227-143000",
|
||||
"status": "active",
|
||||
"solutions": [
|
||||
{
|
||||
"item_id": "S-1",
|
||||
"issue_id": "ISS-20251227-003",
|
||||
"solution_id": "SOL-ISS-20251227-003-1",
|
||||
"status": "pending",
|
||||
"execution_order": 1,
|
||||
"execution_group": "P1",
|
||||
"depends_on": [],
|
||||
"semantic_priority": 0.8,
|
||||
"files_touched": ["src/auth.ts", "src/utils.ts"],
|
||||
"task_count": 3
|
||||
}
|
||||
],
|
||||
"conflicts": [
|
||||
{
|
||||
"type": "file_conflict",
|
||||
"file": "src/auth.ts",
|
||||
"solutions": ["S-1", "S-3"],
|
||||
"resolution": "sequential",
|
||||
"resolution_order": ["S-1", "S-3"],
|
||||
"rationale": "S-1 creates auth module, S-3 extends it"
|
||||
}
|
||||
],
|
||||
"execution_groups": [
|
||||
{ "id": "P1", "type": "parallel", "solutions": ["S-1", "S-2"], "solution_count": 2 },
|
||||
{ "id": "S2", "type": "sequential", "solutions": ["S-3"], "solution_count": 1 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 Return Summary (Brief)
|
||||
|
||||
Return brief summaries; full conflict details in separate files:
|
||||
|
||||
```json
|
||||
{
|
||||
"queue_id": "QUE-20251227-143000",
|
||||
"total_solutions": N,
|
||||
"total_tasks": N,
|
||||
"execution_groups": [{ "id": "P1", "type": "parallel", "count": N }],
|
||||
"conflicts_summary": [{
|
||||
"id": "CFT-001",
|
||||
"type": "api_conflict",
|
||||
"severity": "high",
|
||||
"summary": "Brief 1-line description",
|
||||
"resolution": "sequential",
|
||||
"details_path": ".workflow/issues/conflicts/CFT-001.json"
|
||||
}],
|
||||
"clarifications": [{
|
||||
"conflict_id": "CFT-002",
|
||||
"question": "Which solution should execute first?",
|
||||
"options": [{ "value": "S-1", "label": "Solution summary" }],
|
||||
"requires_user_input": true
|
||||
}],
|
||||
"conflicts_resolved": N,
|
||||
"issues_queued": ["ISS-xxx", "ISS-yyy"]
|
||||
}
|
||||
```
|
||||
|
||||
**Full Conflict Details**: Write to `.workflow/issues/conflicts/{conflict-id}.json`
|
||||
|
||||
---
|
||||
|
||||
## 4. Quality Standards
|
||||
|
||||
### 4.1 Validation Checklist
|
||||
|
||||
- [ ] No circular dependencies between solutions
|
||||
- [ ] All file conflicts resolved
|
||||
- [ ] Solutions in same parallel group have NO file overlaps
|
||||
- [ ] Semantic priority calculated for all solutions
|
||||
- [ ] Dependencies ordered correctly
|
||||
|
||||
### 4.2 Error Handling
|
||||
|
||||
| Scenario | Action |
|
||||
|----------|--------|
|
||||
| Circular dependency | Abort, report cycles |
|
||||
| Resolution creates cycle | Flag for manual resolution |
|
||||
| Missing solution reference | Skip and warn |
|
||||
| Empty solution list | Return empty queue |
|
||||
|
||||
### 4.3 Guidelines
|
||||
|
||||
**ALWAYS**:
|
||||
1. Build dependency graph before ordering
|
||||
2. Detect file overlaps between solutions
|
||||
3. Apply resolution rules consistently
|
||||
4. Calculate semantic priority for all solutions
|
||||
5. Include rationale for conflict resolutions
|
||||
6. Validate ordering before output
|
||||
|
||||
**NEVER**:
|
||||
1. Execute solutions (ordering only)
|
||||
2. Ignore circular dependencies
|
||||
3. Skip conflict detection
|
||||
4. Output invalid DAG
|
||||
5. Merge conflicting solutions in parallel group
|
||||
6. Split tasks from their solution
|
||||
|
||||
**WRITE** (exactly 2 files):
|
||||
- `.workflow/issues/queues/{Queue ID}.json` - Full queue with solutions, groups
|
||||
- `.workflow/issues/queues/index.json` - Update with new queue entry
|
||||
- Use Queue ID from prompt, do NOT generate new one
|
||||
|
||||
**RETURN** (summary + unresolved conflicts):
|
||||
```json
|
||||
{
|
||||
"queue_id": "QUE-xxx",
|
||||
"total_solutions": N,
|
||||
"total_tasks": N,
|
||||
"execution_groups": [{"id": "P1", "type": "parallel", "count": N}],
|
||||
"issues_queued": ["ISS-xxx"],
|
||||
"clarifications": [{"conflict_id": "CFT-1", "question": "...", "options": [...]}]
|
||||
}
|
||||
```
|
||||
- `clarifications`: Only present if unresolved high-severity conflicts exist
|
||||
- No markdown, no prose - PURE JSON only
|
||||
427
.claude/commands/issue/discover.md
Normal file
427
.claude/commands/issue/discover.md
Normal file
@@ -0,0 +1,427 @@
|
||||
---
|
||||
name: issue:discover
|
||||
description: Discover potential issues from multiple perspectives (bug, UX, test, quality, security, performance, maintainability, best-practices) using CLI explore. Supports Exa external research for security and best-practices perspectives.
|
||||
argument-hint: "<path-pattern> [--perspectives=bug,ux,...] [--external]"
|
||||
allowed-tools: SlashCommand(*), TodoWrite(*), Read(*), Bash(*), Task(*), AskUserQuestion(*), Glob(*), Grep(*)
|
||||
---
|
||||
|
||||
# Issue Discovery Command
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Discover issues in specific module (interactive perspective selection)
|
||||
/issue:discover src/auth/**
|
||||
|
||||
# Discover with specific perspectives
|
||||
/issue:discover src/payment/** --perspectives=bug,security,test
|
||||
|
||||
# Discover with external research for all perspectives
|
||||
/issue:discover src/api/** --external
|
||||
|
||||
# Discover in multiple modules
|
||||
/issue:discover src/auth/**,src/payment/**
|
||||
```
|
||||
|
||||
**Discovery Scope**: Specified modules/files only
|
||||
**Output Directory**: `.workflow/issues/discoveries/{discovery-id}/`
|
||||
**Available Perspectives**: bug, ux, test, quality, security, performance, maintainability, best-practices
|
||||
**Exa Integration**: Auto-enabled for security and best-practices perspectives
|
||||
**CLI Tools**: Gemini → Qwen → Codex (fallback chain)
|
||||
|
||||
## What & Why
|
||||
|
||||
### Core Concept
|
||||
Multi-perspective issue discovery orchestrator that explores code from different angles to identify potential bugs, UX improvements, test gaps, and other actionable items. Unlike code review (which assesses existing code quality), discovery focuses on **finding opportunities for improvement and potential problems**.
|
||||
|
||||
**vs Code Review**:
|
||||
- **Code Review** (`review-module-cycle`): Evaluates code quality against standards
|
||||
- **Issue Discovery** (`issue:discover`): Finds actionable issues, bugs, and improvement opportunities
|
||||
|
||||
### Value Proposition
|
||||
1. **Proactive Issue Detection**: Find problems before they become bugs
|
||||
2. **Multi-Perspective Analysis**: Each perspective surfaces different types of issues
|
||||
3. **External Benchmarking**: Compare against industry best practices via Exa
|
||||
4. **Direct Issue Integration**: Discoveries can be exported to issue tracker
|
||||
5. **Dashboard Management**: View, filter, and export discoveries via CCW dashboard
|
||||
|
||||
## How It Works
|
||||
|
||||
### Execution Flow
|
||||
|
||||
```
|
||||
Phase 1: Discovery & Initialization
|
||||
└─ Parse target pattern, create session, initialize output structure
|
||||
|
||||
Phase 2: Interactive Perspective Selection
|
||||
└─ AskUserQuestion for perspective selection (or use --perspectives)
|
||||
|
||||
Phase 3: Parallel Perspective Analysis
|
||||
├─ Launch N @cli-explore-agent instances (one per perspective)
|
||||
├─ Security & Best-Practices auto-trigger Exa research
|
||||
├─ Agent writes perspective JSON, returns summary
|
||||
└─ Update discovery-progress.json
|
||||
|
||||
Phase 4: Aggregation & Prioritization
|
||||
├─ Collect agent return summaries
|
||||
├─ Load perspective JSON files
|
||||
├─ Merge findings, deduplicate by file+line
|
||||
└─ Calculate priority scores
|
||||
|
||||
Phase 5: Issue Generation & Summary
|
||||
├─ Convert high-priority discoveries to issue format
|
||||
├─ Write to discovery-issues.jsonl
|
||||
├─ Generate single summary.md from agent returns
|
||||
└─ Update discovery-state.json to complete
|
||||
```
|
||||
|
||||
## Perspectives
|
||||
|
||||
### Available Perspectives
|
||||
|
||||
| Perspective | Focus | Categories | Exa |
|
||||
|-------------|-------|------------|-----|
|
||||
| **bug** | Potential Bugs | edge-case, null-check, resource-leak, race-condition, boundary, exception-handling | - |
|
||||
| **ux** | User Experience | error-message, loading-state, feedback, accessibility, interaction, consistency | - |
|
||||
| **test** | Test Coverage | missing-test, edge-case-test, integration-gap, coverage-hole, assertion-quality | - |
|
||||
| **quality** | Code Quality | complexity, duplication, naming, documentation, code-smell, readability | - |
|
||||
| **security** | Security Issues | injection, auth, encryption, input-validation, data-exposure, access-control | ✓ |
|
||||
| **performance** | Performance | n-plus-one, memory-usage, caching, algorithm, blocking-operation, resource | - |
|
||||
| **maintainability** | Maintainability | coupling, cohesion, tech-debt, extensibility, module-boundary, interface-design | - |
|
||||
| **best-practices** | Best Practices | convention, pattern, framework-usage, anti-pattern, industry-standard | ✓ |
|
||||
|
||||
### Interactive Perspective Selection
|
||||
|
||||
When no `--perspectives` flag is provided, the command uses AskUserQuestion:
|
||||
|
||||
```javascript
|
||||
AskUserQuestion({
|
||||
questions: [{
|
||||
question: "Select primary discovery focus:",
|
||||
header: "Focus",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "Bug + Test + Quality", description: "Quick scan: potential bugs, test gaps, code quality (Recommended)" },
|
||||
{ label: "Security + Performance", description: "System audit: security issues, performance bottlenecks" },
|
||||
{ label: "Maintainability + Best-practices", description: "Long-term health: coupling, tech debt, conventions" },
|
||||
{ label: "Full analysis", description: "All 7 perspectives (comprehensive, takes longer)" }
|
||||
]
|
||||
}]
|
||||
})
|
||||
```
|
||||
|
||||
**Recommended Combinations**:
|
||||
- Quick scan: bug, test, quality
|
||||
- Full analysis: all perspectives
|
||||
- Security audit: security, bug, quality
|
||||
|
||||
## Core Responsibilities
|
||||
|
||||
### Orchestrator
|
||||
|
||||
**Phase 1: Discovery & Initialization**
|
||||
|
||||
```javascript
|
||||
// Step 1: Parse target pattern and resolve files
|
||||
const resolvedFiles = await expandGlobPattern(targetPattern);
|
||||
if (resolvedFiles.length === 0) {
|
||||
throw new Error(`No files matched pattern: ${targetPattern}`);
|
||||
}
|
||||
|
||||
// Step 2: Generate discovery ID
|
||||
const discoveryId = `DSC-${formatDate(new Date(), 'YYYYMMDD-HHmmss')}`;
|
||||
|
||||
// Step 3: Create output directory
|
||||
const outputDir = `.workflow/issues/discoveries/${discoveryId}`;
|
||||
await mkdir(outputDir, { recursive: true });
|
||||
await mkdir(`${outputDir}/perspectives`, { recursive: true });
|
||||
|
||||
// Step 4: Initialize unified discovery state (merged state+progress)
|
||||
await writeJson(`${outputDir}/discovery-state.json`, {
|
||||
discovery_id: discoveryId,
|
||||
target_pattern: targetPattern,
|
||||
phase: "initialization",
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
target: { files_count: { total: resolvedFiles.length }, project: {} },
|
||||
perspectives: [], // filled after selection: [{name, status, findings}]
|
||||
external_research: { enabled: false, completed: false },
|
||||
results: { total_findings: 0, issues_generated: 0, priority_distribution: {} }
|
||||
});
|
||||
```
|
||||
|
||||
**Phase 2: Perspective Selection**
|
||||
|
||||
```javascript
|
||||
// Check for --perspectives flag
|
||||
let selectedPerspectives = [];
|
||||
|
||||
if (args.perspectives) {
|
||||
selectedPerspectives = args.perspectives.split(',').map(p => p.trim());
|
||||
} else {
|
||||
// Interactive selection via AskUserQuestion
|
||||
const response = await AskUserQuestion({...});
|
||||
selectedPerspectives = parseSelectedPerspectives(response);
|
||||
}
|
||||
|
||||
// Validate and update state
|
||||
await updateDiscoveryState(outputDir, {
|
||||
'metadata.perspectives': selectedPerspectives,
|
||||
phase: 'parallel'
|
||||
});
|
||||
```
|
||||
|
||||
**Phase 3: Parallel Perspective Analysis**
|
||||
|
||||
Launch N agents in parallel (one per selected perspective):
|
||||
|
||||
```javascript
|
||||
// Launch agents in parallel - agents write JSON and return summary
|
||||
const agentPromises = selectedPerspectives.map(perspective =>
|
||||
Task({
|
||||
subagent_type: "cli-explore-agent",
|
||||
run_in_background: false,
|
||||
description: `Discover ${perspective} issues`,
|
||||
prompt: buildPerspectivePrompt(perspective, discoveryId, resolvedFiles, outputDir)
|
||||
})
|
||||
);
|
||||
|
||||
// Wait for all agents - collect their return summaries
|
||||
const results = await Promise.all(agentPromises);
|
||||
// results contain agent summaries for final report
|
||||
```
|
||||
|
||||
**Phase 4: Aggregation & Prioritization**
|
||||
|
||||
```javascript
|
||||
// Load all perspective JSON files written by agents
|
||||
const allFindings = [];
|
||||
for (const perspective of selectedPerspectives) {
|
||||
const jsonPath = `${outputDir}/perspectives/${perspective}.json`;
|
||||
if (await fileExists(jsonPath)) {
|
||||
const data = await readJson(jsonPath);
|
||||
allFindings.push(...data.findings.map(f => ({ ...f, perspective })));
|
||||
}
|
||||
}
|
||||
|
||||
// Deduplicate and prioritize
|
||||
const prioritizedFindings = deduplicateAndPrioritize(allFindings);
|
||||
|
||||
// Update unified state
|
||||
await updateDiscoveryState(outputDir, {
|
||||
phase: 'aggregation',
|
||||
'results.total_findings': prioritizedFindings.length,
|
||||
'results.priority_distribution': countByPriority(prioritizedFindings)
|
||||
});
|
||||
```
|
||||
|
||||
**Phase 5: Issue Generation & Summary**
|
||||
|
||||
```javascript
|
||||
// Convert high-priority findings to issues
|
||||
const issueWorthy = prioritizedFindings.filter(f =>
|
||||
f.priority === 'critical' || f.priority === 'high' || f.priority_score >= 0.7
|
||||
);
|
||||
|
||||
// Write discovery-issues.jsonl
|
||||
await writeJsonl(`${outputDir}/discovery-issues.jsonl`, issues);
|
||||
|
||||
// Generate single summary.md from agent return summaries
|
||||
// Orchestrator briefly summarizes what agents returned (NO detailed reports)
|
||||
await writeSummaryFromAgentReturns(outputDir, results, prioritizedFindings, issues);
|
||||
|
||||
// Update final state
|
||||
await updateDiscoveryState(outputDir, {
|
||||
phase: 'complete',
|
||||
updated_at: new Date().toISOString(),
|
||||
'results.issues_generated': issues.length
|
||||
});
|
||||
```
|
||||
|
||||
### Output File Structure
|
||||
|
||||
```
|
||||
.workflow/issues/discoveries/
|
||||
├── index.json # Discovery session index
|
||||
└── {discovery-id}/
|
||||
├── discovery-state.json # Unified state (merged state+progress)
|
||||
├── perspectives/
|
||||
│ └── {perspective}.json # Per-perspective findings
|
||||
├── external-research.json # Exa research results (if enabled)
|
||||
├── discovery-issues.jsonl # Generated candidate issues
|
||||
└── summary.md # Single summary (from agent returns)
|
||||
```
|
||||
|
||||
### Schema References
|
||||
|
||||
**External Schema Files** (agent MUST read and follow exactly):
|
||||
|
||||
| Schema | Path | Purpose |
|
||||
|--------|------|---------|
|
||||
| **Discovery State** | `~/.claude/workflows/cli-templates/schemas/discovery-state-schema.json` | Session state machine |
|
||||
| **Discovery Finding** | `~/.claude/workflows/cli-templates/schemas/discovery-finding-schema.json` | Perspective analysis results |
|
||||
|
||||
### Agent Invocation Template
|
||||
|
||||
**Perspective Analysis Agent**:
|
||||
|
||||
```javascript
|
||||
Task({
|
||||
subagent_type: "cli-explore-agent",
|
||||
run_in_background: false,
|
||||
description: `Discover ${perspective} issues`,
|
||||
prompt: `
|
||||
## Task Objective
|
||||
Discover potential ${perspective} issues in specified module files.
|
||||
|
||||
## Discovery Context
|
||||
- Discovery ID: ${discoveryId}
|
||||
- Perspective: ${perspective}
|
||||
- Target Pattern: ${targetPattern}
|
||||
- Resolved Files: ${resolvedFiles.length} files
|
||||
- Output Directory: ${outputDir}
|
||||
|
||||
## MANDATORY FIRST STEPS
|
||||
1. Read discovery state: ${outputDir}/discovery-state.json
|
||||
2. Read schema: ~/.claude/workflows/cli-templates/schemas/discovery-finding-schema.json
|
||||
3. Analyze target files for ${perspective} concerns
|
||||
|
||||
## Output Requirements
|
||||
|
||||
**1. Write JSON file**: ${outputDir}/perspectives/${perspective}.json
|
||||
- Follow discovery-finding-schema.json exactly
|
||||
- Each finding: id, title, priority, category, description, file, line, snippet, suggested_issue, confidence
|
||||
|
||||
**2. Return summary** (DO NOT write report file):
|
||||
- Return a brief text summary of findings
|
||||
- Include: total findings, priority breakdown, key issues
|
||||
- This summary will be used by orchestrator for final report
|
||||
|
||||
## Perspective-Specific Guidance
|
||||
${getPerspectiveGuidance(perspective)}
|
||||
|
||||
## Success Criteria
|
||||
- [ ] JSON written to ${outputDir}/perspectives/${perspective}.json
|
||||
- [ ] Summary returned with findings count and key issues
|
||||
- [ ] Each finding includes actionable suggested_issue
|
||||
- [ ] Priority uses lowercase enum: critical/high/medium/low
|
||||
`
|
||||
})
|
||||
```
|
||||
|
||||
**Exa Research Agent** (for security and best-practices):
|
||||
|
||||
```javascript
|
||||
Task({
|
||||
subagent_type: "cli-explore-agent",
|
||||
run_in_background: false,
|
||||
description: `External research for ${perspective} via Exa`,
|
||||
prompt: `
|
||||
## Task Objective
|
||||
Research industry best practices for ${perspective} using Exa search
|
||||
|
||||
## Research Steps
|
||||
1. Read project tech stack: .workflow/project-tech.json
|
||||
2. Use Exa to search for best practices
|
||||
3. Synthesize findings relevant to this project
|
||||
|
||||
## Output Requirements
|
||||
|
||||
**1. Write JSON file**: ${outputDir}/external-research.json
|
||||
- Include sources, key_findings, gap_analysis, recommendations
|
||||
|
||||
**2. Return summary** (DO NOT write report file):
|
||||
- Brief summary of external research findings
|
||||
- Key recommendations for the project
|
||||
|
||||
## Success Criteria
|
||||
- [ ] JSON written to ${outputDir}/external-research.json
|
||||
- [ ] Summary returned with key recommendations
|
||||
- [ ] Findings are relevant to project's tech stack
|
||||
`
|
||||
})
|
||||
```
|
||||
|
||||
### Perspective Guidance Reference
|
||||
|
||||
```javascript
|
||||
function getPerspectiveGuidance(perspective) {
|
||||
const guidance = {
|
||||
bug: `
|
||||
Focus: Null checks, edge cases, resource leaks, race conditions, boundary conditions, exception handling
|
||||
Priority: Critical=data corruption/crash, High=malfunction, Medium=edge case issues, Low=minor
|
||||
`,
|
||||
ux: `
|
||||
Focus: Error messages, loading states, feedback, accessibility, interaction patterns, form validation
|
||||
Priority: Critical=inaccessible, High=confusing, Medium=inconsistent, Low=cosmetic
|
||||
`,
|
||||
test: `
|
||||
Focus: Missing unit tests, edge case coverage, integration gaps, assertion quality, test isolation
|
||||
Priority: Critical=no security tests, High=no core logic tests, Medium=weak coverage, Low=minor gaps
|
||||
`,
|
||||
quality: `
|
||||
Focus: Complexity, duplication, naming, documentation, code smells, readability
|
||||
Priority: Critical=unmaintainable, High=significant issues, Medium=naming/docs, Low=minor refactoring
|
||||
`,
|
||||
security: `
|
||||
Focus: Input validation, auth/authz, injection, XSS/CSRF, data exposure, access control
|
||||
Priority: Critical=auth bypass/injection, High=missing authz, Medium=weak validation, Low=headers
|
||||
`,
|
||||
performance: `
|
||||
Focus: N+1 queries, memory leaks, caching, algorithm efficiency, blocking operations
|
||||
Priority: Critical=memory leaks, High=N+1/inefficient, Medium=missing cache, Low=minor optimization
|
||||
`,
|
||||
maintainability: `
|
||||
Focus: Coupling, interface design, tech debt, extensibility, module boundaries, configuration
|
||||
Priority: Critical=unrelated code changes, High=unclear boundaries, Medium=coupling, Low=refactoring
|
||||
`,
|
||||
'best-practices': `
|
||||
Focus: Framework conventions, language patterns, anti-patterns, deprecated APIs, coding standards
|
||||
Priority: Critical=anti-patterns causing bugs, High=convention violations, Medium=style, Low=cosmetic
|
||||
`
|
||||
};
|
||||
return guidance[perspective] || 'General code discovery analysis';
|
||||
}
|
||||
```
|
||||
|
||||
## Dashboard Integration
|
||||
|
||||
### Viewing Discoveries
|
||||
|
||||
Open CCW dashboard to manage discoveries:
|
||||
|
||||
```bash
|
||||
ccw view
|
||||
```
|
||||
|
||||
Navigate to **Issues > Discovery** to:
|
||||
- View all discovery sessions
|
||||
- Filter findings by perspective and priority
|
||||
- Preview finding details
|
||||
- Select and export findings as issues
|
||||
|
||||
### Exporting to Issues
|
||||
|
||||
From the dashboard, select findings and click "Export as Issues" to:
|
||||
1. Convert discoveries to standard issue format
|
||||
2. Append to `.workflow/issues/issues.jsonl`
|
||||
3. Set status to `registered`
|
||||
4. Continue with `/issue:plan` workflow
|
||||
|
||||
## Related Commands
|
||||
|
||||
```bash
|
||||
# After discovery, plan solutions for exported issues
|
||||
/issue:plan DSC-001,DSC-002,DSC-003
|
||||
|
||||
# Or use interactive management
|
||||
/issue:manage
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Start Focused**: Begin with specific modules rather than entire codebase
|
||||
2. **Use Quick Scan First**: Start with bug, test, quality for fast results
|
||||
3. **Review Before Export**: Not all discoveries warrant issues - use dashboard to filter
|
||||
4. **Combine Perspectives**: Run related perspectives together (e.g., security + bug)
|
||||
5. **Enable Exa for New Tech**: When using unfamiliar frameworks, enable external research
|
||||
307
.claude/commands/issue/execute.md
Normal file
307
.claude/commands/issue/execute.md
Normal file
@@ -0,0 +1,307 @@
|
||||
---
|
||||
name: execute
|
||||
description: Execute queue with codex using DAG-based parallel orchestration (solution-level)
|
||||
argument-hint: ""
|
||||
allowed-tools: TodoWrite(*), Bash(*), Read(*), AskUserQuestion(*)
|
||||
---
|
||||
|
||||
# Issue Execute Command (/issue:execute)
|
||||
|
||||
## Overview
|
||||
|
||||
Minimal orchestrator that dispatches **solution IDs** to executors. Each executor receives a complete solution with all its tasks.
|
||||
|
||||
**Design Principles:**
|
||||
- `queue dag` → returns parallel batches with solution IDs (S-1, S-2, ...)
|
||||
- `detail <id>` → READ-ONLY solution fetch (returns full solution with all tasks)
|
||||
- `done <id>` → update solution completion status
|
||||
- No race conditions: status changes only via `done`
|
||||
- **Executor handles all tasks within a solution sequentially**
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
/issue:execute
|
||||
```
|
||||
|
||||
**Parallelism**: Determined automatically by task dependency DAG (no manual control)
|
||||
**Executor & Dry-run**: Selected via interactive prompt (AskUserQuestion)
|
||||
|
||||
## Execution Flow
|
||||
|
||||
```
|
||||
Phase 1: Get DAG & User Selection
|
||||
├─ ccw issue queue dag → { parallel_batches: [["S-1","S-2"], ["S-3"]] }
|
||||
└─ AskUserQuestion → executor type (codex|gemini|agent), dry-run mode
|
||||
|
||||
Phase 2: Dispatch Parallel Batch (DAG-driven)
|
||||
├─ Parallelism determined by DAG (no manual limit)
|
||||
├─ For each solution ID in batch (parallel - all at once):
|
||||
│ ├─ Executor calls: ccw issue detail <id> (READ-ONLY)
|
||||
│ ├─ Executor gets FULL SOLUTION with all tasks
|
||||
│ ├─ Executor implements all tasks sequentially (T1 → T2 → T3)
|
||||
│ ├─ Executor tests + commits per task
|
||||
│ └─ Executor calls: ccw issue done <id>
|
||||
└─ Wait for batch completion
|
||||
|
||||
Phase 3: Next Batch
|
||||
└─ ccw issue queue dag → check for newly-ready solutions
|
||||
```
|
||||
|
||||
## Implementation
|
||||
|
||||
### Phase 1: Get DAG & User Selection
|
||||
|
||||
```javascript
|
||||
// Get dependency graph and parallel batches
|
||||
const dagJson = Bash(`ccw issue queue dag`).trim();
|
||||
const dag = JSON.parse(dagJson);
|
||||
|
||||
if (dag.error || dag.ready_count === 0) {
|
||||
console.log(dag.error || 'No solutions ready for execution');
|
||||
console.log('Use /issue:queue to form a queue first');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`
|
||||
## Queue DAG (Solution-Level)
|
||||
|
||||
- Total Solutions: ${dag.total}
|
||||
- Ready: ${dag.ready_count}
|
||||
- Completed: ${dag.completed_count}
|
||||
- Parallel in batch 1: ${dag.parallel_batches[0]?.length || 0}
|
||||
`);
|
||||
|
||||
// Interactive selection via AskUserQuestion
|
||||
const answer = AskUserQuestion({
|
||||
questions: [
|
||||
{
|
||||
question: 'Select executor type:',
|
||||
header: 'Executor',
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: 'Codex (Recommended)', description: 'Autonomous coding with full write access' },
|
||||
{ label: 'Gemini', description: 'Large context analysis and implementation' },
|
||||
{ label: 'Agent', description: 'Claude Code sub-agent for complex tasks' }
|
||||
]
|
||||
},
|
||||
{
|
||||
question: 'Execution mode:',
|
||||
header: 'Mode',
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: 'Execute (Recommended)', description: 'Run all ready solutions' },
|
||||
{ label: 'Dry-run', description: 'Show DAG and batches without executing' }
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const executor = answer['Executor'].toLowerCase().split(' ')[0]; // codex|gemini|agent
|
||||
const isDryRun = answer['Mode'].includes('Dry-run');
|
||||
|
||||
// Dry run mode
|
||||
if (isDryRun) {
|
||||
console.log('### Parallel Batches (Dry-run):\n');
|
||||
dag.parallel_batches.forEach((batch, i) => {
|
||||
console.log(`Batch ${i + 1}: ${batch.join(', ')}`);
|
||||
});
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 2: Dispatch Parallel Batch (DAG-driven)
|
||||
|
||||
```javascript
|
||||
// Parallelism determined by DAG - no manual limit
|
||||
// All solutions in same batch have NO file conflicts and can run in parallel
|
||||
const batch = dag.parallel_batches[0] || [];
|
||||
|
||||
// Initialize TodoWrite
|
||||
TodoWrite({
|
||||
todos: batch.map(id => ({
|
||||
content: `Execute solution ${id}`,
|
||||
status: 'pending',
|
||||
activeForm: `Executing solution ${id}`
|
||||
}))
|
||||
});
|
||||
|
||||
console.log(`\n### Executing Solutions (DAG batch 1): ${batch.join(', ')}`);
|
||||
|
||||
// Launch ALL solutions in batch in parallel (DAG guarantees no conflicts)
|
||||
const executions = batch.map(solutionId => {
|
||||
updateTodo(solutionId, 'in_progress');
|
||||
return dispatchExecutor(solutionId, executor);
|
||||
});
|
||||
|
||||
await Promise.all(executions);
|
||||
batch.forEach(id => updateTodo(id, 'completed'));
|
||||
```
|
||||
|
||||
### Executor Dispatch
|
||||
|
||||
```javascript
|
||||
function dispatchExecutor(solutionId, executorType) {
|
||||
// Executor fetches FULL SOLUTION via READ-ONLY detail command
|
||||
// Executor handles all tasks within solution sequentially
|
||||
// Then reports completion via done command
|
||||
const prompt = `
|
||||
## Execute Solution ${solutionId}
|
||||
|
||||
### Step 1: Get Solution (read-only)
|
||||
\`\`\`bash
|
||||
ccw issue detail ${solutionId}
|
||||
\`\`\`
|
||||
|
||||
### Step 2: Execute All Tasks Sequentially
|
||||
The detail command returns a FULL SOLUTION with all tasks.
|
||||
Execute each task in order (T1 → T2 → T3 → ...):
|
||||
|
||||
For each task:
|
||||
1. Follow task.implementation steps
|
||||
2. Run task.test commands
|
||||
3. Verify task.acceptance criteria
|
||||
4. Commit using task.commit specification
|
||||
|
||||
### Step 3: Report Completion
|
||||
When ALL tasks in solution are done:
|
||||
\`\`\`bash
|
||||
ccw issue done ${solutionId} --result '{"summary": "...", "files_modified": [...], "tasks_completed": N}'
|
||||
\`\`\`
|
||||
|
||||
If any task failed:
|
||||
\`\`\`bash
|
||||
ccw issue done ${solutionId} --fail --reason "Task TX failed: ..."
|
||||
\`\`\`
|
||||
`;
|
||||
|
||||
if (executorType === 'codex') {
|
||||
return Bash(
|
||||
`ccw cli -p "${escapePrompt(prompt)}" --tool codex --mode write --id exec-${solutionId}`,
|
||||
{ timeout: 7200000, run_in_background: true } // 2hr for full solution
|
||||
);
|
||||
} else if (executorType === 'gemini') {
|
||||
return Bash(
|
||||
`ccw cli -p "${escapePrompt(prompt)}" --tool gemini --mode write --id exec-${solutionId}`,
|
||||
{ timeout: 3600000, run_in_background: true }
|
||||
);
|
||||
} else {
|
||||
return Task({
|
||||
subagent_type: 'code-developer',
|
||||
run_in_background: false,
|
||||
description: `Execute solution ${solutionId}`,
|
||||
prompt: prompt
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 3: Check Next Batch
|
||||
|
||||
```javascript
|
||||
// Refresh DAG after batch completes
|
||||
const refreshedDag = JSON.parse(Bash(`ccw issue queue dag`).trim());
|
||||
|
||||
console.log(`
|
||||
## Batch Complete
|
||||
|
||||
- Solutions Completed: ${refreshedDag.completed_count}/${refreshedDag.total}
|
||||
- Next ready: ${refreshedDag.ready_count}
|
||||
`);
|
||||
|
||||
if (refreshedDag.ready_count > 0) {
|
||||
console.log('Run `/issue:execute` again for next batch.');
|
||||
}
|
||||
```
|
||||
|
||||
## Parallel Execution Model
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Orchestrator │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 1. ccw issue queue dag │
|
||||
│ → { parallel_batches: [["S-1","S-2"], ["S-3"]] } │
|
||||
│ │
|
||||
│ 2. Dispatch batch 1 (parallel): │
|
||||
│ ┌──────────────────────┐ ┌──────────────────────┐ │
|
||||
│ │ Executor 1 │ │ Executor 2 │ │
|
||||
│ │ detail S-1 │ │ detail S-2 │ │
|
||||
│ │ → gets full solution │ │ → gets full solution │ │
|
||||
│ │ [T1→T2→T3 sequential]│ │ [T1→T2 sequential] │ │
|
||||
│ │ done S-1 │ │ done S-2 │ │
|
||||
│ └──────────────────────┘ └──────────────────────┘ │
|
||||
│ │
|
||||
│ 3. ccw issue queue dag (refresh) │
|
||||
│ → S-3 now ready (S-1 completed, file conflict resolved) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Why this works for parallel:**
|
||||
- `detail <id>` is READ-ONLY → no race conditions
|
||||
- Each executor handles **all tasks within a solution** sequentially
|
||||
- `done <id>` updates only its own solution status
|
||||
- `queue dag` recalculates ready solutions after each batch
|
||||
- Solutions in same batch have NO file conflicts
|
||||
|
||||
## CLI Endpoint Contract
|
||||
|
||||
### `ccw issue queue dag`
|
||||
Returns dependency graph with parallel batches (solution-level):
|
||||
```json
|
||||
{
|
||||
"queue_id": "QUE-...",
|
||||
"total": 3,
|
||||
"ready_count": 2,
|
||||
"completed_count": 0,
|
||||
"nodes": [
|
||||
{ "id": "S-1", "issue_id": "ISS-xxx", "status": "pending", "ready": true, "task_count": 3 },
|
||||
{ "id": "S-2", "issue_id": "ISS-yyy", "status": "pending", "ready": true, "task_count": 2 },
|
||||
{ "id": "S-3", "issue_id": "ISS-zzz", "status": "pending", "ready": false, "depends_on": ["S-1"] }
|
||||
],
|
||||
"parallel_batches": [["S-1", "S-2"], ["S-3"]]
|
||||
}
|
||||
```
|
||||
|
||||
### `ccw issue detail <item_id>`
|
||||
Returns FULL SOLUTION with all tasks (READ-ONLY):
|
||||
```json
|
||||
{
|
||||
"item_id": "S-1",
|
||||
"issue_id": "ISS-xxx",
|
||||
"solution_id": "SOL-xxx",
|
||||
"status": "pending",
|
||||
"solution": {
|
||||
"id": "SOL-xxx",
|
||||
"approach": "...",
|
||||
"tasks": [
|
||||
{ "id": "T1", "title": "...", "implementation": [...], "test": {...} },
|
||||
{ "id": "T2", "title": "...", "implementation": [...], "test": {...} },
|
||||
{ "id": "T3", "title": "...", "implementation": [...], "test": {...} }
|
||||
],
|
||||
"exploration_context": { "relevant_files": [...] }
|
||||
},
|
||||
"execution_hints": { "executor": "codex", "estimated_minutes": 180 }
|
||||
}
|
||||
```
|
||||
|
||||
### `ccw issue done <item_id>`
|
||||
Marks solution completed/failed, updates queue state, checks for queue completion.
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Error | Resolution |
|
||||
|-------|------------|
|
||||
| No queue | Run /issue:queue first |
|
||||
| No ready solutions | Dependencies blocked, check DAG |
|
||||
| Executor timeout | Solution not marked done, can retry |
|
||||
| Solution failure | Use `ccw issue retry` to reset |
|
||||
| Partial task failure | Executor reports which task failed via `done --fail` |
|
||||
|
||||
## Related Commands
|
||||
|
||||
- `/issue:plan` - Plan issues with solutions
|
||||
- `/issue:queue` - Form execution queue
|
||||
- `ccw issue queue dag` - View dependency graph
|
||||
- `ccw issue detail <id>` - View task details
|
||||
- `ccw issue retry` - Reset failed tasks
|
||||
309
.claude/commands/issue/new.md
Normal file
309
.claude/commands/issue/new.md
Normal file
@@ -0,0 +1,309 @@
|
||||
---
|
||||
name: new
|
||||
description: Create structured issue from GitHub URL or text description
|
||||
argument-hint: "<github-url | text-description> [--priority 1-5]"
|
||||
allowed-tools: TodoWrite(*), Bash(*), Read(*), AskUserQuestion(*), mcp__ace-tool__search_context(*)
|
||||
---
|
||||
|
||||
# Issue New Command (/issue:new)
|
||||
|
||||
## Core Principle
|
||||
|
||||
**Requirement Clarity Detection** → Ask only when needed
|
||||
|
||||
```
|
||||
Clear Input (GitHub URL, structured text) → Direct creation
|
||||
Unclear Input (vague description) → Minimal clarifying questions
|
||||
```
|
||||
|
||||
## Issue Structure
|
||||
|
||||
```typescript
|
||||
interface Issue {
|
||||
id: string; // GH-123 or ISS-YYYYMMDD-HHMMSS
|
||||
title: string;
|
||||
status: 'registered' | 'planned' | 'queued' | 'in_progress' | 'completed' | 'failed';
|
||||
priority: number; // 1 (critical) to 5 (low)
|
||||
context: string; // Problem description (single source of truth)
|
||||
source: 'github' | 'text' | 'discovery';
|
||||
source_url?: string;
|
||||
labels?: string[];
|
||||
|
||||
// Optional structured fields
|
||||
expected_behavior?: string;
|
||||
actual_behavior?: string;
|
||||
affected_components?: string[];
|
||||
|
||||
// Feedback history (failures + human clarifications)
|
||||
feedback?: {
|
||||
type: 'failure' | 'clarification' | 'rejection';
|
||||
stage: string; // new/plan/execute
|
||||
content: string;
|
||||
created_at: string;
|
||||
}[];
|
||||
|
||||
// Solution binding
|
||||
bound_solution_id: string | null;
|
||||
|
||||
// Timestamps
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
```bash
|
||||
# Clear inputs - direct creation
|
||||
/issue:new https://github.com/owner/repo/issues/123
|
||||
/issue:new "Login fails with special chars. Expected: success. Actual: 500 error"
|
||||
|
||||
# Vague input - will ask clarifying questions
|
||||
/issue:new "something wrong with auth"
|
||||
```
|
||||
|
||||
## Implementation
|
||||
|
||||
### Phase 1: Input Analysis & Clarity Detection
|
||||
|
||||
```javascript
|
||||
const input = userInput.trim();
|
||||
const flags = parseFlags(userInput); // --priority
|
||||
|
||||
// Detect input type and clarity
|
||||
const isGitHubUrl = input.match(/github\.com\/[\w-]+\/[\w-]+\/issues\/\d+/);
|
||||
const isGitHubShort = input.match(/^#(\d+)$/);
|
||||
const hasStructure = input.match(/(expected|actual|affects|steps):/i);
|
||||
|
||||
// Clarity score: 0-3
|
||||
let clarityScore = 0;
|
||||
if (isGitHubUrl || isGitHubShort) clarityScore = 3; // GitHub = fully clear
|
||||
else if (hasStructure) clarityScore = 2; // Structured text = clear
|
||||
else if (input.length > 50) clarityScore = 1; // Long text = somewhat clear
|
||||
else clarityScore = 0; // Vague
|
||||
|
||||
let issueData = {};
|
||||
```
|
||||
|
||||
### Phase 2: Data Extraction (GitHub or Text)
|
||||
|
||||
```javascript
|
||||
if (isGitHubUrl || isGitHubShort) {
|
||||
// GitHub - fetch via gh CLI
|
||||
const result = Bash(`gh issue view ${extractIssueRef(input)} --json number,title,body,labels,url`);
|
||||
const gh = JSON.parse(result);
|
||||
issueData = {
|
||||
id: `GH-${gh.number}`,
|
||||
title: gh.title,
|
||||
source: 'github',
|
||||
source_url: gh.url,
|
||||
labels: gh.labels.map(l => l.name),
|
||||
context: gh.body?.substring(0, 500) || gh.title,
|
||||
...parseMarkdownBody(gh.body)
|
||||
};
|
||||
} else {
|
||||
// Text description
|
||||
issueData = {
|
||||
id: `ISS-${new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14)}`,
|
||||
source: 'text',
|
||||
...parseTextDescription(input)
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 3: Lightweight Context Hint (Conditional)
|
||||
|
||||
```javascript
|
||||
// ACE search ONLY for medium clarity (1-2) AND missing components
|
||||
// Skip for: GitHub (has context), vague (needs clarification first)
|
||||
// Note: Deep exploration happens in /issue:plan, this is just a quick hint
|
||||
|
||||
if (clarityScore >= 1 && clarityScore <= 2 && !issueData.affected_components?.length) {
|
||||
const keywords = extractKeywords(issueData.context);
|
||||
|
||||
if (keywords.length >= 2) {
|
||||
try {
|
||||
const aceResult = mcp__ace-tool__search_context({
|
||||
project_root_path: process.cwd(),
|
||||
query: keywords.slice(0, 3).join(' ')
|
||||
});
|
||||
issueData.affected_components = aceResult.files?.slice(0, 3) || [];
|
||||
} catch {
|
||||
// ACE failure is non-blocking
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 4: Conditional Clarification (Only if Unclear)
|
||||
|
||||
```javascript
|
||||
// ONLY ask questions if clarity is low - simple open-ended prompt
|
||||
if (clarityScore < 2 && (!issueData.context || issueData.context.length < 20)) {
|
||||
const answer = AskUserQuestion({
|
||||
questions: [{
|
||||
question: 'Please describe the issue in more detail:',
|
||||
header: 'Clarify',
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: 'Provide details', description: 'Describe what, where, and expected behavior' }
|
||||
]
|
||||
}]
|
||||
});
|
||||
|
||||
// Use custom text input (via "Other")
|
||||
if (answer.customText) {
|
||||
issueData.context = answer.customText;
|
||||
issueData.title = answer.customText.split(/[.\n]/)[0].substring(0, 60);
|
||||
issueData.feedback = [{
|
||||
type: 'clarification',
|
||||
stage: 'new',
|
||||
content: answer.customText,
|
||||
created_at: new Date().toISOString()
|
||||
}];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 5: Create Issue
|
||||
|
||||
**Summary Display:**
|
||||
- Show ID, title, source, affected files (if any)
|
||||
|
||||
**Confirmation** (only for vague inputs, clarityScore < 2):
|
||||
- Use `AskUserQuestion` to confirm before creation
|
||||
|
||||
**Issue Creation** (via CLI endpoint):
|
||||
```bash
|
||||
ccw issue create --data '{"title":"...", "context":"...", "priority":3, ...}'
|
||||
```
|
||||
|
||||
**CLI Endpoint Features:**
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| Auto-increment ID | `ISS-YYYYMMDD-NNN` (e.g., `ISS-20251229-001`) |
|
||||
| Trailing newline | Proper JSONL format, no corruption |
|
||||
| JSON output | Returns created issue with all fields |
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
# Create issue via CLI
|
||||
ccw issue create --data '{
|
||||
"title": "Login fails with special chars",
|
||||
"context": "500 error when password contains quotes",
|
||||
"priority": 2,
|
||||
"source": "text",
|
||||
"expected_behavior": "Login succeeds",
|
||||
"actual_behavior": "500 Internal Server Error"
|
||||
}'
|
||||
|
||||
# Output (JSON)
|
||||
{
|
||||
"id": "ISS-20251229-001",
|
||||
"title": "Login fails with special chars",
|
||||
"status": "registered",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**Completion:**
|
||||
- Display created issue ID
|
||||
- Show next step: `/issue:plan <id>`
|
||||
|
||||
## Execution Flow
|
||||
|
||||
```
|
||||
Phase 1: Input Analysis
|
||||
└─ Detect clarity score (GitHub URL? Structured text? Keywords?)
|
||||
|
||||
Phase 2: Data Extraction (branched by clarity)
|
||||
┌────────────┬─────────────────┬──────────────┐
|
||||
│ Score 3 │ Score 1-2 │ Score 0 │
|
||||
│ GitHub │ Text + ACE │ Vague │
|
||||
├────────────┼─────────────────┼──────────────┤
|
||||
│ gh CLI │ Parse struct │ AskQuestion │
|
||||
│ → parse │ + quick hint │ (1 question) │
|
||||
│ │ (3 files max) │ → feedback │
|
||||
└────────────┴─────────────────┴──────────────┘
|
||||
|
||||
Phase 3: Create Issue
|
||||
├─ Score ≥ 2: Direct creation
|
||||
└─ Score < 2: Confirm first → Create
|
||||
|
||||
Note: Deep exploration & lifecycle deferred to /issue:plan
|
||||
```
|
||||
|
||||
## Helper Functions
|
||||
|
||||
```javascript
|
||||
function extractKeywords(text) {
|
||||
const stopWords = new Set(['the', 'a', 'an', 'is', 'are', 'was', 'were', 'not', 'with']);
|
||||
return text
|
||||
.toLowerCase()
|
||||
.split(/\W+/)
|
||||
.filter(w => w.length > 3 && !stopWords.has(w))
|
||||
.slice(0, 5);
|
||||
}
|
||||
|
||||
function parseTextDescription(text) {
|
||||
const result = { title: '', context: '' };
|
||||
const sentences = text.split(/\.(?=\s|$)/);
|
||||
|
||||
result.title = sentences[0]?.trim().substring(0, 60) || 'Untitled';
|
||||
result.context = text.substring(0, 500);
|
||||
|
||||
// Extract structured fields if present
|
||||
const expected = text.match(/expected:?\s*([^.]+)/i);
|
||||
const actual = text.match(/actual:?\s*([^.]+)/i);
|
||||
const affects = text.match(/affects?:?\s*([^.]+)/i);
|
||||
|
||||
if (expected) result.expected_behavior = expected[1].trim();
|
||||
if (actual) result.actual_behavior = actual[1].trim();
|
||||
if (affects) {
|
||||
result.affected_components = affects[1].split(/[,\s]+/).filter(c => c.includes('/') || c.includes('.'));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function parseMarkdownBody(body) {
|
||||
if (!body) return {};
|
||||
const result = {};
|
||||
|
||||
const problem = body.match(/##?\s*(problem|description)[:\s]*([\s\S]*?)(?=##|$)/i);
|
||||
const expected = body.match(/##?\s*expected[:\s]*([\s\S]*?)(?=##|$)/i);
|
||||
const actual = body.match(/##?\s*actual[:\s]*([\s\S]*?)(?=##|$)/i);
|
||||
|
||||
if (problem) result.context = problem[2].trim().substring(0, 500);
|
||||
if (expected) result.expected_behavior = expected[2].trim();
|
||||
if (actual) result.actual_behavior = actual[2].trim();
|
||||
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Clear Input (No Questions)
|
||||
|
||||
```bash
|
||||
/issue:new https://github.com/org/repo/issues/42
|
||||
# → Fetches, parses, creates immediately
|
||||
|
||||
/issue:new "Login fails with special chars. Expected: success. Actual: 500"
|
||||
# → Parses structure, creates immediately
|
||||
```
|
||||
|
||||
### Vague Input (1 Question)
|
||||
|
||||
```bash
|
||||
/issue:new "auth broken"
|
||||
# → Asks: "Input unclear. What is the issue about?"
|
||||
# → User provides details → saved to feedback[]
|
||||
# → Creates issue
|
||||
```
|
||||
|
||||
## Related Commands
|
||||
|
||||
- `/issue:plan` - Plan solution for issue
|
||||
- `/issue:manage` - Interactive issue management
|
||||
324
.claude/commands/issue/plan.md
Normal file
324
.claude/commands/issue/plan.md
Normal file
@@ -0,0 +1,324 @@
|
||||
---
|
||||
name: plan
|
||||
description: Batch plan issue resolution using issue-plan-agent (explore + plan closed-loop)
|
||||
argument-hint: "--all-pending <issue-id>[,<issue-id>,...] [--batch-size 3] "
|
||||
allowed-tools: TodoWrite(*), Task(*), SlashCommand(*), AskUserQuestion(*), Bash(*), Read(*), Write(*)
|
||||
---
|
||||
|
||||
# Issue Plan Command (/issue:plan)
|
||||
|
||||
## Overview
|
||||
|
||||
Unified planning command using **issue-plan-agent** that combines exploration and planning into a single closed-loop workflow.
|
||||
|
||||
**Behavior:**
|
||||
- Single solution per issue → auto-bind
|
||||
- Multiple solutions → return for user selection
|
||||
- Agent handles file generation
|
||||
|
||||
## Core Guidelines
|
||||
|
||||
**⚠️ Data Access Principle**: Issues and solutions files can grow very large. To avoid context overflow:
|
||||
|
||||
| Operation | Correct | Incorrect |
|
||||
|-----------|---------|-----------|
|
||||
| List issues (brief) | `ccw issue list --status pending --brief` | `Read('issues.jsonl')` |
|
||||
| Read issue details | `ccw issue status <id> --json` | `Read('issues.jsonl')` |
|
||||
| Update status | `ccw issue update <id> --status ...` | Direct file edit |
|
||||
| Bind solution | `ccw issue bind <id> <sol-id>` | Direct file edit |
|
||||
|
||||
**Output Options**:
|
||||
- `--brief`: JSON with minimal fields (id, title, status, priority, tags)
|
||||
- `--json`: Full JSON (agent use only)
|
||||
|
||||
**Orchestration vs Execution**:
|
||||
- **Command (orchestrator)**: Use `--brief` for minimal context
|
||||
- **Agent (executor)**: Fetch full details → `ccw issue status <id> --json`
|
||||
|
||||
**ALWAYS** use CLI commands for CRUD operations. **NEVER** read entire `issues.jsonl` or `solutions/*.jsonl` directly.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
/issue:plan [<issue-id>[,<issue-id>,...]] [FLAGS]
|
||||
|
||||
# Examples
|
||||
/issue:plan # Default: --all-pending
|
||||
/issue:plan GH-123 # Single issue
|
||||
/issue:plan GH-123,GH-124,GH-125 # Batch (up to 3)
|
||||
/issue:plan --all-pending # All pending issues (explicit)
|
||||
|
||||
# Flags
|
||||
--batch-size <n> Max issues per agent batch (default: 3)
|
||||
```
|
||||
|
||||
## Execution Process
|
||||
|
||||
```
|
||||
Phase 1: Issue Loading
|
||||
├─ Parse input (single, comma-separated, or --all-pending)
|
||||
├─ Fetch issue metadata (ID, title, tags)
|
||||
├─ Validate issues exist (create if needed)
|
||||
└─ Group by similarity (shared tags or title keywords, max 3 per batch)
|
||||
|
||||
Phase 2: Unified Explore + Plan (issue-plan-agent)
|
||||
├─ Launch issue-plan-agent per batch
|
||||
├─ Agent performs:
|
||||
│ ├─ ACE semantic search for each issue
|
||||
│ ├─ Codebase exploration (files, patterns, dependencies)
|
||||
│ ├─ Solution generation with task breakdown
|
||||
│ └─ Conflict detection across issues
|
||||
└─ Output: solution JSON per issue
|
||||
|
||||
Phase 3: Solution Registration & Binding
|
||||
├─ Append solutions to solutions/{issue-id}.jsonl
|
||||
├─ Single solution per issue → auto-bind
|
||||
├─ Multiple candidates → AskUserQuestion to select
|
||||
└─ Update issues.jsonl with bound_solution_id
|
||||
|
||||
Phase 4: Summary
|
||||
├─ Display bound solutions
|
||||
├─ Show task counts per issue
|
||||
└─ Display next steps (/issue:queue)
|
||||
```
|
||||
|
||||
## Implementation
|
||||
|
||||
### Phase 1: Issue Loading (Brief Info Only)
|
||||
|
||||
```javascript
|
||||
const batchSize = flags.batchSize || 3;
|
||||
let issues = []; // {id, title, tags} - brief info for grouping only
|
||||
|
||||
// Default to --all-pending if no input provided
|
||||
const useAllPending = flags.allPending || !userInput || userInput.trim() === '';
|
||||
|
||||
if (useAllPending) {
|
||||
// Get pending issues with brief metadata via CLI
|
||||
const result = Bash(`ccw issue list --status pending,registered --json`).trim();
|
||||
const parsed = result ? JSON.parse(result) : [];
|
||||
issues = parsed.map(i => ({ id: i.id, title: i.title || '', tags: i.tags || [] }));
|
||||
|
||||
if (issues.length === 0) {
|
||||
console.log('No pending issues found.');
|
||||
return;
|
||||
}
|
||||
console.log(`Found ${issues.length} pending issues`);
|
||||
} else {
|
||||
// Parse comma-separated issue IDs, fetch brief metadata
|
||||
const ids = userInput.includes(',')
|
||||
? userInput.split(',').map(s => s.trim())
|
||||
: [userInput.trim()];
|
||||
|
||||
for (const id of ids) {
|
||||
Bash(`ccw issue init ${id} --title "Issue ${id}" 2>/dev/null || true`);
|
||||
const info = Bash(`ccw issue status ${id} --json`).trim();
|
||||
const parsed = info ? JSON.parse(info) : {};
|
||||
issues.push({ id, title: parsed.title || '', tags: parsed.tags || [] });
|
||||
}
|
||||
}
|
||||
// Note: Agent fetches full issue content via `ccw issue status <id> --json`
|
||||
|
||||
// Semantic grouping via Gemini CLI (max 4 issues per group)
|
||||
async function groupBySimilarityGemini(issues) {
|
||||
const issueSummaries = issues.map(i => ({
|
||||
id: i.id, title: i.title, tags: i.tags
|
||||
}));
|
||||
|
||||
const prompt = `
|
||||
PURPOSE: Group similar issues by semantic similarity for batch processing; maximize within-group coherence; max 4 issues per group
|
||||
TASK: • Analyze issue titles/tags semantically • Identify functional/architectural clusters • Assign each issue to one group
|
||||
MODE: analysis
|
||||
CONTEXT: Issue metadata only
|
||||
EXPECTED: JSON with groups array, each containing max 4 issue_ids, theme, rationale
|
||||
RULES: $(cat ~/.claude/workflows/cli-templates/protocols/analysis-protocol.md) | Each issue in exactly one group | Max 4 issues per group | Balance group sizes
|
||||
|
||||
INPUT:
|
||||
${JSON.stringify(issueSummaries, null, 2)}
|
||||
|
||||
OUTPUT FORMAT:
|
||||
{"groups":[{"group_id":1,"theme":"...","issue_ids":["..."],"rationale":"..."}],"ungrouped":[]}
|
||||
`;
|
||||
|
||||
const taskId = Bash({
|
||||
command: `ccw cli -p "${prompt}" --tool gemini --mode analysis`,
|
||||
run_in_background: true, timeout: 600000
|
||||
});
|
||||
const output = TaskOutput({ task_id: taskId, block: true });
|
||||
|
||||
// Extract JSON from potential markdown code blocks
|
||||
function extractJsonFromMarkdown(text) {
|
||||
const jsonMatch = text.match(/```json\s*\n([\s\S]*?)\n```/) ||
|
||||
text.match(/```\s*\n([\s\S]*?)\n```/);
|
||||
return jsonMatch ? jsonMatch[1] : text;
|
||||
}
|
||||
|
||||
const result = JSON.parse(extractJsonFromMarkdown(output));
|
||||
return result.groups.map(g => g.issue_ids.map(id => issues.find(i => i.id === id)));
|
||||
}
|
||||
|
||||
const batches = await groupBySimilarityGemini(issues);
|
||||
console.log(`Processing ${issues.length} issues in ${batches.length} batch(es) (max 4 issues/agent)`);
|
||||
|
||||
TodoWrite({
|
||||
todos: batches.map((_, i) => ({
|
||||
content: `Plan batch ${i+1}`,
|
||||
status: 'pending',
|
||||
activeForm: `Planning batch ${i+1}`
|
||||
}))
|
||||
});
|
||||
```
|
||||
|
||||
### Phase 2: Unified Explore + Plan (issue-plan-agent) - PARALLEL
|
||||
|
||||
```javascript
|
||||
Bash(`mkdir -p .workflow/issues/solutions`);
|
||||
const pendingSelections = []; // Collect multi-solution issues for user selection
|
||||
const agentResults = []; // Collect all agent results for conflict aggregation
|
||||
|
||||
// Build prompts for all batches
|
||||
const agentTasks = batches.map((batch, batchIndex) => {
|
||||
const issueList = batch.map(i => `- ${i.id}: ${i.title}${i.tags.length ? ` [${i.tags.join(', ')}]` : ''}`).join('\n');
|
||||
const batchIds = batch.map(i => i.id);
|
||||
|
||||
const issuePrompt = `
|
||||
## Plan Issues
|
||||
|
||||
**Issues** (grouped by similarity):
|
||||
${issueList}
|
||||
|
||||
**Project Root**: ${process.cwd()}
|
||||
|
||||
### Project Context (MANDATORY)
|
||||
1. Read: .workflow/project-tech.json (technology stack, architecture)
|
||||
2. Read: .workflow/project-guidelines.json (constraints and conventions)
|
||||
|
||||
### Workflow
|
||||
1. Fetch issue details: ccw issue status <id> --json
|
||||
2. Load project context files
|
||||
3. Explore codebase (ACE semantic search)
|
||||
4. Plan solution with tasks (schema: solution-schema.json)
|
||||
5. Write solution to: .workflow/issues/solutions/{issue-id}.jsonl
|
||||
6. Single solution → auto-bind; Multiple → return for selection
|
||||
|
||||
### Rules
|
||||
- Solution ID format: SOL-{issue-id}-{seq}
|
||||
- Single solution per issue → auto-bind via ccw issue bind
|
||||
- Multiple solutions → register only, return pending_selection
|
||||
- Tasks must have quantified acceptance.criteria
|
||||
|
||||
### Return Summary
|
||||
{"bound":[{"issue_id":"...","solution_id":"...","task_count":N}],"pending_selection":[{"issue_id":"...","solutions":[{"id":"...","description":"...","task_count":N}]}]}
|
||||
`;
|
||||
|
||||
return { batchIndex, batchIds, issuePrompt, batch };
|
||||
});
|
||||
|
||||
// Launch agents in parallel (max 10 concurrent)
|
||||
const MAX_PARALLEL = 10;
|
||||
for (let i = 0; i < agentTasks.length; i += MAX_PARALLEL) {
|
||||
const chunk = agentTasks.slice(i, i + MAX_PARALLEL);
|
||||
const taskIds = [];
|
||||
|
||||
// Launch chunk in parallel
|
||||
for (const { batchIndex, batchIds, issuePrompt, batch } of chunk) {
|
||||
updateTodo(`Plan batch ${batchIndex + 1}`, 'in_progress');
|
||||
const taskId = Task(
|
||||
subagent_type="issue-plan-agent",
|
||||
run_in_background=true,
|
||||
description=`Explore & plan ${batch.length} issues: ${batchIds.join(', ')}`,
|
||||
prompt=issuePrompt
|
||||
);
|
||||
taskIds.push({ taskId, batchIndex });
|
||||
}
|
||||
|
||||
console.log(`Launched ${taskIds.length} agents (batch ${i/MAX_PARALLEL + 1}/${Math.ceil(agentTasks.length/MAX_PARALLEL)})...`);
|
||||
|
||||
// Collect results from this chunk
|
||||
for (const { taskId, batchIndex } of taskIds) {
|
||||
const result = TaskOutput(task_id=taskId, block=true);
|
||||
|
||||
// Extract JSON from potential markdown code blocks (agent may wrap in ```json...```)
|
||||
const jsonText = extractJsonFromMarkdown(result);
|
||||
let summary;
|
||||
try {
|
||||
summary = JSON.parse(jsonText);
|
||||
} catch (e) {
|
||||
console.log(`⚠ Batch ${batchIndex + 1}: Failed to parse agent result, skipping`);
|
||||
updateTodo(`Plan batch ${batchIndex + 1}`, 'completed');
|
||||
continue;
|
||||
}
|
||||
agentResults.push(summary); // Store for Phase 3 conflict aggregation
|
||||
|
||||
for (const item of summary.bound || []) {
|
||||
console.log(`✓ ${item.issue_id}: ${item.solution_id} (${item.task_count} tasks)`);
|
||||
}
|
||||
// Collect and notify pending selections
|
||||
for (const pending of summary.pending_selection || []) {
|
||||
console.log(`⏳ ${pending.issue_id}: ${pending.solutions.length} solutions → awaiting selection`);
|
||||
pendingSelections.push(pending);
|
||||
}
|
||||
if (summary.conflicts?.length > 0) {
|
||||
console.log(`⚠ Conflicts: ${summary.conflicts.length} detected (will resolve in Phase 3)`);
|
||||
}
|
||||
updateTodo(`Plan batch ${batchIndex + 1}`, 'completed');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 3: Conflict Resolution & Solution Selection
|
||||
|
||||
**Conflict Handling:**
|
||||
- Collect `conflicts` from all agent results
|
||||
- Low/Medium severity → auto-resolve with `recommended_resolution`
|
||||
- High severity → use `AskUserQuestion` to let user choose resolution
|
||||
|
||||
**Multi-Solution Selection:**
|
||||
- If `pending_selection` contains issues with multiple solutions:
|
||||
- Use `AskUserQuestion` to present options (solution ID + task count + description)
|
||||
- Extract selected solution ID from user response
|
||||
- Verify solution file exists, recover from payload if missing
|
||||
- Bind selected solution via `ccw issue bind <issue-id> <solution-id>`
|
||||
|
||||
### Phase 4: Summary
|
||||
|
||||
```javascript
|
||||
// Count planned issues via CLI
|
||||
const planned = JSON.parse(Bash(`ccw issue list --status planned --brief`) || '[]');
|
||||
const plannedCount = planned.length;
|
||||
|
||||
console.log(`
|
||||
## Done: ${issues.length} issues → ${plannedCount} planned
|
||||
|
||||
Next: \`/issue:queue\` → \`/issue:execute\`
|
||||
`);
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Error | Resolution |
|
||||
|-------|------------|
|
||||
| Issue not found | Auto-create in issues.jsonl |
|
||||
| ACE search fails | Agent falls back to ripgrep |
|
||||
| No solutions generated | Display error, suggest manual planning |
|
||||
| User cancels selection | Skip issue, continue with others |
|
||||
| File conflicts | Agent detects and suggests resolution order |
|
||||
|
||||
## Quality Checklist
|
||||
|
||||
Before completing, verify:
|
||||
|
||||
- [ ] All input issues have solutions in `solutions/{issue-id}.jsonl`
|
||||
- [ ] Single solution issues are auto-bound (`bound_solution_id` set)
|
||||
- [ ] Multi-solution issues returned in `pending_selection` for user choice
|
||||
- [ ] Each solution has executable tasks with `modification_points`
|
||||
- [ ] Task acceptance criteria are quantified (not vague)
|
||||
- [ ] Conflicts detected and reported (if multiple issues touch same files)
|
||||
- [ ] Issue status updated to `planned` after binding
|
||||
|
||||
## Related Commands
|
||||
|
||||
- `/issue:queue` - Form execution queue from bound solutions
|
||||
- `/issue:execute` - Execute queue with codex
|
||||
- `ccw issue list` - List all issues
|
||||
- `ccw issue status` - View issue and solution details
|
||||
327
.claude/commands/issue/queue.md
Normal file
327
.claude/commands/issue/queue.md
Normal file
@@ -0,0 +1,327 @@
|
||||
---
|
||||
name: queue
|
||||
description: Form execution queue from bound solutions using issue-queue-agent (solution-level)
|
||||
argument-hint: "[--rebuild] [--issue <id>]"
|
||||
allowed-tools: TodoWrite(*), Task(*), Bash(*), Read(*), Write(*)
|
||||
---
|
||||
|
||||
# Issue Queue Command (/issue:queue)
|
||||
|
||||
## Overview
|
||||
|
||||
Queue formation command using **issue-queue-agent** that analyzes all bound solutions, resolves **inter-solution** conflicts, and creates an ordered execution queue at **solution level**.
|
||||
|
||||
**Design Principle**: Queue items are **solutions**, not individual tasks. Each executor receives a complete solution with all its tasks.
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
- **Agent-driven**: issue-queue-agent handles all ordering logic
|
||||
- **Solution-level granularity**: Queue items are solutions, not tasks
|
||||
- **Conflict clarification**: High-severity conflicts prompt user decision
|
||||
- Semantic priority calculation per solution (0.0-1.0)
|
||||
- Parallel/Sequential group assignment for solutions
|
||||
|
||||
## Core Guidelines
|
||||
|
||||
**⚠️ Data Access Principle**: Issues and queue files can grow very large. To avoid context overflow:
|
||||
|
||||
| Operation | Correct | Incorrect |
|
||||
|-----------|---------|-----------|
|
||||
| List issues (brief) | `ccw issue list --status planned --brief` | `Read('issues.jsonl')` |
|
||||
| List queue (brief) | `ccw issue queue --brief` | `Read('queues/*.json')` |
|
||||
| Read issue details | `ccw issue status <id> --json` | `Read('issues.jsonl')` |
|
||||
| Get next item | `ccw issue next --json` | `Read('queues/*.json')` |
|
||||
| Update status | `ccw issue update <id> --status ...` | Direct file edit |
|
||||
| Sync from queue | `ccw issue update --from-queue` | Direct file edit |
|
||||
|
||||
**Output Options**:
|
||||
- `--brief`: JSON with minimal fields (id, status, counts)
|
||||
- `--json`: Full JSON (agent use only)
|
||||
|
||||
**Orchestration vs Execution**:
|
||||
- **Command (orchestrator)**: Use `--brief` for minimal context
|
||||
- **Agent (executor)**: Fetch full details → `ccw issue status <id> --json`
|
||||
|
||||
**ALWAYS** use CLI commands for CRUD operations. **NEVER** read entire `issues.jsonl` or `queues/*.json` directly.
|
||||
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
/issue:queue [FLAGS]
|
||||
|
||||
# Examples
|
||||
/issue:queue # Form NEW queue from all bound solutions
|
||||
/issue:queue --issue GH-123 # Form queue for specific issue only
|
||||
/issue:queue --append GH-124 # Append to active queue
|
||||
/issue:queue --list # List all queues (history)
|
||||
/issue:queue --switch QUE-xxx # Switch active queue
|
||||
/issue:queue --archive # Archive completed active queue
|
||||
|
||||
# Flags
|
||||
--issue <id> Form queue for specific issue only
|
||||
--append <id> Append issue to active queue (don't create new)
|
||||
|
||||
# CLI subcommands (ccw issue queue ...)
|
||||
ccw issue queue list List all queues with status
|
||||
ccw issue queue switch <queue-id> Switch active queue
|
||||
ccw issue queue archive Archive current queue
|
||||
ccw issue queue delete <queue-id> Delete queue from history
|
||||
```
|
||||
|
||||
## Execution Process
|
||||
|
||||
```
|
||||
Phase 1: Solution Loading
|
||||
├─ Load issues.jsonl, filter by status='planned' + bound_solution_id
|
||||
├─ Read solutions/{issue-id}.jsonl, find bound solution
|
||||
├─ Extract files_touched from task modification_points
|
||||
└─ Build solution objects array
|
||||
|
||||
Phase 2-4: Agent-Driven Queue Formation (issue-queue-agent)
|
||||
├─ Launch issue-queue-agent with solutions array
|
||||
├─ Agent performs:
|
||||
│ ├─ Conflict analysis (5 types via Gemini CLI)
|
||||
│ ├─ Build dependency DAG from conflicts
|
||||
│ ├─ Calculate semantic priority per solution
|
||||
│ └─ Assign execution groups (parallel/sequential)
|
||||
└─ Agent writes: queue JSON + index update
|
||||
|
||||
Phase 5: Conflict Clarification (if needed)
|
||||
├─ Check agent return for `clarifications` array
|
||||
├─ If clarifications exist → AskUserQuestion
|
||||
├─ Pass user decisions back to agent (resume)
|
||||
└─ Agent updates queue with resolved conflicts
|
||||
|
||||
Phase 6: Status Update & Summary
|
||||
├─ Update issue statuses to 'queued'
|
||||
└─ Display queue summary, next step: /issue:execute
|
||||
```
|
||||
|
||||
## Implementation
|
||||
|
||||
### Phase 1: Solution Loading
|
||||
|
||||
**Data Loading:**
|
||||
- Load `issues.jsonl` and filter issues with `status === 'planned'` and `bound_solution_id`
|
||||
- If no planned issues found → display message, suggest `/issue:plan`
|
||||
|
||||
**Solution Collection** (for each planned issue):
|
||||
- Read `solutions/{issue-id}.jsonl`
|
||||
- Find bound solution by `bound_solution_id`
|
||||
- If bound solution not found → warn and skip issue
|
||||
- Extract `files_touched` from all task `modification_points`
|
||||
|
||||
**Build Solution Objects:**
|
||||
```json
|
||||
{
|
||||
"issue_id": "ISS-xxx",
|
||||
"solution_id": "SOL-ISS-xxx-1",
|
||||
"task_count": 3,
|
||||
"files_touched": ["src/auth.ts", "src/utils.ts"],
|
||||
"priority": "medium"
|
||||
}
|
||||
```
|
||||
|
||||
**Output:** Array of solution objects ready for agent processing
|
||||
|
||||
### Phase 2-4: Agent-Driven Queue Formation
|
||||
|
||||
**Generate Queue ID** (command layer, pass to agent):
|
||||
```javascript
|
||||
const queueId = `QUE-${new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14)}`;
|
||||
```
|
||||
|
||||
**Agent Prompt**:
|
||||
```
|
||||
## Order Solutions into Execution Queue
|
||||
|
||||
**Queue ID**: ${queueId}
|
||||
**Solutions**: ${solutions.length} from ${issues.length} issues
|
||||
**Project Root**: ${cwd}
|
||||
|
||||
### Input
|
||||
${JSON.stringify(solutions)}
|
||||
|
||||
### Workflow
|
||||
|
||||
Step 1: Build dependency graph from solutions (nodes=solutions, edges=file conflicts)
|
||||
Step 2: Use Gemini CLI for conflict analysis (5 types: file, API, data, dependency, architecture)
|
||||
Step 3: For high-severity conflicts without clear resolution → add to `clarifications`
|
||||
Step 4: Calculate semantic priority (base from issue priority + task_count boost)
|
||||
Step 5: Assign execution groups: P* (parallel, no overlaps) / S* (sequential, shared files)
|
||||
Step 6: Write queue JSON + update index
|
||||
|
||||
### Output Requirements
|
||||
|
||||
**Write files** (exactly 2):
|
||||
- `.workflow/issues/queues/${queueId}.json` - Full queue with solutions, conflicts, groups
|
||||
- `.workflow/issues/queues/index.json` - Update with new queue entry
|
||||
|
||||
**Return JSON**:
|
||||
\`\`\`json
|
||||
{
|
||||
"queue_id": "${queueId}",
|
||||
"total_solutions": N,
|
||||
"total_tasks": N,
|
||||
"execution_groups": [{"id": "P1", "type": "parallel", "count": N}],
|
||||
"issues_queued": ["ISS-xxx"],
|
||||
"clarifications": [{"conflict_id": "CFT-1", "question": "...", "options": [...]}]
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
### Rules
|
||||
- Solution granularity (NOT individual tasks)
|
||||
- Queue Item ID format: S-1, S-2, S-3, ...
|
||||
- Use provided Queue ID (do NOT generate new)
|
||||
- `clarifications` only present if high-severity unresolved conflicts exist
|
||||
|
||||
### Done Criteria
|
||||
- [ ] Queue JSON written with all solutions ordered
|
||||
- [ ] Index updated with active_queue_id
|
||||
- [ ] No circular dependencies
|
||||
- [ ] Parallel groups have no file overlaps
|
||||
- [ ] Return JSON matches required shape
|
||||
```
|
||||
|
||||
**Launch Agent**:
|
||||
```javascript
|
||||
const result = Task(
|
||||
subagent_type="issue-queue-agent",
|
||||
prompt=agentPrompt,
|
||||
description=`Order ${solutions.length} solutions`
|
||||
);
|
||||
```
|
||||
|
||||
### Phase 5: Conflict Clarification
|
||||
|
||||
**Check Agent Return:**
|
||||
- Parse agent result JSON
|
||||
- If `clarifications` array exists and non-empty → user decision required
|
||||
|
||||
**Clarification Flow:**
|
||||
```javascript
|
||||
if (result.clarifications?.length > 0) {
|
||||
for (const clarification of result.clarifications) {
|
||||
// Present to user via AskUserQuestion
|
||||
const answer = AskUserQuestion({
|
||||
questions: [{
|
||||
question: clarification.question,
|
||||
header: clarification.conflict_id,
|
||||
options: clarification.options,
|
||||
multiSelect: false
|
||||
}]
|
||||
});
|
||||
|
||||
// Resume agent with user decision
|
||||
Task(
|
||||
subagent_type="issue-queue-agent",
|
||||
resume=agentId,
|
||||
prompt=`Conflict ${clarification.conflict_id} resolved: ${answer.selected}`
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 6: Status Update & Summary
|
||||
|
||||
**Status Update** (MUST use CLI command, NOT direct file operations):
|
||||
|
||||
```bash
|
||||
# Option 1: Batch update from queue (recommended)
|
||||
ccw issue update --from-queue [queue-id] --json
|
||||
ccw issue update --from-queue --json # Use active queue
|
||||
ccw issue update --from-queue QUE-xxx --json # Use specific queue
|
||||
|
||||
# Option 2: Individual issue update
|
||||
ccw issue update <issue-id> --status queued
|
||||
```
|
||||
|
||||
**⚠️ IMPORTANT**: Do NOT directly modify `issues.jsonl`. Always use CLI command to ensure proper validation and history tracking.
|
||||
|
||||
**Output** (JSON):
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"queue_id": "QUE-xxx",
|
||||
"queued": ["ISS-001", "ISS-002"],
|
||||
"queued_count": 2,
|
||||
"unplanned": ["ISS-003"],
|
||||
"unplanned_count": 1
|
||||
}
|
||||
```
|
||||
|
||||
**Behavior:**
|
||||
- Updates issues in queue to `status: 'queued'` (skips already queued/executing/completed)
|
||||
- Identifies planned issues with `bound_solution_id` NOT in queue → `unplanned` array
|
||||
- Optional `queue-id`: defaults to active queue if omitted
|
||||
|
||||
**Summary Output:**
|
||||
- Display queue ID, solution count, task count
|
||||
- Show unplanned issues (planned but NOT in queue)
|
||||
- Show next step: `/issue:execute`
|
||||
|
||||
|
||||
## Storage Structure (Queue History)
|
||||
|
||||
```
|
||||
.workflow/issues/
|
||||
├── issues.jsonl # All issues (one per line)
|
||||
├── queues/ # Queue history directory
|
||||
│ ├── index.json # Queue index (active + history)
|
||||
│ ├── {queue-id}.json # Individual queue files
|
||||
│ └── ...
|
||||
└── solutions/
|
||||
├── {issue-id}.jsonl # Solutions for issue
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Queue Index Schema
|
||||
|
||||
```json
|
||||
{
|
||||
"active_queue_id": "QUE-20251227-143000",
|
||||
"queues": [
|
||||
{
|
||||
"id": "QUE-20251227-143000",
|
||||
"status": "active",
|
||||
"issue_ids": ["ISS-xxx", "ISS-yyy"],
|
||||
"total_solutions": 3,
|
||||
"completed_solutions": 1,
|
||||
"created_at": "2025-12-27T14:30:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: Queue file schema is produced by `issue-queue-agent`. See agent documentation for details.
|
||||
## Error Handling
|
||||
|
||||
| Error | Resolution |
|
||||
|-------|------------|
|
||||
| No bound solutions | Display message, suggest /issue:plan |
|
||||
| Circular dependency | List cycles, abort queue formation |
|
||||
| High-severity conflict | Return `clarifications`, prompt user decision |
|
||||
| User cancels clarification | Abort queue formation |
|
||||
| **index.json not updated** | Auto-fix: Set active_queue_id to new queue |
|
||||
| **Queue file missing solutions** | Abort with error, agent must regenerate |
|
||||
|
||||
## Quality Checklist
|
||||
|
||||
Before completing, verify:
|
||||
|
||||
- [ ] All planned issues with `bound_solution_id` are included
|
||||
- [ ] Queue JSON written to `queues/{queue-id}.json`
|
||||
- [ ] Index updated in `queues/index.json` with `active_queue_id`
|
||||
- [ ] No circular dependencies in solution DAG
|
||||
- [ ] All conflicts resolved (auto or via user clarification)
|
||||
- [ ] Parallel groups have no file overlaps
|
||||
- [ ] Issue statuses updated to `queued`
|
||||
|
||||
## Related Commands
|
||||
|
||||
- `/issue:plan` - Plan issues and bind solutions
|
||||
- `/issue:execute` - Execute queue with codex
|
||||
- `ccw issue queue list` - View current queue
|
||||
- `ccw issue update --from-queue [queue-id]` - Sync issue statuses from queue
|
||||
@@ -410,7 +410,6 @@ Task(subagent_type="{meta.agent}",
|
||||
1. Read complete task JSON: {session.task_json_path}
|
||||
2. Load context package: {session.context_package_path}
|
||||
|
||||
Follow complete execution guidelines in @.claude/agents/{meta.agent}.md
|
||||
|
||||
**Session Paths**:
|
||||
- Workflow Dir: {session.workflow_dir}
|
||||
|
||||
@@ -10,7 +10,11 @@ examples:
|
||||
# Workflow Init Command (/workflow:init)
|
||||
|
||||
## Overview
|
||||
Initialize `.workflow/project.json` with comprehensive project understanding by delegating analysis to **cli-explore-agent**.
|
||||
Initialize `.workflow/project-tech.json` and `.workflow/project-guidelines.json` with comprehensive project understanding by delegating analysis to **cli-explore-agent**.
|
||||
|
||||
**Dual File System**:
|
||||
- `project-tech.json`: Auto-generated technical analysis (stack, architecture, components)
|
||||
- `project-guidelines.json`: User-maintained rules and constraints (created as scaffold)
|
||||
|
||||
**Note**: This command may be called by other workflow commands. Upon completion, return immediately to continue the calling workflow without interrupting the task flow.
|
||||
|
||||
@@ -27,7 +31,7 @@ Input Parsing:
|
||||
└─ Parse --regenerate flag → regenerate = true | false
|
||||
|
||||
Decision:
|
||||
├─ EXISTS + no --regenerate → Exit: "Already initialized"
|
||||
├─ BOTH_EXIST + no --regenerate → Exit: "Already initialized"
|
||||
├─ EXISTS + --regenerate → Backup existing → Continue analysis
|
||||
└─ NOT_FOUND → Continue analysis
|
||||
|
||||
@@ -37,11 +41,14 @@ Analysis Flow:
|
||||
│ ├─ Structural scan (get_modules_by_depth.sh, find, wc)
|
||||
│ ├─ Semantic analysis (Gemini CLI)
|
||||
│ ├─ Synthesis and merge
|
||||
│ └─ Write .workflow/project.json
|
||||
│ └─ Write .workflow/project-tech.json
|
||||
├─ Create guidelines scaffold (if not exists)
|
||||
│ └─ Write .workflow/project-guidelines.json (empty structure)
|
||||
└─ Display summary
|
||||
|
||||
Output:
|
||||
└─ .workflow/project.json (+ .backup if regenerate)
|
||||
├─ .workflow/project-tech.json (+ .backup if regenerate)
|
||||
└─ .workflow/project-guidelines.json (scaffold if new)
|
||||
```
|
||||
|
||||
## Implementation
|
||||
@@ -56,13 +63,18 @@ const regenerate = $ARGUMENTS.includes('--regenerate')
|
||||
**Check existing state**:
|
||||
|
||||
```bash
|
||||
bash(test -f .workflow/project.json && echo "EXISTS" || echo "NOT_FOUND")
|
||||
bash(test -f .workflow/project-tech.json && echo "TECH_EXISTS" || echo "TECH_NOT_FOUND")
|
||||
bash(test -f .workflow/project-guidelines.json && echo "GUIDELINES_EXISTS" || echo "GUIDELINES_NOT_FOUND")
|
||||
```
|
||||
|
||||
**If EXISTS and no --regenerate**: Exit early
|
||||
**If BOTH_EXIST and no --regenerate**: Exit early
|
||||
```
|
||||
Project already initialized at .workflow/project.json
|
||||
Use /workflow:init --regenerate to rebuild
|
||||
Project already initialized:
|
||||
- Tech analysis: .workflow/project-tech.json
|
||||
- Guidelines: .workflow/project-guidelines.json
|
||||
|
||||
Use /workflow:init --regenerate to rebuild tech analysis
|
||||
Use /workflow:session:solidify to add guidelines
|
||||
Use /workflow:status --project to view state
|
||||
```
|
||||
|
||||
@@ -78,7 +90,7 @@ bash(mkdir -p .workflow)
|
||||
|
||||
**For --regenerate**: Backup and preserve existing data
|
||||
```bash
|
||||
bash(cp .workflow/project.json .workflow/project.json.backup)
|
||||
bash(cp .workflow/project-tech.json .workflow/project-tech.json.backup)
|
||||
```
|
||||
|
||||
**Delegate analysis to agent**:
|
||||
@@ -89,20 +101,17 @@ Task(
|
||||
run_in_background=false,
|
||||
description="Deep project analysis",
|
||||
prompt=`
|
||||
Analyze project for workflow initialization and generate .workflow/project.json.
|
||||
Analyze project for workflow initialization and generate .workflow/project-tech.json.
|
||||
|
||||
## MANDATORY FIRST STEPS
|
||||
1. Execute: cat ~/.claude/workflows/cli-templates/schemas/project-json-schema.json (get schema reference)
|
||||
1. Execute: cat ~/.claude/workflows/cli-templates/schemas/project-tech-schema.json (get schema reference)
|
||||
2. Execute: ccw tool exec get_modules_by_depth '{}' (get project structure)
|
||||
|
||||
## Task
|
||||
Generate complete project.json with:
|
||||
- project_name: ${projectName}
|
||||
- initialized_at: current ISO timestamp
|
||||
- overview: {description, technology_stack, architecture, key_components}
|
||||
- features: ${regenerate ? 'preserve from backup' : '[] (empty)'}
|
||||
- development_index: ${regenerate ? 'preserve from backup' : '{feature: [], enhancement: [], bugfix: [], refactor: [], docs: []}'}
|
||||
- statistics: ${regenerate ? 'preserve from backup' : '{total_features: 0, total_sessions: 0, last_updated}'}
|
||||
Generate complete project-tech.json with:
|
||||
- project_metadata: {name: ${projectName}, root_path: ${projectRoot}, initialized_at, updated_at}
|
||||
- technology_analysis: {description, languages, frameworks, build_tools, test_frameworks, architecture, key_components, dependencies}
|
||||
- development_status: ${regenerate ? 'preserve from backup' : '{completed_features: [], development_index: {feature: [], enhancement: [], bugfix: [], refactor: [], docs: []}, statistics: {total_features: 0, total_sessions: 0, last_updated}}'}
|
||||
- _metadata: {initialized_by: "cli-explore-agent", analysis_timestamp, analysis_mode}
|
||||
|
||||
## Analysis Requirements
|
||||
@@ -123,8 +132,8 @@ Generate complete project.json with:
|
||||
1. Structural scan: get_modules_by_depth.sh, find, wc -l
|
||||
2. Semantic analysis: Gemini for patterns/architecture
|
||||
3. Synthesis: Merge findings
|
||||
4. ${regenerate ? 'Merge with preserved features/development_index/statistics from .workflow/project.json.backup' : ''}
|
||||
5. Write JSON: Write('.workflow/project.json', jsonContent)
|
||||
4. ${regenerate ? 'Merge with preserved development_status from .workflow/project-tech.json.backup' : ''}
|
||||
5. Write JSON: Write('.workflow/project-tech.json', jsonContent)
|
||||
6. Report: Return brief completion summary
|
||||
|
||||
Project root: ${projectRoot}
|
||||
@@ -132,29 +141,66 @@ Project root: ${projectRoot}
|
||||
)
|
||||
```
|
||||
|
||||
### Step 3.5: Create Guidelines Scaffold (if not exists)
|
||||
|
||||
```javascript
|
||||
// Only create if not exists (never overwrite user guidelines)
|
||||
if (!file_exists('.workflow/project-guidelines.json')) {
|
||||
const guidelinesScaffold = {
|
||||
conventions: {
|
||||
coding_style: [],
|
||||
naming_patterns: [],
|
||||
file_structure: [],
|
||||
documentation: []
|
||||
},
|
||||
constraints: {
|
||||
architecture: [],
|
||||
tech_stack: [],
|
||||
performance: [],
|
||||
security: []
|
||||
},
|
||||
quality_rules: [],
|
||||
learnings: [],
|
||||
_metadata: {
|
||||
created_at: new Date().toISOString(),
|
||||
version: "1.0.0"
|
||||
}
|
||||
};
|
||||
|
||||
Write('.workflow/project-guidelines.json', JSON.stringify(guidelinesScaffold, null, 2));
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Display Summary
|
||||
|
||||
```javascript
|
||||
const projectJson = JSON.parse(Read('.workflow/project.json'));
|
||||
const projectTech = JSON.parse(Read('.workflow/project-tech.json'));
|
||||
const guidelinesExists = file_exists('.workflow/project-guidelines.json');
|
||||
|
||||
console.log(`
|
||||
✓ Project initialized successfully
|
||||
|
||||
## Project Overview
|
||||
Name: ${projectJson.project_name}
|
||||
Description: ${projectJson.overview.description}
|
||||
Name: ${projectTech.project_metadata.name}
|
||||
Description: ${projectTech.technology_analysis.description}
|
||||
|
||||
### Technology Stack
|
||||
Languages: ${projectJson.overview.technology_stack.languages.map(l => l.name).join(', ')}
|
||||
Frameworks: ${projectJson.overview.technology_stack.frameworks.join(', ')}
|
||||
Languages: ${projectTech.technology_analysis.languages.map(l => l.name).join(', ')}
|
||||
Frameworks: ${projectTech.technology_analysis.frameworks.join(', ')}
|
||||
|
||||
### Architecture
|
||||
Style: ${projectJson.overview.architecture.style}
|
||||
Components: ${projectJson.overview.key_components.length} core modules
|
||||
Style: ${projectTech.technology_analysis.architecture.style}
|
||||
Components: ${projectTech.technology_analysis.key_components.length} core modules
|
||||
|
||||
---
|
||||
Project state: .workflow/project.json
|
||||
${regenerate ? 'Backup: .workflow/project.json.backup' : ''}
|
||||
Files created:
|
||||
- Tech analysis: .workflow/project-tech.json
|
||||
- Guidelines: .workflow/project-guidelines.json ${guidelinesExists ? '(scaffold)' : ''}
|
||||
${regenerate ? '- Backup: .workflow/project-tech.json.backup' : ''}
|
||||
|
||||
Next steps:
|
||||
- Use /workflow:session:solidify to add project guidelines
|
||||
- Use /workflow:plan to start planning
|
||||
`);
|
||||
```
|
||||
|
||||
|
||||
@@ -181,6 +181,8 @@ Execute **${angle}** diagnosis for bug root cause analysis. Analyze codebase fro
|
||||
1. Run: ccw tool exec get_modules_by_depth '{}' (project structure)
|
||||
2. Run: rg -l "{error_keyword_from_bug}" --type ts (locate relevant files)
|
||||
3. Execute: cat ~/.claude/workflows/cli-templates/schemas/diagnosis-json-schema.json (get output schema reference)
|
||||
4. Read: .workflow/project-tech.json (technology stack and architecture context)
|
||||
5. Read: .workflow/project-guidelines.json (user-defined constraints and conventions)
|
||||
|
||||
## Diagnosis Strategy (${angle} focus)
|
||||
|
||||
@@ -409,6 +411,12 @@ Generate fix plan and write fix-plan.json.
|
||||
## Output Schema Reference
|
||||
Execute: cat ~/.claude/workflows/cli-templates/schemas/fix-plan-json-schema.json (get schema reference before generating plan)
|
||||
|
||||
## Project Context (MANDATORY - Read Both Files)
|
||||
1. Read: .workflow/project-tech.json (technology stack, architecture, key components)
|
||||
2. Read: .workflow/project-guidelines.json (user-defined constraints and conventions)
|
||||
|
||||
**CRITICAL**: All fix tasks MUST comply with constraints in project-guidelines.json
|
||||
|
||||
## Bug Description
|
||||
${bug_description}
|
||||
|
||||
|
||||
@@ -184,6 +184,8 @@ Execute **${angle}** exploration for task planning context. Analyze codebase fro
|
||||
1. Run: ccw tool exec get_modules_by_depth '{}' (project structure)
|
||||
2. Run: rg -l "{keyword_from_task}" --type ts (locate relevant files)
|
||||
3. Execute: cat ~/.claude/workflows/cli-templates/schemas/explore-json-schema.json (get output schema reference)
|
||||
4. Read: .workflow/project-tech.json (technology stack and architecture context)
|
||||
5. Read: .workflow/project-guidelines.json (user-defined constraints and conventions)
|
||||
|
||||
## Exploration Strategy (${angle} focus)
|
||||
|
||||
@@ -416,6 +418,12 @@ Generate implementation plan and write plan.json.
|
||||
## Output Schema Reference
|
||||
Execute: cat ~/.claude/workflows/cli-templates/schemas/plan-json-schema.json (get schema reference before generating plan)
|
||||
|
||||
## Project Context (MANDATORY - Read Both Files)
|
||||
1. Read: .workflow/project-tech.json (technology stack, architecture, key components)
|
||||
2. Read: .workflow/project-guidelines.json (user-defined constraints and conventions)
|
||||
|
||||
**CRITICAL**: All generated tasks MUST comply with constraints in project-guidelines.json
|
||||
|
||||
## Task Description
|
||||
${task_description}
|
||||
|
||||
|
||||
@@ -409,6 +409,8 @@ Task(
|
||||
2. Get target files: Read resolved_files from review-state.json
|
||||
3. Validate file access: bash(ls -la ${targetFiles.join(' ')})
|
||||
4. Execute: cat ~/.claude/workflows/cli-templates/schemas/review-dimension-results-schema.json (get output schema reference)
|
||||
5. Read: .workflow/project-tech.json (technology stack and architecture context)
|
||||
6. Read: .workflow/project-guidelines.json (user-defined constraints and conventions to validate against)
|
||||
|
||||
## Review Context
|
||||
- Review Type: module (independent)
|
||||
@@ -511,6 +513,8 @@ Task(
|
||||
3. Identify related code: bash(grep -r "import.*${basename(file)}" ${projectDir}/src --include="*.ts")
|
||||
4. Read test files: bash(find ${projectDir}/tests -name "*${basename(file, '.ts')}*" -type f)
|
||||
5. Execute: cat ~/.claude/workflows/cli-templates/schemas/review-deep-dive-results-schema.json (get output schema reference)
|
||||
6. Read: .workflow/project-tech.json (technology stack and architecture context)
|
||||
7. Read: .workflow/project-guidelines.json (user-defined constraints for remediation compliance)
|
||||
|
||||
## CLI Configuration
|
||||
- Tool Priority: gemini → qwen → codex
|
||||
|
||||
@@ -420,6 +420,8 @@ Task(
|
||||
3. Get changed files: bash(cd ${workflowDir} && git log --since="${sessionCreatedAt}" --name-only --pretty=format: | sort -u)
|
||||
4. Read review state: ${reviewStateJsonPath}
|
||||
5. Execute: cat ~/.claude/workflows/cli-templates/schemas/review-dimension-results-schema.json (get output schema reference)
|
||||
6. Read: .workflow/project-tech.json (technology stack and architecture context)
|
||||
7. Read: .workflow/project-guidelines.json (user-defined constraints and conventions to validate against)
|
||||
|
||||
## Session Context
|
||||
- Session ID: ${sessionId}
|
||||
@@ -522,6 +524,8 @@ Task(
|
||||
3. Identify related code: bash(grep -r "import.*${basename(file)}" ${workflowDir}/src --include="*.ts")
|
||||
4. Read test files: bash(find ${workflowDir}/tests -name "*${basename(file, '.ts')}*" -type f)
|
||||
5. Execute: cat ~/.claude/workflows/cli-templates/schemas/review-deep-dive-results-schema.json (get output schema reference)
|
||||
6. Read: .workflow/project-tech.json (technology stack and architecture context)
|
||||
7. Read: .workflow/project-guidelines.json (user-defined constraints for remediation compliance)
|
||||
|
||||
## CLI Configuration
|
||||
- Tool Priority: gemini → qwen → codex
|
||||
|
||||
@@ -139,7 +139,7 @@ After bash validation, the model takes control to:
|
||||
ccw cli -p "
|
||||
PURPOSE: Security audit of completed implementation
|
||||
TASK: Review code for security vulnerabilities, insecure patterns, auth/authz issues
|
||||
CONTEXT: @.summaries/IMPL-*.md,../.. @../../CLAUDE.md
|
||||
CONTEXT: @.summaries/IMPL-*.md,../.. @../../project-tech.json @../../project-guidelines.json
|
||||
EXPECTED: Security findings report with severity levels
|
||||
RULES: Focus on OWASP Top 10, authentication, authorization, data validation, injection risks
|
||||
" --tool gemini --mode write --cd .workflow/active/${sessionId}
|
||||
@@ -151,7 +151,7 @@ After bash validation, the model takes control to:
|
||||
ccw cli -p "
|
||||
PURPOSE: Architecture compliance review
|
||||
TASK: Evaluate adherence to architectural patterns, identify technical debt, review design decisions
|
||||
CONTEXT: @.summaries/IMPL-*.md,../.. @../../CLAUDE.md
|
||||
CONTEXT: @.summaries/IMPL-*.md,../.. @../../project-tech.json @../../project-guidelines.json
|
||||
EXPECTED: Architecture assessment with recommendations
|
||||
RULES: Check for patterns, separation of concerns, modularity, scalability
|
||||
" --tool qwen --mode write --cd .workflow/active/${sessionId}
|
||||
@@ -163,7 +163,7 @@ After bash validation, the model takes control to:
|
||||
ccw cli -p "
|
||||
PURPOSE: Code quality and best practices review
|
||||
TASK: Assess code readability, maintainability, adherence to best practices
|
||||
CONTEXT: @.summaries/IMPL-*.md,../.. @../../CLAUDE.md
|
||||
CONTEXT: @.summaries/IMPL-*.md,../.. @../../project-tech.json @../../project-guidelines.json
|
||||
EXPECTED: Quality assessment with improvement suggestions
|
||||
RULES: Check for code smells, duplication, complexity, naming conventions
|
||||
" --tool gemini --mode write --cd .workflow/active/${sessionId}
|
||||
@@ -185,7 +185,7 @@ After bash validation, the model takes control to:
|
||||
ccw cli -p "
|
||||
PURPOSE: Verify all requirements and acceptance criteria are met
|
||||
TASK: Cross-check implementation summaries against original requirements
|
||||
CONTEXT: @.task/IMPL-*.json,.summaries/IMPL-*.md,../.. @../../CLAUDE.md
|
||||
CONTEXT: @.task/IMPL-*.json,.summaries/IMPL-*.md,../.. @../../project-tech.json @../../project-guidelines.json
|
||||
EXPECTED:
|
||||
- Requirements coverage matrix
|
||||
- Acceptance criteria verification
|
||||
|
||||
299
.claude/commands/workflow/session/solidify.md
Normal file
299
.claude/commands/workflow/session/solidify.md
Normal file
@@ -0,0 +1,299 @@
|
||||
---
|
||||
name: solidify
|
||||
description: Crystallize session learnings and user-defined constraints into permanent project guidelines
|
||||
argument-hint: "[--type <convention|constraint|learning>] [--category <category>] \"rule or insight\""
|
||||
examples:
|
||||
- /workflow:session:solidify "Use functional components for all React code" --type convention
|
||||
- /workflow:session:solidify "No direct DB access from controllers" --type constraint --category architecture
|
||||
- /workflow:session:solidify "Cache invalidation requires event sourcing" --type learning --category architecture
|
||||
- /workflow:session:solidify --interactive
|
||||
---
|
||||
|
||||
# Session Solidify Command (/workflow:session:solidify)
|
||||
|
||||
## Overview
|
||||
|
||||
Crystallizes ephemeral session context (insights, decisions, constraints) into permanent project guidelines stored in `.workflow/project-guidelines.json`. This ensures valuable learnings persist across sessions and inform future planning.
|
||||
|
||||
## Use Cases
|
||||
|
||||
1. **During Session**: Capture important decisions as they're made
|
||||
2. **After Session**: Reflect on lessons learned before archiving
|
||||
3. **Proactive**: Add team conventions or architectural rules
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `rule` | string | ✅ (unless --interactive) | The rule, convention, or insight to solidify |
|
||||
| `--type` | enum | ❌ | Type: `convention`, `constraint`, `learning` (default: auto-detect) |
|
||||
| `--category` | string | ❌ | Category for organization (see categories below) |
|
||||
| `--interactive` | flag | ❌ | Launch guided wizard for adding rules |
|
||||
|
||||
### Type Categories
|
||||
|
||||
**convention** → Coding style preferences (goes to `conventions` section)
|
||||
- Subcategories: `coding_style`, `naming_patterns`, `file_structure`, `documentation`
|
||||
|
||||
**constraint** → Hard rules that must not be violated (goes to `constraints` section)
|
||||
- Subcategories: `architecture`, `tech_stack`, `performance`, `security`
|
||||
|
||||
**learning** → Session-specific insights (goes to `learnings` array)
|
||||
- Subcategories: `architecture`, `performance`, `security`, `testing`, `process`, `other`
|
||||
|
||||
## Execution Process
|
||||
|
||||
```
|
||||
Input Parsing:
|
||||
├─ Parse: rule text (required unless --interactive)
|
||||
├─ Parse: --type (convention|constraint|learning)
|
||||
├─ Parse: --category (subcategory)
|
||||
└─ Parse: --interactive (flag)
|
||||
|
||||
Step 1: Ensure Guidelines File Exists
|
||||
└─ If not exists → Create with empty structure
|
||||
|
||||
Step 2: Auto-detect Type (if not specified)
|
||||
└─ Analyze rule text for keywords
|
||||
|
||||
Step 3: Validate and Format Entry
|
||||
└─ Build entry object based on type
|
||||
|
||||
Step 4: Update Guidelines File
|
||||
└─ Add entry to appropriate section
|
||||
|
||||
Step 5: Display Confirmation
|
||||
└─ Show what was added and where
|
||||
```
|
||||
|
||||
## Implementation
|
||||
|
||||
### Step 1: Ensure Guidelines File Exists
|
||||
|
||||
```bash
|
||||
bash(test -f .workflow/project-guidelines.json && echo "EXISTS" || echo "NOT_FOUND")
|
||||
```
|
||||
|
||||
**If NOT_FOUND**, create scaffold:
|
||||
|
||||
```javascript
|
||||
const scaffold = {
|
||||
conventions: {
|
||||
coding_style: [],
|
||||
naming_patterns: [],
|
||||
file_structure: [],
|
||||
documentation: []
|
||||
},
|
||||
constraints: {
|
||||
architecture: [],
|
||||
tech_stack: [],
|
||||
performance: [],
|
||||
security: []
|
||||
},
|
||||
quality_rules: [],
|
||||
learnings: [],
|
||||
_metadata: {
|
||||
created_at: new Date().toISOString(),
|
||||
version: "1.0.0"
|
||||
}
|
||||
};
|
||||
|
||||
Write('.workflow/project-guidelines.json', JSON.stringify(scaffold, null, 2));
|
||||
```
|
||||
|
||||
### Step 2: Auto-detect Type (if not specified)
|
||||
|
||||
```javascript
|
||||
function detectType(ruleText) {
|
||||
const text = ruleText.toLowerCase();
|
||||
|
||||
// Constraint indicators
|
||||
if (/\b(no|never|must not|forbidden|prohibited|always must)\b/.test(text)) {
|
||||
return 'constraint';
|
||||
}
|
||||
|
||||
// Learning indicators
|
||||
if (/\b(learned|discovered|realized|found that|turns out)\b/.test(text)) {
|
||||
return 'learning';
|
||||
}
|
||||
|
||||
// Default to convention
|
||||
return 'convention';
|
||||
}
|
||||
|
||||
function detectCategory(ruleText, type) {
|
||||
const text = ruleText.toLowerCase();
|
||||
|
||||
if (type === 'constraint' || type === 'learning') {
|
||||
if (/\b(architecture|layer|module|dependency|circular)\b/.test(text)) return 'architecture';
|
||||
if (/\b(security|auth|permission|sanitize|xss|sql)\b/.test(text)) return 'security';
|
||||
if (/\b(performance|cache|lazy|async|sync|slow)\b/.test(text)) return 'performance';
|
||||
if (/\b(test|coverage|mock|stub)\b/.test(text)) return 'testing';
|
||||
}
|
||||
|
||||
if (type === 'convention') {
|
||||
if (/\b(name|naming|prefix|suffix|camel|pascal)\b/.test(text)) return 'naming_patterns';
|
||||
if (/\b(file|folder|directory|structure|organize)\b/.test(text)) return 'file_structure';
|
||||
if (/\b(doc|comment|jsdoc|readme)\b/.test(text)) return 'documentation';
|
||||
return 'coding_style';
|
||||
}
|
||||
|
||||
return type === 'constraint' ? 'tech_stack' : 'other';
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Build Entry
|
||||
|
||||
```javascript
|
||||
function buildEntry(rule, type, category, sessionId) {
|
||||
if (type === 'learning') {
|
||||
return {
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
session_id: sessionId || null,
|
||||
insight: rule,
|
||||
category: category,
|
||||
context: null
|
||||
};
|
||||
}
|
||||
|
||||
// For conventions and constraints, just return the rule string
|
||||
return rule;
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Update Guidelines File
|
||||
|
||||
```javascript
|
||||
const guidelines = JSON.parse(Read('.workflow/project-guidelines.json'));
|
||||
|
||||
if (type === 'convention') {
|
||||
if (!guidelines.conventions[category]) {
|
||||
guidelines.conventions[category] = [];
|
||||
}
|
||||
if (!guidelines.conventions[category].includes(rule)) {
|
||||
guidelines.conventions[category].push(rule);
|
||||
}
|
||||
} else if (type === 'constraint') {
|
||||
if (!guidelines.constraints[category]) {
|
||||
guidelines.constraints[category] = [];
|
||||
}
|
||||
if (!guidelines.constraints[category].includes(rule)) {
|
||||
guidelines.constraints[category].push(rule);
|
||||
}
|
||||
} else if (type === 'learning') {
|
||||
guidelines.learnings.push(buildEntry(rule, type, category, sessionId));
|
||||
}
|
||||
|
||||
guidelines._metadata.updated_at = new Date().toISOString();
|
||||
guidelines._metadata.last_solidified_by = sessionId;
|
||||
|
||||
Write('.workflow/project-guidelines.json', JSON.stringify(guidelines, null, 2));
|
||||
```
|
||||
|
||||
### Step 5: Display Confirmation
|
||||
|
||||
```
|
||||
✓ Guideline solidified
|
||||
|
||||
Type: ${type}
|
||||
Category: ${category}
|
||||
Rule: "${rule}"
|
||||
|
||||
Location: .workflow/project-guidelines.json → ${type}s.${category}
|
||||
|
||||
Total ${type}s in ${category}: ${count}
|
||||
```
|
||||
|
||||
## Interactive Mode
|
||||
|
||||
When `--interactive` flag is provided:
|
||||
|
||||
```javascript
|
||||
AskUserQuestion({
|
||||
questions: [
|
||||
{
|
||||
question: "What type of guideline are you adding?",
|
||||
header: "Type",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "Convention", description: "Coding style preference (e.g., use functional components)" },
|
||||
{ label: "Constraint", description: "Hard rule that must not be violated (e.g., no direct DB access)" },
|
||||
{ label: "Learning", description: "Insight from this session (e.g., cache invalidation needs events)" }
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// Follow-up based on type selection...
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Add a Convention
|
||||
```bash
|
||||
/workflow:session:solidify "Use async/await instead of callbacks" --type convention --category coding_style
|
||||
```
|
||||
|
||||
Result in `project-guidelines.json`:
|
||||
```json
|
||||
{
|
||||
"conventions": {
|
||||
"coding_style": ["Use async/await instead of callbacks"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Add an Architectural Constraint
|
||||
```bash
|
||||
/workflow:session:solidify "No direct DB access from controllers" --type constraint --category architecture
|
||||
```
|
||||
|
||||
Result:
|
||||
```json
|
||||
{
|
||||
"constraints": {
|
||||
"architecture": ["No direct DB access from controllers"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Capture a Session Learning
|
||||
```bash
|
||||
/workflow:session:solidify "Cache invalidation requires event sourcing for consistency" --type learning
|
||||
```
|
||||
|
||||
Result:
|
||||
```json
|
||||
{
|
||||
"learnings": [
|
||||
{
|
||||
"date": "2024-12-28",
|
||||
"session_id": "WFS-auth-feature",
|
||||
"insight": "Cache invalidation requires event sourcing for consistency",
|
||||
"category": "architecture"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Integration with Planning
|
||||
|
||||
The `project-guidelines.json` is consumed by:
|
||||
|
||||
1. **`/workflow:tools:context-gather`**: Loads guidelines into context-package.json
|
||||
2. **`/workflow:plan`**: Passes guidelines to task generation agent
|
||||
3. **`task-generate-agent`**: Includes guidelines as "CRITICAL CONSTRAINTS" in system prompt
|
||||
|
||||
This ensures all future planning respects solidified rules without users needing to re-state them.
|
||||
|
||||
## Error Handling
|
||||
|
||||
- **Duplicate Rule**: Warn and skip if exact rule already exists
|
||||
- **Invalid Category**: Suggest valid categories for the type
|
||||
- **File Corruption**: Backup existing file before modification
|
||||
|
||||
## Related Commands
|
||||
|
||||
- `/workflow:session:start` - Start a session (may prompt for solidify at end)
|
||||
- `/workflow:session:complete` - Complete session (prompts for learnings to solidify)
|
||||
- `/workflow:init` - Creates project-guidelines.json scaffold if missing
|
||||
@@ -38,26 +38,29 @@ ERROR: Invalid session type. Valid types: workflow, review, tdd, test, docs
|
||||
|
||||
## Step 0: Initialize Project State (First-time Only)
|
||||
|
||||
**Executed before all modes** - Ensures project-level state file exists by calling `/workflow:init`.
|
||||
**Executed before all modes** - Ensures project-level state files exist by calling `/workflow:init`.
|
||||
|
||||
### Check and Initialize
|
||||
```bash
|
||||
# Check if project state exists
|
||||
bash(test -f .workflow/project.json && echo "EXISTS" || echo "NOT_FOUND")
|
||||
# Check if project state exists (both files required)
|
||||
bash(test -f .workflow/project-tech.json && echo "TECH_EXISTS" || echo "TECH_NOT_FOUND")
|
||||
bash(test -f .workflow/project-guidelines.json && echo "GUIDELINES_EXISTS" || echo "GUIDELINES_NOT_FOUND")
|
||||
```
|
||||
|
||||
**If NOT_FOUND**, delegate to `/workflow:init`:
|
||||
**If either NOT_FOUND**, delegate to `/workflow:init`:
|
||||
```javascript
|
||||
// Call workflow:init for intelligent project analysis
|
||||
SlashCommand({command: "/workflow:init"});
|
||||
|
||||
// Wait for init completion
|
||||
// project.json will be created with comprehensive project overview
|
||||
// project-tech.json and project-guidelines.json will be created
|
||||
```
|
||||
|
||||
**Output**:
|
||||
- If EXISTS: `PROJECT_STATE: initialized`
|
||||
- If NOT_FOUND: Calls `/workflow:init` → creates `.workflow/project.json` with full project analysis
|
||||
- If BOTH_EXIST: `PROJECT_STATE: initialized`
|
||||
- If NOT_FOUND: Calls `/workflow:init` → creates:
|
||||
- `.workflow/project-tech.json` with full technical analysis
|
||||
- `.workflow/project-guidelines.json` with empty scaffold
|
||||
|
||||
**Note**: `/workflow:init` uses cli-explore-agent to build comprehensive project understanding (technology stack, architecture, key components). This step runs once per project. Subsequent executions skip initialization.
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ allowed-tools: Task(*), Read(*), Glob(*)
|
||||
|
||||
Orchestrator command that invokes `context-search-agent` to gather comprehensive project context for implementation planning. Generates standardized `context-package.json` with codebase analysis, dependencies, and conflict detection.
|
||||
|
||||
**Agent**: `context-search-agent` (`.claude/agents/context-search-agent.md`)
|
||||
|
||||
## Core Philosophy
|
||||
|
||||
@@ -237,7 +236,10 @@ Task(
|
||||
Execute complete context-search-agent workflow for implementation planning:
|
||||
|
||||
### Phase 1: Initialization & Pre-Analysis
|
||||
1. **Project State Loading**: Read and parse `.workflow/project.json`. Use its `overview` section as the foundational `project_context`. This is your primary source for architecture, tech stack, and key components. If file doesn't exist, proceed with fresh analysis.
|
||||
1. **Project State Loading**:
|
||||
- Read and parse `.workflow/project-tech.json`. Use its `technology_analysis` section as the foundational `project_context`. This is your primary source for architecture, tech stack, and key components.
|
||||
- Read and parse `.workflow/project-guidelines.json`. Load `conventions`, `constraints`, and `learnings` into a `project_guidelines` section.
|
||||
- If files don't exist, proceed with fresh analysis.
|
||||
2. **Detection**: Check for existing context-package (early exit if valid)
|
||||
3. **Foundation**: Initialize CodexLens, get project structure, load docs
|
||||
4. **Analysis**: Extract keywords, determine scope, classify complexity based on task description and project state
|
||||
@@ -252,17 +254,19 @@ Execute all discovery tracks:
|
||||
|
||||
### Phase 3: Synthesis, Assessment & Packaging
|
||||
1. Apply relevance scoring and build dependency graph
|
||||
2. **Synthesize 4-source data**: Merge findings from all sources (archive > docs > code > web). **Prioritize the context from `project.json`** for architecture and tech stack unless code analysis reveals it's outdated.
|
||||
3. **Populate `project_context`**: Directly use the `overview` from `project.json` to fill the `project_context` section of the output `context-package.json`. Include description, technology_stack, architecture, and key_components.
|
||||
4. Integrate brainstorm artifacts (if .brainstorming/ exists, read content)
|
||||
5. Perform conflict detection with risk assessment
|
||||
6. **Inject historical conflicts** from archive analysis into conflict_detection
|
||||
7. Generate and validate context-package.json
|
||||
2. **Synthesize 4-source data**: Merge findings from all sources (archive > docs > code > web). **Prioritize the context from `project-tech.json`** for architecture and tech stack unless code analysis reveals it's outdated.
|
||||
3. **Populate `project_context`**: Directly use the `technology_analysis` from `project-tech.json` to fill the `project_context` section. Include description, technology_stack, architecture, and key_components.
|
||||
4. **Populate `project_guidelines`**: Load conventions, constraints, and learnings from `project-guidelines.json` into a dedicated section.
|
||||
5. Integrate brainstorm artifacts (if .brainstorming/ exists, read content)
|
||||
6. Perform conflict detection with risk assessment
|
||||
7. **Inject historical conflicts** from archive analysis into conflict_detection
|
||||
8. Generate and validate context-package.json
|
||||
|
||||
## Output Requirements
|
||||
Complete context-package.json with:
|
||||
- **metadata**: task_description, keywords, complexity, tech_stack, session_id
|
||||
- **project_context**: description, technology_stack, architecture, key_components (sourced from `project.json` overview)
|
||||
- **project_context**: description, technology_stack, architecture, key_components (sourced from `project-tech.json`)
|
||||
- **project_guidelines**: {conventions, constraints, quality_rules, learnings} (sourced from `project-guidelines.json`)
|
||||
- **assets**: {documentation[], source_code[], config[], tests[]} with relevance scores
|
||||
- **dependencies**: {internal[], external[]} with dependency graph
|
||||
- **brainstorm_artifacts**: {guidance_specification, role_analyses[], synthesis_output} with content
|
||||
@@ -315,7 +319,8 @@ Refer to `context-search-agent.md` Phase 3.7 for complete `context-package.json`
|
||||
|
||||
**Key Sections**:
|
||||
- **metadata**: Session info, keywords, complexity, tech stack
|
||||
- **project_context**: Architecture patterns, conventions, tech stack (populated from `project.json` overview)
|
||||
- **project_context**: Architecture patterns, conventions, tech stack (populated from `project-tech.json`)
|
||||
- **project_guidelines**: Conventions, constraints, quality rules, learnings (populated from `project-guidelines.json`)
|
||||
- **assets**: Categorized files with relevance scores (documentation, source_code, config, tests)
|
||||
- **dependencies**: Internal and external dependency graphs
|
||||
- **brainstorm_artifacts**: Brainstorm documents with full content (if exists)
|
||||
@@ -430,7 +435,7 @@ if (historicalConflicts.length > 0 && currentRisk === "low") {
|
||||
## Notes
|
||||
|
||||
- **Detection-first**: Always check for existing package before invoking agent
|
||||
- **Project.json integration**: Agent reads `.workflow/project.json` as primary source for project context, avoiding redundant analysis
|
||||
- **Agent autonomy**: Agent handles all discovery logic per `.claude/agents/context-search-agent.md`
|
||||
- **Dual project file integration**: Agent reads both `.workflow/project-tech.json` (tech analysis) and `.workflow/project-guidelines.json` (user constraints) as primary sources
|
||||
- **Guidelines injection**: Project guidelines are included in context-package to ensure task generation respects user-defined constraints
|
||||
- **No redundancy**: This command is a thin orchestrator, all logic in agent
|
||||
- **Plan-specific**: Use this for implementation planning; brainstorm mode uses direct agent call
|
||||
|
||||
@@ -239,15 +239,6 @@ If conflict_risk was medium/high, modifications have been applied to:
|
||||
|
||||
**Agent Configuration Reference**: All TDD task generation rules, quantification requirements, Red-Green-Refactor cycle structure, quality standards, and execution details are defined in action-planning-agent.
|
||||
|
||||
Refer to: @.claude/agents/action-planning-agent.md for:
|
||||
- TDD Task Decomposition Standards
|
||||
- Red-Green-Refactor Cycle Requirements
|
||||
- Quantification Requirements (MANDATORY)
|
||||
- 5-Field Task JSON Schema
|
||||
- IMPL_PLAN.md Structure (TDD variant)
|
||||
- TODO_LIST.md Format
|
||||
- TDD Execution Flow & Quality Validation
|
||||
|
||||
### TDD-Specific Requirements Summary
|
||||
|
||||
#### Task Structure Philosophy
|
||||
|
||||
@@ -14,7 +14,7 @@ allowed-tools: Task(*), Read(*), Glob(*)
|
||||
|
||||
Orchestrator command that invokes `test-context-search-agent` to gather comprehensive test coverage context for test generation workflows. Generates standardized `test-context-package.json` with coverage analysis, framework detection, and source implementation context.
|
||||
|
||||
**Agent**: `test-context-search-agent` (`.claude/agents/test-context-search-agent.md`)
|
||||
|
||||
|
||||
## Core Philosophy
|
||||
|
||||
@@ -89,7 +89,6 @@ Task(
|
||||
run_in_background=false,
|
||||
description="Gather test coverage context",
|
||||
prompt=`
|
||||
You are executing as test-context-search-agent (.claude/agents/test-context-search-agent.md).
|
||||
|
||||
## Execution Mode
|
||||
**PLAN MODE** (Comprehensive) - Full Phase 1-3 execution
|
||||
@@ -229,7 +228,7 @@ Refer to `test-context-search-agent.md` Phase 3.2 for complete `test-context-pac
|
||||
## Notes
|
||||
|
||||
- **Detection-first**: Always check for existing test-context-package before invoking agent
|
||||
- **Agent autonomy**: Agent handles all coverage analysis logic per `.claude/agents/test-context-search-agent.md`
|
||||
|
||||
- **No redundancy**: This command is a thin orchestrator, all logic in agent
|
||||
- **Framework agnostic**: Supports Jest, Mocha, pytest, RSpec, Go testing, etc.
|
||||
- **Coverage focus**: Primary goal is identifying implementation files without tests
|
||||
|
||||
@@ -107,8 +107,6 @@ CRITICAL:
|
||||
- Follow the progressive loading strategy defined in your agent specification (load context incrementally from memory-first approach)
|
||||
|
||||
## AGENT CONFIGURATION REFERENCE
|
||||
All test task generation rules, schemas, and quality standards are defined in your agent specification:
|
||||
@.claude/agents/action-planning-agent.md
|
||||
|
||||
Refer to your specification for:
|
||||
- Test Task JSON Schema (6-field structure with test-specific metadata)
|
||||
|
||||
@@ -806,8 +806,6 @@ Use `analysis_results.complexity` or task count to determine structure:
|
||||
**Examples**:
|
||||
- GOOD: `"Implement 5 commands: [cmd1, cmd2, cmd3, cmd4, cmd5]"`
|
||||
- BAD: `"Implement new commands"`
|
||||
- GOOD: `"5 files created: verify by ls .claude/commands/*.md | wc -l = 5"`
|
||||
- BAD: `"All commands implemented successfully"`
|
||||
|
||||
### 3.2 Planning & Organization Standards
|
||||
|
||||
|
||||
@@ -400,7 +400,7 @@ Task(subagent_type="{meta.agent}",
|
||||
1. Read complete task JSON: {session.task_json_path}
|
||||
2. Load context package: {session.context_package_path}
|
||||
|
||||
Follow complete execution guidelines in @.claude/agents/{meta.agent}.md
|
||||
|
||||
|
||||
**Session Paths**:
|
||||
- Workflow Dir: {session.workflow_dir}
|
||||
|
||||
@@ -15,7 +15,6 @@ allowed-tools: Task(*), Read(*), Glob(*)
|
||||
|
||||
Orchestrator command that invokes `context-search-agent` to gather comprehensive project context for implementation planning. Generates standardized `context-package.json` with codebase analysis, dependencies, and conflict detection.
|
||||
|
||||
**Agent**: `context-search-agent` (`.claude/agents/context-search-agent.md`)
|
||||
|
||||
## Core Philosophy
|
||||
|
||||
@@ -429,6 +428,6 @@ if (historicalConflicts.length > 0 && currentRisk === "low") {
|
||||
|
||||
- **Detection-first**: Always check for existing package before invoking agent
|
||||
- **Project.json integration**: Agent reads `.workflow/project.json` as primary source for project context, avoiding redundant analysis
|
||||
- **Agent autonomy**: Agent handles all discovery logic per `.claude/agents/context-search-agent.md`
|
||||
|
||||
- **No redundancy**: This command is a thin orchestrator, all logic in agent
|
||||
- **Plan-specific**: Use this for implementation planning; brainstorm mode uses direct agent call
|
||||
|
||||
@@ -238,14 +238,7 @@ If conflict_risk was medium/high, modifications have been applied to:
|
||||
|
||||
**Agent Configuration Reference**: All TDD task generation rules, quantification requirements, Red-Green-Refactor cycle structure, quality standards, and execution details are defined in action-planning-agent.
|
||||
|
||||
Refer to: @.claude/agents/action-planning-agent.md for:
|
||||
- TDD Task Decomposition Standards
|
||||
- Red-Green-Refactor Cycle Requirements
|
||||
- Quantification Requirements (MANDATORY)
|
||||
- 5-Field Task JSON Schema
|
||||
- IMPL_PLAN.md Structure (TDD variant)
|
||||
- TODO_LIST.md Format
|
||||
- TDD Execution Flow & Quality Validation
|
||||
|
||||
|
||||
### TDD-Specific Requirements Summary
|
||||
|
||||
|
||||
@@ -14,8 +14,6 @@ allowed-tools: Task(*), Read(*), Glob(*)
|
||||
|
||||
Orchestrator command that invokes `test-context-search-agent` to gather comprehensive test coverage context for test generation workflows. Generates standardized `test-context-package.json` with coverage analysis, framework detection, and source implementation context.
|
||||
|
||||
**Agent**: `test-context-search-agent` (`.claude/agents/test-context-search-agent.md`)
|
||||
|
||||
## Core Philosophy
|
||||
|
||||
- **Agent Delegation**: Delegate all test coverage analysis to `test-context-search-agent` for autonomous execution
|
||||
@@ -88,7 +86,6 @@ Task(
|
||||
subagent_type="test-context-search-agent",
|
||||
description="Gather test coverage context",
|
||||
prompt=`
|
||||
You are executing as test-context-search-agent (.claude/agents/test-context-search-agent.md).
|
||||
|
||||
## Execution Mode
|
||||
**PLAN MODE** (Comprehensive) - Full Phase 1-3 execution
|
||||
@@ -228,7 +225,7 @@ Refer to `test-context-search-agent.md` Phase 3.2 for complete `test-context-pac
|
||||
## Notes
|
||||
|
||||
- **Detection-first**: Always check for existing test-context-package before invoking agent
|
||||
- **Agent autonomy**: Agent handles all coverage analysis logic per `.claude/agents/test-context-search-agent.md`
|
||||
|
||||
- **No redundancy**: This command is a thin orchestrator, all logic in agent
|
||||
- **Framework agnostic**: Supports Jest, Mocha, pytest, RSpec, Go testing, etc.
|
||||
- **Coverage focus**: Primary goal is identifying implementation files without tests
|
||||
|
||||
@@ -106,8 +106,6 @@ CRITICAL:
|
||||
- Follow the progressive loading strategy defined in your agent specification (load context incrementally from memory-first approach)
|
||||
|
||||
## AGENT CONFIGURATION REFERENCE
|
||||
All test task generation rules, schemas, and quality standards are defined in your agent specification:
|
||||
@.claude/agents/action-planning-agent.md
|
||||
|
||||
Refer to your specification for:
|
||||
- Test Task JSON Schema (6-field structure with test-specific metadata)
|
||||
|
||||
150
.claude/skills/copyright-docs/phases/01.5-project-exploration.md
Normal file
150
.claude/skills/copyright-docs/phases/01.5-project-exploration.md
Normal file
@@ -0,0 +1,150 @@
|
||||
# Phase 1.5: Project Exploration
|
||||
|
||||
基于元数据,启动并行探索 Agent 收集代码信息。
|
||||
|
||||
## Execution
|
||||
|
||||
### Step 1: Intelligent Angle Selection
|
||||
|
||||
```javascript
|
||||
// 根据软件类型选择探索角度
|
||||
const ANGLE_PRESETS = {
|
||||
'CLI': ['architecture', 'commands', 'algorithms', 'exceptions'],
|
||||
'API': ['architecture', 'endpoints', 'data-structures', 'interfaces'],
|
||||
'SDK': ['architecture', 'interfaces', 'data-structures', 'algorithms'],
|
||||
'DataProcessing': ['architecture', 'algorithms', 'data-structures', 'dataflow'],
|
||||
'Automation': ['architecture', 'algorithms', 'exceptions', 'dataflow']
|
||||
};
|
||||
|
||||
// 从 metadata.category 映射到预设
|
||||
function getCategoryKey(category) {
|
||||
if (category.includes('CLI') || category.includes('命令行')) return 'CLI';
|
||||
if (category.includes('API') || category.includes('后端')) return 'API';
|
||||
if (category.includes('SDK') || category.includes('库')) return 'SDK';
|
||||
if (category.includes('数据处理')) return 'DataProcessing';
|
||||
if (category.includes('自动化')) return 'Automation';
|
||||
return 'API'; // default
|
||||
}
|
||||
|
||||
const categoryKey = getCategoryKey(metadata.category);
|
||||
const selectedAngles = ANGLE_PRESETS[categoryKey];
|
||||
|
||||
console.log(`
|
||||
## Exploration Plan
|
||||
|
||||
Software: ${metadata.software_name}
|
||||
Category: ${metadata.category} → ${categoryKey}
|
||||
Selected Angles: ${selectedAngles.join(', ')}
|
||||
|
||||
Launching ${selectedAngles.length} parallel explorations...
|
||||
`);
|
||||
```
|
||||
|
||||
### Step 2: Launch Parallel Agents (Direct Output)
|
||||
|
||||
**⚠️ CRITICAL**: Agents write output files directly.
|
||||
|
||||
```javascript
|
||||
const explorationTasks = selectedAngles.map((angle, index) =>
|
||||
Task({
|
||||
subagent_type: "cli-explore-agent",
|
||||
run_in_background: false,
|
||||
description: `Explore: ${angle}`,
|
||||
prompt: `
|
||||
## Exploration Objective
|
||||
为 CPCC 软著申请文档执行 **${angle}** 探索。
|
||||
|
||||
## Assigned Context
|
||||
- **Exploration Angle**: ${angle}
|
||||
- **Software Name**: ${metadata.software_name}
|
||||
- **Scope Path**: ${metadata.scope_path}
|
||||
- **Category**: ${metadata.category}
|
||||
- **Exploration Index**: ${index + 1} of ${selectedAngles.length}
|
||||
- **Output File**: ${sessionFolder}/exploration-${angle}.json
|
||||
|
||||
## MANDATORY FIRST STEPS
|
||||
1. Run: ccw tool exec get_modules_by_depth '{}' (project structure)
|
||||
2. Run: rg -l "{relevant_keyword}" --type ts (locate relevant files)
|
||||
3. Analyze from ${angle} perspective
|
||||
|
||||
## Exploration Strategy (${angle} focus)
|
||||
|
||||
**Step 1: Structural Scan**
|
||||
- 识别与 ${angle} 相关的模块和文件
|
||||
- 分析导入/导出关系
|
||||
|
||||
**Step 2: Pattern Recognition**
|
||||
- ${angle} 相关的设计模式
|
||||
- 代码组织方式
|
||||
|
||||
**Step 3: Write Output**
|
||||
- 输出 JSON 到指定路径
|
||||
|
||||
## Expected Output Schema
|
||||
|
||||
**File**: ${sessionFolder}/exploration-${angle}.json
|
||||
|
||||
\`\`\`json
|
||||
{
|
||||
"angle": "${angle}",
|
||||
"findings": {
|
||||
"structure": [
|
||||
{ "component": "...", "type": "module|layer|service", "path": "...", "description": "..." }
|
||||
],
|
||||
"patterns": [
|
||||
{ "name": "...", "usage": "...", "files": ["path1", "path2"] }
|
||||
],
|
||||
"key_files": [
|
||||
{ "path": "src/file.ts", "relevance": 0.85, "rationale": "Core ${angle} logic" }
|
||||
]
|
||||
},
|
||||
"insights": [
|
||||
{ "observation": "...", "cpcc_section": "2|3|4|5|6|7", "recommendation": "..." }
|
||||
],
|
||||
"_metadata": {
|
||||
"exploration_angle": "${angle}",
|
||||
"exploration_index": ${index + 1},
|
||||
"software_name": "${metadata.software_name}",
|
||||
"timestamp": "ISO8601"
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
## Success Criteria
|
||||
- [ ] get_modules_by_depth 执行完成
|
||||
- [ ] 至少识别 3 个相关文件
|
||||
- [ ] patterns 包含具体代码示例
|
||||
- [ ] insights 关联到 CPCC 章节 (2-7)
|
||||
- [ ] JSON 输出到指定路径
|
||||
- [ ] Return: 2-3 句话总结 ${angle} 发现
|
||||
`
|
||||
})
|
||||
);
|
||||
|
||||
// Execute all exploration tasks in parallel
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
Session folder structure after exploration:
|
||||
|
||||
```
|
||||
${sessionFolder}/
|
||||
├── exploration-architecture.json
|
||||
├── exploration-{angle2}.json
|
||||
├── exploration-{angle3}.json
|
||||
└── exploration-{angle4}.json
|
||||
```
|
||||
|
||||
## Downstream Usage (Phase 2 Analysis Input)
|
||||
|
||||
Phase 2 agents read exploration files as context:
|
||||
|
||||
```javascript
|
||||
// Discover exploration files by known angle pattern
|
||||
const explorationData = {};
|
||||
selectedAngles.forEach(angle => {
|
||||
const filePath = `${sessionFolder}/exploration-${angle}.json`;
|
||||
explorationData[angle] = JSON.parse(Read(filePath));
|
||||
});
|
||||
```
|
||||
@@ -5,15 +5,161 @@
|
||||
> **模板参考**: [../templates/agent-base.md](../templates/agent-base.md)
|
||||
> **规范参考**: [../specs/cpcc-requirements.md](../specs/cpcc-requirements.md)
|
||||
|
||||
## Agent 执行前置条件
|
||||
## Exploration → Agent 自动分配
|
||||
|
||||
**每个 Agent 必须首先读取以下规范文件**:
|
||||
根据 Phase 1.5 生成的 exploration 文件名自动分配对应的 analysis agent。
|
||||
|
||||
### 映射规则
|
||||
|
||||
```javascript
|
||||
// Agent 启动时的第一步操作
|
||||
const specs = {
|
||||
cpcc: Read(`${skillRoot}/specs/cpcc-requirements.md`)
|
||||
// Exploration 角度 → Agent 映射(基于文件名识别,不读取内容)
|
||||
const EXPLORATION_TO_AGENT = {
|
||||
'architecture': 'architecture',
|
||||
'commands': 'functions', // CLI 命令 → 功能模块
|
||||
'endpoints': 'interfaces', // API 端点 → 接口设计
|
||||
'algorithms': 'algorithms',
|
||||
'data-structures': 'data_structures',
|
||||
'dataflow': 'data_structures', // 数据流 → 数据结构
|
||||
'interfaces': 'interfaces',
|
||||
'exceptions': 'exceptions'
|
||||
};
|
||||
|
||||
// 从文件名提取角度
|
||||
function extractAngle(filename) {
|
||||
// exploration-architecture.json → architecture
|
||||
const match = filename.match(/exploration-(.+)\.json$/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
// 分配 agent
|
||||
function assignAgent(explorationFile) {
|
||||
const angle = extractAngle(path.basename(explorationFile));
|
||||
return EXPLORATION_TO_AGENT[angle] || null;
|
||||
}
|
||||
|
||||
// Agent 配置(用于 buildAgentPrompt)
|
||||
const AGENT_CONFIGS = {
|
||||
architecture: {
|
||||
role: '系统架构师,专注于分层设计和模块依赖',
|
||||
section: '2',
|
||||
output: 'section-2-architecture.md',
|
||||
focus: '分层结构、模块依赖、数据流向'
|
||||
},
|
||||
functions: {
|
||||
role: '功能分析师,专注于功能点识别和交互',
|
||||
section: '3',
|
||||
output: 'section-3-functions.md',
|
||||
focus: '功能点枚举、模块分组、入口文件、功能交互'
|
||||
},
|
||||
algorithms: {
|
||||
role: '算法工程师,专注于核心逻辑和复杂度分析',
|
||||
section: '4',
|
||||
output: 'section-4-algorithms.md',
|
||||
focus: '核心算法、流程步骤、复杂度、输入输出'
|
||||
},
|
||||
data_structures: {
|
||||
role: '数据建模师,专注于实体关系和类型定义',
|
||||
section: '5',
|
||||
output: 'section-5-data-structures.md',
|
||||
focus: '实体定义、属性类型、关系映射、枚举'
|
||||
},
|
||||
interfaces: {
|
||||
role: 'API设计师,专注于接口契约和协议',
|
||||
section: '6',
|
||||
output: 'section-6-interfaces.md',
|
||||
focus: 'API端点、参数校验、响应格式、时序'
|
||||
},
|
||||
exceptions: {
|
||||
role: '可靠性工程师,专注于异常处理和恢复策略',
|
||||
section: '7',
|
||||
output: 'section-7-exceptions.md',
|
||||
focus: '异常类型、错误码、处理模式、恢复策略'
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 自动发现与分配流程
|
||||
|
||||
```javascript
|
||||
// 1. 发现所有 exploration 文件(仅看文件名)
|
||||
const explorationFiles = bash(`find ${sessionFolder} -name "exploration-*.json" -type f`)
|
||||
.split('\n')
|
||||
.filter(f => f.trim());
|
||||
|
||||
// 2. 按文件名自动分配 agent
|
||||
const agentAssignments = explorationFiles.map(file => {
|
||||
const angle = extractAngle(path.basename(file));
|
||||
const agentName = EXPLORATION_TO_AGENT[angle];
|
||||
return {
|
||||
exploration_file: file,
|
||||
angle: angle,
|
||||
agent: agentName,
|
||||
output_file: AGENT_CONFIGS[agentName]?.output
|
||||
};
|
||||
}).filter(a => a.agent);
|
||||
|
||||
// 3. 补充未被 exploration 覆盖的必需 agent(分配相关 exploration)
|
||||
const coveredAgents = new Set(agentAssignments.map(a => a.agent));
|
||||
const requiredAgents = ['architecture', 'functions', 'algorithms', 'data_structures', 'interfaces', 'exceptions'];
|
||||
const missingAgents = requiredAgents.filter(a => !coveredAgents.has(a));
|
||||
|
||||
// 相关性映射:为缺失 agent 分配最相关的 exploration
|
||||
const RELATED_EXPLORATIONS = {
|
||||
architecture: ['architecture', 'dataflow', 'interfaces'],
|
||||
functions: ['commands', 'endpoints', 'architecture'],
|
||||
algorithms: ['algorithms', 'dataflow', 'architecture'],
|
||||
data_structures: ['data-structures', 'dataflow', 'architecture'],
|
||||
interfaces: ['interfaces', 'endpoints', 'architecture'],
|
||||
exceptions: ['exceptions', 'algorithms', 'architecture']
|
||||
};
|
||||
|
||||
function findRelatedExploration(agent, availableFiles) {
|
||||
const preferences = RELATED_EXPLORATIONS[agent] || ['architecture'];
|
||||
for (const pref of preferences) {
|
||||
const match = availableFiles.find(f => f.includes(`exploration-${pref}.json`));
|
||||
if (match) return { file: match, angle: pref, isRelated: true };
|
||||
}
|
||||
// 最后兜底:任意 exploration 都比没有强
|
||||
return availableFiles.length > 0
|
||||
? { file: availableFiles[0], angle: extractAngle(path.basename(availableFiles[0])), isRelated: true }
|
||||
: { file: null, angle: null, isRelated: false };
|
||||
}
|
||||
|
||||
missingAgents.forEach(agent => {
|
||||
const related = findRelatedExploration(agent, explorationFiles);
|
||||
agentAssignments.push({
|
||||
exploration_file: related.file,
|
||||
angle: related.angle,
|
||||
agent: agent,
|
||||
output_file: AGENT_CONFIGS[agent].output,
|
||||
is_related: related.isRelated // 标记为相关而非直接匹配
|
||||
});
|
||||
});
|
||||
|
||||
console.log(`
|
||||
## Agent Auto-Assignment
|
||||
|
||||
Found ${explorationFiles.length} exploration files:
|
||||
${agentAssignments.map(a => {
|
||||
if (!a.exploration_file) return `- ${a.agent} agent (no exploration)`;
|
||||
if (a.is_related) return `- ${a.agent} agent ← ${a.angle} (related)`;
|
||||
return `- ${a.agent} agent ← ${a.angle} (direct)`;
|
||||
}).join('\n')}
|
||||
`);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Agent 执行前置条件
|
||||
|
||||
**每个 Agent 接收 exploration 文件路径,自行读取内容**:
|
||||
|
||||
```javascript
|
||||
// Agent prompt 中包含文件路径
|
||||
// Agent 启动后的操作顺序:
|
||||
// 1. Read exploration 文件(如有)
|
||||
// 2. Read CPCC 规范文件
|
||||
// 3. 执行分析任务
|
||||
```
|
||||
|
||||
规范文件路径(相对于 skill 根目录):
|
||||
@@ -47,26 +193,90 @@ const specs = {
|
||||
## 执行流程
|
||||
|
||||
```javascript
|
||||
// 1. 准备目录
|
||||
// 1. 发现 exploration 文件并自动分配 agent
|
||||
const explorationFiles = bash(`find ${sessionFolder} -name "exploration-*.json" -type f`)
|
||||
.split('\n')
|
||||
.filter(f => f.trim());
|
||||
|
||||
const agentAssignments = explorationFiles.map(file => {
|
||||
const angle = extractAngle(path.basename(file));
|
||||
const agentName = EXPLORATION_TO_AGENT[angle];
|
||||
return { exploration_file: file, angle, agent: agentName };
|
||||
}).filter(a => a.agent);
|
||||
|
||||
// 补充必需 agent
|
||||
const coveredAgents = new Set(agentAssignments.map(a => a.agent));
|
||||
const requiredAgents = ['architecture', 'functions', 'algorithms', 'data_structures', 'interfaces', 'exceptions'];
|
||||
requiredAgents.filter(a => !coveredAgents.has(a)).forEach(agent => {
|
||||
agentAssignments.push({ exploration_file: null, angle: null, agent });
|
||||
});
|
||||
|
||||
// 2. 准备目录
|
||||
Bash(`mkdir -p ${outputDir}/sections`);
|
||||
|
||||
// 2. 并行启动 6 个 Agent
|
||||
const results = await Promise.all([
|
||||
launchAgent('architecture', metadata, outputDir),
|
||||
launchAgent('functions', metadata, outputDir),
|
||||
launchAgent('algorithms', metadata, outputDir),
|
||||
launchAgent('data_structures', metadata, outputDir),
|
||||
launchAgent('interfaces', metadata, outputDir),
|
||||
launchAgent('exceptions', metadata, outputDir)
|
||||
]);
|
||||
// 3. 并行启动所有 Agent(传递 exploration 文件路径)
|
||||
const results = await Promise.all(
|
||||
agentAssignments.map(assignment =>
|
||||
Task({
|
||||
subagent_type: "cli-explore-agent",
|
||||
run_in_background: false,
|
||||
description: `Analyze: ${assignment.agent}`,
|
||||
prompt: buildAgentPrompt(assignment, metadata, outputDir)
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
// 3. 收集返回信息
|
||||
// 4. 收集返回信息
|
||||
const summaries = results.map(r => JSON.parse(r));
|
||||
|
||||
// 4. 传递给 Phase 2.5
|
||||
// 5. 传递给 Phase 2.5
|
||||
return { summaries, cross_notes: summaries.flatMap(s => s.cross_module_notes) };
|
||||
```
|
||||
|
||||
### Agent Prompt 构建
|
||||
|
||||
```javascript
|
||||
function buildAgentPrompt(assignment, metadata, outputDir) {
|
||||
const config = AGENT_CONFIGS[assignment.agent];
|
||||
let contextSection = '';
|
||||
|
||||
if (assignment.exploration_file) {
|
||||
const matchType = assignment.is_related ? '相关' : '直接匹配';
|
||||
contextSection = `[CONTEXT]
|
||||
**Exploration 文件**: ${assignment.exploration_file}
|
||||
**匹配类型**: ${matchType}
|
||||
首先读取此文件获取 ${assignment.angle} 探索结果作为分析上下文。
|
||||
${assignment.is_related ? `注意:这是相关探索结果(非直接匹配),请提取与 ${config.focus} 相关的信息。` : ''}
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
${contextSection}
|
||||
[SPEC]
|
||||
读取规范文件:
|
||||
- Read: ${skillRoot}/specs/cpcc-requirements.md
|
||||
|
||||
[ROLE] ${config.role}
|
||||
|
||||
[TASK]
|
||||
分析 ${metadata.scope_path},生成 Section ${config.section}。
|
||||
输出: ${outputDir}/sections/${config.output}
|
||||
|
||||
[CPCC_SPEC]
|
||||
- 内容基于代码分析,无臆测
|
||||
- 图表编号: 图${config.section}-1, 图${config.section}-2...
|
||||
- 每个子章节 ≥100字
|
||||
- 包含文件路径引用
|
||||
|
||||
[FOCUS]
|
||||
${config.focus}
|
||||
|
||||
[RETURN JSON]
|
||||
{"status":"completed","output_file":"${config.output}","summary":"<50字>","cross_module_notes":[],"stats":{}}
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Agent 提示词
|
||||
|
||||
285
.claude/skills/issue-manage/SKILL.md
Normal file
285
.claude/skills/issue-manage/SKILL.md
Normal file
@@ -0,0 +1,285 @@
|
||||
---
|
||||
name: issue-manage
|
||||
description: Interactive issue management with menu-driven CRUD operations. Use when managing issues, viewing issue status, editing issue fields, performing bulk operations, or viewing issue history. Triggers on "manage issue", "list issues", "edit issue", "delete issue", "bulk update", "issue dashboard", "issue history", "completed issues".
|
||||
allowed-tools: Bash, Read, Write, AskUserQuestion, Task, Glob
|
||||
---
|
||||
|
||||
# Issue Management Skill
|
||||
|
||||
Interactive menu-driven interface for issue CRUD operations via `ccw issue` CLI.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Ask me:
|
||||
- "Show all issues" → List with filters
|
||||
- "View issue GH-123" → Detailed inspection
|
||||
- "Edit issue priority" → Modify fields
|
||||
- "Delete old issues" → Remove with confirmation
|
||||
- "Bulk update status" → Batch operations
|
||||
- "Show completed issues" → View issue history
|
||||
- "Archive old issues" → Move to history
|
||||
|
||||
## CLI Endpoints
|
||||
|
||||
```bash
|
||||
# Core operations
|
||||
ccw issue list # List active issues
|
||||
ccw issue list <id> --json # Get issue details
|
||||
ccw issue history # List completed issues (from history)
|
||||
ccw issue history --json # Completed issues as JSON
|
||||
ccw issue status <id> # Detailed status
|
||||
ccw issue init <id> --title "..." # Create issue
|
||||
ccw issue task <id> --title "..." # Add task
|
||||
ccw issue bind <id> <solution-id> # Bind solution
|
||||
ccw issue update <id> --status completed # Complete & auto-archive
|
||||
|
||||
# Queue management
|
||||
ccw issue queue # List current queue
|
||||
ccw issue queue add <id> # Add to queue
|
||||
ccw issue queue list # Queue history
|
||||
ccw issue queue switch <queue-id> # Switch queue
|
||||
ccw issue queue archive # Archive queue
|
||||
ccw issue queue delete <queue-id> # Delete queue
|
||||
ccw issue next # Get next task
|
||||
ccw issue done <queue-id> # Mark completed
|
||||
ccw issue update --from-queue # Sync statuses from queue
|
||||
```
|
||||
|
||||
## Operations
|
||||
|
||||
### 1. LIST 📋
|
||||
|
||||
Filter and browse issues:
|
||||
|
||||
```
|
||||
┌─ Filter by Status ─────────────────┐
|
||||
│ □ All □ Registered │
|
||||
│ □ Planned □ Queued │
|
||||
│ □ Executing □ Completed │
|
||||
└────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Flow**:
|
||||
1. Ask filter preferences → `ccw issue list --json`
|
||||
2. Display table: ID | Status | Priority | Title
|
||||
3. Select issue for detail view
|
||||
|
||||
### 2. VIEW 🔍
|
||||
|
||||
Detailed issue inspection:
|
||||
|
||||
```
|
||||
┌─ Issue: GH-123 ─────────────────────┐
|
||||
│ Title: Fix authentication bug │
|
||||
│ Status: planned | Priority: P2 │
|
||||
│ Solutions: 2 (1 bound) │
|
||||
│ Tasks: 5 pending │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Flow**:
|
||||
1. Fetch `ccw issue status <id> --json`
|
||||
2. Display issue + solutions + tasks
|
||||
3. Offer actions: Edit | Plan | Queue | Delete
|
||||
|
||||
### 3. EDIT ✏️
|
||||
|
||||
Modify issue fields:
|
||||
|
||||
| Field | Options |
|
||||
|-------|---------|
|
||||
| Title | Free text |
|
||||
| Priority | P1-P5 |
|
||||
| Status | registered → completed |
|
||||
| Context | Problem description |
|
||||
| Labels | Comma-separated |
|
||||
|
||||
**Flow**:
|
||||
1. Select field to edit
|
||||
2. Show current value
|
||||
3. Collect new value via AskUserQuestion
|
||||
4. Update `.workflow/issues/issues.jsonl`
|
||||
|
||||
### 4. DELETE 🗑️
|
||||
|
||||
Remove with confirmation:
|
||||
|
||||
```
|
||||
⚠️ Delete issue GH-123?
|
||||
This will also remove:
|
||||
- Associated solutions
|
||||
- Queued tasks
|
||||
|
||||
[Delete] [Cancel]
|
||||
```
|
||||
|
||||
**Flow**:
|
||||
1. Confirm deletion via AskUserQuestion
|
||||
2. Remove from `issues.jsonl`
|
||||
3. Clean up `solutions/<id>.jsonl`
|
||||
4. Remove from `queue.json`
|
||||
|
||||
### 5. HISTORY 📚
|
||||
|
||||
View and manage completed issues:
|
||||
|
||||
```
|
||||
┌─ Issue History ─────────────────────┐
|
||||
│ ID Completed Title │
|
||||
│ ISS-001 2025-12-28 12:00 Fix bug │
|
||||
│ ISS-002 2025-12-27 15:30 Feature │
|
||||
└──────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Flow**:
|
||||
1. Fetch `ccw issue history --json`
|
||||
2. Display table: ID | Completed At | Title
|
||||
3. Optional: Filter by date range
|
||||
|
||||
**Auto-Archive**: When issue status → `completed`:
|
||||
- Issue moves from `issues.jsonl` → `issue-history.jsonl`
|
||||
- Solutions remain in `solutions/<id>.jsonl`
|
||||
- Queue items marked completed
|
||||
|
||||
### 6. BULK 📦
|
||||
|
||||
Batch operations:
|
||||
|
||||
| Operation | Description |
|
||||
|-----------|-------------|
|
||||
| Update Status | Change multiple issues |
|
||||
| Update Priority | Batch priority change |
|
||||
| Add Labels | Tag multiple issues |
|
||||
| Delete Multiple | Bulk removal |
|
||||
| Queue All Planned | Add all planned to queue |
|
||||
| Retry All Failed | Reset failed tasks |
|
||||
| Sync from Queue | Update statuses from active queue |
|
||||
|
||||
## Workflow
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────┐
|
||||
│ Main Menu │
|
||||
│ ┌────┐ ┌────┐ ┌────┐ ┌─────┐ ┌────┐ │
|
||||
│ │List│ │View│ │Edit│ │Hist.│ │Bulk│ │
|
||||
│ └──┬─┘ └──┬─┘ └──┬─┘ └──┬──┘ └──┬─┘ │
|
||||
└─────┼──────┼──────┼──────┼───────┼─────────────┘
|
||||
│ │ │ │ │
|
||||
▼ ▼ ▼ ▼ ▼
|
||||
Filter Detail Fields History Multi
|
||||
Select Actions Update Browse Select
|
||||
│ │ │ │ │
|
||||
└──────┴──────┴──────┴───────┘
|
||||
│
|
||||
▼
|
||||
Back to Menu
|
||||
```
|
||||
|
||||
**Issue Lifecycle**:
|
||||
```
|
||||
registered → planned → queued → executing → completed
|
||||
│
|
||||
▼
|
||||
issue-history.jsonl
|
||||
```
|
||||
|
||||
## Implementation Guide
|
||||
|
||||
### Entry Point
|
||||
|
||||
```javascript
|
||||
// Parse input for issue ID
|
||||
const issueId = input.match(/^([A-Z]+-\d+|ISS-\d+)/i)?.[1];
|
||||
|
||||
// Show main menu
|
||||
await showMainMenu(issueId);
|
||||
```
|
||||
|
||||
### Main Menu Pattern
|
||||
|
||||
```javascript
|
||||
// 1. Fetch dashboard data
|
||||
const issues = JSON.parse(Bash('ccw issue list --json') || '[]');
|
||||
const history = JSON.parse(Bash('ccw issue history --json 2>/dev/null') || '[]');
|
||||
const queue = JSON.parse(Bash('ccw issue queue --json 2>/dev/null') || '{}');
|
||||
|
||||
// 2. Display summary
|
||||
console.log(`Active: ${issues.length} | Completed: ${history.length} | Queue: ${queue.pending_count || 0} pending`);
|
||||
|
||||
// 3. Ask action via AskUserQuestion
|
||||
const action = AskUserQuestion({
|
||||
questions: [{
|
||||
question: 'What would you like to do?',
|
||||
header: 'Action',
|
||||
options: [
|
||||
{ label: 'List Issues', description: 'Browse active issues' },
|
||||
{ label: 'View Issue', description: 'Detail view' },
|
||||
{ label: 'Edit Issue', description: 'Modify fields' },
|
||||
{ label: 'View History', description: 'Completed issues' },
|
||||
{ label: 'Bulk Operations', description: 'Batch actions' }
|
||||
]
|
||||
}]
|
||||
});
|
||||
|
||||
// 4. Route to handler
|
||||
```
|
||||
|
||||
### Filter Pattern
|
||||
|
||||
```javascript
|
||||
const filter = AskUserQuestion({
|
||||
questions: [{
|
||||
question: 'Filter by status?',
|
||||
header: 'Filter',
|
||||
multiSelect: true,
|
||||
options: [
|
||||
{ label: 'All', description: 'Show all' },
|
||||
{ label: 'Registered', description: 'Unplanned' },
|
||||
{ label: 'Planned', description: 'Has solution' },
|
||||
{ label: 'Executing', description: 'In progress' }
|
||||
]
|
||||
}]
|
||||
});
|
||||
```
|
||||
|
||||
### Edit Pattern
|
||||
|
||||
```javascript
|
||||
// Select field
|
||||
const field = AskUserQuestion({...});
|
||||
|
||||
// Get new value based on field type
|
||||
// For Priority: show P1-P5 options
|
||||
// For Status: show status options
|
||||
// For Title: accept free text via "Other"
|
||||
|
||||
// Update file
|
||||
const issuesPath = '.workflow/issues/issues.jsonl';
|
||||
// Read → Parse → Update → Write
|
||||
```
|
||||
|
||||
## Data Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `.workflow/issues/issues.jsonl` | Active issue records |
|
||||
| `.workflow/issues/issue-history.jsonl` | Completed issues (archived) |
|
||||
| `.workflow/issues/solutions/<id>.jsonl` | Solutions per issue |
|
||||
| `.workflow/issues/queues/index.json` | Queue index (multi-queue) |
|
||||
| `.workflow/issues/queues/<queue-id>.json` | Individual queue files |
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Error | Resolution |
|
||||
|-------|------------|
|
||||
| No issues found | Suggest `/issue:new` to create |
|
||||
| Issue not found | Show available issues, re-prompt |
|
||||
| Write failure | Check file permissions |
|
||||
| Queue error | Display ccw error message |
|
||||
|
||||
## Related Commands
|
||||
|
||||
- `/issue:new` - Create structured issue
|
||||
- `/issue:plan` - Generate solution
|
||||
- `/issue:queue` - Form execution queue
|
||||
- `/issue:execute` - Execute tasks
|
||||
@@ -1,75 +1,176 @@
|
||||
# Phase 2: Project Exploration
|
||||
|
||||
Launch parallel exploration agents based on report type.
|
||||
Launch parallel exploration agents based on report type and task context.
|
||||
|
||||
## Execution
|
||||
|
||||
### Step 1: Map Exploration Angles
|
||||
### Step 1: Intelligent Angle Selection
|
||||
|
||||
```javascript
|
||||
const angleMapping = {
|
||||
architecture: ["Layer Structure", "Module Dependencies", "Entry Points", "Data Flow"],
|
||||
design: ["Design Patterns", "Class Relationships", "Interface Contracts", "State Management"],
|
||||
methods: ["Core Algorithms", "Critical Paths", "Public APIs", "Complex Logic"],
|
||||
comprehensive: ["Layer Structure", "Design Patterns", "Core Algorithms", "Data Flow"]
|
||||
// Angle presets based on report type (adapted from lite-plan.md)
|
||||
const ANGLE_PRESETS = {
|
||||
architecture: ['layer-structure', 'module-dependencies', 'entry-points', 'data-flow'],
|
||||
design: ['design-patterns', 'class-relationships', 'interface-contracts', 'state-management'],
|
||||
methods: ['core-algorithms', 'critical-paths', 'public-apis', 'complex-logic'],
|
||||
comprehensive: ['architecture', 'patterns', 'dependencies', 'integration-points']
|
||||
};
|
||||
|
||||
const angles = angleMapping[config.type];
|
||||
// Depth-based angle count
|
||||
const angleCount = {
|
||||
shallow: 2,
|
||||
standard: 3,
|
||||
deep: 4
|
||||
};
|
||||
|
||||
function selectAngles(reportType, depth) {
|
||||
const preset = ANGLE_PRESETS[reportType] || ANGLE_PRESETS.comprehensive;
|
||||
const count = angleCount[depth] || 3;
|
||||
return preset.slice(0, count);
|
||||
}
|
||||
|
||||
const selectedAngles = selectAngles(config.type, config.depth);
|
||||
|
||||
console.log(`
|
||||
## Exploration Plan
|
||||
|
||||
Report Type: ${config.type}
|
||||
Depth: ${config.depth}
|
||||
Selected Angles: ${selectedAngles.join(', ')}
|
||||
|
||||
Launching ${selectedAngles.length} parallel explorations...
|
||||
`);
|
||||
```
|
||||
|
||||
### Step 2: Launch Parallel Agents
|
||||
### Step 2: Launch Parallel Agents (Direct Output)
|
||||
|
||||
For each angle, launch an exploration agent:
|
||||
**⚠️ CRITICAL**: Agents write output files directly. No aggregation needed.
|
||||
|
||||
```javascript
|
||||
Task({
|
||||
subagent_type: "cli-explore-agent",
|
||||
run_in_background: false,
|
||||
description: `Explore: ${angle}`,
|
||||
prompt: `
|
||||
// Launch agents with pre-assigned angles
|
||||
const explorationTasks = selectedAngles.map((angle, index) =>
|
||||
Task({
|
||||
subagent_type: "cli-explore-agent",
|
||||
run_in_background: false, // ⚠️ MANDATORY: Must wait for results
|
||||
description: `Explore: ${angle}`,
|
||||
prompt: `
|
||||
## Exploration Objective
|
||||
Execute **${angle}** exploration for project analysis report.
|
||||
Execute **${angle}** exploration for ${config.type} project analysis report.
|
||||
|
||||
## Context
|
||||
- **Angle**: ${angle}
|
||||
## Assigned Context
|
||||
- **Exploration Angle**: ${angle}
|
||||
- **Report Type**: ${config.type}
|
||||
- **Depth**: ${config.depth}
|
||||
- **Scope**: ${config.scope}
|
||||
- **Exploration Index**: ${index + 1} of ${selectedAngles.length}
|
||||
- **Output File**: ${sessionFolder}/exploration-${angle}.json
|
||||
|
||||
## Exploration Protocol
|
||||
1. Structural Discovery (get_modules_by_depth, rg, glob)
|
||||
2. Pattern Recognition (conventions, naming, organization)
|
||||
3. Relationship Mapping (dependencies, integration points)
|
||||
## MANDATORY FIRST STEPS (Execute by Agent)
|
||||
**You (cli-explore-agent) MUST execute these steps in order:**
|
||||
1. Run: ccw tool exec get_modules_by_depth '{}' (project structure)
|
||||
2. Run: rg -l "{relevant_keyword}" --type ts (locate relevant files)
|
||||
3. Analyze project from ${angle} perspective
|
||||
|
||||
## Output Format
|
||||
## Exploration Strategy (${angle} focus)
|
||||
|
||||
**Step 1: Structural Scan** (Bash)
|
||||
- get_modules_by_depth.sh → identify modules related to ${angle}
|
||||
- find/rg → locate files relevant to ${angle} aspect
|
||||
- Analyze imports/dependencies from ${angle} perspective
|
||||
|
||||
**Step 2: Semantic Analysis** (Gemini/Qwen CLI)
|
||||
- How does existing code handle ${angle} concerns?
|
||||
- What patterns are used for ${angle}?
|
||||
- Identify key architectural decisions related to ${angle}
|
||||
|
||||
**Step 3: Write Output Directly**
|
||||
- Consolidate ${angle} findings into JSON
|
||||
- Write to output file path specified above
|
||||
|
||||
## Expected Output Schema
|
||||
|
||||
**File**: ${sessionFolder}/exploration-${angle}.json
|
||||
|
||||
\`\`\`json
|
||||
{
|
||||
"angle": "${angle}",
|
||||
"findings": {
|
||||
"structure": [...],
|
||||
"patterns": [...],
|
||||
"relationships": [...],
|
||||
"key_files": [{path, relevance, rationale}]
|
||||
"structure": [
|
||||
{ "component": "...", "type": "module|layer|service", "description": "..." }
|
||||
],
|
||||
"patterns": [
|
||||
{ "name": "...", "usage": "...", "files": ["path1", "path2"] }
|
||||
],
|
||||
"relationships": [
|
||||
{ "from": "...", "to": "...", "type": "depends|imports|calls", "strength": "high|medium|low" }
|
||||
],
|
||||
"key_files": [
|
||||
{ "path": "src/file.ts", "relevance": 0.85, "rationale": "Core ${angle} logic" }
|
||||
]
|
||||
},
|
||||
"insights": [...]
|
||||
"insights": [
|
||||
{ "observation": "...", "impact": "high|medium|low", "recommendation": "..." }
|
||||
],
|
||||
"_metadata": {
|
||||
"exploration_angle": "${angle}",
|
||||
"exploration_index": ${index + 1},
|
||||
"report_type": "${config.type}",
|
||||
"timestamp": "ISO8601"
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
## Success Criteria
|
||||
- [ ] get_modules_by_depth.sh executed
|
||||
- [ ] At least 3 relevant files identified with ${angle} rationale
|
||||
- [ ] Patterns are actionable (code examples, not generic advice)
|
||||
- [ ] Relationships include concrete file references
|
||||
- [ ] JSON output written to ${sessionFolder}/exploration-${angle}.json
|
||||
- [ ] Return: 2-3 sentence summary of ${angle} findings
|
||||
`
|
||||
})
|
||||
```
|
||||
})
|
||||
);
|
||||
|
||||
### Step 3: Aggregate Results
|
||||
|
||||
Merge all exploration results into unified findings:
|
||||
|
||||
```javascript
|
||||
const aggregatedFindings = {
|
||||
structure: [], // from all angles
|
||||
patterns: [], // from all angles
|
||||
relationships: [], // from all angles
|
||||
key_files: [], // deduplicated
|
||||
insights: [] // prioritized
|
||||
};
|
||||
// Execute all exploration tasks in parallel
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
Save exploration results to `exploration-{angle}.json` files.
|
||||
Session folder structure after exploration:
|
||||
|
||||
```
|
||||
${sessionFolder}/
|
||||
├── exploration-{angle1}.json # Agent 1 direct output
|
||||
├── exploration-{angle2}.json # Agent 2 direct output
|
||||
├── exploration-{angle3}.json # Agent 3 direct output (if applicable)
|
||||
└── exploration-{angle4}.json # Agent 4 direct output (if applicable)
|
||||
```
|
||||
|
||||
## Downstream Usage (Phase 3 Analysis Input)
|
||||
|
||||
Subsequent analysis phases MUST read exploration outputs as input:
|
||||
|
||||
```javascript
|
||||
// Discover exploration files by known angle pattern
|
||||
const explorationData = {};
|
||||
selectedAngles.forEach(angle => {
|
||||
const filePath = `${sessionFolder}/exploration-${angle}.json`;
|
||||
explorationData[angle] = JSON.parse(Read(filePath));
|
||||
});
|
||||
|
||||
// Pass to analysis agent
|
||||
Task({
|
||||
subagent_type: "analysis-agent",
|
||||
prompt: `
|
||||
## Analysis Input
|
||||
|
||||
### Exploration Data by Angle
|
||||
${Object.entries(explorationData).map(([angle, data]) => `
|
||||
#### ${angle}
|
||||
${JSON.stringify(data, null, 2)}
|
||||
`).join('\n')}
|
||||
|
||||
## Analysis Task
|
||||
Synthesize findings from all exploration angles...
|
||||
`
|
||||
});
|
||||
```
|
||||
|
||||
@@ -5,16 +5,176 @@
|
||||
> **规范参考**: [../specs/quality-standards.md](../specs/quality-standards.md)
|
||||
> **写作风格**: [../specs/writing-style.md](../specs/writing-style.md)
|
||||
|
||||
## Agent 执行前置条件
|
||||
## Exploration → Agent 自动分配
|
||||
|
||||
**每个 Agent 必须首先读取以下规范文件**:
|
||||
根据 Phase 2 生成的 exploration 文件名自动分配对应的 analysis agent。
|
||||
|
||||
### 映射规则
|
||||
|
||||
```javascript
|
||||
// Agent 启动时的第一步操作
|
||||
const specs = {
|
||||
quality: Read(`${skillRoot}/specs/quality-standards.md`),
|
||||
style: Read(`${skillRoot}/specs/writing-style.md`)
|
||||
// Exploration 角度 → Agent 映射(基于文件名识别,不读取内容)
|
||||
const EXPLORATION_TO_AGENT = {
|
||||
// Architecture Report 角度
|
||||
'layer-structure': 'layers',
|
||||
'module-dependencies': 'dependencies',
|
||||
'entry-points': 'entrypoints',
|
||||
'data-flow': 'dataflow',
|
||||
|
||||
// Design Report 角度
|
||||
'design-patterns': 'patterns',
|
||||
'class-relationships': 'classes',
|
||||
'interface-contracts': 'interfaces',
|
||||
'state-management': 'state',
|
||||
|
||||
// Methods Report 角度
|
||||
'core-algorithms': 'algorithms',
|
||||
'critical-paths': 'paths',
|
||||
'public-apis': 'apis',
|
||||
'complex-logic': 'logic',
|
||||
|
||||
// Comprehensive 角度
|
||||
'architecture': 'overview',
|
||||
'patterns': 'patterns',
|
||||
'dependencies': 'dependencies',
|
||||
'integration-points': 'entrypoints'
|
||||
};
|
||||
|
||||
// 从文件名提取角度
|
||||
function extractAngle(filename) {
|
||||
// exploration-layer-structure.json → layer-structure
|
||||
const match = filename.match(/exploration-(.+)\.json$/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
// 分配 agent
|
||||
function assignAgent(explorationFile) {
|
||||
const angle = extractAngle(path.basename(explorationFile));
|
||||
return EXPLORATION_TO_AGENT[angle] || null;
|
||||
}
|
||||
|
||||
// Agent 配置(用于 buildAgentPrompt)
|
||||
const AGENT_CONFIGS = {
|
||||
overview: {
|
||||
role: '首席系统架构师',
|
||||
task: '基于代码库全貌,撰写"总体架构"章节,洞察核心价值主张和顶层技术决策',
|
||||
focus: '领域边界与定位、架构范式、核心技术决策、顶层模块划分',
|
||||
constraint: '避免罗列目录结构,重点阐述设计意图,包含至少1个 Mermaid 架构图'
|
||||
},
|
||||
layers: {
|
||||
role: '资深软件设计师',
|
||||
task: '分析系统逻辑分层结构,撰写"逻辑视点与分层架构"章节',
|
||||
focus: '职责分配体系、数据流向与约束、边界隔离策略、异常处理流',
|
||||
constraint: '不要列举具体文件名,关注层级间契约和隔离艺术'
|
||||
},
|
||||
dependencies: {
|
||||
role: '集成架构专家',
|
||||
task: '审视系统外部连接与内部耦合,撰写"依赖管理与生态集成"章节',
|
||||
focus: '外部集成拓扑、核心依赖分析、依赖注入与控制反转、供应链安全',
|
||||
constraint: '禁止简单列出依赖配置,必须分析集成策略和风险控制模型'
|
||||
},
|
||||
dataflow: {
|
||||
role: '数据架构师',
|
||||
task: '追踪系统数据流转机制,撰写"数据流与状态管理"章节',
|
||||
focus: '数据入口与出口、数据转换管道、持久化策略、一致性保障',
|
||||
constraint: '关注数据生命周期和形态演变,不要罗列数据库表结构'
|
||||
},
|
||||
entrypoints: {
|
||||
role: '系统边界分析师',
|
||||
task: '识别系统入口设计和关键路径,撰写"系统入口与调用链"章节',
|
||||
focus: '入口类型与职责、请求处理管道、关键业务路径、异常与边界处理',
|
||||
constraint: '关注入口设计哲学,不要逐个列举所有端点'
|
||||
},
|
||||
patterns: {
|
||||
role: '核心开发规范制定者',
|
||||
task: '挖掘代码中的复用机制和标准化实践,撰写"设计模式与工程规范"章节',
|
||||
focus: '架构级模式、通信与并发模式、横切关注点实现、抽象与复用策略',
|
||||
constraint: '避免教科书式解释,必须结合项目上下文说明应用场景'
|
||||
},
|
||||
classes: {
|
||||
role: '领域模型设计师',
|
||||
task: '分析系统类型体系和领域模型,撰写"类型体系与领域建模"章节',
|
||||
focus: '领域模型设计、继承与组合策略、职责分配原则、类型安全与约束',
|
||||
constraint: '关注建模思想,用 UML 类图辅助说明核心关系'
|
||||
},
|
||||
interfaces: {
|
||||
role: '契约设计专家',
|
||||
task: '分析系统接口设计和抽象层次,撰写"接口契约与抽象设计"章节',
|
||||
focus: '抽象层次设计、契约与实现分离、扩展点设计、版本演进策略',
|
||||
constraint: '关注接口设计哲学,不要逐个列举接口方法签名'
|
||||
},
|
||||
state: {
|
||||
role: '状态管理架构师',
|
||||
task: '分析系统状态管理机制,撰写"状态管理与生命周期"章节',
|
||||
focus: '状态模型设计、状态生命周期、并发与一致性、状态恢复与容错',
|
||||
constraint: '关注状态管理设计决策,不要列举具体变量名'
|
||||
},
|
||||
algorithms: {
|
||||
role: '算法架构师',
|
||||
task: '分析系统核心算法设计,撰写"核心算法与计算模型"章节',
|
||||
focus: '算法选型与权衡、计算模型设计、性能与可扩展性、正确性保障',
|
||||
constraint: '关注算法思想,用流程图辅助说明复杂逻辑'
|
||||
},
|
||||
paths: {
|
||||
role: '性能架构师',
|
||||
task: '分析系统关键执行路径,撰写"关键路径与性能设计"章节',
|
||||
focus: '关键业务路径、性能敏感区域、瓶颈识别与缓解、降级与熔断',
|
||||
constraint: '关注路径设计战略考量,不要罗列所有代码执行步骤'
|
||||
},
|
||||
apis: {
|
||||
role: 'API 设计规范专家',
|
||||
task: '分析系统对外接口设计规范,撰写"API 设计与规范"章节',
|
||||
focus: 'API 设计风格、命名与结构规范、版本管理策略、错误处理规范',
|
||||
constraint: '关注设计规范和一致性,不要逐个列举所有 API 端点'
|
||||
},
|
||||
logic: {
|
||||
role: '业务逻辑架构师',
|
||||
task: '分析系统业务逻辑建模,撰写"业务逻辑与规则引擎"章节',
|
||||
focus: '业务规则建模、决策点设计、边界条件处理、业务流程编排',
|
||||
constraint: '关注业务逻辑组织方式,不要逐行解释代码逻辑'
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 自动发现与分配流程
|
||||
|
||||
```javascript
|
||||
// 1. 发现所有 exploration 文件(仅看文件名)
|
||||
const explorationFiles = bash(`find ${sessionFolder} -name "exploration-*.json" -type f`)
|
||||
.split('\n')
|
||||
.filter(f => f.trim());
|
||||
|
||||
// 2. 按文件名自动分配 agent
|
||||
const agentAssignments = explorationFiles.map(file => {
|
||||
const angle = extractAngle(path.basename(file));
|
||||
const agentName = EXPLORATION_TO_AGENT[angle];
|
||||
return {
|
||||
exploration_file: file,
|
||||
angle: angle,
|
||||
agent: agentName,
|
||||
output_file: `section-${agentName}.md`
|
||||
};
|
||||
}).filter(a => a.agent); // 过滤未映射的角度
|
||||
|
||||
console.log(`
|
||||
## Agent Auto-Assignment
|
||||
|
||||
Found ${explorationFiles.length} exploration files:
|
||||
${agentAssignments.map(a => `- ${a.angle} → ${a.agent} agent`).join('\n')}
|
||||
`);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Agent 执行前置条件
|
||||
|
||||
**每个 Agent 接收 exploration 文件路径,自行读取内容**:
|
||||
|
||||
```javascript
|
||||
// Agent prompt 中包含文件路径
|
||||
// Agent 启动后的操作顺序:
|
||||
// 1. Read exploration 文件(上下文输入)
|
||||
// 2. Read 规范文件
|
||||
// 3. 执行分析任务
|
||||
```
|
||||
|
||||
规范文件路径(相对于 skill 根目录):
|
||||
@@ -617,15 +777,30 @@ Task({
|
||||
## 执行流程
|
||||
|
||||
```javascript
|
||||
// 1. 根据报告类型选择 Agent 配置
|
||||
const agentConfigs = getAgentConfigs(config.type);
|
||||
// 1. 发现 exploration 文件并自动分配 agent
|
||||
const explorationFiles = bash(`find ${sessionFolder} -name "exploration-*.json" -type f`)
|
||||
.split('\n')
|
||||
.filter(f => f.trim());
|
||||
|
||||
const agentAssignments = explorationFiles.map(file => {
|
||||
const angle = extractAngle(path.basename(file));
|
||||
const agentName = EXPLORATION_TO_AGENT[angle];
|
||||
return { exploration_file: file, angle, agent: agentName };
|
||||
}).filter(a => a.agent);
|
||||
|
||||
// 2. 准备目录
|
||||
Bash(`mkdir "${outputDir}\\sections"`);
|
||||
|
||||
// 3. 并行启动所有 Agent
|
||||
// 3. 并行启动所有 Agent(传递 exploration 文件路径)
|
||||
const results = await Promise.all(
|
||||
agentConfigs.map(agent => launchAgent(agent, config, outputDir))
|
||||
agentAssignments.map(assignment =>
|
||||
Task({
|
||||
subagent_type: "cli-explore-agent",
|
||||
run_in_background: false,
|
||||
description: `Analyze: ${assignment.agent}`,
|
||||
prompt: buildAgentPrompt(assignment, config, outputDir)
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
// 4. 收集简要返回信息
|
||||
@@ -635,6 +810,45 @@ const summaries = results.map(r => JSON.parse(r));
|
||||
return { summaries, cross_notes: summaries.flatMap(s => s.cross_module_notes) };
|
||||
```
|
||||
|
||||
### Agent Prompt 构建
|
||||
|
||||
```javascript
|
||||
function buildAgentPrompt(assignment, config, outputDir) {
|
||||
const agentConfig = AGENT_CONFIGS[assignment.agent];
|
||||
return `
|
||||
[CONTEXT]
|
||||
**Exploration 文件**: ${assignment.exploration_file}
|
||||
首先读取此文件获取 ${assignment.angle} 探索结果作为分析上下文。
|
||||
|
||||
[SPEC]
|
||||
读取规范文件:
|
||||
- Read: ${skillRoot}/specs/quality-standards.md
|
||||
- Read: ${skillRoot}/specs/writing-style.md
|
||||
|
||||
[ROLE] ${agentConfig.role}
|
||||
|
||||
[TASK]
|
||||
${agentConfig.task}
|
||||
输出: ${outputDir}/sections/section-${assignment.agent}.md
|
||||
|
||||
[STYLE]
|
||||
- 严谨专业的中文技术写作,专业术语保留英文
|
||||
- 完全客观的第三人称视角,严禁"我们"、"开发者"
|
||||
- 段落式叙述,采用"论点-论据-结论"结构
|
||||
- 善用逻辑连接词体现设计推演过程
|
||||
|
||||
[FOCUS]
|
||||
${agentConfig.focus}
|
||||
|
||||
[CONSTRAINT]
|
||||
${agentConfig.constraint}
|
||||
|
||||
[RETURN JSON]
|
||||
{"status":"completed","output_file":"section-${assignment.agent}.md","summary":"<50字>","cross_module_notes":[],"stats":{}}
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
各 Agent 写入 `sections/section-xxx.md`,返回简要 JSON 供 Phase 3.5 汇总。
|
||||
|
||||
@@ -4,6 +4,29 @@
|
||||
|
||||
> **写作规范**: [../specs/writing-style.md](../specs/writing-style.md)
|
||||
|
||||
## 执行要求
|
||||
|
||||
**必须执行**:Phase 3 所有 Analysis Agents 完成后,主编排器**必须**调用此 Consolidation Agent。
|
||||
|
||||
**触发条件**:
|
||||
- Phase 3 所有 agent 已返回结果(status: completed/partial/failed)
|
||||
- `sections/section-*.md` 文件已生成
|
||||
|
||||
**输入来源**:
|
||||
- `agent_summaries`: Phase 3 各 agent 返回的 JSON(包含 status, output_file, summary, cross_module_notes)
|
||||
- `cross_module_notes`: 从各 agent 返回中提取的跨模块备注数组
|
||||
|
||||
**调用时机**:
|
||||
```javascript
|
||||
// Phase 3 完成后,主编排器执行:
|
||||
const phase3Results = await runPhase3Agents(); // 并行执行所有 analysis agents
|
||||
const agentSummaries = phase3Results.map(r => JSON.parse(r));
|
||||
const crossNotes = agentSummaries.flatMap(s => s.cross_module_notes || []);
|
||||
|
||||
// 必须调用 Phase 3.5 Consolidation Agent
|
||||
await runPhase35Consolidation(agentSummaries, crossNotes);
|
||||
```
|
||||
|
||||
## 核心职责
|
||||
|
||||
1. **跨章节综合分析**:生成 synthesis(报告综述)
|
||||
@@ -22,7 +45,9 @@ interface ConsolidationInput {
|
||||
}
|
||||
```
|
||||
|
||||
## 执行
|
||||
## Agent 调用代码
|
||||
|
||||
主编排器使用以下代码调用 Consolidation Agent:
|
||||
|
||||
```javascript
|
||||
Task({
|
||||
|
||||
184
.claude/skills/software-manual/SKILL.md
Normal file
184
.claude/skills/software-manual/SKILL.md
Normal file
@@ -0,0 +1,184 @@
|
||||
---
|
||||
name: software-manual
|
||||
description: Generate interactive TiddlyWiki-style HTML software manuals with screenshots, API docs, and multi-level code examples. Use when creating user guides, software documentation, or API references. Triggers on "software manual", "user guide", "generate manual", "create docs".
|
||||
allowed-tools: Task, AskUserQuestion, Read, Bash, Glob, Grep, Write, mcp__chrome__*
|
||||
---
|
||||
|
||||
# Software Manual Skill
|
||||
|
||||
Generate comprehensive, interactive software manuals in TiddlyWiki-style single-file HTML format.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Context-Optimized Architecture │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Phase 1: Requirements → manual-config.json │
|
||||
│ ↓ │
|
||||
│ Phase 2: Exploration → exploration-*.json │
|
||||
│ ↓ │
|
||||
│ Phase 3: Parallel Agents → sections/section-*.md │
|
||||
│ ↓ (6 Agents) │
|
||||
│ Phase 3.5: Consolidation → consolidation-summary.md │
|
||||
│ ↓ │
|
||||
│ Phase 4: Screenshot → screenshots/*.png │
|
||||
│ Capture (via Chrome MCP) │
|
||||
│ ↓ │
|
||||
│ Phase 5: HTML Assembly → {name}-使用手册.html │
|
||||
│ ↓ │
|
||||
│ Phase 6: Refinement → iterations/ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Key Design Principles
|
||||
|
||||
1. **主 Agent 编排,子 Agent 执行**: 所有繁重计算委托给 `universal-executor` 子 Agent
|
||||
2. **Brief Returns**: Agents return path + summary, not full content (avoid context overflow)
|
||||
3. **System Agents**: 使用 `cli-explore-agent` (探索) 和 `universal-executor` (执行)
|
||||
4. **成熟库内嵌**: marked.js (MD 解析) + highlight.js (语法高亮),无 CDN 依赖
|
||||
5. **Single-File HTML**: TiddlyWiki-style interactive document with embedded resources
|
||||
6. **动态标签**: 根据实际章节自动生成导航标签
|
||||
|
||||
## Execution Flow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Phase 1: Requirements Discovery (主 Agent) │
|
||||
│ → AskUserQuestion: 收集软件类型、目标用户、文档范围 │
|
||||
│ → Output: manual-config.json │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Phase 2: Project Exploration (cli-explore-agent × N) │
|
||||
│ → 并行探索: architecture, ui-routes, api-endpoints, config │
|
||||
│ → Output: exploration-*.json │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Phase 2.5: API Extraction (extract_apis.py) │
|
||||
│ → 自动提取: FastAPI/TypeDoc/pdoc │
|
||||
│ → Output: api-docs/{backend,frontend,modules}/*.md │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Phase 3: Parallel Analysis (universal-executor × 6) │
|
||||
│ → 6 个子 Agent 并行: overview, ui-guide, api-docs, config, │
|
||||
│ troubleshooting, code-examples │
|
||||
│ → Output: sections/section-*.md │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Phase 3.5: Consolidation (universal-executor) │
|
||||
│ → 质量检查: 一致性、交叉引用、截图标记 │
|
||||
│ → Output: consolidation-summary.md, screenshots-list.json │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Phase 4: Screenshot Capture (universal-executor + Chrome MCP) │
|
||||
│ → 批量截图: 调用 mcp__chrome__screenshot │
|
||||
│ → Output: screenshots/*.png + manifest.json │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Phase 5: HTML Assembly (universal-executor) │
|
||||
│ → 组装 HTML: MD→tiddlers, 嵌入 CSS/JS/图片 │
|
||||
│ → Output: {name}-使用手册.html │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Phase 6: Iterative Refinement (主 Agent) │
|
||||
│ → 预览 + 用户反馈 + 迭代修复 │
|
||||
│ → Output: iterations/v*.html │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Agent Configuration
|
||||
|
||||
| Agent | Role | Output File | Focus Areas |
|
||||
|-------|------|-------------|-------------|
|
||||
| overview | Product Manager | section-overview.md | Product intro, features, quick start |
|
||||
| ui-guide | UX Expert | section-ui-guide.md | UI operations, step-by-step guides |
|
||||
| api-docs | API Architect | section-api-reference.md | REST API, Frontend API |
|
||||
| config | DevOps Engineer | section-configuration.md | Env vars, deployment, settings |
|
||||
| troubleshooting | Support Engineer | section-troubleshooting.md | FAQs, error codes, solutions |
|
||||
| code-examples | Developer Advocate | section-examples.md | Beginner/Intermediate/Advanced examples |
|
||||
|
||||
## Agent Return Format
|
||||
|
||||
```typescript
|
||||
interface ManualAgentReturn {
|
||||
status: "completed" | "partial" | "failed";
|
||||
output_file: string;
|
||||
summary: string; // Max 50 chars
|
||||
screenshots_needed: Array<{
|
||||
id: string; // e.g., "ss-login-form"
|
||||
url: string; // Relative or absolute URL
|
||||
description: string; // "Login form interface"
|
||||
selector?: string; // CSS selector for partial screenshot
|
||||
wait_for?: string; // Element to wait for
|
||||
}>;
|
||||
cross_references: string[]; // Other sections referenced
|
||||
difficulty_level: "beginner" | "intermediate" | "advanced";
|
||||
}
|
||||
```
|
||||
|
||||
## HTML Features (TiddlyWiki-style)
|
||||
|
||||
1. **Search**: Full-text search with result highlighting
|
||||
2. **Collapse/Expand**: Per-section collapsible content
|
||||
3. **Tag Navigation**: Filter by category tags
|
||||
4. **Theme Toggle**: Light/Dark mode with localStorage persistence
|
||||
5. **Single File**: All CSS/JS/images embedded as Base64
|
||||
6. **Offline**: Works without internet connection
|
||||
7. **Print-friendly**: Optimized print stylesheet
|
||||
|
||||
## Directory Setup
|
||||
|
||||
```javascript
|
||||
// Generate timestamp directory name
|
||||
const timestamp = new Date().toISOString().slice(0,19).replace(/[-:T]/g, '');
|
||||
const dir = `.workflow/.scratchpad/manual-${timestamp}`;
|
||||
|
||||
// Windows
|
||||
Bash(`mkdir "${dir}\\sections" && mkdir "${dir}\\screenshots" && mkdir "${dir}\\api-docs" && mkdir "${dir}\\iterations"`);
|
||||
```
|
||||
|
||||
## Output Structure
|
||||
|
||||
```
|
||||
.workflow/.scratchpad/manual-{timestamp}/
|
||||
├── manual-config.json # Phase 1
|
||||
├── exploration/ # Phase 2
|
||||
│ ├── exploration-architecture.json
|
||||
│ ├── exploration-ui-routes.json
|
||||
│ └── exploration-api-endpoints.json
|
||||
├── sections/ # Phase 3
|
||||
│ ├── section-overview.md
|
||||
│ ├── section-ui-guide.md
|
||||
│ ├── section-api-reference.md
|
||||
│ ├── section-configuration.md
|
||||
│ ├── section-troubleshooting.md
|
||||
│ └── section-examples.md
|
||||
├── consolidation-summary.md # Phase 3.5
|
||||
├── api-docs/ # API documentation
|
||||
│ ├── frontend/ # TypeDoc output
|
||||
│ └── backend/ # Swagger/OpenAPI output
|
||||
├── screenshots/ # Phase 4
|
||||
│ ├── ss-*.png
|
||||
│ └── screenshots-manifest.json
|
||||
├── iterations/ # Phase 6
|
||||
│ ├── v1.html
|
||||
│ └── v2.html
|
||||
└── {软件名}-使用手册.html # Final Output
|
||||
```
|
||||
|
||||
## Reference Documents
|
||||
|
||||
| Document | Purpose |
|
||||
|----------|---------|
|
||||
| [phases/01-requirements-discovery.md](phases/01-requirements-discovery.md) | 用户配置收集 |
|
||||
| [phases/02-project-exploration.md](phases/02-project-exploration.md) | 项目类型检测 |
|
||||
| [phases/02.5-api-extraction.md](phases/02.5-api-extraction.md) | API 自动提取 |
|
||||
| [phases/03-parallel-analysis.md](phases/03-parallel-analysis.md) | 6 Agent 并行分析 |
|
||||
| [phases/03.5-consolidation.md](phases/03.5-consolidation.md) | 整合与质量检查 |
|
||||
| [phases/04-screenshot-capture.md](phases/04-screenshot-capture.md) | Chrome MCP 截图 |
|
||||
| [phases/05-html-assembly.md](phases/05-html-assembly.md) | HTML 组装 |
|
||||
| [phases/06-iterative-refinement.md](phases/06-iterative-refinement.md) | 迭代优化 |
|
||||
| [specs/quality-standards.md](specs/quality-standards.md) | 质量标准 |
|
||||
| [specs/writing-style.md](specs/writing-style.md) | 写作风格 |
|
||||
| [templates/tiddlywiki-shell.html](templates/tiddlywiki-shell.html) | HTML 模板 |
|
||||
| [templates/css/wiki-base.css](templates/css/wiki-base.css) | 基础样式 |
|
||||
| [templates/css/wiki-dark.css](templates/css/wiki-dark.css) | 暗色主题 |
|
||||
| [scripts/bundle-libraries.md](scripts/bundle-libraries.md) | 库文件打包 |
|
||||
| [scripts/api-extractor.md](scripts/api-extractor.md) | API 提取说明 |
|
||||
| [scripts/extract_apis.py](scripts/extract_apis.py) | API 提取脚本 |
|
||||
| [scripts/screenshot-helper.md](scripts/screenshot-helper.md) | 截图辅助 |
|
||||
@@ -0,0 +1,162 @@
|
||||
# Phase 1: Requirements Discovery
|
||||
|
||||
Collect user requirements and generate configuration for the manual generation process.
|
||||
|
||||
## Objective
|
||||
|
||||
Gather essential information about the software project to customize the manual generation:
|
||||
- Software type and characteristics
|
||||
- Target user audience
|
||||
- Documentation scope and depth
|
||||
- Special requirements
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### Step 1: Software Information Collection
|
||||
|
||||
Use `AskUserQuestion` to collect:
|
||||
|
||||
```javascript
|
||||
AskUserQuestion({
|
||||
questions: [
|
||||
{
|
||||
question: "What type of software is this project?",
|
||||
header: "Software Type",
|
||||
options: [
|
||||
{ label: "Web Application", description: "Frontend + Backend web app with UI" },
|
||||
{ label: "CLI Tool", description: "Command-line interface tool" },
|
||||
{ label: "SDK/Library", description: "Developer library or SDK" },
|
||||
{ label: "Desktop App", description: "Desktop application (Electron, etc.)" }
|
||||
],
|
||||
multiSelect: false
|
||||
},
|
||||
{
|
||||
question: "Who is the target audience for this manual?",
|
||||
header: "Target Users",
|
||||
options: [
|
||||
{ label: "End Users", description: "Non-technical users who use the product" },
|
||||
{ label: "Developers", description: "Developers integrating or extending the product" },
|
||||
{ label: "Administrators", description: "System admins deploying and maintaining" },
|
||||
{ label: "All Audiences", description: "Mixed audience with different sections" }
|
||||
],
|
||||
multiSelect: false
|
||||
},
|
||||
{
|
||||
question: "What documentation scope do you need?",
|
||||
header: "Doc Scope",
|
||||
options: [
|
||||
{ label: "Quick Start", description: "Essential getting started guide only" },
|
||||
{ label: "User Guide", description: "Complete user-facing documentation" },
|
||||
{ label: "API Reference", description: "Focus on API documentation" },
|
||||
{ label: "Comprehensive", description: "Full documentation including all sections" }
|
||||
],
|
||||
multiSelect: false
|
||||
},
|
||||
{
|
||||
question: "What difficulty levels should code examples cover?",
|
||||
header: "Example Levels",
|
||||
options: [
|
||||
{ label: "Beginner Only", description: "Simple, basic examples" },
|
||||
{ label: "Beginner + Intermediate", description: "Basic to moderate complexity" },
|
||||
{ label: "All Levels", description: "Beginner, Intermediate, and Advanced" }
|
||||
],
|
||||
multiSelect: false
|
||||
}
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
### Step 2: Auto-Detection (Supplement)
|
||||
|
||||
Automatically detect project characteristics:
|
||||
|
||||
```javascript
|
||||
// Detect from package.json
|
||||
const packageJson = Read('package.json');
|
||||
const softwareName = packageJson.name;
|
||||
const version = packageJson.version;
|
||||
const description = packageJson.description;
|
||||
|
||||
// Detect tech stack
|
||||
const hasReact = packageJson.dependencies?.react;
|
||||
const hasVue = packageJson.dependencies?.vue;
|
||||
const hasExpress = packageJson.dependencies?.express;
|
||||
const hasNestJS = packageJson.dependencies?.['@nestjs/core'];
|
||||
|
||||
// Detect CLI
|
||||
const hasBin = !!packageJson.bin;
|
||||
|
||||
// Detect UI
|
||||
const hasPages = Glob('src/pages/**/*').length > 0 || Glob('pages/**/*').length > 0;
|
||||
const hasRoutes = Glob('**/routes.*').length > 0;
|
||||
```
|
||||
|
||||
### Step 3: Generate Configuration
|
||||
|
||||
Create `manual-config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"software": {
|
||||
"name": "{{detected or user input}}",
|
||||
"version": "{{from package.json}}",
|
||||
"description": "{{from package.json}}",
|
||||
"type": "{{web|cli|sdk|desktop}}"
|
||||
},
|
||||
"target_audience": "{{end_users|developers|admins|all}}",
|
||||
"doc_scope": "{{quick_start|user_guide|api_reference|comprehensive}}",
|
||||
"example_levels": ["beginner", "intermediate", "advanced"],
|
||||
"tech_stack": {
|
||||
"frontend": "{{react|vue|angular|vanilla}}",
|
||||
"backend": "{{express|nestjs|fastify|none}}",
|
||||
"language": "{{typescript|javascript}}",
|
||||
"ui_framework": "{{tailwind|mui|antd|none}}"
|
||||
},
|
||||
"features": {
|
||||
"has_ui": true,
|
||||
"has_api": true,
|
||||
"has_cli": false,
|
||||
"has_config": true
|
||||
},
|
||||
"agents_to_run": [
|
||||
"overview",
|
||||
"ui-guide",
|
||||
"api-docs",
|
||||
"config",
|
||||
"troubleshooting",
|
||||
"code-examples"
|
||||
],
|
||||
"screenshot_config": {
|
||||
"enabled": true,
|
||||
"dev_command": "npm run dev",
|
||||
"dev_url": "http://localhost:3000",
|
||||
"wait_timeout": 5000
|
||||
},
|
||||
"output": {
|
||||
"filename": "{{name}}-使用手册.html",
|
||||
"theme": "light",
|
||||
"language": "zh-CN"
|
||||
},
|
||||
"timestamp": "{{ISO8601}}"
|
||||
}
|
||||
```
|
||||
|
||||
## Agent Selection Logic
|
||||
|
||||
Based on `doc_scope`, select agents to run:
|
||||
|
||||
| Scope | Agents |
|
||||
|-------|--------|
|
||||
| quick_start | overview |
|
||||
| user_guide | overview, ui-guide, config, troubleshooting |
|
||||
| api_reference | overview, api-docs, code-examples |
|
||||
| comprehensive | ALL 6 agents |
|
||||
|
||||
## Output
|
||||
|
||||
- **File**: `manual-config.json`
|
||||
- **Location**: `.workflow/.scratchpad/manual-{timestamp}/`
|
||||
|
||||
## Next Phase
|
||||
|
||||
Proceed to [Phase 2: Project Exploration](02-project-exploration.md) with the generated configuration.
|
||||
101
.claude/skills/software-manual/phases/02-project-exploration.md
Normal file
101
.claude/skills/software-manual/phases/02-project-exploration.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# Phase 2: Project Exploration
|
||||
|
||||
使用 `cli-explore-agent` 探索项目结构,生成文档所需的结构化数据。
|
||||
|
||||
## 探索角度
|
||||
|
||||
```javascript
|
||||
const EXPLORATION_ANGLES = {
|
||||
web: ['architecture', 'ui-routes', 'api-endpoints', 'config'],
|
||||
cli: ['architecture', 'commands', 'config'],
|
||||
sdk: ['architecture', 'public-api', 'types', 'config'],
|
||||
desktop: ['architecture', 'ui-screens', 'config']
|
||||
};
|
||||
```
|
||||
|
||||
## 执行流程
|
||||
|
||||
```javascript
|
||||
const config = JSON.parse(Read(`${workDir}/manual-config.json`));
|
||||
const angles = EXPLORATION_ANGLES[config.software.type];
|
||||
|
||||
// 并行探索
|
||||
const tasks = angles.map(angle => Task({
|
||||
subagent_type: 'cli-explore-agent',
|
||||
run_in_background: false,
|
||||
prompt: buildExplorationPrompt(angle, config, workDir)
|
||||
}));
|
||||
|
||||
const results = await Promise.all(tasks);
|
||||
```
|
||||
|
||||
## 探索配置
|
||||
|
||||
```javascript
|
||||
const EXPLORATION_CONFIGS = {
|
||||
architecture: {
|
||||
task: '分析项目模块结构、入口点、依赖关系',
|
||||
patterns: ['src/*/', 'package.json', 'tsconfig.json'],
|
||||
output: 'exploration-architecture.json'
|
||||
},
|
||||
'ui-routes': {
|
||||
task: '提取 UI 路由、页面组件、导航结构',
|
||||
patterns: ['src/pages/**', 'src/views/**', 'app/**/page.*', 'src/router/**'],
|
||||
output: 'exploration-ui-routes.json'
|
||||
},
|
||||
'api-endpoints': {
|
||||
task: '提取 REST API 端点、请求/响应类型',
|
||||
patterns: ['src/**/*.controller.*', 'src/routes/**', 'openapi.*', 'swagger.*'],
|
||||
output: 'exploration-api-endpoints.json'
|
||||
},
|
||||
config: {
|
||||
task: '提取环境变量、配置文件选项',
|
||||
patterns: ['.env.example', 'config/**', 'docker-compose.yml'],
|
||||
output: 'exploration-config.json'
|
||||
},
|
||||
commands: {
|
||||
task: '提取 CLI 命令、选项、示例',
|
||||
patterns: ['src/cli*', 'bin/*', 'src/commands/**'],
|
||||
output: 'exploration-commands.json'
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Prompt 构建
|
||||
|
||||
```javascript
|
||||
function buildExplorationPrompt(angle, config, workDir) {
|
||||
const cfg = EXPLORATION_CONFIGS[angle];
|
||||
return `
|
||||
[TASK]
|
||||
${cfg.task}
|
||||
|
||||
[SCOPE]
|
||||
项目类型: ${config.software.type}
|
||||
扫描模式: deep-scan
|
||||
文件模式: ${cfg.patterns.join(', ')}
|
||||
|
||||
[OUTPUT]
|
||||
文件: ${workDir}/exploration/${cfg.output}
|
||||
格式: JSON (schema-compliant)
|
||||
|
||||
[RETURN]
|
||||
简要说明发现的内容数量和关键发现
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
## 输出结构
|
||||
|
||||
```
|
||||
exploration/
|
||||
├── exploration-architecture.json # 模块结构
|
||||
├── exploration-ui-routes.json # UI 路由
|
||||
├── exploration-api-endpoints.json # API 端点
|
||||
├── exploration-config.json # 配置选项
|
||||
└── exploration-commands.json # CLI 命令 (if CLI)
|
||||
```
|
||||
|
||||
## 下一阶段
|
||||
|
||||
→ [Phase 3: Parallel Analysis](03-parallel-analysis.md)
|
||||
161
.claude/skills/software-manual/phases/02.5-api-extraction.md
Normal file
161
.claude/skills/software-manual/phases/02.5-api-extraction.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# Phase 2.5: API Extraction
|
||||
|
||||
在项目探索后、并行分析前,自动提取 API 文档。
|
||||
|
||||
## 核心原则
|
||||
|
||||
**使用成熟工具提取,确保输出格式与 wiki 模板兼容。**
|
||||
|
||||
## 执行流程
|
||||
|
||||
```javascript
|
||||
const config = JSON.parse(Read(`${workDir}/manual-config.json`));
|
||||
|
||||
// 检查项目路径配置
|
||||
const apiSources = config.api_sources || detectApiSources(config.project_path);
|
||||
|
||||
// 执行 API 提取
|
||||
Bash({
|
||||
command: `python .claude/skills/software-manual/scripts/extract_apis.py -o "${workDir}" -p ${apiSources.join(' ')}`
|
||||
});
|
||||
|
||||
// 验证输出
|
||||
const apiDocsDir = `${workDir}/api-docs`;
|
||||
const extractedFiles = Glob(`${apiDocsDir}/**/*.{json,md}`);
|
||||
console.log(`Extracted ${extractedFiles.length} API documentation files`);
|
||||
```
|
||||
|
||||
## 支持的项目类型
|
||||
|
||||
| 类型 | 检测方式 | 提取工具 | 输出格式 |
|
||||
|------|----------|----------|----------|
|
||||
| FastAPI | `app/main.py` + FastAPI import | OpenAPI JSON | `openapi.json` + `API_SUMMARY.md` |
|
||||
| Next.js | `package.json` + next | TypeDoc | `*.md` (Markdown) |
|
||||
| Python Module | `__init__.py` + setup.py/pyproject.toml | pdoc | `*.md` (Markdown) |
|
||||
| Express | `package.json` + express | swagger-jsdoc | `openapi.json` |
|
||||
| NestJS | `package.json` + @nestjs | @nestjs/swagger | `openapi.json` |
|
||||
|
||||
## 输出格式规范
|
||||
|
||||
### Markdown 兼容性要求
|
||||
|
||||
确保输出 Markdown 与 wiki CSS 样式兼容:
|
||||
|
||||
```markdown
|
||||
# API Reference → <h1> (wiki-base.css)
|
||||
|
||||
## Endpoints → <h2>
|
||||
|
||||
| Method | Path | Summary | → <table> 蓝色表头
|
||||
|--------|------|---------|
|
||||
| `GET` | `/api/...` | ... | → <code> 红色高亮
|
||||
|
||||
### GET /api/users → <h3>
|
||||
|
||||
\`\`\`json → <pre><code> 深色背景
|
||||
{
|
||||
"id": 1,
|
||||
"name": "example"
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
- Parameter: `id` (required) → <ul><li> + <code>
|
||||
```
|
||||
|
||||
### 格式验证检查
|
||||
|
||||
```javascript
|
||||
function validateApiDocsFormat(apiDocsDir) {
|
||||
const issues = [];
|
||||
const mdFiles = Glob(`${apiDocsDir}/**/*.md`);
|
||||
|
||||
for (const file of mdFiles) {
|
||||
const content = Read(file);
|
||||
|
||||
// 检查表格格式
|
||||
if (content.includes('|') && !content.match(/\|.*\|.*\|/)) {
|
||||
issues.push(`${file}: 表格格式不完整`);
|
||||
}
|
||||
|
||||
// 检查代码块语言标注
|
||||
const codeBlocks = content.match(/```(\w*)\n/g) || [];
|
||||
const unlabeled = codeBlocks.filter(b => b === '```\n');
|
||||
if (unlabeled.length > 0) {
|
||||
issues.push(`${file}: ${unlabeled.length} 个代码块缺少语言标注`);
|
||||
}
|
||||
|
||||
// 检查标题层级
|
||||
if (!content.match(/^# /m)) {
|
||||
issues.push(`${file}: 缺少一级标题`);
|
||||
}
|
||||
}
|
||||
|
||||
return issues;
|
||||
}
|
||||
```
|
||||
|
||||
## 项目配置示例
|
||||
|
||||
在 `manual-config.json` 中配置 API 源:
|
||||
|
||||
```json
|
||||
{
|
||||
"software": {
|
||||
"name": "Hydro Generator Workbench",
|
||||
"type": "web"
|
||||
},
|
||||
"api_sources": {
|
||||
"backend": {
|
||||
"path": "D:/dongdiankaifa9/backend",
|
||||
"type": "fastapi",
|
||||
"entry": "app.main:app"
|
||||
},
|
||||
"frontend": {
|
||||
"path": "D:/dongdiankaifa9/frontend",
|
||||
"type": "typescript",
|
||||
"entries": ["lib", "hooks", "components"]
|
||||
},
|
||||
"hydro_generator_module": {
|
||||
"path": "D:/dongdiankaifa9/hydro_generator_module",
|
||||
"type": "python"
|
||||
},
|
||||
"multiphysics_network": {
|
||||
"path": "D:/dongdiankaifa9/multiphysics_network",
|
||||
"type": "python"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 输出结构
|
||||
|
||||
```
|
||||
{workDir}/api-docs/
|
||||
├── backend/
|
||||
│ ├── openapi.json # OpenAPI 3.0 规范
|
||||
│ └── API_SUMMARY.md # Markdown 摘要(wiki 兼容)
|
||||
├── frontend/
|
||||
│ ├── modules.md # TypeDoc 模块文档
|
||||
│ ├── classes/ # 类文档
|
||||
│ └── functions/ # 函数文档
|
||||
├── hydro_generator/
|
||||
│ ├── assembler.md # pdoc 模块文档
|
||||
│ ├── blueprint.md
|
||||
│ └── builders/
|
||||
└── multiphysics/
|
||||
├── analysis_domain.md
|
||||
├── builders.md
|
||||
└── compilers.md
|
||||
```
|
||||
|
||||
## 质量门禁
|
||||
|
||||
- [ ] 所有配置的 API 源已提取
|
||||
- [ ] Markdown 格式与 wiki CSS 兼容
|
||||
- [ ] 表格正确渲染(蓝色表头)
|
||||
- [ ] 代码块有语言标注
|
||||
- [ ] 无空文件或错误文件
|
||||
|
||||
## 下一阶段
|
||||
|
||||
→ [Phase 3: Parallel Analysis](03-parallel-analysis.md)
|
||||
183
.claude/skills/software-manual/phases/03-parallel-analysis.md
Normal file
183
.claude/skills/software-manual/phases/03-parallel-analysis.md
Normal file
@@ -0,0 +1,183 @@
|
||||
# Phase 3: Parallel Analysis
|
||||
|
||||
使用 `universal-executor` 并行生成 6 个文档章节。
|
||||
|
||||
## Agent 配置
|
||||
|
||||
```javascript
|
||||
const AGENT_CONFIGS = {
|
||||
overview: {
|
||||
role: 'Product Manager',
|
||||
output: 'section-overview.md',
|
||||
task: '撰写产品概览、核心功能、快速入门指南',
|
||||
focus: '产品定位、目标用户、5步快速入门、系统要求',
|
||||
input: ['exploration-architecture.json', 'README.md', 'package.json'],
|
||||
tag: 'getting-started'
|
||||
},
|
||||
'interface-guide': {
|
||||
role: 'Product Designer',
|
||||
output: 'section-interface.md',
|
||||
task: '撰写界面或交互指南(Web 截图、CLI 命令交互、桌面应用操作)',
|
||||
focus: '视觉布局、交互流程、命令行参数、输入/输出示例',
|
||||
input: ['exploration-ui-routes.json', 'src/**', 'pages/**', 'views/**', 'components/**', 'src/commands/**'],
|
||||
tag: 'interface',
|
||||
screenshot_rules: `
|
||||
根据项目类型标注交互点:
|
||||
|
||||
[Web] <!-- SCREENSHOT: id="ss-{功能}" url="{路由}" selector="{CSS选择器}" description="{描述}" -->
|
||||
[CLI] 使用代码块展示命令交互:
|
||||
\`\`\`bash
|
||||
$ command --flag value
|
||||
Expected output here
|
||||
\`\`\`
|
||||
[Desktop] <!-- SCREENSHOT: id="ss-{功能}" description="{描述}" -->
|
||||
`
|
||||
},
|
||||
'api-reference': {
|
||||
role: 'Technical Architect',
|
||||
output: 'section-reference.md',
|
||||
task: '撰写接口参考文档(REST API / 函数库 / CLI 命令)',
|
||||
focus: '函数签名、端点定义、参数说明、返回值、错误代码',
|
||||
pre_extract: 'python .claude/skills/software-manual/scripts/extract_apis.py -o ${workDir}',
|
||||
input: [
|
||||
'${workDir}/api-docs/backend/openapi.json', // FastAPI OpenAPI
|
||||
'${workDir}/api-docs/backend/API_SUMMARY.md', // Backend summary
|
||||
'${workDir}/api-docs/frontend/**/*.md', // TypeDoc output
|
||||
'${workDir}/api-docs/hydro_generator/**/*.md', // Python module
|
||||
'${workDir}/api-docs/multiphysics/**/*.md' // Python module
|
||||
],
|
||||
tag: 'api'
|
||||
},
|
||||
config: {
|
||||
role: 'DevOps Engineer',
|
||||
output: 'section-configuration.md',
|
||||
task: '撰写配置指南,涵盖环境变量、配置文件、部署设置',
|
||||
focus: '环境变量表格、配置文件格式、部署选项、安全设置',
|
||||
input: ['exploration-config.json', '.env.example', 'config/**', '*.config.*'],
|
||||
tag: 'config'
|
||||
},
|
||||
troubleshooting: {
|
||||
role: 'Support Engineer',
|
||||
output: 'section-troubleshooting.md',
|
||||
task: '撰写故障排查指南,涵盖常见问题、错误码、FAQ',
|
||||
focus: '常见问题与解决方案、错误码参考、FAQ、获取帮助',
|
||||
input: ['docs/troubleshooting.md', 'src/**/errors.*', 'src/**/exceptions.*', 'TROUBLESHOOTING.md'],
|
||||
tag: 'troubleshooting'
|
||||
},
|
||||
'code-examples': {
|
||||
role: 'Developer Advocate',
|
||||
output: 'section-examples.md',
|
||||
task: '撰写多难度级别代码示例(入门40%/进阶40%/高级20%)',
|
||||
focus: '完整可运行代码、分步解释、预期输出、最佳实践',
|
||||
input: ['examples/**', 'tests/**', 'demo/**', 'samples/**'],
|
||||
tag: 'examples'
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## 执行流程
|
||||
|
||||
```javascript
|
||||
const config = JSON.parse(Read(`${workDir}/manual-config.json`));
|
||||
|
||||
// 1. 预提取 API 文档(如有 pre_extract 配置)
|
||||
for (const [name, cfg] of Object.entries(AGENT_CONFIGS)) {
|
||||
if (cfg.pre_extract) {
|
||||
const cmd = cfg.pre_extract.replace(/\$\{workDir\}/g, workDir);
|
||||
console.log(`[Pre-extract] ${name}: ${cmd}`);
|
||||
Bash({ command: cmd });
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 并行启动 6 个 universal-executor
|
||||
const tasks = Object.entries(AGENT_CONFIGS).map(([name, cfg]) =>
|
||||
Task({
|
||||
subagent_type: 'universal-executor',
|
||||
run_in_background: false,
|
||||
prompt: buildAgentPrompt(name, cfg, config, workDir)
|
||||
})
|
||||
);
|
||||
|
||||
const results = await Promise.all(tasks);
|
||||
```
|
||||
|
||||
## Prompt 构建
|
||||
|
||||
```javascript
|
||||
function buildAgentPrompt(name, cfg, config, workDir) {
|
||||
const screenshotSection = cfg.screenshot_rules
|
||||
? `\n[SCREENSHOT RULES]\n${cfg.screenshot_rules}`
|
||||
: '';
|
||||
|
||||
return `
|
||||
[ROLE] ${cfg.role}
|
||||
|
||||
[PROJECT CONTEXT]
|
||||
项目类型: ${config.software.type} (web/cli/sdk/desktop)
|
||||
语言: ${config.software.language || 'auto-detect'}
|
||||
名称: ${config.software.name}
|
||||
|
||||
[TASK]
|
||||
${cfg.task}
|
||||
输出: ${workDir}/sections/${cfg.output}
|
||||
|
||||
[INPUT]
|
||||
- 配置: ${workDir}/manual-config.json
|
||||
- 探索结果: ${workDir}/exploration/
|
||||
- 扫描路径: ${cfg.input.join(', ')}
|
||||
|
||||
[CONTENT REQUIREMENTS]
|
||||
- 标题层级: # ## ### (最多3级)
|
||||
- 代码块: \`\`\`language ... \`\`\` (必须标注语言)
|
||||
- 表格: | col1 | col2 | 格式
|
||||
- 列表: 有序 1. 2. 3. / 无序 - - -
|
||||
- 内联代码: \`code\`
|
||||
- 链接: [text](url)
|
||||
${screenshotSection}
|
||||
|
||||
[FOCUS]
|
||||
${cfg.focus}
|
||||
|
||||
[OUTPUT FORMAT]
|
||||
Markdown 文件,包含:
|
||||
- 清晰的章节结构
|
||||
- 具体的代码示例
|
||||
- 参数/配置表格
|
||||
- 常见用例说明
|
||||
|
||||
[RETURN JSON]
|
||||
{
|
||||
"status": "completed",
|
||||
"output_file": "sections/${cfg.output}",
|
||||
"summary": "<50字>",
|
||||
"tag": "${cfg.tag}",
|
||||
"screenshots_needed": []
|
||||
}
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
## 结果收集
|
||||
|
||||
```javascript
|
||||
const agentResults = results.map(r => JSON.parse(r));
|
||||
const allScreenshots = agentResults.flatMap(r => r.screenshots_needed);
|
||||
|
||||
Write(`${workDir}/agent-results.json`, JSON.stringify({
|
||||
results: agentResults,
|
||||
screenshots_needed: allScreenshots,
|
||||
timestamp: new Date().toISOString()
|
||||
}, null, 2));
|
||||
```
|
||||
|
||||
## 质量检查
|
||||
|
||||
- [ ] Markdown 语法有效
|
||||
- [ ] 无占位符文本
|
||||
- [ ] 代码块标注语言
|
||||
- [ ] 截图标记格式正确
|
||||
- [ ] 交叉引用有效
|
||||
|
||||
## 下一阶段
|
||||
|
||||
→ [Phase 3.5: Consolidation](03.5-consolidation.md)
|
||||
82
.claude/skills/software-manual/phases/03.5-consolidation.md
Normal file
82
.claude/skills/software-manual/phases/03.5-consolidation.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# Phase 3.5: Consolidation
|
||||
|
||||
使用 `universal-executor` 子 Agent 执行质量检查,避免主 Agent 内存溢出。
|
||||
|
||||
## 核心原则
|
||||
|
||||
**主 Agent 负责编排,子 Agent 负责繁重计算。**
|
||||
|
||||
## 执行流程
|
||||
|
||||
```javascript
|
||||
const agentResults = JSON.parse(Read(`${workDir}/agent-results.json`));
|
||||
|
||||
// 委托给 universal-executor 执行整合检查
|
||||
const result = Task({
|
||||
subagent_type: 'universal-executor',
|
||||
run_in_background: false,
|
||||
prompt: buildConsolidationPrompt(workDir)
|
||||
});
|
||||
|
||||
const consolidationResult = JSON.parse(result);
|
||||
```
|
||||
|
||||
## Prompt 构建
|
||||
|
||||
```javascript
|
||||
function buildConsolidationPrompt(workDir) {
|
||||
return `
|
||||
[ROLE] Quality Analyst
|
||||
|
||||
[TASK]
|
||||
检查所有章节的一致性和完整性
|
||||
|
||||
[INPUT]
|
||||
- 章节文件: ${workDir}/sections/section-*.md
|
||||
- Agent 结果: ${workDir}/agent-results.json
|
||||
|
||||
[CHECKS]
|
||||
1. Markdown 语法有效性
|
||||
2. 截图标记格式 (<!-- SCREENSHOT: id="..." -->)
|
||||
3. 交叉引用有效性
|
||||
4. 术语一致性
|
||||
5. 代码块语言标注
|
||||
|
||||
[OUTPUT]
|
||||
1. 写入 ${workDir}/consolidation-summary.md
|
||||
2. 写入 ${workDir}/screenshots-list.json (截图清单)
|
||||
|
||||
[RETURN JSON]
|
||||
{
|
||||
"status": "completed",
|
||||
"sections_checked": <n>,
|
||||
"screenshots_found": <n>,
|
||||
"issues": { "errors": <n>, "warnings": <n> },
|
||||
"quality_score": <0-100>
|
||||
}
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
## Agent 职责
|
||||
|
||||
1. **读取章节** → 逐个检查 section-*.md
|
||||
2. **提取截图** → 收集所有截图标记
|
||||
3. **验证引用** → 检查交叉引用有效性
|
||||
4. **评估质量** → 计算综合分数
|
||||
5. **输出报告** → consolidation-summary.md
|
||||
|
||||
## 输出
|
||||
|
||||
- `consolidation-summary.md` - 质量报告
|
||||
- `screenshots-list.json` - 截图清单(供 Phase 4 使用)
|
||||
|
||||
## 质量门禁
|
||||
|
||||
- [ ] 无错误
|
||||
- [ ] 总分 >= 60%
|
||||
- [ ] 交叉引用有效
|
||||
|
||||
## 下一阶段
|
||||
|
||||
→ [Phase 4: Screenshot Capture](04-screenshot-capture.md)
|
||||
@@ -0,0 +1,89 @@
|
||||
# Phase 4: Screenshot Capture
|
||||
|
||||
使用 `universal-executor` 子 Agent 调用 Chrome MCP 截图。
|
||||
|
||||
## 核心原则
|
||||
|
||||
**主 Agent 负责编排,子 Agent 负责截图采集。**
|
||||
|
||||
## 执行流程
|
||||
|
||||
```javascript
|
||||
const config = JSON.parse(Read(`${workDir}/manual-config.json`));
|
||||
const screenshotsList = JSON.parse(Read(`${workDir}/screenshots-list.json`));
|
||||
|
||||
// 委托给 universal-executor 执行截图
|
||||
const result = Task({
|
||||
subagent_type: 'universal-executor',
|
||||
run_in_background: false,
|
||||
prompt: buildScreenshotPrompt(config, screenshotsList, workDir)
|
||||
});
|
||||
|
||||
const captureResult = JSON.parse(result);
|
||||
```
|
||||
|
||||
## Prompt 构建
|
||||
|
||||
```javascript
|
||||
function buildScreenshotPrompt(config, screenshotsList, workDir) {
|
||||
return `
|
||||
[ROLE] Screenshot Capturer
|
||||
|
||||
[TASK]
|
||||
使用 Chrome MCP 批量截图
|
||||
|
||||
[INPUT]
|
||||
- 配置: ${workDir}/manual-config.json
|
||||
- 截图清单: ${workDir}/screenshots-list.json
|
||||
|
||||
[STEPS]
|
||||
1. 检查 Chrome MCP 可用性 (mcp__chrome__*)
|
||||
2. 启动开发服务器: ${config.screenshot_config?.dev_command || 'npm run dev'}
|
||||
3. 等待服务器就绪: ${config.screenshot_config?.dev_url || 'http://localhost:3000'}
|
||||
4. 遍历截图清单,逐个调用 mcp__chrome__screenshot
|
||||
5. 保存截图到 ${workDir}/screenshots/
|
||||
6. 生成 manifest: ${workDir}/screenshots/screenshots-manifest.json
|
||||
7. 停止开发服务器
|
||||
|
||||
[MCP CALLS]
|
||||
- mcp__chrome__screenshot({ url, selector?, viewport })
|
||||
- 保存为 PNG 文件
|
||||
|
||||
[FALLBACK]
|
||||
若 Chrome MCP 不可用,生成手动截图指南: MANUAL_CAPTURE.md
|
||||
|
||||
[RETURN JSON]
|
||||
{
|
||||
"status": "completed|skipped",
|
||||
"captured": <n>,
|
||||
"failed": <n>,
|
||||
"manifest_file": "screenshots-manifest.json"
|
||||
}
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
## Agent 职责
|
||||
|
||||
1. **检查 MCP** → Chrome MCP 可用性
|
||||
2. **启动服务** → 开发服务器
|
||||
3. **批量截图** → 调用 mcp__chrome__screenshot
|
||||
4. **保存文件** → screenshots/*.png
|
||||
5. **生成清单** → screenshots-manifest.json
|
||||
|
||||
## 输出
|
||||
|
||||
- `screenshots/*.png` - 截图文件
|
||||
- `screenshots/screenshots-manifest.json` - 清单
|
||||
- `screenshots/MANUAL_CAPTURE.md` - 手动指南(fallback)
|
||||
|
||||
## 质量门禁
|
||||
|
||||
- [ ] 高优先级截图完成
|
||||
- [ ] 尺寸一致 (1280×800)
|
||||
- [ ] 无空白截图
|
||||
- [ ] Manifest 完整
|
||||
|
||||
## 下一阶段
|
||||
|
||||
→ [Phase 5: HTML Assembly](05-html-assembly.md)
|
||||
132
.claude/skills/software-manual/phases/05-html-assembly.md
Normal file
132
.claude/skills/software-manual/phases/05-html-assembly.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# Phase 5: HTML Assembly
|
||||
|
||||
使用 `universal-executor` 子 Agent 生成最终 HTML,避免主 Agent 内存溢出。
|
||||
|
||||
## 核心原则
|
||||
|
||||
**主 Agent 负责编排,子 Agent 负责繁重计算。**
|
||||
|
||||
## 执行流程
|
||||
|
||||
```javascript
|
||||
const config = JSON.parse(Read(`${workDir}/manual-config.json`));
|
||||
|
||||
// 委托给 universal-executor 执行 HTML 组装
|
||||
const result = Task({
|
||||
subagent_type: 'universal-executor',
|
||||
run_in_background: false,
|
||||
prompt: buildAssemblyPrompt(config, workDir)
|
||||
});
|
||||
|
||||
const buildResult = JSON.parse(result);
|
||||
```
|
||||
|
||||
## Prompt 构建
|
||||
|
||||
```javascript
|
||||
function buildAssemblyPrompt(config, workDir) {
|
||||
return `
|
||||
[ROLE] HTML Assembler
|
||||
|
||||
[TASK]
|
||||
生成 TiddlyWiki 风格的交互式 HTML 手册(使用成熟库,无外部 CDN 依赖)
|
||||
|
||||
[INPUT]
|
||||
- 模板: .claude/skills/software-manual/templates/tiddlywiki-shell.html
|
||||
- CSS: .claude/skills/software-manual/templates/css/wiki-base.css, wiki-dark.css
|
||||
- 配置: ${workDir}/manual-config.json
|
||||
- 章节: ${workDir}/sections/section-*.md
|
||||
- Agent 结果: ${workDir}/agent-results.json (含 tag 信息)
|
||||
- 截图: ${workDir}/screenshots/
|
||||
|
||||
[LIBRARIES TO EMBED]
|
||||
1. marked.js (v14+) - Markdown 转 HTML
|
||||
- 从 https://unpkg.com/marked/marked.min.js 获取内容内嵌
|
||||
2. highlight.js (v11+) - 代码语法高亮
|
||||
- 核心 + 常用语言包 (js, ts, python, bash, json, yaml, html, css)
|
||||
- 使用 github-dark 主题
|
||||
|
||||
[STEPS]
|
||||
1. 读取 HTML 模板和 CSS
|
||||
2. 内嵌 marked.js 和 highlight.js 代码
|
||||
3. 读取 agent-results.json 提取各章节 tag
|
||||
4. 动态生成 {{TAG_BUTTONS_HTML}} (基于实际使用的 tags)
|
||||
5. 逐个读取 section-*.md,使用 marked 转换为 HTML
|
||||
6. 为代码块添加 data-language 属性和语法高亮
|
||||
7. 处理 <!-- SCREENSHOT: id="..." --> 标记,嵌入 Base64 图片
|
||||
8. 生成目录、搜索索引
|
||||
9. 组装最终 HTML,写入 ${workDir}/${config.software.name}-使用手册.html
|
||||
|
||||
[CONTENT FORMATTING]
|
||||
- 代码块: 深色背景 + 语言标签 + 语法高亮
|
||||
- 表格: 蓝色表头 + 边框 + 悬停效果
|
||||
- 内联代码: 红色高亮
|
||||
- 列表: 有序/无序样式增强
|
||||
- 左侧导航: 固定侧边栏 + TOC
|
||||
|
||||
[RETURN JSON]
|
||||
{
|
||||
"status": "completed",
|
||||
"output_file": "${config.software.name}-使用手册.html",
|
||||
"file_size": "<size>",
|
||||
"sections_count": <n>,
|
||||
"tags_generated": [],
|
||||
"screenshots_embedded": <n>
|
||||
}
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
## Agent 职责
|
||||
|
||||
1. **读取模板** → HTML + CSS
|
||||
2. **转换章节** → Markdown → HTML tiddlers
|
||||
3. **嵌入截图** → Base64 编码
|
||||
4. **生成索引** → 搜索数据
|
||||
5. **组装输出** → 单文件 HTML
|
||||
|
||||
## Markdown 转换规则
|
||||
|
||||
Agent 内部实现:
|
||||
|
||||
```
|
||||
# H1 → <h1>
|
||||
## H2 → <h2>
|
||||
### H3 → <h3>
|
||||
```code``` → <pre><code>
|
||||
**bold** → <strong>
|
||||
*italic* → <em>
|
||||
[text](url) → <a href>
|
||||
- item → <li>
|
||||
<!-- SCREENSHOT: id="xxx" --> → <figure><img src="data:..."></figure>
|
||||
```
|
||||
|
||||
## Tiddler 结构
|
||||
|
||||
```html
|
||||
<article class="tiddler" id="tiddler-{name}" data-tags="..." data-difficulty="...">
|
||||
<header class="tiddler-header">
|
||||
<h2><button class="collapse-toggle">▼</button> {title}</h2>
|
||||
<div class="tiddler-meta">{badges}</div>
|
||||
</header>
|
||||
<div class="tiddler-content">{html}</div>
|
||||
</article>
|
||||
```
|
||||
|
||||
## 输出
|
||||
|
||||
- `{软件名}-使用手册.html` - 最终 HTML
|
||||
- `build-report.json` - 构建报告
|
||||
|
||||
## 质量门禁
|
||||
|
||||
- [ ] HTML 渲染正确
|
||||
- [ ] 搜索功能可用
|
||||
- [ ] 折叠/展开正常
|
||||
- [ ] 主题切换持久化
|
||||
- [ ] 截图显示正确
|
||||
- [ ] 文件大小 < 10MB
|
||||
|
||||
## 下一阶段
|
||||
|
||||
→ [Phase 6: Iterative Refinement](06-iterative-refinement.md)
|
||||
259
.claude/skills/software-manual/phases/06-iterative-refinement.md
Normal file
259
.claude/skills/software-manual/phases/06-iterative-refinement.md
Normal file
@@ -0,0 +1,259 @@
|
||||
# Phase 6: Iterative Refinement
|
||||
|
||||
Preview, collect feedback, and iterate until quality meets standards.
|
||||
|
||||
## Objective
|
||||
|
||||
- Preview generated HTML in browser
|
||||
- Collect user feedback
|
||||
- Address issues iteratively
|
||||
- Finalize documentation
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### Step 1: Preview HTML
|
||||
|
||||
```javascript
|
||||
const buildReport = JSON.parse(Read(`${workDir}/build-report.json`));
|
||||
const outputFile = `${workDir}/${buildReport.output}`;
|
||||
|
||||
// Open in default browser for preview
|
||||
Bash({ command: `start "${outputFile}"` }); // Windows
|
||||
// Bash({ command: `open "${outputFile}"` }); // macOS
|
||||
|
||||
// Report to user
|
||||
console.log(`
|
||||
📖 Manual Preview
|
||||
|
||||
File: ${buildReport.output}
|
||||
Size: ${buildReport.size_human}
|
||||
Sections: ${buildReport.sections}
|
||||
Screenshots: ${buildReport.screenshots}
|
||||
|
||||
Please review the manual in your browser.
|
||||
`);
|
||||
```
|
||||
|
||||
### Step 2: Collect Feedback
|
||||
|
||||
```javascript
|
||||
const feedback = await AskUserQuestion({
|
||||
questions: [
|
||||
{
|
||||
question: "How does the manual look overall?",
|
||||
header: "Overall",
|
||||
options: [
|
||||
{ label: "Looks great!", description: "Ready to finalize" },
|
||||
{ label: "Minor issues", description: "Small tweaks needed" },
|
||||
{ label: "Major issues", description: "Significant changes required" },
|
||||
{ label: "Missing content", description: "Need to add more sections" }
|
||||
],
|
||||
multiSelect: false
|
||||
},
|
||||
{
|
||||
question: "Which aspects need improvement? (Select all that apply)",
|
||||
header: "Improvements",
|
||||
options: [
|
||||
{ label: "Content accuracy", description: "Fix incorrect information" },
|
||||
{ label: "More examples", description: "Add more code examples" },
|
||||
{ label: "Better screenshots", description: "Retake or add screenshots" },
|
||||
{ label: "Styling/Layout", description: "Improve visual appearance" }
|
||||
],
|
||||
multiSelect: true
|
||||
}
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
### Step 3: Address Feedback
|
||||
|
||||
Based on feedback, take appropriate action:
|
||||
|
||||
#### Minor Issues
|
||||
|
||||
```javascript
|
||||
if (feedback.overall === "Minor issues") {
|
||||
// Prompt for specific changes
|
||||
const details = await AskUserQuestion({
|
||||
questions: [{
|
||||
question: "What specific changes are needed?",
|
||||
header: "Details",
|
||||
options: [
|
||||
{ label: "Typo fixes", description: "Fix spelling/grammar" },
|
||||
{ label: "Reorder sections", description: "Change section order" },
|
||||
{ label: "Update content", description: "Modify existing text" },
|
||||
{ label: "Custom changes", description: "I'll describe the changes" }
|
||||
],
|
||||
multiSelect: true
|
||||
}]
|
||||
});
|
||||
|
||||
// Apply changes based on user input
|
||||
applyMinorChanges(details);
|
||||
}
|
||||
```
|
||||
|
||||
#### Major Issues
|
||||
|
||||
```javascript
|
||||
if (feedback.overall === "Major issues") {
|
||||
// Return to relevant phase
|
||||
console.log(`
|
||||
Major issues require returning to an earlier phase:
|
||||
|
||||
- Content issues → Phase 3 (Parallel Analysis)
|
||||
- Screenshot issues → Phase 4 (Screenshot Capture)
|
||||
- Structure issues → Phase 2 (Project Exploration)
|
||||
|
||||
Which phase should we return to?
|
||||
`);
|
||||
|
||||
const phase = await selectPhase();
|
||||
return { action: 'restart', from_phase: phase };
|
||||
}
|
||||
```
|
||||
|
||||
#### Missing Content
|
||||
|
||||
```javascript
|
||||
if (feedback.overall === "Missing content") {
|
||||
// Identify missing sections
|
||||
const missing = await AskUserQuestion({
|
||||
questions: [{
|
||||
question: "What content is missing?",
|
||||
header: "Missing",
|
||||
options: [
|
||||
{ label: "API endpoints", description: "More API documentation" },
|
||||
{ label: "UI features", description: "Additional UI guides" },
|
||||
{ label: "Examples", description: "More code examples" },
|
||||
{ label: "Troubleshooting", description: "More FAQ items" }
|
||||
],
|
||||
multiSelect: true
|
||||
}]
|
||||
});
|
||||
|
||||
// Run additional agent(s) for missing content
|
||||
await runSupplementaryAgents(missing);
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Save Iteration
|
||||
|
||||
```javascript
|
||||
// Save current version before changes
|
||||
const iterationNum = getNextIterationNumber(workDir);
|
||||
const iterationDir = `${workDir}/iterations`;
|
||||
|
||||
// Copy current version
|
||||
Bash({ command: `copy "${outputFile}" "${iterationDir}\\v${iterationNum}.html"` });
|
||||
|
||||
// Log iteration
|
||||
const iterationLog = {
|
||||
version: iterationNum,
|
||||
timestamp: new Date().toISOString(),
|
||||
feedback: feedback,
|
||||
changes: appliedChanges
|
||||
};
|
||||
|
||||
Write(`${iterationDir}/iteration-${iterationNum}.json`, JSON.stringify(iterationLog, null, 2));
|
||||
```
|
||||
|
||||
### Step 5: Regenerate if Needed
|
||||
|
||||
```javascript
|
||||
if (changesApplied) {
|
||||
// Re-run HTML assembly with updated sections
|
||||
await runPhase('05-html-assembly');
|
||||
|
||||
// Open updated preview
|
||||
Bash({ command: `start "${outputFile}"` });
|
||||
}
|
||||
```
|
||||
|
||||
### Step 6: Finalize
|
||||
|
||||
When user approves:
|
||||
|
||||
```javascript
|
||||
if (feedback.overall === "Looks great!") {
|
||||
// Final quality check
|
||||
const finalReport = {
|
||||
...buildReport,
|
||||
iterations: iterationNum,
|
||||
finalized_at: new Date().toISOString(),
|
||||
quality_score: calculateFinalQuality()
|
||||
};
|
||||
|
||||
Write(`${workDir}/final-report.json`, JSON.stringify(finalReport, null, 2));
|
||||
|
||||
// Suggest final location
|
||||
console.log(`
|
||||
✅ Manual Finalized!
|
||||
|
||||
Output: ${buildReport.output}
|
||||
Size: ${buildReport.size_human}
|
||||
Quality: ${finalReport.quality_score}%
|
||||
Iterations: ${iterationNum}
|
||||
|
||||
Suggested actions:
|
||||
1. Copy to project root: copy "${outputFile}" "docs/"
|
||||
2. Add to version control
|
||||
3. Publish to documentation site
|
||||
`);
|
||||
|
||||
return { status: 'completed', output: outputFile };
|
||||
}
|
||||
```
|
||||
|
||||
## Iteration History
|
||||
|
||||
Each iteration is logged:
|
||||
|
||||
```
|
||||
iterations/
|
||||
├── v1.html # First version
|
||||
├── iteration-1.json # Feedback and changes
|
||||
├── v2.html # After first iteration
|
||||
├── iteration-2.json # Feedback and changes
|
||||
└── ...
|
||||
```
|
||||
|
||||
## Quality Metrics
|
||||
|
||||
Track improvement across iterations:
|
||||
|
||||
```javascript
|
||||
const qualityMetrics = {
|
||||
content_completeness: 0, // All sections present
|
||||
screenshot_coverage: 0, // Screenshots for all UI
|
||||
example_diversity: 0, // Different difficulty levels
|
||||
search_accuracy: 0, // Search returns relevant results
|
||||
user_satisfaction: 0 // Based on feedback
|
||||
};
|
||||
```
|
||||
|
||||
## Exit Conditions
|
||||
|
||||
The refinement phase ends when:
|
||||
1. User explicitly approves ("Looks great!")
|
||||
2. Maximum iterations reached (configurable, default: 5)
|
||||
3. Quality score exceeds threshold (default: 90%)
|
||||
|
||||
## Output
|
||||
|
||||
- **Final HTML**: `{软件名}-使用手册.html`
|
||||
- **Final Report**: `final-report.json`
|
||||
- **Iteration History**: `iterations/`
|
||||
|
||||
## Completion
|
||||
|
||||
When finalized, the skill is complete. Final output location:
|
||||
|
||||
```
|
||||
.workflow/.scratchpad/manual-{timestamp}/
|
||||
├── {软件名}-使用手册.html ← Final deliverable
|
||||
├── final-report.json
|
||||
└── iterations/
|
||||
```
|
||||
|
||||
Consider copying to a permanent location like `docs/` or project root.
|
||||
245
.claude/skills/software-manual/scripts/api-extractor.md
Normal file
245
.claude/skills/software-manual/scripts/api-extractor.md
Normal file
@@ -0,0 +1,245 @@
|
||||
# API 文档提取脚本
|
||||
|
||||
根据项目类型自动提取 API 文档,支持 FastAPI、Next.js、Python 模块。
|
||||
|
||||
## 支持的技术栈
|
||||
|
||||
| 类型 | 技术栈 | 工具 | 输出格式 |
|
||||
|------|--------|------|----------|
|
||||
| Backend | FastAPI | openapi-to-md | Markdown |
|
||||
| Frontend | Next.js/TypeScript | TypeDoc | Markdown |
|
||||
| Python Module | Python | pdoc | Markdown/HTML |
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. FastAPI Backend (OpenAPI)
|
||||
|
||||
```bash
|
||||
# 提取 OpenAPI JSON
|
||||
cd D:/dongdiankaifa9/backend
|
||||
python -c "
|
||||
from app.main import app
|
||||
import json
|
||||
print(json.dumps(app.openapi(), indent=2))
|
||||
" > api-docs/openapi.json
|
||||
|
||||
# 转换为 Markdown (使用 widdershins)
|
||||
npx widdershins api-docs/openapi.json -o api-docs/API_REFERENCE.md --language_tabs 'python:Python' 'javascript:JavaScript' 'bash:cURL'
|
||||
```
|
||||
|
||||
**备选方案 (无需启动服务)**:
|
||||
```python
|
||||
# scripts/extract_fastapi_openapi.py
|
||||
import sys
|
||||
sys.path.insert(0, 'D:/dongdiankaifa9/backend')
|
||||
|
||||
from app.main import app
|
||||
import json
|
||||
|
||||
openapi_schema = app.openapi()
|
||||
with open('api-docs/openapi.json', 'w', encoding='utf-8') as f:
|
||||
json.dump(openapi_schema, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"Extracted {len(openapi_schema.get('paths', {}))} endpoints")
|
||||
```
|
||||
|
||||
### 2. Next.js Frontend (TypeDoc)
|
||||
|
||||
```bash
|
||||
cd D:/dongdiankaifa9/frontend
|
||||
|
||||
# 安装 TypeDoc
|
||||
npm install --save-dev typedoc typedoc-plugin-markdown
|
||||
|
||||
# 生成文档
|
||||
npx typedoc --plugin typedoc-plugin-markdown \
|
||||
--out api-docs \
|
||||
--entryPoints "./lib" "./hooks" "./components" \
|
||||
--entryPointStrategy expand \
|
||||
--exclude "**/node_modules/**" \
|
||||
--exclude "**/*.test.*" \
|
||||
--readme none
|
||||
```
|
||||
|
||||
**typedoc.json 配置**:
|
||||
```json
|
||||
{
|
||||
"$schema": "https://typedoc.org/schema.json",
|
||||
"entryPoints": ["./lib", "./hooks", "./components"],
|
||||
"entryPointStrategy": "expand",
|
||||
"out": "api-docs",
|
||||
"plugin": ["typedoc-plugin-markdown"],
|
||||
"exclude": ["**/node_modules/**", "**/*.test.*", "**/*.spec.*"],
|
||||
"excludePrivate": true,
|
||||
"excludeInternal": true,
|
||||
"readme": "none",
|
||||
"name": "Frontend API Reference"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Python Module (pdoc)
|
||||
|
||||
```bash
|
||||
# 安装 pdoc
|
||||
pip install pdoc
|
||||
|
||||
# hydro_generator_module
|
||||
cd D:/dongdiankaifa9
|
||||
pdoc hydro_generator_module \
|
||||
--output-dir api-docs/hydro_generator \
|
||||
--format markdown \
|
||||
--no-show-source
|
||||
|
||||
# multiphysics_network
|
||||
pdoc multiphysics_network \
|
||||
--output-dir api-docs/multiphysics \
|
||||
--format markdown \
|
||||
--no-show-source
|
||||
```
|
||||
|
||||
**备选: Sphinx (更强大)**:
|
||||
```bash
|
||||
# 安装 Sphinx
|
||||
pip install sphinx sphinx-markdown-builder
|
||||
|
||||
# 生成 API 文档
|
||||
sphinx-apidoc -o docs/source hydro_generator_module
|
||||
cd docs && make markdown
|
||||
```
|
||||
|
||||
## 集成脚本
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
# scripts/extract_all_apis.py
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
PROJECTS = {
|
||||
'backend': {
|
||||
'path': 'D:/dongdiankaifa9/backend',
|
||||
'type': 'fastapi',
|
||||
'output': 'api-docs/backend'
|
||||
},
|
||||
'frontend': {
|
||||
'path': 'D:/dongdiankaifa9/frontend',
|
||||
'type': 'typescript',
|
||||
'output': 'api-docs/frontend'
|
||||
},
|
||||
'hydro_generator_module': {
|
||||
'path': 'D:/dongdiankaifa9/hydro_generator_module',
|
||||
'type': 'python',
|
||||
'output': 'api-docs/hydro_generator'
|
||||
},
|
||||
'multiphysics_network': {
|
||||
'path': 'D:/dongdiankaifa9/multiphysics_network',
|
||||
'type': 'python',
|
||||
'output': 'api-docs/multiphysics'
|
||||
}
|
||||
}
|
||||
|
||||
def extract_fastapi(config):
|
||||
"""提取 FastAPI OpenAPI 文档"""
|
||||
path = Path(config['path'])
|
||||
sys.path.insert(0, str(path))
|
||||
|
||||
try:
|
||||
from app.main import app
|
||||
import json
|
||||
|
||||
output_dir = Path(config['output'])
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 导出 OpenAPI JSON
|
||||
with open(output_dir / 'openapi.json', 'w', encoding='utf-8') as f:
|
||||
json.dump(app.openapi(), f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"✓ FastAPI: {len(app.openapi().get('paths', {}))} endpoints")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"✗ FastAPI error: {e}")
|
||||
return False
|
||||
|
||||
def extract_typescript(config):
|
||||
"""提取 TypeScript 文档"""
|
||||
try:
|
||||
subprocess.run([
|
||||
'npx', 'typedoc',
|
||||
'--plugin', 'typedoc-plugin-markdown',
|
||||
'--out', config['output'],
|
||||
'--entryPoints', './lib', './hooks',
|
||||
'--entryPointStrategy', 'expand'
|
||||
], cwd=config['path'], check=True)
|
||||
print(f"✓ TypeDoc: {config['path']}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"✗ TypeDoc error: {e}")
|
||||
return False
|
||||
|
||||
def extract_python(config):
|
||||
"""提取 Python 模块文档"""
|
||||
try:
|
||||
module_name = Path(config['path']).name
|
||||
subprocess.run([
|
||||
'pdoc', module_name,
|
||||
'--output-dir', config['output'],
|
||||
'--format', 'markdown'
|
||||
], cwd=Path(config['path']).parent, check=True)
|
||||
print(f"✓ pdoc: {module_name}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"✗ pdoc error: {e}")
|
||||
return False
|
||||
|
||||
EXTRACTORS = {
|
||||
'fastapi': extract_fastapi,
|
||||
'typescript': extract_typescript,
|
||||
'python': extract_python
|
||||
}
|
||||
|
||||
if __name__ == '__main__':
|
||||
for name, config in PROJECTS.items():
|
||||
print(f"\n[{name}]")
|
||||
extractor = EXTRACTORS.get(config['type'])
|
||||
if extractor:
|
||||
extractor(config)
|
||||
```
|
||||
|
||||
## Phase 3 集成
|
||||
|
||||
在 `api-reference` Agent 提示词中添加:
|
||||
|
||||
```
|
||||
[PRE-EXTRACTION]
|
||||
运行 API 提取脚本获取结构化文档:
|
||||
- python scripts/extract_all_apis.py
|
||||
|
||||
[INPUT FILES]
|
||||
- api-docs/backend/openapi.json (FastAPI endpoints)
|
||||
- api-docs/frontend/*.md (TypeDoc output)
|
||||
- api-docs/hydro_generator/*.md (pdoc output)
|
||||
- api-docs/multiphysics/*.md (pdoc output)
|
||||
```
|
||||
|
||||
## 输出结构
|
||||
|
||||
```
|
||||
api-docs/
|
||||
├── backend/
|
||||
│ ├── openapi.json # Raw OpenAPI spec
|
||||
│ └── API_REFERENCE.md # Converted Markdown
|
||||
├── frontend/
|
||||
│ ├── modules.md
|
||||
│ ├── functions.md
|
||||
│ └── classes/
|
||||
├── hydro_generator/
|
||||
│ ├── assembler.md
|
||||
│ ├── blueprint.md
|
||||
│ └── builders/
|
||||
└── multiphysics/
|
||||
├── analysis_domain.md
|
||||
├── builders.md
|
||||
└── compilers.md
|
||||
```
|
||||
584
.claude/skills/software-manual/scripts/assemble_docsify.py
Normal file
584
.claude/skills/software-manual/scripts/assemble_docsify.py
Normal file
@@ -0,0 +1,584 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Docsify-Style HTML Manual Assembly Script Template
|
||||
Generates interactive single-file documentation with hierarchical navigation
|
||||
|
||||
Usage:
|
||||
1. Copy this script to your manual output directory
|
||||
2. Customize MANUAL_META and NAV_STRUCTURE
|
||||
3. Run: python assemble_docsify.py
|
||||
"""
|
||||
|
||||
import json
|
||||
import base64
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Any
|
||||
|
||||
# Try to import markdown library
|
||||
try:
|
||||
import markdown
|
||||
from markdown.extensions.codehilite import CodeHiliteExtension
|
||||
from markdown.extensions.fenced_code import FencedCodeExtension
|
||||
from markdown.extensions.tables import TableExtension
|
||||
from markdown.extensions.toc import TocExtension
|
||||
HAS_MARKDOWN = True
|
||||
except ImportError:
|
||||
HAS_MARKDOWN = False
|
||||
print("Warning: markdown library not found. Install with: pip install markdown pygments")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# CONFIGURATION - Customize these for your project
|
||||
# ============================================================
|
||||
|
||||
# Paths - Update these paths for your environment
|
||||
BASE_DIR = Path(__file__).parent
|
||||
SECTIONS_DIR = BASE_DIR / "sections"
|
||||
SCREENSHOTS_DIR = BASE_DIR / "screenshots"
|
||||
|
||||
# Template paths - Point to skill templates directory
|
||||
SKILL_DIR = Path(__file__).parent.parent # Adjust based on where script is placed
|
||||
TEMPLATE_FILE = SKILL_DIR / "templates" / "docsify-shell.html"
|
||||
CSS_BASE_FILE = SKILL_DIR / "templates" / "css" / "docsify-base.css"
|
||||
|
||||
# Manual metadata - Customize for your software
|
||||
MANUAL_META = {
|
||||
"title": "Your Software",
|
||||
"subtitle": "使用手册",
|
||||
"version": "v1.0.0",
|
||||
"timestamp": "2025-01-01",
|
||||
"language": "zh-CN",
|
||||
"logo_icon": "Y" # First letter or emoji
|
||||
}
|
||||
|
||||
# Output file
|
||||
OUTPUT_FILE = BASE_DIR / f"{MANUAL_META['title']}{MANUAL_META['subtitle']}.html"
|
||||
|
||||
# Hierarchical navigation structure
|
||||
# Customize groups and items based on your sections
|
||||
NAV_STRUCTURE = [
|
||||
{
|
||||
"type": "group",
|
||||
"title": "入门指南",
|
||||
"icon": "📚",
|
||||
"expanded": True,
|
||||
"items": [
|
||||
{"id": "overview", "title": "产品概述", "file": "section-overview.md"},
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "group",
|
||||
"title": "使用教程",
|
||||
"icon": "🎯",
|
||||
"expanded": False,
|
||||
"items": [
|
||||
{"id": "ui-guide", "title": "UI操作指南", "file": "section-ui-guide.md"},
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "group",
|
||||
"title": "API参考",
|
||||
"icon": "🔧",
|
||||
"expanded": False,
|
||||
"items": [
|
||||
{"id": "api-reference", "title": "API文档", "file": "section-api-reference.md"},
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "group",
|
||||
"title": "配置与部署",
|
||||
"icon": "⚙️",
|
||||
"expanded": False,
|
||||
"items": [
|
||||
{"id": "configuration", "title": "配置指南", "file": "section-configuration.md"},
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "group",
|
||||
"title": "帮助与支持",
|
||||
"icon": "💡",
|
||||
"expanded": False,
|
||||
"items": [
|
||||
{"id": "troubleshooting", "title": "故障排除", "file": "section-troubleshooting.md"},
|
||||
{"id": "examples", "title": "代码示例", "file": "section-examples.md"},
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
# Screenshot ID to filename mapping - Customize for your screenshots
|
||||
SCREENSHOT_MAPPING = {
|
||||
# "截图ID": "filename.png",
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# CORE FUNCTIONS - Generally don't need to modify
|
||||
# ============================================================
|
||||
|
||||
# Global cache for embedded images
|
||||
_embedded_images = {}
|
||||
|
||||
|
||||
def read_file(filepath: Path) -> str:
|
||||
"""Read file content with UTF-8 encoding"""
|
||||
return filepath.read_text(encoding='utf-8')
|
||||
|
||||
|
||||
# ============================================================
|
||||
# MERMAID VALIDATION
|
||||
# ============================================================
|
||||
|
||||
# Valid Mermaid diagram types
|
||||
MERMAID_DIAGRAM_TYPES = [
|
||||
'graph', 'flowchart', 'sequenceDiagram', 'classDiagram',
|
||||
'stateDiagram', 'stateDiagram-v2', 'erDiagram', 'journey',
|
||||
'gantt', 'pie', 'quadrantChart', 'requirementDiagram',
|
||||
'gitGraph', 'mindmap', 'timeline', 'zenuml', 'sankey-beta',
|
||||
'xychart-beta', 'block-beta'
|
||||
]
|
||||
|
||||
# Common Mermaid syntax patterns
|
||||
MERMAID_PATTERNS = {
|
||||
'graph': r'^graph\s+(TB|BT|LR|RL|TD)\s*$',
|
||||
'flowchart': r'^flowchart\s+(TB|BT|LR|RL|TD)\s*$',
|
||||
'sequenceDiagram': r'^sequenceDiagram\s*$',
|
||||
'classDiagram': r'^classDiagram\s*$',
|
||||
'stateDiagram': r'^stateDiagram(-v2)?\s*$',
|
||||
'erDiagram': r'^erDiagram\s*$',
|
||||
'gantt': r'^gantt\s*$',
|
||||
'pie': r'^pie\s*(showData|title\s+.*)?\s*$',
|
||||
'journey': r'^journey\s*$',
|
||||
}
|
||||
|
||||
|
||||
class MermaidBlock:
|
||||
"""Represents a mermaid code block found in markdown"""
|
||||
def __init__(self, content: str, file: str, line_num: int, indented: bool = False):
|
||||
self.content = content
|
||||
self.file = file
|
||||
self.line_num = line_num
|
||||
self.indented = indented
|
||||
self.errors: List[str] = []
|
||||
self.warnings: List[str] = []
|
||||
self.diagram_type: str = None
|
||||
|
||||
def __repr__(self):
|
||||
return f"MermaidBlock({self.diagram_type}, {self.file}:{self.line_num})"
|
||||
|
||||
|
||||
def extract_mermaid_blocks(markdown_text: str, filename: str) -> List[MermaidBlock]:
|
||||
"""Extract all mermaid code blocks from markdown text"""
|
||||
blocks = []
|
||||
|
||||
# More flexible pattern - matches opening fence with optional indent,
|
||||
# then captures content until closing fence (with any indent)
|
||||
pattern = r'^(\s*)(```|~~~)mermaid\s*\n(.*?)\n\s*\2\s*$'
|
||||
|
||||
for match in re.finditer(pattern, markdown_text, re.MULTILINE | re.DOTALL):
|
||||
indent = match.group(1)
|
||||
content = match.group(3)
|
||||
# Calculate line number
|
||||
line_num = markdown_text[:match.start()].count('\n') + 1
|
||||
indented = len(indent) > 0
|
||||
|
||||
block = MermaidBlock(
|
||||
content=content,
|
||||
file=filename,
|
||||
line_num=line_num,
|
||||
indented=indented
|
||||
)
|
||||
blocks.append(block)
|
||||
|
||||
return blocks
|
||||
|
||||
|
||||
def validate_mermaid_block(block: MermaidBlock) -> bool:
|
||||
"""Validate a mermaid block and populate errors/warnings"""
|
||||
content = block.content.strip()
|
||||
lines = content.split('\n')
|
||||
|
||||
if not lines:
|
||||
block.errors.append("Empty mermaid block")
|
||||
return False
|
||||
|
||||
first_line = lines[0].strip()
|
||||
|
||||
# Detect diagram type
|
||||
for dtype in MERMAID_DIAGRAM_TYPES:
|
||||
if first_line.startswith(dtype):
|
||||
block.diagram_type = dtype
|
||||
break
|
||||
|
||||
if not block.diagram_type:
|
||||
block.errors.append(f"Unknown diagram type: '{first_line[:30]}...'")
|
||||
block.errors.append(f"Valid types: {', '.join(MERMAID_DIAGRAM_TYPES[:8])}...")
|
||||
return False
|
||||
|
||||
# Check for balanced brackets/braces
|
||||
brackets = {'[': ']', '{': '}', '(': ')'}
|
||||
stack = []
|
||||
for i, char in enumerate(content):
|
||||
if char in brackets:
|
||||
stack.append((char, i))
|
||||
elif char in brackets.values():
|
||||
if not stack:
|
||||
block.errors.append(f"Unmatched closing bracket '{char}' at position {i}")
|
||||
else:
|
||||
open_char, _ = stack.pop()
|
||||
if brackets[open_char] != char:
|
||||
block.errors.append(f"Mismatched brackets: '{open_char}' and '{char}'")
|
||||
|
||||
if stack:
|
||||
for open_char, pos in stack:
|
||||
block.warnings.append(f"Unclosed bracket '{open_char}' at position {pos}")
|
||||
|
||||
# Check for common graph/flowchart issues
|
||||
if block.diagram_type in ['graph', 'flowchart']:
|
||||
# Check direction specifier
|
||||
if not re.match(r'^(graph|flowchart)\s+(TB|BT|LR|RL|TD)', first_line):
|
||||
block.warnings.append("Missing or invalid direction (TB/BT/LR/RL/TD)")
|
||||
|
||||
# Check for arrow syntax
|
||||
arrow_count = content.count('-->') + content.count('---') + content.count('-.->') + content.count('==>')
|
||||
if arrow_count == 0 and len(lines) > 1:
|
||||
block.warnings.append("No arrows found - graph may be incomplete")
|
||||
|
||||
# Check for sequenceDiagram issues
|
||||
if block.diagram_type == 'sequenceDiagram':
|
||||
if '->' not in content and '->>' not in content:
|
||||
block.warnings.append("No message arrows found in sequence diagram")
|
||||
|
||||
# Indentation warning
|
||||
if block.indented:
|
||||
block.warnings.append("Indented code block - may not render in some markdown parsers")
|
||||
|
||||
return len(block.errors) == 0
|
||||
|
||||
|
||||
def validate_all_mermaid(nav_structure: List[Dict], sections_dir: Path) -> Dict[str, Any]:
|
||||
"""Validate all mermaid blocks in all section files"""
|
||||
report = {
|
||||
'total_blocks': 0,
|
||||
'valid_blocks': 0,
|
||||
'error_blocks': 0,
|
||||
'warning_blocks': 0,
|
||||
'blocks': [],
|
||||
'by_file': {},
|
||||
'by_type': {}
|
||||
}
|
||||
|
||||
for group in nav_structure:
|
||||
for item in group.get("items", []):
|
||||
section_file = item.get("file")
|
||||
if not section_file:
|
||||
continue
|
||||
|
||||
filepath = sections_dir / section_file
|
||||
if not filepath.exists():
|
||||
continue
|
||||
|
||||
content = read_file(filepath)
|
||||
blocks = extract_mermaid_blocks(content, section_file)
|
||||
|
||||
file_report = {'blocks': [], 'errors': 0, 'warnings': 0}
|
||||
|
||||
for block in blocks:
|
||||
report['total_blocks'] += 1
|
||||
is_valid = validate_mermaid_block(block)
|
||||
|
||||
if is_valid:
|
||||
report['valid_blocks'] += 1
|
||||
else:
|
||||
report['error_blocks'] += 1
|
||||
file_report['errors'] += 1
|
||||
|
||||
if block.warnings:
|
||||
report['warning_blocks'] += 1
|
||||
file_report['warnings'] += len(block.warnings)
|
||||
|
||||
# Track by diagram type
|
||||
if block.diagram_type:
|
||||
if block.diagram_type not in report['by_type']:
|
||||
report['by_type'][block.diagram_type] = 0
|
||||
report['by_type'][block.diagram_type] += 1
|
||||
|
||||
report['blocks'].append(block)
|
||||
file_report['blocks'].append(block)
|
||||
|
||||
if blocks:
|
||||
report['by_file'][section_file] = file_report
|
||||
|
||||
return report
|
||||
|
||||
|
||||
def print_mermaid_report(report: Dict[str, Any]) -> None:
|
||||
"""Print mermaid validation report"""
|
||||
print("\n" + "=" * 60)
|
||||
print("MERMAID DIAGRAM VALIDATION REPORT")
|
||||
print("=" * 60)
|
||||
|
||||
print(f"\nSummary:")
|
||||
print(f" Total blocks: {report['total_blocks']}")
|
||||
print(f" Valid: {report['valid_blocks']}")
|
||||
print(f" With errors: {report['error_blocks']}")
|
||||
print(f" With warnings: {report['warning_blocks']}")
|
||||
|
||||
if report['by_type']:
|
||||
print(f"\nDiagram Types:")
|
||||
for dtype, count in sorted(report['by_type'].items()):
|
||||
print(f" {dtype}: {count}")
|
||||
|
||||
# Print errors and warnings
|
||||
has_issues = False
|
||||
for block in report['blocks']:
|
||||
if block.errors or block.warnings:
|
||||
if not has_issues:
|
||||
print(f"\nIssues Found:")
|
||||
has_issues = True
|
||||
|
||||
print(f"\n [{block.file}:{block.line_num}] {block.diagram_type or 'unknown'}")
|
||||
for error in block.errors:
|
||||
print(f" [ERROR] {error}")
|
||||
for warning in block.warnings:
|
||||
print(f" [WARN] {warning}")
|
||||
|
||||
if not has_issues:
|
||||
print(f"\n No issues found!")
|
||||
|
||||
print("=" * 60 + "\n")
|
||||
|
||||
|
||||
def convert_md_to_html(markdown_text: str) -> str:
|
||||
"""Convert Markdown to HTML with syntax highlighting"""
|
||||
if not HAS_MARKDOWN:
|
||||
# Fallback: just escape HTML and wrap in pre
|
||||
escaped = markdown_text.replace('&', '&').replace('<', '<').replace('>', '>')
|
||||
return f'<pre>{escaped}</pre>'
|
||||
|
||||
md = markdown.Markdown(
|
||||
extensions=[
|
||||
FencedCodeExtension(),
|
||||
TableExtension(),
|
||||
TocExtension(toc_depth=3),
|
||||
CodeHiliteExtension(
|
||||
css_class='highlight',
|
||||
linenums=False,
|
||||
guess_lang=True,
|
||||
use_pygments=True
|
||||
),
|
||||
],
|
||||
output_format='html5'
|
||||
)
|
||||
html = md.convert(markdown_text)
|
||||
md.reset()
|
||||
return html
|
||||
|
||||
|
||||
def embed_screenshot_base64(screenshot_id: str) -> str:
|
||||
"""Embed screenshot as base64, using cache to avoid duplicates"""
|
||||
global _embedded_images
|
||||
|
||||
filename = SCREENSHOT_MAPPING.get(screenshot_id)
|
||||
|
||||
if not filename:
|
||||
return f'<div class="screenshot-placeholder">📷 {screenshot_id}</div>'
|
||||
|
||||
filepath = SCREENSHOTS_DIR / filename
|
||||
|
||||
if not filepath.exists():
|
||||
return f'<div class="screenshot-placeholder">📷 {screenshot_id}</div>'
|
||||
|
||||
# Check cache
|
||||
if filename not in _embedded_images:
|
||||
try:
|
||||
with open(filepath, 'rb') as f:
|
||||
image_data = base64.b64encode(f.read()).decode('utf-8')
|
||||
ext = filepath.suffix[1:].lower()
|
||||
_embedded_images[filename] = f"data:image/{ext};base64,{image_data}"
|
||||
except Exception as e:
|
||||
return f'<div class="screenshot-placeholder">📷 {screenshot_id} (加载失败)</div>'
|
||||
|
||||
return f'''<figure class="screenshot">
|
||||
<img src="{_embedded_images[filename]}" alt="{screenshot_id}" loading="lazy" />
|
||||
<figcaption>{screenshot_id}</figcaption>
|
||||
</figure>'''
|
||||
|
||||
|
||||
def process_markdown_screenshots(markdown_text: str) -> str:
|
||||
"""Replace [[screenshot:xxx]] placeholders with embedded images"""
|
||||
pattern = r'\[\[screenshot:(.*?)\]\]'
|
||||
|
||||
def replacer(match):
|
||||
screenshot_id = match.group(1)
|
||||
return embed_screenshot_base64(screenshot_id)
|
||||
|
||||
return re.sub(pattern, replacer, markdown_text)
|
||||
|
||||
|
||||
def generate_sidebar_nav_html(nav_structure: List[Dict]) -> str:
|
||||
"""Generate hierarchical sidebar navigation HTML"""
|
||||
html_parts = []
|
||||
|
||||
for group in nav_structure:
|
||||
if group["type"] == "group":
|
||||
expanded_class = "expanded" if group.get("expanded", False) else ""
|
||||
html_parts.append(f'''
|
||||
<div class="nav-group {expanded_class}">
|
||||
<div class="nav-group-header">
|
||||
<button class="nav-group-toggle" aria-expanded="{str(group.get('expanded', False)).lower()}">
|
||||
<svg viewBox="0 0 24 24"><path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6z" fill="currentColor"/></svg>
|
||||
</button>
|
||||
<span class="nav-group-title">{group.get('icon', '')} {group['title']}</span>
|
||||
</div>
|
||||
<div class="nav-group-items">''')
|
||||
|
||||
for item in group.get("items", []):
|
||||
html_parts.append(f'''
|
||||
<a class="nav-item" href="#/{item['id']}" data-section="{item['id']}">{item['title']}</a>''')
|
||||
|
||||
html_parts.append('''
|
||||
</div>
|
||||
</div>''')
|
||||
|
||||
return '\n'.join(html_parts)
|
||||
|
||||
|
||||
def generate_sections_html(nav_structure: List[Dict]) -> str:
|
||||
"""Generate content sections HTML"""
|
||||
sections_html = []
|
||||
|
||||
for group in nav_structure:
|
||||
for item in group.get("items", []):
|
||||
section_id = item["id"]
|
||||
section_title = item["title"]
|
||||
section_file = item.get("file")
|
||||
|
||||
if not section_file:
|
||||
continue
|
||||
|
||||
filepath = SECTIONS_DIR / section_file
|
||||
if not filepath.exists():
|
||||
print(f"Warning: Section file not found: {filepath}")
|
||||
continue
|
||||
|
||||
# Read and convert markdown
|
||||
markdown_content = read_file(filepath)
|
||||
markdown_content = process_markdown_screenshots(markdown_content)
|
||||
html_content = convert_md_to_html(markdown_content)
|
||||
|
||||
sections_html.append(f'''
|
||||
<section class="content-section" id="section-{section_id}" data-title="{section_title}">
|
||||
{html_content}
|
||||
</section>''')
|
||||
|
||||
return '\n'.join(sections_html)
|
||||
|
||||
|
||||
def generate_search_index(nav_structure: List[Dict]) -> str:
|
||||
"""Generate search index JSON"""
|
||||
search_index = {}
|
||||
|
||||
for group in nav_structure:
|
||||
for item in group.get("items", []):
|
||||
section_id = item["id"]
|
||||
section_file = item.get("file")
|
||||
|
||||
if not section_file:
|
||||
continue
|
||||
|
||||
filepath = SECTIONS_DIR / section_file
|
||||
if filepath.exists():
|
||||
content = read_file(filepath)
|
||||
clean_content = re.sub(r'[#*`\[\]()]', '', content)
|
||||
clean_content = re.sub(r'\s+', ' ', clean_content)[:1500]
|
||||
|
||||
search_index[section_id] = {
|
||||
"title": item["title"],
|
||||
"body": clean_content,
|
||||
"group": group["title"]
|
||||
}
|
||||
|
||||
return json.dumps(search_index, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
def generate_nav_structure_json(nav_structure: List[Dict]) -> str:
|
||||
"""Generate navigation structure JSON for client-side"""
|
||||
return json.dumps(nav_structure, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
def assemble_manual(validate_mermaid: bool = True):
|
||||
"""Main assembly function
|
||||
|
||||
Args:
|
||||
validate_mermaid: Whether to validate mermaid diagrams (default: True)
|
||||
"""
|
||||
global _embedded_images
|
||||
_embedded_images = {}
|
||||
|
||||
full_title = f"{MANUAL_META['title']} {MANUAL_META['subtitle']}"
|
||||
print(f"Assembling Docsify-style manual: {full_title}")
|
||||
|
||||
# Verify template exists
|
||||
if not TEMPLATE_FILE.exists():
|
||||
print(f"Error: Template not found at {TEMPLATE_FILE}")
|
||||
print("Please update TEMPLATE_FILE path in this script.")
|
||||
return None, 0
|
||||
|
||||
if not CSS_BASE_FILE.exists():
|
||||
print(f"Error: CSS not found at {CSS_BASE_FILE}")
|
||||
print("Please update CSS_BASE_FILE path in this script.")
|
||||
return None, 0
|
||||
|
||||
# Validate Mermaid diagrams
|
||||
mermaid_report = None
|
||||
if validate_mermaid:
|
||||
print("\nValidating Mermaid diagrams...")
|
||||
mermaid_report = validate_all_mermaid(NAV_STRUCTURE, SECTIONS_DIR)
|
||||
print_mermaid_report(mermaid_report)
|
||||
|
||||
# Warn if there are errors (but continue)
|
||||
if mermaid_report['error_blocks'] > 0:
|
||||
print(f"[WARN] {mermaid_report['error_blocks']} mermaid block(s) have errors!")
|
||||
print(" These diagrams may not render correctly.")
|
||||
|
||||
# Read template and CSS
|
||||
template_html = read_file(TEMPLATE_FILE)
|
||||
css_content = read_file(CSS_BASE_FILE)
|
||||
|
||||
# Generate components
|
||||
sidebar_nav_html = generate_sidebar_nav_html(NAV_STRUCTURE)
|
||||
sections_html = generate_sections_html(NAV_STRUCTURE)
|
||||
search_index_json = generate_search_index(NAV_STRUCTURE)
|
||||
nav_structure_json = generate_nav_structure_json(NAV_STRUCTURE)
|
||||
|
||||
# Replace placeholders
|
||||
output_html = template_html
|
||||
output_html = output_html.replace('{{SOFTWARE_NAME}}', full_title)
|
||||
output_html = output_html.replace('{{VERSION}}', MANUAL_META['version'])
|
||||
output_html = output_html.replace('{{TIMESTAMP}}', MANUAL_META['timestamp'])
|
||||
output_html = output_html.replace('{{LOGO_ICON}}', MANUAL_META['logo_icon'])
|
||||
output_html = output_html.replace('{{EMBEDDED_CSS}}', css_content)
|
||||
output_html = output_html.replace('{{SIDEBAR_NAV_HTML}}', sidebar_nav_html)
|
||||
output_html = output_html.replace('{{SECTIONS_HTML}}', sections_html)
|
||||
output_html = output_html.replace('{{SEARCH_INDEX_JSON}}', search_index_json)
|
||||
output_html = output_html.replace('{{NAV_STRUCTURE_JSON}}', nav_structure_json)
|
||||
|
||||
# Write output file
|
||||
OUTPUT_FILE.write_text(output_html, encoding='utf-8')
|
||||
|
||||
file_size = OUTPUT_FILE.stat().st_size
|
||||
file_size_mb = file_size / (1024 * 1024)
|
||||
section_count = sum(len(g.get("items", [])) for g in NAV_STRUCTURE)
|
||||
|
||||
print("[OK] Docsify-style manual generated successfully!")
|
||||
print(f" Output: {OUTPUT_FILE}")
|
||||
print(f" Size: {file_size_mb:.2f} MB ({file_size:,} bytes)")
|
||||
print(f" Navigation Groups: {len(NAV_STRUCTURE)}")
|
||||
print(f" Sections: {section_count}")
|
||||
|
||||
return str(OUTPUT_FILE), file_size
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
output_path, size = assemble_manual()
|
||||
85
.claude/skills/software-manual/scripts/bundle-libraries.md
Normal file
85
.claude/skills/software-manual/scripts/bundle-libraries.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# 库文件打包说明
|
||||
|
||||
## 依赖库
|
||||
|
||||
HTML 组装阶段需要内嵌以下成熟库(无 CDN 依赖):
|
||||
|
||||
### 1. marked.js - Markdown 解析
|
||||
|
||||
```bash
|
||||
# 获取最新版本
|
||||
curl -o templates/libs/marked.min.js https://unpkg.com/marked/marked.min.js
|
||||
```
|
||||
|
||||
### 2. highlight.js - 代码语法高亮
|
||||
|
||||
```bash
|
||||
# 获取核心 + 常用语言包
|
||||
curl -o templates/libs/highlight.min.js https://unpkg.com/@highlightjs/cdn-assets/highlight.min.js
|
||||
|
||||
# 获取 github-dark 主题
|
||||
curl -o templates/libs/github-dark.min.css https://unpkg.com/@highlightjs/cdn-assets/styles/github-dark.min.css
|
||||
```
|
||||
|
||||
## 内嵌方式
|
||||
|
||||
Phase 5 Agent 应:
|
||||
|
||||
1. 读取 `templates/libs/*.js` 和 `*.css`
|
||||
2. 将内容嵌入 HTML 的 `<script>` 和 `<style>` 标签
|
||||
3. 在 `DOMContentLoaded` 后初始化:
|
||||
|
||||
```javascript
|
||||
// 初始化 marked
|
||||
marked.setOptions({
|
||||
highlight: function(code, lang) {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
return hljs.highlight(code, { language: lang }).value;
|
||||
}
|
||||
return hljs.highlightAuto(code).value;
|
||||
},
|
||||
breaks: true,
|
||||
gfm: true
|
||||
});
|
||||
|
||||
// 应用高亮
|
||||
document.querySelectorAll('pre code').forEach(block => {
|
||||
hljs.highlightElement(block);
|
||||
});
|
||||
```
|
||||
|
||||
## 备选方案
|
||||
|
||||
如果无法获取外部库,使用内置的简化 Markdown 转换:
|
||||
|
||||
```javascript
|
||||
function simpleMarkdown(md) {
|
||||
return md
|
||||
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
||||
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
||||
.replace(/^# (.+)$/gm, '<h1>$1</h1>')
|
||||
.replace(/```(\w+)?\n([\s\S]*?)```/g, (m, lang, code) =>
|
||||
`<pre data-language="${lang || ''}"><code class="language-${lang || ''}">${escapeHtml(code)}</code></pre>`)
|
||||
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
||||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
||||
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>')
|
||||
.replace(/^\|(.+)\|$/gm, processTableRow)
|
||||
.replace(/^- (.+)$/gm, '<li>$1</li>')
|
||||
.replace(/^\d+\. (.+)$/gm, '<li>$1</li>');
|
||||
}
|
||||
```
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
templates/
|
||||
├── libs/
|
||||
│ ├── marked.min.js # Markdown parser
|
||||
│ ├── highlight.min.js # Syntax highlighting
|
||||
│ └── github-dark.min.css # Code theme
|
||||
├── tiddlywiki-shell.html
|
||||
└── css/
|
||||
├── wiki-base.css
|
||||
└── wiki-dark.css
|
||||
```
|
||||
270
.claude/skills/software-manual/scripts/extract_apis.py
Normal file
270
.claude/skills/software-manual/scripts/extract_apis.py
Normal file
@@ -0,0 +1,270 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
API 文档提取脚本
|
||||
支持 FastAPI、TypeScript、Python 模块
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
# 项目配置
|
||||
PROJECTS = {
|
||||
'backend': {
|
||||
'path': Path('D:/dongdiankaifa9/backend'),
|
||||
'type': 'fastapi',
|
||||
'entry': 'app.main:app',
|
||||
'output': 'api-docs/backend'
|
||||
},
|
||||
'frontend': {
|
||||
'path': Path('D:/dongdiankaifa9/frontend'),
|
||||
'type': 'typescript',
|
||||
'entries': ['lib', 'hooks', 'components'],
|
||||
'output': 'api-docs/frontend'
|
||||
},
|
||||
'hydro_generator_module': {
|
||||
'path': Path('D:/dongdiankaifa9/hydro_generator_module'),
|
||||
'type': 'python',
|
||||
'output': 'api-docs/hydro_generator'
|
||||
},
|
||||
'multiphysics_network': {
|
||||
'path': Path('D:/dongdiankaifa9/multiphysics_network'),
|
||||
'type': 'python',
|
||||
'output': 'api-docs/multiphysics'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def extract_fastapi(name: str, config: Dict[str, Any], output_base: Path) -> bool:
|
||||
"""提取 FastAPI OpenAPI 文档"""
|
||||
path = config['path']
|
||||
output_dir = output_base / config['output']
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 添加路径到 sys.path
|
||||
if str(path) not in sys.path:
|
||||
sys.path.insert(0, str(path))
|
||||
|
||||
try:
|
||||
# 动态导入 app
|
||||
from app.main import app
|
||||
|
||||
# 获取 OpenAPI schema
|
||||
openapi_schema = app.openapi()
|
||||
|
||||
# 保存 JSON
|
||||
json_path = output_dir / 'openapi.json'
|
||||
with open(json_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(openapi_schema, f, indent=2, ensure_ascii=False)
|
||||
|
||||
# 生成 Markdown 摘要
|
||||
md_path = output_dir / 'API_SUMMARY.md'
|
||||
generate_api_markdown(openapi_schema, md_path)
|
||||
|
||||
endpoints = len(openapi_schema.get('paths', {}))
|
||||
print(f" ✓ Extracted {endpoints} endpoints → {output_dir}")
|
||||
return True
|
||||
|
||||
except ImportError as e:
|
||||
print(f" ✗ Import error: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f" ✗ Error: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def generate_api_markdown(schema: Dict, output_path: Path):
|
||||
"""从 OpenAPI schema 生成 Markdown"""
|
||||
lines = [
|
||||
f"# {schema.get('info', {}).get('title', 'API Reference')}",
|
||||
"",
|
||||
f"Version: {schema.get('info', {}).get('version', '1.0.0')}",
|
||||
"",
|
||||
"## Endpoints",
|
||||
"",
|
||||
"| Method | Path | Summary |",
|
||||
"|--------|------|---------|"
|
||||
]
|
||||
|
||||
for path, methods in schema.get('paths', {}).items():
|
||||
for method, details in methods.items():
|
||||
if method in ('get', 'post', 'put', 'delete', 'patch'):
|
||||
summary = details.get('summary', details.get('operationId', '-'))
|
||||
lines.append(f"| `{method.upper()}` | `{path}` | {summary} |")
|
||||
|
||||
lines.extend([
|
||||
"",
|
||||
"## Schemas",
|
||||
""
|
||||
])
|
||||
|
||||
for name, schema_def in schema.get('components', {}).get('schemas', {}).items():
|
||||
lines.append(f"### {name}")
|
||||
lines.append("")
|
||||
if 'properties' in schema_def:
|
||||
lines.append("| Property | Type | Required |")
|
||||
lines.append("|----------|------|----------|")
|
||||
required = schema_def.get('required', [])
|
||||
for prop, prop_def in schema_def['properties'].items():
|
||||
prop_type = prop_def.get('type', prop_def.get('$ref', 'any'))
|
||||
is_required = '✓' if prop in required else ''
|
||||
lines.append(f"| `{prop}` | {prop_type} | {is_required} |")
|
||||
lines.append("")
|
||||
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
f.write('\n'.join(lines))
|
||||
|
||||
|
||||
def extract_typescript(name: str, config: Dict[str, Any], output_base: Path) -> bool:
|
||||
"""提取 TypeScript 文档 (TypeDoc)"""
|
||||
path = config['path']
|
||||
output_dir = output_base / config['output']
|
||||
|
||||
# 检查 TypeDoc 是否已安装
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['npx', 'typedoc', '--version'],
|
||||
cwd=path,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
if result.returncode != 0:
|
||||
print(f" ⚠ TypeDoc not installed, installing...")
|
||||
subprocess.run(
|
||||
['npm', 'install', '--save-dev', 'typedoc', 'typedoc-plugin-markdown'],
|
||||
cwd=path,
|
||||
check=True
|
||||
)
|
||||
except FileNotFoundError:
|
||||
print(f" ✗ npm/npx not found")
|
||||
return False
|
||||
|
||||
# 运行 TypeDoc
|
||||
try:
|
||||
entries = config.get('entries', ['lib'])
|
||||
cmd = [
|
||||
'npx', 'typedoc',
|
||||
'--plugin', 'typedoc-plugin-markdown',
|
||||
'--out', str(output_dir),
|
||||
'--entryPointStrategy', 'expand',
|
||||
'--exclude', '**/node_modules/**',
|
||||
'--exclude', '**/*.test.*',
|
||||
'--readme', 'none'
|
||||
]
|
||||
for entry in entries:
|
||||
entry_path = path / entry
|
||||
if entry_path.exists():
|
||||
cmd.extend(['--entryPoints', str(entry_path)])
|
||||
|
||||
result = subprocess.run(cmd, cwd=path, capture_output=True, text=True)
|
||||
|
||||
if result.returncode == 0:
|
||||
print(f" ✓ TypeDoc generated → {output_dir}")
|
||||
return True
|
||||
else:
|
||||
print(f" ✗ TypeDoc error: {result.stderr[:200]}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f" ✗ Error: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def extract_python_module(name: str, config: Dict[str, Any], output_base: Path) -> bool:
|
||||
"""提取 Python 模块文档 (pdoc)"""
|
||||
path = config['path']
|
||||
output_dir = output_base / config['output']
|
||||
module_name = path.name
|
||||
|
||||
# 检查 pdoc
|
||||
try:
|
||||
subprocess.run(['pdoc', '--version'], capture_output=True, check=True)
|
||||
except (FileNotFoundError, subprocess.CalledProcessError):
|
||||
print(f" ⚠ pdoc not installed, installing...")
|
||||
subprocess.run([sys.executable, '-m', 'pip', 'install', 'pdoc'], check=True)
|
||||
|
||||
# 运行 pdoc
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[
|
||||
'pdoc', module_name,
|
||||
'--output-dir', str(output_dir),
|
||||
'--format', 'markdown'
|
||||
],
|
||||
cwd=path.parent,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
# 统计生成的文件
|
||||
md_files = list(output_dir.glob('**/*.md'))
|
||||
print(f" ✓ pdoc generated {len(md_files)} files → {output_dir}")
|
||||
return True
|
||||
else:
|
||||
print(f" ✗ pdoc error: {result.stderr[:200]}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f" ✗ Error: {e}")
|
||||
return False
|
||||
|
||||
|
||||
EXTRACTORS = {
|
||||
'fastapi': extract_fastapi,
|
||||
'typescript': extract_typescript,
|
||||
'python': extract_python_module
|
||||
}
|
||||
|
||||
|
||||
def main(output_base: Optional[str] = None, projects: Optional[list] = None):
|
||||
"""主入口"""
|
||||
base = Path(output_base) if output_base else Path.cwd()
|
||||
|
||||
print("=" * 50)
|
||||
print("API Documentation Extraction")
|
||||
print("=" * 50)
|
||||
|
||||
results = {}
|
||||
|
||||
for name, config in PROJECTS.items():
|
||||
if projects and name not in projects:
|
||||
continue
|
||||
|
||||
print(f"\n[{name}] ({config['type']})")
|
||||
|
||||
if not config['path'].exists():
|
||||
print(f" ✗ Path not found: {config['path']}")
|
||||
results[name] = False
|
||||
continue
|
||||
|
||||
extractor = EXTRACTORS.get(config['type'])
|
||||
if extractor:
|
||||
results[name] = extractor(name, config, base)
|
||||
else:
|
||||
print(f" ✗ Unknown type: {config['type']}")
|
||||
results[name] = False
|
||||
|
||||
# 汇总
|
||||
print("\n" + "=" * 50)
|
||||
print("Summary")
|
||||
print("=" * 50)
|
||||
success = sum(1 for v in results.values() if v)
|
||||
print(f"Success: {success}/{len(results)}")
|
||||
|
||||
return all(results.values())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description='Extract API documentation')
|
||||
parser.add_argument('--output', '-o', default='.', help='Output base directory')
|
||||
parser.add_argument('--projects', '-p', nargs='+', help='Specific projects to extract')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
success = main(args.output, args.projects)
|
||||
sys.exit(0 if success else 1)
|
||||
447
.claude/skills/software-manual/scripts/screenshot-helper.md
Normal file
447
.claude/skills/software-manual/scripts/screenshot-helper.md
Normal file
@@ -0,0 +1,447 @@
|
||||
# Screenshot Helper
|
||||
|
||||
Guide for capturing screenshots using Chrome MCP.
|
||||
|
||||
## Overview
|
||||
|
||||
This script helps capture screenshots of web interfaces for the software manual using Chrome MCP or fallback methods.
|
||||
|
||||
## Chrome MCP Prerequisites
|
||||
|
||||
### Check MCP Availability
|
||||
|
||||
```javascript
|
||||
async function checkChromeMCPAvailability() {
|
||||
try {
|
||||
// Attempt to get Chrome version via MCP
|
||||
const version = await mcp__chrome__getVersion();
|
||||
return {
|
||||
available: true,
|
||||
browser: version.browser,
|
||||
version: version.version
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
available: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### MCP Configuration
|
||||
|
||||
Expected Claude configuration for Chrome MCP:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"chrome": {
|
||||
"command": "npx",
|
||||
"args": ["@anthropic-ai/mcp-chrome"],
|
||||
"env": {
|
||||
"CHROME_PATH": "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Screenshot Workflow
|
||||
|
||||
### Step 1: Prepare Environment
|
||||
|
||||
```javascript
|
||||
async function prepareScreenshotEnvironment(workDir, config) {
|
||||
const screenshotDir = `${workDir}/screenshots`;
|
||||
|
||||
// Create directory
|
||||
Bash({ command: `mkdir -p "${screenshotDir}"` });
|
||||
|
||||
// Check Chrome MCP
|
||||
const chromeMCP = await checkChromeMCPAvailability();
|
||||
|
||||
if (!chromeMCP.available) {
|
||||
console.log('Chrome MCP not available. Will generate manual guide.');
|
||||
return { mode: 'manual' };
|
||||
}
|
||||
|
||||
// Start development server if needed
|
||||
if (config.screenshot_config?.dev_command) {
|
||||
const server = await startDevServer(config);
|
||||
return { mode: 'auto', server, screenshotDir };
|
||||
}
|
||||
|
||||
return { mode: 'auto', screenshotDir };
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Start Development Server
|
||||
|
||||
```javascript
|
||||
async function startDevServer(config) {
|
||||
const devCommand = config.screenshot_config.dev_command;
|
||||
const devUrl = config.screenshot_config.dev_url;
|
||||
|
||||
// Start server in background
|
||||
const server = Bash({
|
||||
command: devCommand,
|
||||
run_in_background: true
|
||||
});
|
||||
|
||||
console.log(`Starting dev server: ${devCommand}`);
|
||||
|
||||
// Wait for server to be ready
|
||||
const ready = await waitForServer(devUrl, 30000);
|
||||
|
||||
if (!ready) {
|
||||
throw new Error(`Server at ${devUrl} did not start in time`);
|
||||
}
|
||||
|
||||
console.log(`Dev server ready at ${devUrl}`);
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
async function waitForServer(url, timeout = 30000) {
|
||||
const start = Date.now();
|
||||
|
||||
while (Date.now() - start < timeout) {
|
||||
try {
|
||||
const response = await fetch(url, { method: 'HEAD' });
|
||||
if (response.ok) return true;
|
||||
} catch (e) {
|
||||
// Server not ready
|
||||
}
|
||||
await sleep(1000);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Capture Screenshots
|
||||
|
||||
```javascript
|
||||
async function captureScreenshots(screenshots, config, workDir) {
|
||||
const results = {
|
||||
captured: [],
|
||||
failed: []
|
||||
};
|
||||
|
||||
const devUrl = config.screenshot_config.dev_url;
|
||||
const screenshotDir = `${workDir}/screenshots`;
|
||||
|
||||
for (const ss of screenshots) {
|
||||
try {
|
||||
// Build full URL
|
||||
const fullUrl = new URL(ss.url, devUrl).href;
|
||||
|
||||
console.log(`Capturing: ${ss.id} (${fullUrl})`);
|
||||
|
||||
// Configure capture options
|
||||
const options = {
|
||||
url: fullUrl,
|
||||
viewport: { width: 1280, height: 800 },
|
||||
fullPage: ss.fullPage || false
|
||||
};
|
||||
|
||||
// Wait for specific element if specified
|
||||
if (ss.wait_for) {
|
||||
options.waitFor = ss.wait_for;
|
||||
}
|
||||
|
||||
// Capture specific element if selector provided
|
||||
if (ss.selector) {
|
||||
options.selector = ss.selector;
|
||||
}
|
||||
|
||||
// Add delay for animations
|
||||
await sleep(500);
|
||||
|
||||
// Capture via Chrome MCP
|
||||
const result = await mcp__chrome__screenshot(options);
|
||||
|
||||
// Save as PNG
|
||||
const filename = `${ss.id}.png`;
|
||||
Write(`${screenshotDir}/${filename}`, result.data, { encoding: 'base64' });
|
||||
|
||||
results.captured.push({
|
||||
id: ss.id,
|
||||
file: filename,
|
||||
url: ss.url,
|
||||
description: ss.description,
|
||||
size: result.data.length
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Failed to capture ${ss.id}:`, error.message);
|
||||
results.failed.push({
|
||||
id: ss.id,
|
||||
url: ss.url,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Generate Manifest
|
||||
|
||||
```javascript
|
||||
function generateScreenshotManifest(results, workDir) {
|
||||
const manifest = {
|
||||
generated: new Date().toISOString(),
|
||||
total: results.captured.length + results.failed.length,
|
||||
captured: results.captured.length,
|
||||
failed: results.failed.length,
|
||||
screenshots: results.captured,
|
||||
failures: results.failed
|
||||
};
|
||||
|
||||
Write(`${workDir}/screenshots/screenshots-manifest.json`,
|
||||
JSON.stringify(manifest, null, 2));
|
||||
|
||||
return manifest;
|
||||
}
|
||||
```
|
||||
|
||||
### Step 5: Cleanup
|
||||
|
||||
```javascript
|
||||
async function cleanupScreenshotEnvironment(env) {
|
||||
if (env.server) {
|
||||
console.log('Stopping dev server...');
|
||||
KillShell({ shell_id: env.server.task_id });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Main Runner
|
||||
|
||||
```javascript
|
||||
async function runScreenshotCapture(workDir, screenshots) {
|
||||
const config = JSON.parse(Read(`${workDir}/manual-config.json`));
|
||||
|
||||
// Prepare environment
|
||||
const env = await prepareScreenshotEnvironment(workDir, config);
|
||||
|
||||
if (env.mode === 'manual') {
|
||||
// Generate manual capture guide
|
||||
generateManualCaptureGuide(screenshots, workDir);
|
||||
return { success: false, mode: 'manual' };
|
||||
}
|
||||
|
||||
try {
|
||||
// Capture screenshots
|
||||
const results = await captureScreenshots(screenshots, config, workDir);
|
||||
|
||||
// Generate manifest
|
||||
const manifest = generateScreenshotManifest(results, workDir);
|
||||
|
||||
// Generate manual guide for failed captures
|
||||
if (results.failed.length > 0) {
|
||||
generateManualCaptureGuide(results.failed, workDir);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
captured: results.captured.length,
|
||||
failed: results.failed.length,
|
||||
manifest
|
||||
};
|
||||
|
||||
} finally {
|
||||
// Cleanup
|
||||
await cleanupScreenshotEnvironment(env);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Manual Capture Fallback
|
||||
|
||||
When Chrome MCP is unavailable:
|
||||
|
||||
```javascript
|
||||
function generateManualCaptureGuide(screenshots, workDir) {
|
||||
const guide = `
|
||||
# Manual Screenshot Capture Guide
|
||||
|
||||
Chrome MCP is not available. Please capture screenshots manually.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. Start your development server
|
||||
2. Open a browser
|
||||
3. Use a screenshot tool (Snipping Tool, Screenshot, etc.)
|
||||
|
||||
## Screenshots Required
|
||||
|
||||
${screenshots.map((ss, i) => `
|
||||
### ${i + 1}. ${ss.id}
|
||||
|
||||
- **URL**: ${ss.url}
|
||||
- **Description**: ${ss.description}
|
||||
- **Save as**: \`screenshots/${ss.id}.png\`
|
||||
${ss.selector ? `- **Capture area**: \`${ss.selector}\` element only` : '- **Type**: Full page or viewport'}
|
||||
${ss.wait_for ? `- **Wait for**: \`${ss.wait_for}\` to be visible` : ''}
|
||||
|
||||
**Steps:**
|
||||
1. Navigate to ${ss.url}
|
||||
${ss.wait_for ? `2. Wait for ${ss.wait_for} to appear` : ''}
|
||||
${ss.selector ? `2. Capture only the ${ss.selector} area` : '2. Capture the full viewport'}
|
||||
3. Save as \`${ss.id}.png\`
|
||||
`).join('\n')}
|
||||
|
||||
## After Capturing
|
||||
|
||||
1. Place all PNG files in the \`screenshots/\` directory
|
||||
2. Ensure filenames match exactly (case-sensitive)
|
||||
3. Run Phase 5 (HTML Assembly) to continue
|
||||
|
||||
## Screenshot Specifications
|
||||
|
||||
- **Format**: PNG
|
||||
- **Width**: 1280px recommended
|
||||
- **Quality**: High
|
||||
- **Annotations**: None (add in post-processing if needed)
|
||||
`;
|
||||
|
||||
Write(`${workDir}/screenshots/MANUAL_CAPTURE.md`, guide);
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Options
|
||||
|
||||
### Viewport Sizes
|
||||
|
||||
```javascript
|
||||
const viewportPresets = {
|
||||
desktop: { width: 1280, height: 800 },
|
||||
tablet: { width: 768, height: 1024 },
|
||||
mobile: { width: 375, height: 667 },
|
||||
wide: { width: 1920, height: 1080 }
|
||||
};
|
||||
|
||||
async function captureResponsive(ss, config, workDir) {
|
||||
const results = [];
|
||||
|
||||
for (const [name, viewport] of Object.entries(viewportPresets)) {
|
||||
const result = await mcp__chrome__screenshot({
|
||||
url: ss.url,
|
||||
viewport
|
||||
});
|
||||
|
||||
const filename = `${ss.id}-${name}.png`;
|
||||
Write(`${workDir}/screenshots/${filename}`, result.data, { encoding: 'base64' });
|
||||
|
||||
results.push({ viewport: name, file: filename });
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
```
|
||||
|
||||
### Before/After Comparisons
|
||||
|
||||
```javascript
|
||||
async function captureInteraction(ss, config, workDir) {
|
||||
const baseUrl = config.screenshot_config.dev_url;
|
||||
const fullUrl = new URL(ss.url, baseUrl).href;
|
||||
|
||||
// Capture before state
|
||||
const before = await mcp__chrome__screenshot({
|
||||
url: fullUrl,
|
||||
viewport: { width: 1280, height: 800 }
|
||||
});
|
||||
Write(`${workDir}/screenshots/${ss.id}-before.png`, before.data, { encoding: 'base64' });
|
||||
|
||||
// Perform interaction (click, type, etc.)
|
||||
if (ss.interaction) {
|
||||
await mcp__chrome__click({ selector: ss.interaction.click });
|
||||
await sleep(500);
|
||||
}
|
||||
|
||||
// Capture after state
|
||||
const after = await mcp__chrome__screenshot({
|
||||
url: fullUrl,
|
||||
viewport: { width: 1280, height: 800 }
|
||||
});
|
||||
Write(`${workDir}/screenshots/${ss.id}-after.png`, after.data, { encoding: 'base64' });
|
||||
|
||||
return {
|
||||
before: `${ss.id}-before.png`,
|
||||
after: `${ss.id}-after.png`
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Screenshot Annotation
|
||||
|
||||
```javascript
|
||||
function generateAnnotationGuide(screenshots, workDir) {
|
||||
const guide = `
|
||||
# Screenshot Annotation Guide
|
||||
|
||||
For screenshots requiring callouts or highlights:
|
||||
|
||||
## Tools
|
||||
- macOS: Preview, Skitch
|
||||
- Windows: Snipping Tool, ShareX
|
||||
- Cross-platform: Greenshot, Lightshot
|
||||
|
||||
## Annotation Guidelines
|
||||
|
||||
1. **Callouts**: Use numbered circles (1, 2, 3)
|
||||
2. **Highlights**: Use semi-transparent rectangles
|
||||
3. **Arrows**: Point from text to element
|
||||
4. **Text**: Use sans-serif font, 12-14pt
|
||||
|
||||
## Color Scheme
|
||||
|
||||
- Primary: #0d6efd (blue)
|
||||
- Secondary: #6c757d (gray)
|
||||
- Success: #198754 (green)
|
||||
- Warning: #ffc107 (yellow)
|
||||
- Danger: #dc3545 (red)
|
||||
|
||||
## Screenshots Needing Annotation
|
||||
|
||||
${screenshots.filter(s => s.annotate).map(ss => `
|
||||
- **${ss.id}**: ${ss.description}
|
||||
- Highlight: ${ss.annotate.highlight || 'N/A'}
|
||||
- Callouts: ${ss.annotate.callouts?.join(', ') || 'N/A'}
|
||||
`).join('\n')}
|
||||
`;
|
||||
|
||||
Write(`${workDir}/screenshots/ANNOTATION_GUIDE.md`, guide);
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Chrome MCP Not Found
|
||||
|
||||
1. Check Claude MCP configuration
|
||||
2. Verify Chrome is installed
|
||||
3. Check CHROME_PATH environment variable
|
||||
|
||||
### Screenshots Are Blank
|
||||
|
||||
1. Increase wait time before capture
|
||||
2. Check if page requires authentication
|
||||
3. Verify URL is correct
|
||||
|
||||
### Elements Not Visible
|
||||
|
||||
1. Scroll element into view
|
||||
2. Expand collapsed sections
|
||||
3. Wait for animations to complete
|
||||
|
||||
### Server Not Starting
|
||||
|
||||
1. Check if port is already in use
|
||||
2. Verify dev command is correct
|
||||
3. Check for startup errors in logs
|
||||
419
.claude/skills/software-manual/scripts/swagger-runner.md
Normal file
419
.claude/skills/software-manual/scripts/swagger-runner.md
Normal file
@@ -0,0 +1,419 @@
|
||||
# Swagger/OpenAPI Runner
|
||||
|
||||
Guide for generating backend API documentation from OpenAPI/Swagger specifications.
|
||||
|
||||
## Overview
|
||||
|
||||
This script extracts and converts OpenAPI/Swagger specifications to Markdown format for inclusion in the software manual.
|
||||
|
||||
## Detection Strategy
|
||||
|
||||
### Check for Existing Specification
|
||||
|
||||
```javascript
|
||||
async function detectOpenAPISpec() {
|
||||
// Check for existing spec files
|
||||
const specPatterns = [
|
||||
'openapi.json',
|
||||
'openapi.yaml',
|
||||
'openapi.yml',
|
||||
'swagger.json',
|
||||
'swagger.yaml',
|
||||
'swagger.yml',
|
||||
'**/openapi*.json',
|
||||
'**/swagger*.json'
|
||||
];
|
||||
|
||||
for (const pattern of specPatterns) {
|
||||
const files = Glob(pattern);
|
||||
if (files.length > 0) {
|
||||
return { found: true, type: 'file', path: files[0] };
|
||||
}
|
||||
}
|
||||
|
||||
// Check for swagger-jsdoc in dependencies
|
||||
const packageJson = JSON.parse(Read('package.json'));
|
||||
if (packageJson.dependencies?.['swagger-jsdoc'] ||
|
||||
packageJson.devDependencies?.['swagger-jsdoc']) {
|
||||
return { found: true, type: 'jsdoc' };
|
||||
}
|
||||
|
||||
// Check for NestJS Swagger
|
||||
if (packageJson.dependencies?.['@nestjs/swagger']) {
|
||||
return { found: true, type: 'nestjs' };
|
||||
}
|
||||
|
||||
// Check for runtime endpoint
|
||||
return { found: false, suggestion: 'runtime' };
|
||||
}
|
||||
```
|
||||
|
||||
## Extraction Methods
|
||||
|
||||
### Method A: From Existing Spec File
|
||||
|
||||
```javascript
|
||||
async function extractFromFile(specPath, workDir) {
|
||||
const outputDir = `${workDir}/api-docs/backend`;
|
||||
Bash({ command: `mkdir -p "${outputDir}"` });
|
||||
|
||||
// Copy spec to output
|
||||
Bash({ command: `cp "${specPath}" "${outputDir}/openapi.json"` });
|
||||
|
||||
// Convert to Markdown using widdershins
|
||||
const result = Bash({
|
||||
command: `npx widdershins "${specPath}" -o "${outputDir}/api-reference.md" --language_tabs 'javascript:JavaScript' 'python:Python' 'bash:cURL'`,
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
return { success: result.exitCode === 0, outputDir };
|
||||
}
|
||||
```
|
||||
|
||||
### Method B: From swagger-jsdoc
|
||||
|
||||
```javascript
|
||||
async function extractFromJsDoc(workDir) {
|
||||
const outputDir = `${workDir}/api-docs/backend`;
|
||||
|
||||
// Look for swagger definition file
|
||||
const defFiles = Glob('**/swagger*.js').concat(Glob('**/openapi*.js'));
|
||||
if (defFiles.length === 0) {
|
||||
return { success: false, error: 'No swagger definition found' };
|
||||
}
|
||||
|
||||
// Generate spec
|
||||
const result = Bash({
|
||||
command: `npx swagger-jsdoc -d "${defFiles[0]}" -o "${outputDir}/openapi.json"`,
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
return { success: false, error: result.stderr };
|
||||
}
|
||||
|
||||
// Convert to Markdown
|
||||
Bash({
|
||||
command: `npx widdershins "${outputDir}/openapi.json" -o "${outputDir}/api-reference.md" --language_tabs 'javascript:JavaScript' 'bash:cURL'`
|
||||
});
|
||||
|
||||
return { success: true, outputDir };
|
||||
}
|
||||
```
|
||||
|
||||
### Method C: From NestJS Swagger
|
||||
|
||||
```javascript
|
||||
async function extractFromNestJS(workDir) {
|
||||
const outputDir = `${workDir}/api-docs/backend`;
|
||||
|
||||
// NestJS typically exposes /api-docs-json at runtime
|
||||
// We need to start the server temporarily
|
||||
|
||||
// Start server in background
|
||||
const server = Bash({
|
||||
command: 'npm run start:dev',
|
||||
run_in_background: true,
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
// Wait for server to be ready
|
||||
await waitForServer('http://localhost:3000', 30000);
|
||||
|
||||
// Fetch OpenAPI spec
|
||||
const spec = await fetch('http://localhost:3000/api-docs-json');
|
||||
const specJson = await spec.json();
|
||||
|
||||
// Save spec
|
||||
Write(`${outputDir}/openapi.json`, JSON.stringify(specJson, null, 2));
|
||||
|
||||
// Stop server
|
||||
KillShell({ shell_id: server.task_id });
|
||||
|
||||
// Convert to Markdown
|
||||
Bash({
|
||||
command: `npx widdershins "${outputDir}/openapi.json" -o "${outputDir}/api-reference.md" --language_tabs 'javascript:JavaScript' 'bash:cURL'`
|
||||
});
|
||||
|
||||
return { success: true, outputDir };
|
||||
}
|
||||
```
|
||||
|
||||
### Method D: From Runtime Endpoint
|
||||
|
||||
```javascript
|
||||
async function extractFromRuntime(workDir, serverUrl = 'http://localhost:3000') {
|
||||
const outputDir = `${workDir}/api-docs/backend`;
|
||||
|
||||
// Common OpenAPI endpoint paths
|
||||
const endpointPaths = [
|
||||
'/api-docs-json',
|
||||
'/swagger.json',
|
||||
'/openapi.json',
|
||||
'/docs/json',
|
||||
'/api/v1/docs.json'
|
||||
];
|
||||
|
||||
let specJson = null;
|
||||
|
||||
for (const path of endpointPaths) {
|
||||
try {
|
||||
const response = await fetch(`${serverUrl}${path}`);
|
||||
if (response.ok) {
|
||||
specJson = await response.json();
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!specJson) {
|
||||
return { success: false, error: 'Could not fetch OpenAPI spec from server' };
|
||||
}
|
||||
|
||||
// Save and convert
|
||||
Write(`${outputDir}/openapi.json`, JSON.stringify(specJson, null, 2));
|
||||
|
||||
Bash({
|
||||
command: `npx widdershins "${outputDir}/openapi.json" -o "${outputDir}/api-reference.md"`
|
||||
});
|
||||
|
||||
return { success: true, outputDir };
|
||||
}
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
### Required Tools
|
||||
|
||||
```bash
|
||||
# For OpenAPI to Markdown conversion
|
||||
npm install -g widdershins
|
||||
|
||||
# Or as dev dependency
|
||||
npm install --save-dev widdershins
|
||||
|
||||
# For generating from JSDoc comments
|
||||
npm install --save-dev swagger-jsdoc
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### widdershins Options
|
||||
|
||||
```bash
|
||||
npx widdershins openapi.json \
|
||||
-o api-reference.md \
|
||||
--language_tabs 'javascript:JavaScript' 'python:Python' 'bash:cURL' \
|
||||
--summary \
|
||||
--omitHeader \
|
||||
--resolve \
|
||||
--expandBody
|
||||
```
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--language_tabs` | Code example languages |
|
||||
| `--summary` | Use summary as operation heading |
|
||||
| `--omitHeader` | Don't include title header |
|
||||
| `--resolve` | Resolve $ref references |
|
||||
| `--expandBody` | Show full request body |
|
||||
|
||||
### swagger-jsdoc Definition
|
||||
|
||||
Example `swagger-def.js`:
|
||||
|
||||
```javascript
|
||||
module.exports = {
|
||||
definition: {
|
||||
openapi: '3.0.0',
|
||||
info: {
|
||||
title: 'MyApp API',
|
||||
version: '1.0.0',
|
||||
description: 'API documentation for MyApp'
|
||||
},
|
||||
servers: [
|
||||
{ url: 'http://localhost:3000/api/v1' }
|
||||
]
|
||||
},
|
||||
apis: ['./src/routes/*.js', './src/controllers/*.js']
|
||||
};
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
### Generated Markdown Structure
|
||||
|
||||
```markdown
|
||||
# MyApp API
|
||||
|
||||
## Overview
|
||||
|
||||
Base URL: `http://localhost:3000/api/v1`
|
||||
|
||||
## Authentication
|
||||
|
||||
This API uses Bearer token authentication.
|
||||
|
||||
---
|
||||
|
||||
## Projects
|
||||
|
||||
### List Projects
|
||||
|
||||
`GET /projects`
|
||||
|
||||
Returns a list of all projects.
|
||||
|
||||
**Parameters**
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
|------|-----|------|----------|-------------|
|
||||
| status | query | string | false | Filter by status |
|
||||
| page | query | integer | false | Page number |
|
||||
|
||||
**Responses**
|
||||
|
||||
| Status | Description |
|
||||
|--------|-------------|
|
||||
| 200 | Successful response |
|
||||
| 401 | Unauthorized |
|
||||
|
||||
**Example Request**
|
||||
|
||||
```javascript
|
||||
fetch('/api/v1/projects?status=active')
|
||||
.then(res => res.json())
|
||||
.then(data => console.log(data));
|
||||
```
|
||||
|
||||
**Example Response**
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{ "id": "1", "name": "Project 1" }
|
||||
],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"total": 10
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
## Integration
|
||||
|
||||
### Main Runner
|
||||
|
||||
```javascript
|
||||
async function runSwaggerExtraction(workDir) {
|
||||
const detection = await detectOpenAPISpec();
|
||||
|
||||
if (!detection.found) {
|
||||
console.log('No OpenAPI spec detected. Skipping backend API docs.');
|
||||
return { success: false, skipped: true };
|
||||
}
|
||||
|
||||
let result;
|
||||
|
||||
switch (detection.type) {
|
||||
case 'file':
|
||||
result = await extractFromFile(detection.path, workDir);
|
||||
break;
|
||||
case 'jsdoc':
|
||||
result = await extractFromJsDoc(workDir);
|
||||
break;
|
||||
case 'nestjs':
|
||||
result = await extractFromNestJS(workDir);
|
||||
break;
|
||||
default:
|
||||
result = await extractFromRuntime(workDir);
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
// Post-process the Markdown
|
||||
await postProcessApiDocs(result.outputDir);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function postProcessApiDocs(outputDir) {
|
||||
const mdFile = `${outputDir}/api-reference.md`;
|
||||
let content = Read(mdFile);
|
||||
|
||||
// Remove widdershins header
|
||||
content = content.replace(/^---[\s\S]*?---\n/, '');
|
||||
|
||||
// Add custom styling hints
|
||||
content = content.replace(/^(#{1,3} .+)$/gm, '$1\n');
|
||||
|
||||
Write(mdFile, content);
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### "widdershins: command not found"
|
||||
|
||||
```bash
|
||||
npm install -g widdershins
|
||||
# Or use npx
|
||||
npx widdershins openapi.json -o api.md
|
||||
```
|
||||
|
||||
#### "Error parsing OpenAPI spec"
|
||||
|
||||
```bash
|
||||
# Validate spec first
|
||||
npx @redocly/cli lint openapi.json
|
||||
|
||||
# Fix common issues
|
||||
npx @redocly/cli bundle openapi.json -o fixed.json
|
||||
```
|
||||
|
||||
#### "Server not responding"
|
||||
|
||||
Ensure the development server is running and accessible:
|
||||
|
||||
```bash
|
||||
# Check if server is running
|
||||
curl http://localhost:3000/health
|
||||
|
||||
# Check OpenAPI endpoint
|
||||
curl http://localhost:3000/api-docs-json
|
||||
```
|
||||
|
||||
### Manual Fallback
|
||||
|
||||
If automatic extraction fails, document APIs manually:
|
||||
|
||||
1. List all route files: `Glob('**/routes/*.js')`
|
||||
2. Extract route definitions using regex
|
||||
3. Build documentation structure manually
|
||||
|
||||
```javascript
|
||||
async function manualApiExtraction(workDir) {
|
||||
const routeFiles = Glob('src/routes/*.js').concat(Glob('src/routes/*.ts'));
|
||||
const endpoints = [];
|
||||
|
||||
for (const file of routeFiles) {
|
||||
const content = Read(file);
|
||||
const routes = content.matchAll(/router\.(get|post|put|delete|patch)\(['"]([^'"]+)['"]/g);
|
||||
|
||||
for (const match of routes) {
|
||||
endpoints.push({
|
||||
method: match[1].toUpperCase(),
|
||||
path: match[2],
|
||||
file: file
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return endpoints;
|
||||
}
|
||||
```
|
||||
357
.claude/skills/software-manual/scripts/typedoc-runner.md
Normal file
357
.claude/skills/software-manual/scripts/typedoc-runner.md
Normal file
@@ -0,0 +1,357 @@
|
||||
# TypeDoc Runner
|
||||
|
||||
Guide for generating frontend API documentation using TypeDoc.
|
||||
|
||||
## Overview
|
||||
|
||||
TypeDoc generates API documentation from TypeScript source code by analyzing type annotations and JSDoc comments.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Check TypeScript Project
|
||||
|
||||
```javascript
|
||||
// Verify TypeScript is used
|
||||
const packageJson = JSON.parse(Read('package.json'));
|
||||
const hasTypeScript = packageJson.devDependencies?.typescript ||
|
||||
packageJson.dependencies?.typescript;
|
||||
|
||||
if (!hasTypeScript) {
|
||||
console.log('Not a TypeScript project. Skipping TypeDoc.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for tsconfig.json
|
||||
const hasTsConfig = Glob('tsconfig.json').length > 0;
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
### Install TypeDoc
|
||||
|
||||
```bash
|
||||
npm install --save-dev typedoc typedoc-plugin-markdown
|
||||
```
|
||||
|
||||
### Optional Plugins
|
||||
|
||||
```bash
|
||||
# For better Markdown output
|
||||
npm install --save-dev typedoc-plugin-markdown
|
||||
|
||||
# For README inclusion
|
||||
npm install --save-dev typedoc-plugin-rename-defaults
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### typedoc.json
|
||||
|
||||
Create `typedoc.json` in project root:
|
||||
|
||||
```json
|
||||
{
|
||||
"entryPoints": ["./src/index.ts"],
|
||||
"entryPointStrategy": "expand",
|
||||
"out": ".workflow/.scratchpad/manual-{timestamp}/api-docs/frontend",
|
||||
"plugin": ["typedoc-plugin-markdown"],
|
||||
"exclude": [
|
||||
"**/node_modules/**",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts",
|
||||
"**/tests/**"
|
||||
],
|
||||
"excludePrivate": true,
|
||||
"excludeProtected": true,
|
||||
"excludeInternal": true,
|
||||
"hideGenerator": true,
|
||||
"readme": "none",
|
||||
"categorizeByGroup": true,
|
||||
"navigation": {
|
||||
"includeCategories": true,
|
||||
"includeGroups": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Alternative: CLI Options
|
||||
|
||||
```bash
|
||||
npx typedoc \
|
||||
--entryPoints src/index.ts \
|
||||
--entryPointStrategy expand \
|
||||
--out api-docs/frontend \
|
||||
--plugin typedoc-plugin-markdown \
|
||||
--exclude "**/node_modules/**" \
|
||||
--exclude "**/*.test.ts" \
|
||||
--excludePrivate \
|
||||
--excludeProtected \
|
||||
--readme none
|
||||
```
|
||||
|
||||
## Execution
|
||||
|
||||
### Basic Run
|
||||
|
||||
```javascript
|
||||
async function runTypeDoc(workDir) {
|
||||
const outputDir = `${workDir}/api-docs/frontend`;
|
||||
|
||||
// Create output directory
|
||||
Bash({ command: `mkdir -p "${outputDir}"` });
|
||||
|
||||
// Run TypeDoc
|
||||
const result = Bash({
|
||||
command: `npx typedoc --out "${outputDir}" --plugin typedoc-plugin-markdown src/`,
|
||||
timeout: 120000 // 2 minutes
|
||||
});
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
console.error('TypeDoc failed:', result.stderr);
|
||||
return { success: false, error: result.stderr };
|
||||
}
|
||||
|
||||
// List generated files
|
||||
const files = Glob(`${outputDir}/**/*.md`);
|
||||
console.log(`Generated ${files.length} documentation files`);
|
||||
|
||||
return { success: true, files };
|
||||
}
|
||||
```
|
||||
|
||||
### With Custom Entry Points
|
||||
|
||||
```javascript
|
||||
async function runTypeDocCustom(workDir, entryPoints) {
|
||||
const outputDir = `${workDir}/api-docs/frontend`;
|
||||
|
||||
// Build entry points string
|
||||
const entries = entryPoints.map(e => `--entryPoints "${e}"`).join(' ');
|
||||
|
||||
const result = Bash({
|
||||
command: `npx typedoc ${entries} --out "${outputDir}" --plugin typedoc-plugin-markdown`,
|
||||
timeout: 120000
|
||||
});
|
||||
|
||||
return { success: result.exitCode === 0 };
|
||||
}
|
||||
|
||||
// Example: Document specific files
|
||||
await runTypeDocCustom(workDir, [
|
||||
'src/api/index.ts',
|
||||
'src/hooks/index.ts',
|
||||
'src/utils/index.ts'
|
||||
]);
|
||||
```
|
||||
|
||||
## Output Structure
|
||||
|
||||
```
|
||||
api-docs/frontend/
|
||||
├── README.md # Index
|
||||
├── modules.md # Module list
|
||||
├── modules/
|
||||
│ ├── api.md # API module
|
||||
│ ├── hooks.md # Hooks module
|
||||
│ └── utils.md # Utils module
|
||||
├── classes/
|
||||
│ ├── ApiClient.md # Class documentation
|
||||
│ └── ...
|
||||
├── interfaces/
|
||||
│ ├── Config.md # Interface documentation
|
||||
│ └── ...
|
||||
└── functions/
|
||||
├── formatDate.md # Function documentation
|
||||
└── ...
|
||||
```
|
||||
|
||||
## Integration with Manual
|
||||
|
||||
### Reading TypeDoc Output
|
||||
|
||||
```javascript
|
||||
async function integrateTypeDocOutput(workDir) {
|
||||
const apiDocsDir = `${workDir}/api-docs/frontend`;
|
||||
const files = Glob(`${apiDocsDir}/**/*.md`);
|
||||
|
||||
// Build API reference content
|
||||
let content = '## Frontend API Reference\n\n';
|
||||
|
||||
// Add modules
|
||||
const modules = Glob(`${apiDocsDir}/modules/*.md`);
|
||||
for (const mod of modules) {
|
||||
const modContent = Read(mod);
|
||||
content += `### ${extractTitle(modContent)}\n\n`;
|
||||
content += summarizeModule(modContent);
|
||||
}
|
||||
|
||||
// Add functions
|
||||
const functions = Glob(`${apiDocsDir}/functions/*.md`);
|
||||
content += '\n### Functions\n\n';
|
||||
for (const fn of functions) {
|
||||
const fnContent = Read(fn);
|
||||
content += formatFunctionDoc(fnContent);
|
||||
}
|
||||
|
||||
// Add hooks
|
||||
const hooks = Glob(`${apiDocsDir}/functions/*Hook*.md`);
|
||||
if (hooks.length > 0) {
|
||||
content += '\n### Hooks\n\n';
|
||||
for (const hook of hooks) {
|
||||
const hookContent = Read(hook);
|
||||
content += formatHookDoc(hookContent);
|
||||
}
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
```
|
||||
|
||||
### Example Output Format
|
||||
|
||||
```markdown
|
||||
## Frontend API Reference
|
||||
|
||||
### API Module
|
||||
|
||||
Functions for interacting with the backend API.
|
||||
|
||||
#### fetchProjects
|
||||
|
||||
```typescript
|
||||
function fetchProjects(options?: FetchOptions): Promise<Project[]>
|
||||
```
|
||||
|
||||
Fetches all projects for the current user.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| options | FetchOptions | Optional fetch configuration |
|
||||
|
||||
**Returns:** Promise<Project[]>
|
||||
|
||||
### Hooks
|
||||
|
||||
#### useProjects
|
||||
|
||||
```typescript
|
||||
function useProjects(options?: UseProjectsOptions): UseProjectsResult
|
||||
```
|
||||
|
||||
React hook for managing project data.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| options.status | string | Filter by project status |
|
||||
| options.limit | number | Max projects to fetch |
|
||||
|
||||
**Returns:**
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| projects | Project[] | Array of projects |
|
||||
| loading | boolean | Loading state |
|
||||
| error | Error \| null | Error if failed |
|
||||
| refetch | () => void | Refresh data |
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### "Cannot find module 'typescript'"
|
||||
|
||||
```bash
|
||||
npm install --save-dev typescript
|
||||
```
|
||||
|
||||
#### "No entry points found"
|
||||
|
||||
Ensure entry points exist:
|
||||
|
||||
```bash
|
||||
# Check entry points
|
||||
ls src/index.ts
|
||||
|
||||
# Or use glob pattern
|
||||
npx typedoc --entryPoints "src/**/*.ts"
|
||||
```
|
||||
|
||||
#### "Unsupported TypeScript version"
|
||||
|
||||
```bash
|
||||
# Check TypeDoc compatibility
|
||||
npm info typedoc peerDependencies
|
||||
|
||||
# Install compatible version
|
||||
npm install --save-dev typedoc@0.25.x
|
||||
```
|
||||
|
||||
### Debugging
|
||||
|
||||
```bash
|
||||
# Verbose output
|
||||
npx typedoc --logLevel Verbose src/
|
||||
|
||||
# Show warnings
|
||||
npx typedoc --treatWarningsAsErrors src/
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Document Exports Only
|
||||
|
||||
```typescript
|
||||
// Good: Public API documented
|
||||
/**
|
||||
* Fetches projects from the API.
|
||||
* @param options - Fetch options
|
||||
* @returns Promise resolving to projects
|
||||
*/
|
||||
export function fetchProjects(options?: FetchOptions): Promise<Project[]> {
|
||||
// ...
|
||||
}
|
||||
|
||||
// Internal: Not documented
|
||||
function internalHelper() {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Use JSDoc Comments
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* User hook for managing authentication state.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { user, login, logout } = useAuth();
|
||||
* ```
|
||||
*
|
||||
* @returns Authentication state and methods
|
||||
*/
|
||||
export function useAuth(): AuthResult {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Define Types Properly
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Configuration for the API client.
|
||||
*/
|
||||
export interface ApiConfig {
|
||||
/** API base URL */
|
||||
baseUrl: string;
|
||||
/** Request timeout in milliseconds */
|
||||
timeout?: number;
|
||||
/** Custom headers to include */
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
```
|
||||
325
.claude/skills/software-manual/specs/html-template.md
Normal file
325
.claude/skills/software-manual/specs/html-template.md
Normal file
@@ -0,0 +1,325 @@
|
||||
# HTML Template Specification
|
||||
|
||||
Technical specification for the TiddlyWiki-style HTML output.
|
||||
|
||||
## Overview
|
||||
|
||||
The output is a single, self-contained HTML file with:
|
||||
- All CSS embedded inline
|
||||
- All JavaScript embedded inline
|
||||
- All images embedded as Base64
|
||||
- Full offline functionality
|
||||
|
||||
## File Structure
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{SOFTWARE_NAME}} - User Manual</title>
|
||||
<style>{{EMBEDDED_CSS}}</style>
|
||||
</head>
|
||||
<body class="wiki-container" data-theme="light">
|
||||
<aside class="wiki-sidebar">...</aside>
|
||||
<main class="wiki-content">...</main>
|
||||
<button class="theme-toggle">...</button>
|
||||
<script id="search-index" type="application/json">{{SEARCH_INDEX}}</script>
|
||||
<script>{{EMBEDDED_JS}}</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
## Placeholders
|
||||
|
||||
| Placeholder | Description | Source |
|
||||
|-------------|-------------|--------|
|
||||
| `{{SOFTWARE_NAME}}` | Software name | manual-config.json |
|
||||
| `{{VERSION}}` | Version number | manual-config.json |
|
||||
| `{{EMBEDDED_CSS}}` | All CSS content | wiki-base.css + wiki-dark.css |
|
||||
| `{{TOC_HTML}}` | Table of contents | Generated from sections |
|
||||
| `{{TIDDLERS_HTML}}` | All content blocks | Converted from Markdown |
|
||||
| `{{SEARCH_INDEX_JSON}}` | Search data | Generated from content |
|
||||
| `{{EMBEDDED_JS}}` | JavaScript code | Inline in template |
|
||||
| `{{TIMESTAMP}}` | Generation timestamp | ISO 8601 format |
|
||||
| `{{LOGO_BASE64}}` | Logo image | Project logo or generated |
|
||||
|
||||
## Component Specifications
|
||||
|
||||
### Sidebar (`.wiki-sidebar`)
|
||||
|
||||
```
|
||||
Width: 280px (fixed)
|
||||
Position: Fixed left
|
||||
Height: 100vh
|
||||
Components:
|
||||
- Logo area (.wiki-logo)
|
||||
- Search box (.wiki-search)
|
||||
- Tag navigation (.wiki-tags)
|
||||
- Table of contents (.wiki-toc)
|
||||
```
|
||||
|
||||
### Main Content (`.wiki-content`)
|
||||
|
||||
```
|
||||
Margin-left: 280px (sidebar width)
|
||||
Max-width: 900px (content)
|
||||
Components:
|
||||
- Header bar (.content-header)
|
||||
- Tiddler container (.tiddler-container)
|
||||
- Footer (.wiki-footer)
|
||||
```
|
||||
|
||||
### Tiddler (Content Block)
|
||||
|
||||
```html
|
||||
<article class="tiddler"
|
||||
id="tiddler-{{ID}}"
|
||||
data-tags="{{TAGS}}"
|
||||
data-difficulty="{{DIFFICULTY}}">
|
||||
<header class="tiddler-header">
|
||||
<h2 class="tiddler-title">
|
||||
<button class="collapse-toggle">▼</button>
|
||||
{{TITLE}}
|
||||
</h2>
|
||||
<div class="tiddler-meta">
|
||||
<span class="difficulty-badge {{DIFFICULTY}}">{{DIFFICULTY_LABEL}}</span>
|
||||
{{TAG_BADGES}}
|
||||
</div>
|
||||
</header>
|
||||
<div class="tiddler-content">
|
||||
{{CONTENT_HTML}}
|
||||
</div>
|
||||
</article>
|
||||
```
|
||||
|
||||
### Search Index Format
|
||||
|
||||
```json
|
||||
{
|
||||
"tiddler-overview": {
|
||||
"title": "Product Overview",
|
||||
"body": "Plain text content for searching...",
|
||||
"tags": ["getting-started", "overview"]
|
||||
},
|
||||
"tiddler-ui-guide": {
|
||||
"title": "UI Guide",
|
||||
"body": "Plain text content...",
|
||||
"tags": ["ui-guide"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Interactive Features
|
||||
|
||||
### 1. Search
|
||||
|
||||
- Full-text search with result highlighting
|
||||
- Searches title, body, and tags
|
||||
- Shows up to 10 results
|
||||
- Keyboard accessible (Enter to search, Esc to close)
|
||||
|
||||
### 2. Collapse/Expand
|
||||
|
||||
- Per-section toggle via button
|
||||
- Expand All / Collapse All buttons
|
||||
- State indicated by ▼ (expanded) or ▶ (collapsed)
|
||||
- Smooth transition animation
|
||||
|
||||
### 3. Tag Filtering
|
||||
|
||||
- Tags: all, getting-started, ui-guide, api, config, troubleshooting, examples
|
||||
- Single selection (radio behavior)
|
||||
- "all" shows everything
|
||||
- Hidden tiddlers via `display: none`
|
||||
|
||||
### 4. Theme Toggle
|
||||
|
||||
- Light/Dark mode switch
|
||||
- Persists to localStorage (`wiki-theme`)
|
||||
- Applies to entire document via `[data-theme="dark"]`
|
||||
- Toggle button shows sun/moon icon
|
||||
|
||||
### 5. Responsive Design
|
||||
|
||||
```
|
||||
Breakpoints:
|
||||
- Desktop (> 1024px): Sidebar visible
|
||||
- Tablet (768-1024px): Sidebar collapsible
|
||||
- Mobile (< 768px): Sidebar hidden, hamburger menu
|
||||
```
|
||||
|
||||
### 6. Print Support
|
||||
|
||||
- Hides sidebar, toggles, interactive elements
|
||||
- Expands all collapsed sections
|
||||
- Adjusts colors for print
|
||||
- Page breaks between sections
|
||||
|
||||
## Accessibility
|
||||
|
||||
### Keyboard Navigation
|
||||
|
||||
- Tab through interactive elements
|
||||
- Enter to activate buttons
|
||||
- Escape to close search results
|
||||
- Arrow keys in search results
|
||||
|
||||
### ARIA Attributes
|
||||
|
||||
```html
|
||||
<input aria-label="Search">
|
||||
<nav aria-label="Table of Contents">
|
||||
<button aria-label="Toggle theme">
|
||||
<div aria-live="polite"> <!-- For search results -->
|
||||
```
|
||||
|
||||
### Color Contrast
|
||||
|
||||
- Text/background ratio ≥ 4.5:1
|
||||
- Interactive elements clearly visible
|
||||
- Focus indicators visible
|
||||
|
||||
## Performance
|
||||
|
||||
### Target Metrics
|
||||
|
||||
| Metric | Target |
|
||||
|--------|--------|
|
||||
| Total file size | < 10MB |
|
||||
| Time to interactive | < 2s |
|
||||
| Search latency | < 100ms |
|
||||
|
||||
### Optimization Strategies
|
||||
|
||||
1. **Lazy loading for images**: `loading="lazy"`
|
||||
2. **Efficient search**: In-memory index, no external requests
|
||||
3. **CSS containment**: Scope styles to components
|
||||
4. **Minimal JavaScript**: Vanilla JS, no libraries
|
||||
|
||||
## CSS Variables
|
||||
|
||||
### Light Theme
|
||||
|
||||
```css
|
||||
:root {
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #f8f9fa;
|
||||
--text-primary: #212529;
|
||||
--text-secondary: #495057;
|
||||
--accent-color: #0d6efd;
|
||||
--border-color: #dee2e6;
|
||||
}
|
||||
```
|
||||
|
||||
### Dark Theme
|
||||
|
||||
```css
|
||||
[data-theme="dark"] {
|
||||
--bg-primary: #1a1a2e;
|
||||
--bg-secondary: #16213e;
|
||||
--text-primary: #eaeaea;
|
||||
--text-secondary: #b8b8b8;
|
||||
--accent-color: #4dabf7;
|
||||
--border-color: #2d3748;
|
||||
}
|
||||
```
|
||||
|
||||
## Markdown to HTML Mapping
|
||||
|
||||
| Markdown | HTML |
|
||||
|----------|------|
|
||||
| `# Heading` | `<h1>` |
|
||||
| `## Heading` | `<h2>` |
|
||||
| `**bold**` | `<strong>` |
|
||||
| `*italic*` | `<em>` |
|
||||
| `` `code` `` | `<code>` |
|
||||
| `[link](url)` | `<a href="url">` |
|
||||
| `- item` | `<ul><li>` |
|
||||
| `1. item` | `<ol><li>` |
|
||||
| ``` ```js ``` | `<pre><code class="language-js">` |
|
||||
| `> quote` | `<blockquote>` |
|
||||
| `---` | `<hr>` |
|
||||
|
||||
## Screenshot Embedding
|
||||
|
||||
### Marker Format
|
||||
|
||||
```markdown
|
||||
<!-- SCREENSHOT: id="ss-login" url="/login" description="Login form" -->
|
||||
```
|
||||
|
||||
### Embedded Format
|
||||
|
||||
```html
|
||||
<figure class="screenshot">
|
||||
<img src="data:image/png;base64,{{BASE64_DATA}}"
|
||||
alt="Login form"
|
||||
loading="lazy">
|
||||
<figcaption>Login form</figcaption>
|
||||
</figure>
|
||||
```
|
||||
|
||||
### Placeholder (if missing)
|
||||
|
||||
```html
|
||||
<div class="screenshot-placeholder">
|
||||
[Screenshot: ss-login - Login form]
|
||||
</div>
|
||||
```
|
||||
|
||||
## File Size Optimization
|
||||
|
||||
### CSS
|
||||
|
||||
- Minify before embedding
|
||||
- Remove unused styles
|
||||
- Combine duplicate rules
|
||||
|
||||
### JavaScript
|
||||
|
||||
- Minify before embedding
|
||||
- Remove console.log statements
|
||||
- Use IIFE for scoping
|
||||
|
||||
### Images
|
||||
|
||||
- Compress before Base64 encoding
|
||||
- Use appropriate dimensions (max 1280px width)
|
||||
- Consider WebP format if browser support is acceptable
|
||||
|
||||
## Validation
|
||||
|
||||
### HTML Validation
|
||||
|
||||
- W3C HTML5 compliance
|
||||
- Proper nesting
|
||||
- Required attributes present
|
||||
|
||||
### CSS Validation
|
||||
|
||||
- Valid property values
|
||||
- No deprecated properties
|
||||
- Vendor prefixes where needed
|
||||
|
||||
### JavaScript
|
||||
|
||||
- No syntax errors
|
||||
- All functions defined
|
||||
- Error handling for edge cases
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Opens in Chrome/Firefox/Safari/Edge
|
||||
- [ ] Search works correctly
|
||||
- [ ] Collapse/expand works
|
||||
- [ ] Tag filtering works
|
||||
- [ ] Theme toggle works
|
||||
- [ ] Print preview correct
|
||||
- [ ] Keyboard navigation works
|
||||
- [ ] Mobile responsive
|
||||
- [ ] Offline functionality
|
||||
- [ ] All links valid
|
||||
- [ ] All images display
|
||||
- [ ] No console errors
|
||||
253
.claude/skills/software-manual/specs/quality-standards.md
Normal file
253
.claude/skills/software-manual/specs/quality-standards.md
Normal file
@@ -0,0 +1,253 @@
|
||||
# Quality Standards
|
||||
|
||||
Quality gates and standards for software manual generation.
|
||||
|
||||
## Quality Dimensions
|
||||
|
||||
### 1. Completeness (25%)
|
||||
|
||||
All required sections present and adequately covered.
|
||||
|
||||
| Requirement | Weight | Criteria |
|
||||
|-------------|--------|----------|
|
||||
| Overview section | 5 | Product intro, features, quick start |
|
||||
| UI Guide | 5 | All major screens documented |
|
||||
| API Reference | 5 | All public APIs documented |
|
||||
| Configuration | 4 | All config options explained |
|
||||
| Troubleshooting | 3 | Common issues addressed |
|
||||
| Examples | 3 | Multi-level examples provided |
|
||||
|
||||
**Scoring**:
|
||||
- 100%: All sections present with adequate depth
|
||||
- 80%: All sections present, some lacking depth
|
||||
- 60%: Missing 1-2 sections
|
||||
- 40%: Missing 3+ sections
|
||||
- 0%: Critical sections missing (overview, UI guide)
|
||||
|
||||
### 2. Consistency (25%)
|
||||
|
||||
Terminology, style, and structure uniform across sections.
|
||||
|
||||
| Aspect | Check |
|
||||
|--------|-------|
|
||||
| Terminology | Same term for same concept throughout |
|
||||
| Formatting | Consistent heading levels, code block styles |
|
||||
| Tone | Consistent formality level |
|
||||
| Cross-references | All internal links valid |
|
||||
| Screenshot naming | Follow `ss-{feature}-{action}` pattern |
|
||||
|
||||
**Scoring**:
|
||||
- 100%: Zero inconsistencies
|
||||
- 80%: 1-3 minor inconsistencies
|
||||
- 60%: 4-6 inconsistencies
|
||||
- 40%: 7-10 inconsistencies
|
||||
- 0%: Pervasive inconsistencies
|
||||
|
||||
### 3. Depth (25%)
|
||||
|
||||
Content provides sufficient detail for target audience.
|
||||
|
||||
| Level | Criteria |
|
||||
|-------|----------|
|
||||
| Shallow | Basic descriptions only |
|
||||
| Standard | Descriptions + usage examples |
|
||||
| Deep | Descriptions + examples + edge cases + best practices |
|
||||
|
||||
**Per-Section Depth Check**:
|
||||
- [ ] Explains "what" (definition)
|
||||
- [ ] Explains "why" (rationale)
|
||||
- [ ] Explains "how" (procedure)
|
||||
- [ ] Provides examples
|
||||
- [ ] Covers edge cases
|
||||
- [ ] Includes tips/best practices
|
||||
|
||||
**Scoring**:
|
||||
- 100%: Deep coverage on all critical sections
|
||||
- 80%: Standard coverage on all sections
|
||||
- 60%: Shallow coverage on some sections
|
||||
- 40%: Missing depth in critical areas
|
||||
- 0%: Superficial throughout
|
||||
|
||||
### 4. Readability (25%)
|
||||
|
||||
Clear, user-friendly writing that's easy to follow.
|
||||
|
||||
| Metric | Target |
|
||||
|--------|--------|
|
||||
| Sentence length | Average < 20 words |
|
||||
| Paragraph length | Average < 5 sentences |
|
||||
| Heading hierarchy | Proper H1 > H2 > H3 nesting |
|
||||
| Code blocks | Language specified |
|
||||
| Lists | Used for 3+ items |
|
||||
| Screenshots | Placed near relevant text |
|
||||
|
||||
**Structural Elements**:
|
||||
- [ ] Clear section headers
|
||||
- [ ] Numbered steps for procedures
|
||||
- [ ] Bullet lists for options/features
|
||||
- [ ] Tables for comparisons
|
||||
- [ ] Code blocks with syntax highlighting
|
||||
- [ ] Screenshots with captions
|
||||
|
||||
**Scoring**:
|
||||
- 100%: All readability criteria met
|
||||
- 80%: Minor structural issues
|
||||
- 60%: Some sections hard to follow
|
||||
- 40%: Significant readability problems
|
||||
- 0%: Unclear, poorly structured
|
||||
|
||||
## Overall Quality Score
|
||||
|
||||
```
|
||||
Overall = (Completeness × 0.25) + (Consistency × 0.25) +
|
||||
(Depth × 0.25) + (Readability × 0.25)
|
||||
```
|
||||
|
||||
**Quality Gates**:
|
||||
|
||||
| Gate | Threshold | Action |
|
||||
|------|-----------|--------|
|
||||
| Pass | ≥ 80% | Proceed to HTML generation |
|
||||
| Review | 60-79% | Address warnings, proceed with caution |
|
||||
| Fail | < 60% | Must address errors before continuing |
|
||||
|
||||
## Issue Classification
|
||||
|
||||
### Errors (Must Fix)
|
||||
|
||||
- Missing required sections
|
||||
- Invalid cross-references
|
||||
- Broken screenshot markers
|
||||
- Code blocks without language
|
||||
- Incomplete procedures (missing steps)
|
||||
|
||||
### Warnings (Should Fix)
|
||||
|
||||
- Terminology inconsistencies
|
||||
- Sections lacking depth
|
||||
- Missing examples
|
||||
- Long paragraphs (> 7 sentences)
|
||||
- Screenshots missing captions
|
||||
|
||||
### Info (Nice to Have)
|
||||
|
||||
- Optimization suggestions
|
||||
- Additional example opportunities
|
||||
- Alternative explanations
|
||||
- Enhancement ideas
|
||||
|
||||
## Quality Checklist
|
||||
|
||||
### Pre-Generation
|
||||
|
||||
- [ ] All agents completed successfully
|
||||
- [ ] No errors in consolidation report
|
||||
- [ ] Overall score ≥ 60%
|
||||
|
||||
### Post-Generation
|
||||
|
||||
- [ ] HTML renders correctly
|
||||
- [ ] Search returns relevant results
|
||||
- [ ] All screenshots display
|
||||
- [ ] Theme toggle works
|
||||
- [ ] Print preview looks good
|
||||
|
||||
### Final Review
|
||||
|
||||
- [ ] User previewed and approved
|
||||
- [ ] File size reasonable (< 10MB)
|
||||
- [ ] No console errors in browser
|
||||
- [ ] Accessible (keyboard navigation works)
|
||||
|
||||
## Automated Checks
|
||||
|
||||
```javascript
|
||||
function runQualityChecks(workDir) {
|
||||
const results = {
|
||||
completeness: checkCompleteness(workDir),
|
||||
consistency: checkConsistency(workDir),
|
||||
depth: checkDepth(workDir),
|
||||
readability: checkReadability(workDir)
|
||||
};
|
||||
|
||||
results.overall = (
|
||||
results.completeness * 0.25 +
|
||||
results.consistency * 0.25 +
|
||||
results.depth * 0.25 +
|
||||
results.readability * 0.25
|
||||
);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function checkCompleteness(workDir) {
|
||||
const requiredSections = [
|
||||
'section-overview.md',
|
||||
'section-ui-guide.md',
|
||||
'section-api-reference.md',
|
||||
'section-configuration.md',
|
||||
'section-troubleshooting.md',
|
||||
'section-examples.md'
|
||||
];
|
||||
|
||||
const existing = Glob(`${workDir}/sections/section-*.md`);
|
||||
const found = requiredSections.filter(s =>
|
||||
existing.some(e => e.endsWith(s))
|
||||
);
|
||||
|
||||
return (found.length / requiredSections.length) * 100;
|
||||
}
|
||||
|
||||
function checkConsistency(workDir) {
|
||||
// Check terminology, cross-references, naming conventions
|
||||
const issues = [];
|
||||
|
||||
// ... implementation ...
|
||||
|
||||
return Math.max(0, 100 - issues.length * 10);
|
||||
}
|
||||
|
||||
function checkDepth(workDir) {
|
||||
// Check content length, examples, edge cases
|
||||
const sections = Glob(`${workDir}/sections/section-*.md`);
|
||||
let totalScore = 0;
|
||||
|
||||
for (const section of sections) {
|
||||
const content = Read(section);
|
||||
let sectionScore = 0;
|
||||
|
||||
if (content.length > 500) sectionScore += 20;
|
||||
if (content.includes('```')) sectionScore += 20;
|
||||
if (content.includes('Example')) sectionScore += 20;
|
||||
if (content.match(/\d+\./g)?.length > 3) sectionScore += 20;
|
||||
if (content.includes('Note:') || content.includes('Tip:')) sectionScore += 20;
|
||||
|
||||
totalScore += sectionScore;
|
||||
}
|
||||
|
||||
return totalScore / sections.length;
|
||||
}
|
||||
|
||||
function checkReadability(workDir) {
|
||||
// Check structure, formatting, organization
|
||||
const sections = Glob(`${workDir}/sections/section-*.md`);
|
||||
let issues = 0;
|
||||
|
||||
for (const section of sections) {
|
||||
const content = Read(section);
|
||||
|
||||
// Check heading hierarchy
|
||||
if (!content.startsWith('# ')) issues++;
|
||||
|
||||
// Check code block languages
|
||||
const codeBlocks = content.match(/```\w*/g);
|
||||
if (codeBlocks?.some(b => b === '```')) issues++;
|
||||
|
||||
// Check paragraph length
|
||||
const paragraphs = content.split('\n\n');
|
||||
if (paragraphs.some(p => p.split('. ').length > 7)) issues++;
|
||||
}
|
||||
|
||||
return Math.max(0, 100 - issues * 10);
|
||||
}
|
||||
```
|
||||
298
.claude/skills/software-manual/specs/writing-style.md
Normal file
298
.claude/skills/software-manual/specs/writing-style.md
Normal file
@@ -0,0 +1,298 @@
|
||||
# Writing Style Guide
|
||||
|
||||
User-friendly writing standards for software manuals.
|
||||
|
||||
## Core Principles
|
||||
|
||||
### 1. User-Centered
|
||||
|
||||
Write for the user, not the developer.
|
||||
|
||||
**Do**:
|
||||
- "Click the **Save** button to save your changes"
|
||||
- "Enter your email address in the login form"
|
||||
|
||||
**Don't**:
|
||||
- "The onClick handler triggers the save mutation"
|
||||
- "POST to /api/auth/login with email in body"
|
||||
|
||||
### 2. Action-Oriented
|
||||
|
||||
Focus on what users can **do**, not what the system does.
|
||||
|
||||
**Do**:
|
||||
- "You can export your data as CSV"
|
||||
- "To create a new project, click **New Project**"
|
||||
|
||||
**Don't**:
|
||||
- "The system exports data in CSV format"
|
||||
- "A new project is created when the button is clicked"
|
||||
|
||||
### 3. Clear and Direct
|
||||
|
||||
Use simple, straightforward language.
|
||||
|
||||
**Do**:
|
||||
- "Select a file to upload"
|
||||
- "The maximum file size is 10MB"
|
||||
|
||||
**Don't**:
|
||||
- "Utilize the file selection interface to designate a file for uploading"
|
||||
- "File size constraints limit uploads to 10 megabytes"
|
||||
|
||||
## Tone
|
||||
|
||||
### Friendly but Professional
|
||||
|
||||
- Conversational but not casual
|
||||
- Helpful but not condescending
|
||||
- Confident but not arrogant
|
||||
|
||||
**Examples**:
|
||||
|
||||
| Too Casual | Just Right | Too Formal |
|
||||
|------------|------------|------------|
|
||||
| "Yo, here's how..." | "Here's how to..." | "The following procedure describes..." |
|
||||
| "Easy peasy!" | "That's all you need to do." | "The procedure has been completed." |
|
||||
| "Don't worry about it" | "You don't need to change this" | "This parameter does not require modification" |
|
||||
|
||||
### Second Person
|
||||
|
||||
Address the user directly as "you".
|
||||
|
||||
**Do**: "You can customize your dashboard..."
|
||||
**Don't**: "Users can customize their dashboards..."
|
||||
|
||||
## Structure
|
||||
|
||||
### Headings
|
||||
|
||||
Use clear, descriptive headings that tell users what they'll learn.
|
||||
|
||||
**Good Headings**:
|
||||
- "Getting Started"
|
||||
- "Creating Your First Project"
|
||||
- "Configuring Email Notifications"
|
||||
- "Troubleshooting Login Issues"
|
||||
|
||||
**Weak Headings**:
|
||||
- "Overview"
|
||||
- "Step 1"
|
||||
- "Settings"
|
||||
- "FAQ"
|
||||
|
||||
### Procedures
|
||||
|
||||
Number steps for sequential tasks.
|
||||
|
||||
```markdown
|
||||
## Creating a New User
|
||||
|
||||
1. Navigate to **Settings** > **Users**.
|
||||
2. Click the **Add User** button.
|
||||
3. Enter the user's email address.
|
||||
4. Select a role from the dropdown.
|
||||
5. Click **Save**.
|
||||
|
||||
The new user will receive an invitation email.
|
||||
```
|
||||
|
||||
### Features/Options
|
||||
|
||||
Use bullet lists for non-sequential items.
|
||||
|
||||
```markdown
|
||||
## Export Options
|
||||
|
||||
You can export your data in several formats:
|
||||
|
||||
- **CSV**: Compatible with spreadsheets
|
||||
- **JSON**: Best for developers
|
||||
- **PDF**: Ideal for sharing reports
|
||||
```
|
||||
|
||||
### Comparisons
|
||||
|
||||
Use tables for comparing options.
|
||||
|
||||
```markdown
|
||||
## Plan Comparison
|
||||
|
||||
| Feature | Free | Pro | Enterprise |
|
||||
|---------|------|-----|------------|
|
||||
| Projects | 3 | Unlimited | Unlimited |
|
||||
| Storage | 1GB | 10GB | 100GB |
|
||||
| Support | Community | Email | Dedicated |
|
||||
```
|
||||
|
||||
## Content Types
|
||||
|
||||
### Conceptual (What Is)
|
||||
|
||||
Explain what something is and why it matters.
|
||||
|
||||
```markdown
|
||||
## What is a Workspace?
|
||||
|
||||
A workspace is a container for your projects and team members. Each workspace
|
||||
has its own settings, billing, and permissions. You might create separate
|
||||
workspaces for different clients or departments.
|
||||
```
|
||||
|
||||
### Procedural (How To)
|
||||
|
||||
Step-by-step instructions for completing a task.
|
||||
|
||||
```markdown
|
||||
## How to Create a Workspace
|
||||
|
||||
1. Click your profile icon in the top-right corner.
|
||||
2. Select **Create Workspace**.
|
||||
3. Enter a name for your workspace.
|
||||
4. Choose a plan (you can upgrade later).
|
||||
5. Click **Create**.
|
||||
|
||||
Your new workspace is ready to use.
|
||||
```
|
||||
|
||||
### Reference (API/Config)
|
||||
|
||||
Detailed specifications and parameters.
|
||||
|
||||
```markdown
|
||||
## Configuration Options
|
||||
|
||||
### `DATABASE_URL`
|
||||
|
||||
- **Type**: String (required)
|
||||
- **Format**: `postgresql://user:password@host:port/database`
|
||||
- **Example**: `postgresql://admin:secret@localhost:5432/myapp`
|
||||
|
||||
Database connection string for PostgreSQL.
|
||||
```
|
||||
|
||||
## Formatting
|
||||
|
||||
### Bold
|
||||
|
||||
Use for:
|
||||
- UI elements: Click **Save**
|
||||
- First use of key terms: **Workspaces** contain projects
|
||||
- Emphasis: **Never** share your API key
|
||||
|
||||
### Italic
|
||||
|
||||
Use for:
|
||||
- Introducing new terms: A *workspace* is...
|
||||
- Placeholders: Replace *your-api-key* with...
|
||||
- Emphasis (sparingly): This is *really* important
|
||||
|
||||
### Code
|
||||
|
||||
Use for:
|
||||
- Commands: Run `npm install`
|
||||
- File paths: Edit `config/settings.json`
|
||||
- Environment variables: Set `DATABASE_URL`
|
||||
- API endpoints: POST `/api/users`
|
||||
- Code references: The `handleSubmit` function
|
||||
|
||||
### Code Blocks
|
||||
|
||||
Always specify the language.
|
||||
|
||||
```javascript
|
||||
// Example: Fetching user data
|
||||
const response = await fetch('/api/user');
|
||||
const user = await response.json();
|
||||
```
|
||||
|
||||
### Notes and Warnings
|
||||
|
||||
Use for important callouts.
|
||||
|
||||
```markdown
|
||||
> **Note**: This feature requires a Pro plan.
|
||||
|
||||
> **Warning**: Deleting a workspace cannot be undone.
|
||||
|
||||
> **Tip**: Use keyboard shortcuts to work faster.
|
||||
```
|
||||
|
||||
## Screenshots
|
||||
|
||||
### When to Include
|
||||
|
||||
- First time showing a UI element
|
||||
- Complex interfaces
|
||||
- Before/after comparisons
|
||||
- Error states
|
||||
|
||||
### Guidelines
|
||||
|
||||
- Capture just the relevant area
|
||||
- Use consistent dimensions
|
||||
- Highlight important elements
|
||||
- Add descriptive captions
|
||||
|
||||
```markdown
|
||||
<!-- SCREENSHOT: id="ss-dashboard" description="Main dashboard showing project list" -->
|
||||
|
||||
*The dashboard displays all your projects with their status.*
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Good Section Example
|
||||
|
||||
```markdown
|
||||
## Inviting Team Members
|
||||
|
||||
You can invite colleagues to collaborate on your projects.
|
||||
|
||||
### To invite a team member:
|
||||
|
||||
1. Open **Settings** > **Team**.
|
||||
2. Click **Invite Member**.
|
||||
3. Enter their email address.
|
||||
4. Select their role:
|
||||
- **Admin**: Full access to all settings
|
||||
- **Editor**: Can edit projects
|
||||
- **Viewer**: Read-only access
|
||||
5. Click **Send Invite**.
|
||||
|
||||
The person will receive an email with a link to join your workspace.
|
||||
|
||||
> **Note**: You can have up to 5 team members on the Free plan.
|
||||
|
||||
<!-- SCREENSHOT: id="ss-invite-team" description="Team invitation dialog" -->
|
||||
```
|
||||
|
||||
## Language Guidelines
|
||||
|
||||
### Avoid Jargon
|
||||
|
||||
| Technical | User-Friendly |
|
||||
|-----------|---------------|
|
||||
| Execute | Run |
|
||||
| Terminate | Stop, End |
|
||||
| Instantiate | Create |
|
||||
| Invoke | Call, Use |
|
||||
| Parameterize | Set, Configure |
|
||||
| Persist | Save |
|
||||
|
||||
### Be Specific
|
||||
|
||||
| Vague | Specific |
|
||||
|-------|----------|
|
||||
| "Click the button" | "Click **Save**" |
|
||||
| "Enter information" | "Enter your email address" |
|
||||
| "An error occurred" | "Your password must be at least 8 characters" |
|
||||
| "It takes a moment" | "This typically takes 2-3 seconds" |
|
||||
|
||||
### Use Active Voice
|
||||
|
||||
| Passive | Active |
|
||||
|---------|--------|
|
||||
| "The file is uploaded" | "Upload the file" |
|
||||
| "Settings are saved" | "Click **Save** to keep your changes" |
|
||||
| "Errors are displayed" | "The form shows any errors" |
|
||||
984
.claude/skills/software-manual/templates/css/docsify-base.css
Normal file
984
.claude/skills/software-manual/templates/css/docsify-base.css
Normal file
@@ -0,0 +1,984 @@
|
||||
/* ========================================
|
||||
Docsify-Style Documentation CSS
|
||||
Software Manual Skill - Modern Theme
|
||||
======================================== */
|
||||
|
||||
/* ========== CSS Variables ========== */
|
||||
:root {
|
||||
/* Light Theme - Teal Accent */
|
||||
--bg-color: #ffffff;
|
||||
--bg-secondary: #f8fafc;
|
||||
--bg-tertiary: #f1f5f9;
|
||||
--text-color: #1e293b;
|
||||
--text-secondary: #64748b;
|
||||
--text-muted: #94a3b8;
|
||||
--border-color: #e2e8f0;
|
||||
--accent-color: #14b8a6;
|
||||
--accent-hover: #0d9488;
|
||||
--accent-light: rgba(20, 184, 166, 0.1);
|
||||
--link-color: #14b8a6;
|
||||
--sidebar-bg: #ffffff;
|
||||
--sidebar-width: 280px;
|
||||
--code-bg: #1e293b;
|
||||
--code-color: #e2e8f0;
|
||||
--shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1);
|
||||
|
||||
/* Callout Colors */
|
||||
--tip-bg: rgba(20, 184, 166, 0.08);
|
||||
--tip-border: #14b8a6;
|
||||
--warning-bg: rgba(245, 158, 11, 0.08);
|
||||
--warning-border: #f59e0b;
|
||||
--danger-bg: rgba(239, 68, 68, 0.08);
|
||||
--danger-border: #ef4444;
|
||||
--info-bg: rgba(59, 130, 246, 0.08);
|
||||
--info-border: #3b82f6;
|
||||
|
||||
/* Typography */
|
||||
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Noto Sans SC', sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'Fira Code', 'SF Mono', Monaco, Consolas, monospace;
|
||||
--font-size-xs: 0.75rem;
|
||||
--font-size-sm: 0.875rem;
|
||||
--font-size-base: 1rem;
|
||||
--font-size-lg: 1.125rem;
|
||||
--line-height: 1.75;
|
||||
|
||||
/* Spacing */
|
||||
--space-xs: 0.25rem;
|
||||
--space-sm: 0.5rem;
|
||||
--space-md: 1rem;
|
||||
--space-lg: 1.5rem;
|
||||
--space-xl: 2rem;
|
||||
--space-2xl: 3rem;
|
||||
|
||||
/* Border Radius */
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 12px;
|
||||
|
||||
/* Transitions */
|
||||
--transition: 0.2s ease;
|
||||
--transition-slow: 0.3s ease;
|
||||
}
|
||||
|
||||
/* Dark Theme */
|
||||
[data-theme="dark"] {
|
||||
--bg-color: #0f172a;
|
||||
--bg-secondary: #1e293b;
|
||||
--bg-tertiary: #334155;
|
||||
--text-color: #f1f5f9;
|
||||
--text-secondary: #94a3b8;
|
||||
--text-muted: #64748b;
|
||||
--border-color: #334155;
|
||||
--sidebar-bg: #1e293b;
|
||||
--code-bg: #0f172a;
|
||||
--code-color: #e2e8f0;
|
||||
--tip-bg: rgba(20, 184, 166, 0.15);
|
||||
--warning-bg: rgba(245, 158, 11, 0.15);
|
||||
--danger-bg: rgba(239, 68, 68, 0.15);
|
||||
--info-bg: rgba(59, 130, 246, 0.15);
|
||||
}
|
||||
|
||||
/* ========== Reset ========== */
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-family);
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height);
|
||||
color: var(--text-color);
|
||||
background-color: var(--bg-color);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* ========== Layout ========== */
|
||||
.docsify-container {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* ========== Sidebar ========== */
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: var(--sidebar-width);
|
||||
height: 100vh;
|
||||
background: var(--sidebar-bg);
|
||||
border-right: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 100;
|
||||
transition: transform var(--transition);
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: var(--space-lg);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, var(--accent-color), #3eaf7c);
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.logo-text h1 {
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
margin: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.logo-text .version {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ========== Search ========== */
|
||||
.sidebar-search {
|
||||
padding: var(--space-md);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
color: var(--text-muted);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
width: 100%;
|
||||
padding: 10px 60px 10px 36px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: var(--font-size-sm);
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-color);
|
||||
transition: all var(--transition);
|
||||
}
|
||||
|
||||
.search-box input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: 0 0 0 3px var(--accent-light);
|
||||
background: var(--bg-color);
|
||||
}
|
||||
|
||||
.search-box input::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Keyboard shortcut hint */
|
||||
.search-box::after {
|
||||
content: 'Ctrl K';
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--text-muted);
|
||||
background: var(--bg-color);
|
||||
padding: 2px 6px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border-color);
|
||||
font-family: var(--font-mono);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.search-results {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: var(--space-md);
|
||||
right: var(--space-md);
|
||||
background: var(--bg-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translateY(-4px);
|
||||
transition: all var(--transition);
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.search-results.visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.search-result-item {
|
||||
display: block;
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
text-decoration: none;
|
||||
color: var(--text-color);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
transition: background var(--transition);
|
||||
}
|
||||
|
||||
.search-result-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.search-result-item:hover {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.result-title {
|
||||
font-weight: 600;
|
||||
font-size: var(--font-size-sm);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.result-excerpt {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.result-excerpt mark {
|
||||
background: var(--accent-light);
|
||||
color: var(--accent-color);
|
||||
padding: 1px 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
padding: var(--space-md);
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
/* ========== Sidebar Navigation ========== */
|
||||
.sidebar-nav {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: var(--space-md) 0;
|
||||
}
|
||||
|
||||
.nav-group {
|
||||
margin-bottom: var(--space-xs);
|
||||
}
|
||||
|
||||
.nav-group-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: background var(--transition);
|
||||
}
|
||||
|
||||
.nav-group-header:hover {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.nav-group-toggle {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: var(--space-xs);
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
transition: transform var(--transition);
|
||||
}
|
||||
|
||||
.nav-group-toggle svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.nav-group.expanded .nav-group-toggle {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.nav-group-title {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.nav-group-items {
|
||||
display: none;
|
||||
padding-left: var(--space-lg);
|
||||
}
|
||||
|
||||
.nav-group.expanded .nav-group-items {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: block;
|
||||
padding: 8px var(--space-md) 8px calc(var(--space-md) + 4px);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
border-left: 2px solid transparent;
|
||||
margin: 2px 8px 2px 0;
|
||||
border-radius: 0 var(--radius-md) var(--radius-md) 0;
|
||||
transition: all var(--transition);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
color: var(--text-color);
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
color: var(--accent-color);
|
||||
border-left-color: var(--accent-color);
|
||||
background: var(--accent-light);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Top-level nav items (no group) */
|
||||
.nav-item.top-level {
|
||||
padding-left: var(--space-md);
|
||||
border-left: none;
|
||||
margin: 2px 8px;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.nav-item.top-level.active {
|
||||
background: var(--accent-light);
|
||||
}
|
||||
|
||||
/* ========== Main Content ========== */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
margin-left: var(--sidebar-width);
|
||||
min-height: 100vh;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.mobile-header {
|
||||
display: none;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
background: var(--bg-color);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
z-index: 50;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.sidebar-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: var(--space-xs);
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: background var(--transition);
|
||||
}
|
||||
|
||||
.sidebar-toggle:hover {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.current-section {
|
||||
flex: 1;
|
||||
font-weight: 600;
|
||||
font-size: var(--font-size-sm);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.theme-toggle-mobile {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: var(--space-xs);
|
||||
font-size: 1.25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ========== Content Sections ========== */
|
||||
.content-wrapper {
|
||||
flex: 1;
|
||||
max-width: 860px;
|
||||
margin: 0 auto;
|
||||
padding: var(--space-2xl) var(--space-xl);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.content-section {
|
||||
display: none;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
.content-section.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* ========== Content Typography ========== */
|
||||
.content-section h1 {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: var(--space-lg);
|
||||
padding-bottom: var(--space-md);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.content-section h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin-top: var(--space-2xl);
|
||||
margin-bottom: var(--space-md);
|
||||
padding-bottom: var(--space-sm);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.content-section h3 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-top: var(--space-xl);
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.content-section h4 {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin-top: var(--space-lg);
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.content-section p {
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
.content-section a {
|
||||
color: var(--link-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.content-section a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Lists */
|
||||
.content-section ul,
|
||||
.content-section ol {
|
||||
margin: var(--space-md) 0;
|
||||
padding-left: var(--space-xl);
|
||||
}
|
||||
|
||||
.content-section li {
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.content-section li::marker {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
/* Inline Code */
|
||||
.content-section code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.85em;
|
||||
padding: 3px 8px;
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--accent-color);
|
||||
border-radius: var(--radius-sm);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Code Blocks */
|
||||
.code-block-wrapper {
|
||||
position: relative;
|
||||
margin: var(--space-lg) 0;
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.content-section pre {
|
||||
margin: 0;
|
||||
padding: var(--space-lg);
|
||||
padding-top: calc(var(--space-lg) + 40px);
|
||||
background: var(--code-bg);
|
||||
overflow-x: auto;
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.content-section pre code {
|
||||
display: block;
|
||||
padding: 0;
|
||||
background: none;
|
||||
color: var(--code-color);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
/* Code Block Header */
|
||||
.code-block-wrapper::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 40px;
|
||||
background: rgba(255,255,255,0.03);
|
||||
border-bottom: 1px solid rgba(255,255,255,0.05);
|
||||
}
|
||||
|
||||
/* Code Block Actions */
|
||||
.code-block-actions {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 12px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.copy-code-btn {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 12px;
|
||||
padding: 6px 12px;
|
||||
background: rgba(255,255,255,0.08);
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--code-color);
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: all var(--transition);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: var(--font-size-xs);
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.code-block-wrapper:hover .copy-code-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.copy-code-btn:hover {
|
||||
background: rgba(255,255,255,0.15);
|
||||
border-color: rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.copy-code-btn.copied {
|
||||
background: var(--accent-color);
|
||||
border-color: var(--accent-color);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Code syntax colors */
|
||||
.content-section pre .keyword { color: #c678dd; }
|
||||
.content-section pre .string { color: #98c379; }
|
||||
.content-section pre .number { color: #d19a66; }
|
||||
.content-section pre .comment { color: #5c6370; font-style: italic; }
|
||||
.content-section pre .function { color: #61afef; }
|
||||
.content-section pre .operator { color: #56b6c2; }
|
||||
|
||||
/* Tables */
|
||||
.content-section table {
|
||||
width: 100%;
|
||||
margin: var(--space-lg) 0;
|
||||
border-collapse: collapse;
|
||||
font-size: var(--font-size-sm);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.content-section th {
|
||||
padding: var(--space-md);
|
||||
background: var(--accent-color);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
font-size: var(--font-size-sm);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.content-section th:first-child {
|
||||
border-top-left-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.content-section th:last-child {
|
||||
border-top-right-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.content-section td {
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.content-section tbody tr:nth-child(even) {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.content-section tbody tr:hover {
|
||||
background: var(--accent-light);
|
||||
}
|
||||
|
||||
.content-section tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.content-section tbody tr:last-child td:first-child {
|
||||
border-bottom-left-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.content-section tbody tr:last-child td:last-child {
|
||||
border-bottom-right-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
/* Blockquote / Callouts */
|
||||
.content-section blockquote {
|
||||
position: relative;
|
||||
margin: var(--space-lg) 0;
|
||||
padding: var(--space-md) var(--space-lg);
|
||||
padding-left: calc(var(--space-lg) + 32px);
|
||||
background: var(--tip-bg);
|
||||
border: 1px solid var(--tip-border);
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.content-section blockquote::before {
|
||||
content: '💡';
|
||||
position: absolute;
|
||||
left: var(--space-md);
|
||||
top: var(--space-md);
|
||||
font-size: 1.25rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.content-section blockquote p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.content-section blockquote p:first-child {
|
||||
font-weight: 500;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
/* Warning callout */
|
||||
.content-section blockquote.warning,
|
||||
.content-section blockquote:has(strong:first-child:contains("警告")),
|
||||
.content-section blockquote:has(strong:first-child:contains("Warning")) {
|
||||
background: var(--warning-bg);
|
||||
border-color: var(--warning-border);
|
||||
}
|
||||
|
||||
.content-section blockquote.warning::before {
|
||||
content: '⚠️';
|
||||
}
|
||||
|
||||
/* Danger callout */
|
||||
.content-section blockquote.danger,
|
||||
.content-section blockquote:has(strong:first-child:contains("危险")),
|
||||
.content-section blockquote:has(strong:first-child:contains("Danger")) {
|
||||
background: var(--danger-bg);
|
||||
border-color: var(--danger-border);
|
||||
}
|
||||
|
||||
.content-section blockquote.danger::before {
|
||||
content: '🚨';
|
||||
}
|
||||
|
||||
/* Info callout */
|
||||
.content-section blockquote.info,
|
||||
.content-section blockquote:has(strong:first-child:contains("注意")),
|
||||
.content-section blockquote:has(strong:first-child:contains("Note")) {
|
||||
background: var(--info-bg);
|
||||
border-color: var(--info-border);
|
||||
}
|
||||
|
||||
.content-section blockquote.info::before {
|
||||
content: 'ℹ️';
|
||||
}
|
||||
|
||||
/* Images */
|
||||
.content-section img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow-md);
|
||||
margin: var(--space-md) 0;
|
||||
}
|
||||
|
||||
.screenshot-placeholder {
|
||||
padding: var(--space-xl);
|
||||
background: var(--bg-secondary);
|
||||
border: 2px dashed var(--border-color);
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
margin: var(--space-md) 0;
|
||||
}
|
||||
|
||||
/* ========== Footer ========== */
|
||||
.main-footer {
|
||||
padding: var(--space-lg);
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
border-top: 1px solid var(--border-color);
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
/* ========== Theme Toggle (Desktop) ========== */
|
||||
.theme-toggle {
|
||||
position: fixed;
|
||||
bottom: var(--space-lg);
|
||||
right: var(--space-lg);
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-color);
|
||||
box-shadow: var(--shadow-md);
|
||||
cursor: pointer;
|
||||
font-size: 1.25rem;
|
||||
z-index: 100;
|
||||
transition: transform var(--transition);
|
||||
}
|
||||
|
||||
.theme-toggle:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
[data-theme="light"] .moon-icon { display: inline; }
|
||||
[data-theme="light"] .sun-icon { display: none; }
|
||||
[data-theme="dark"] .moon-icon { display: none; }
|
||||
[data-theme="dark"] .sun-icon { display: inline; }
|
||||
|
||||
/* ========== Back to Top ========== */
|
||||
.back-to-top {
|
||||
position: fixed;
|
||||
bottom: calc(var(--space-lg) + 56px);
|
||||
right: var(--space-lg);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-color);
|
||||
box-shadow: var(--shadow-md);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all var(--transition);
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.back-to-top.visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.back-to-top:hover {
|
||||
color: var(--accent-color);
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
/* ========== Responsive ========== */
|
||||
@media (max-width: 960px) {
|
||||
.sidebar {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.sidebar.open {
|
||||
transform: translateX(0);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.mobile-header {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
padding: var(--space-lg);
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.content-section h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.content-section h2 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
padding: var(--space-md);
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== Print Styles ========== */
|
||||
@media print {
|
||||
.sidebar,
|
||||
.mobile-header,
|
||||
.theme-toggle,
|
||||
.back-to-top,
|
||||
.copy-code-btn {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.content-section {
|
||||
display: block !important;
|
||||
page-break-after: always;
|
||||
}
|
||||
|
||||
.content-section pre {
|
||||
background: #f5f5f5 !important;
|
||||
color: #333 !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* ========== Pygments Syntax Highlighting (One Dark Theme) ========== */
|
||||
/* Generated for CodeHilite extension */
|
||||
.highlight { background: #282c34; border-radius: 8px; padding: 1em; overflow-x: auto; margin: var(--spacing-md) 0; }
|
||||
.highlight pre { margin: 0; background: transparent; padding: 0; }
|
||||
.highlight code { background: transparent; border: none; padding: 0; color: #abb2bf; font-size: var(--font-size-sm); }
|
||||
|
||||
/* Pygments Token Colors - One Dark Theme */
|
||||
.highlight .hll { background-color: #3e4451; }
|
||||
.highlight .c { color: #5c6370; font-style: italic; } /* Comment */
|
||||
.highlight .err { color: #e06c75; } /* Error */
|
||||
.highlight .k { color: #c678dd; } /* Keyword */
|
||||
.highlight .l { color: #98c379; } /* Literal */
|
||||
.highlight .n { color: #abb2bf; } /* Name */
|
||||
.highlight .o { color: #56b6c2; } /* Operator */
|
||||
.highlight .p { color: #abb2bf; } /* Punctuation */
|
||||
.highlight .ch { color: #5c6370; font-style: italic; } /* Comment.Hashbang */
|
||||
.highlight .cm { color: #5c6370; font-style: italic; } /* Comment.Multiline */
|
||||
.highlight .cp { color: #5c6370; font-style: italic; } /* Comment.Preproc */
|
||||
.highlight .cpf { color: #5c6370; font-style: italic; } /* Comment.PreprocFile */
|
||||
.highlight .c1 { color: #5c6370; font-style: italic; } /* Comment.Single */
|
||||
.highlight .cs { color: #5c6370; font-style: italic; } /* Comment.Special */
|
||||
.highlight .gd { color: #e06c75; } /* Generic.Deleted */
|
||||
.highlight .ge { font-style: italic; } /* Generic.Emph */
|
||||
.highlight .gh { color: #abb2bf; font-weight: bold; } /* Generic.Heading */
|
||||
.highlight .gi { color: #98c379; } /* Generic.Inserted */
|
||||
.highlight .go { color: #5c6370; } /* Generic.Output */
|
||||
.highlight .gp { color: #5c6370; } /* Generic.Prompt */
|
||||
.highlight .gs { font-weight: bold; } /* Generic.Strong */
|
||||
.highlight .gu { color: #56b6c2; font-weight: bold; } /* Generic.Subheading */
|
||||
.highlight .gt { color: #e06c75; } /* Generic.Traceback */
|
||||
.highlight .kc { color: #c678dd; } /* Keyword.Constant */
|
||||
.highlight .kd { color: #c678dd; } /* Keyword.Declaration */
|
||||
.highlight .kn { color: #c678dd; } /* Keyword.Namespace */
|
||||
.highlight .kp { color: #c678dd; } /* Keyword.Pseudo */
|
||||
.highlight .kr { color: #c678dd; } /* Keyword.Reserved */
|
||||
.highlight .kt { color: #e5c07b; } /* Keyword.Type */
|
||||
.highlight .ld { color: #98c379; } /* Literal.Date */
|
||||
.highlight .m { color: #d19a66; } /* Literal.Number */
|
||||
.highlight .s { color: #98c379; } /* Literal.String */
|
||||
.highlight .na { color: #d19a66; } /* Name.Attribute */
|
||||
.highlight .nb { color: #e5c07b; } /* Name.Builtin */
|
||||
.highlight .nc { color: #e5c07b; } /* Name.Class */
|
||||
.highlight .no { color: #d19a66; } /* Name.Constant */
|
||||
.highlight .nd { color: #e5c07b; } /* Name.Decorator */
|
||||
.highlight .ni { color: #abb2bf; } /* Name.Entity */
|
||||
.highlight .ne { color: #e06c75; } /* Name.Exception */
|
||||
.highlight .nf { color: #61afef; } /* Name.Function */
|
||||
.highlight .nl { color: #abb2bf; } /* Name.Label */
|
||||
.highlight .nn { color: #e5c07b; } /* Name.Namespace */
|
||||
.highlight .nx { color: #abb2bf; } /* Name.Other */
|
||||
.highlight .py { color: #abb2bf; } /* Name.Property */
|
||||
.highlight .nt { color: #e06c75; } /* Name.Tag */
|
||||
.highlight .nv { color: #e06c75; } /* Name.Variable */
|
||||
.highlight .ow { color: #56b6c2; } /* Operator.Word */
|
||||
.highlight .w { color: #abb2bf; } /* Text.Whitespace */
|
||||
.highlight .mb { color: #d19a66; } /* Literal.Number.Bin */
|
||||
.highlight .mf { color: #d19a66; } /* Literal.Number.Float */
|
||||
.highlight .mh { color: #d19a66; } /* Literal.Number.Hex */
|
||||
.highlight .mi { color: #d19a66; } /* Literal.Number.Integer */
|
||||
.highlight .mo { color: #d19a66; } /* Literal.Number.Oct */
|
||||
.highlight .sa { color: #98c379; } /* Literal.String.Affix */
|
||||
.highlight .sb { color: #98c379; } /* Literal.String.Backtick */
|
||||
.highlight .sc { color: #98c379; } /* Literal.String.Char */
|
||||
.highlight .dl { color: #98c379; } /* Literal.String.Delimiter */
|
||||
.highlight .sd { color: #98c379; } /* Literal.String.Doc */
|
||||
.highlight .s2 { color: #98c379; } /* Literal.String.Double */
|
||||
.highlight .se { color: #d19a66; } /* Literal.String.Escape */
|
||||
.highlight .sh { color: #98c379; } /* Literal.String.Heredoc */
|
||||
.highlight .si { color: #98c379; } /* Literal.String.Interpol */
|
||||
.highlight .sx { color: #98c379; } /* Literal.String.Other */
|
||||
.highlight .sr { color: #56b6c2; } /* Literal.String.Regex */
|
||||
.highlight .s1 { color: #98c379; } /* Literal.String.Single */
|
||||
.highlight .ss { color: #56b6c2; } /* Literal.String.Symbol */
|
||||
.highlight .bp { color: #e5c07b; } /* Name.Builtin.Pseudo */
|
||||
.highlight .fm { color: #61afef; } /* Name.Function.Magic */
|
||||
.highlight .vc { color: #e06c75; } /* Name.Variable.Class */
|
||||
.highlight .vg { color: #e06c75; } /* Name.Variable.Global */
|
||||
.highlight .vi { color: #e06c75; } /* Name.Variable.Instance */
|
||||
.highlight .vm { color: #e06c75; } /* Name.Variable.Magic */
|
||||
.highlight .il { color: #d19a66; } /* Literal.Number.Integer.Long */
|
||||
|
||||
/* Dark theme override for highlight */
|
||||
[data-theme="dark"] .highlight {
|
||||
background: #1e2128;
|
||||
border: 1px solid #3d4450;
|
||||
}
|
||||
788
.claude/skills/software-manual/templates/css/wiki-base.css
Normal file
788
.claude/skills/software-manual/templates/css/wiki-base.css
Normal file
@@ -0,0 +1,788 @@
|
||||
/* ========================================
|
||||
TiddlyWiki-Style Base CSS
|
||||
Software Manual Skill
|
||||
======================================== */
|
||||
|
||||
/* ========== CSS Variables ========== */
|
||||
:root {
|
||||
/* Light Theme */
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #f8f9fa;
|
||||
--bg-tertiary: #e9ecef;
|
||||
--text-primary: #212529;
|
||||
--text-secondary: #495057;
|
||||
--text-muted: #6c757d;
|
||||
--border-color: #dee2e6;
|
||||
--accent-color: #0d6efd;
|
||||
--accent-hover: #0b5ed7;
|
||||
--success-color: #198754;
|
||||
--warning-color: #ffc107;
|
||||
--danger-color: #dc3545;
|
||||
--info-color: #0dcaf0;
|
||||
|
||||
/* Layout */
|
||||
--sidebar-width: 280px;
|
||||
--header-height: 60px;
|
||||
--content-max-width: 900px;
|
||||
--spacing-xs: 0.25rem;
|
||||
--spacing-sm: 0.5rem;
|
||||
--spacing-md: 1rem;
|
||||
--spacing-lg: 1.5rem;
|
||||
--spacing-xl: 2rem;
|
||||
|
||||
/* Typography */
|
||||
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
--font-family-mono: 'SF Mono', Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
||||
--font-size-sm: 0.875rem;
|
||||
--font-size-base: 1rem;
|
||||
--font-size-lg: 1.125rem;
|
||||
--font-size-xl: 1.25rem;
|
||||
--line-height: 1.6;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
|
||||
|
||||
/* Transitions */
|
||||
--transition-fast: 150ms ease;
|
||||
--transition-base: 300ms ease;
|
||||
}
|
||||
|
||||
/* ========== Reset & Base ========== */
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-family);
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height);
|
||||
color: var(--text-primary);
|
||||
background-color: var(--bg-secondary);
|
||||
}
|
||||
|
||||
/* ========== Layout ========== */
|
||||
.wiki-container {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* ========== Sidebar ========== */
|
||||
.wiki-sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: var(--sidebar-width);
|
||||
height: 100vh;
|
||||
background-color: var(--bg-primary);
|
||||
border-right: 1px solid var(--border-color);
|
||||
overflow-y: auto;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: transform var(--transition-base);
|
||||
}
|
||||
|
||||
/* Logo Area */
|
||||
.wiki-logo {
|
||||
padding: var(--spacing-lg);
|
||||
text-align: center;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.wiki-logo .logo-placeholder {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin: 0 auto var(--spacing-sm);
|
||||
background: linear-gradient(135deg, var(--accent-color), var(--info-color));
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: var(--font-size-xl);
|
||||
}
|
||||
|
||||
.wiki-logo h1 {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 600;
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.wiki-logo .version {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Search */
|
||||
.wiki-search {
|
||||
padding: var(--spacing-md);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.wiki-search input {
|
||||
width: 100%;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
font-size: var(--font-size-sm);
|
||||
background-color: var(--bg-secondary);
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||
}
|
||||
|
||||
.wiki-search input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: 0 0 0 3px rgba(13, 110, 253, 0.15);
|
||||
}
|
||||
|
||||
.search-results {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: var(--spacing-md);
|
||||
right: var(--spacing-md);
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.search-result-item {
|
||||
display: block;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
text-decoration: none;
|
||||
color: var(--text-primary);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
transition: background-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.search-result-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.search-result-item:hover {
|
||||
background-color: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.result-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.result-excerpt {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.result-excerpt mark {
|
||||
background-color: var(--warning-color);
|
||||
padding: 0 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
padding: var(--spacing-md);
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Tags */
|
||||
.wiki-tags {
|
||||
padding: var(--spacing-md);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-xs);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.wiki-tags .tag {
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
font-size: var(--font-size-sm);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 20px;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.wiki-tags .tag:hover {
|
||||
border-color: var(--accent-color);
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.wiki-tags .tag.active {
|
||||
background-color: var(--accent-color);
|
||||
border-color: var(--accent-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Table of Contents */
|
||||
.wiki-toc {
|
||||
flex: 1;
|
||||
padding: var(--spacing-md);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.wiki-toc h3 {
|
||||
font-size: var(--font-size-sm);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.wiki-toc ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.wiki-toc li {
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.wiki-toc a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--spacing-sm);
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
font-size: var(--font-size-sm);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.wiki-toc a:hover {
|
||||
background-color: var(--bg-secondary);
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
/* ========== Main Content ========== */
|
||||
.wiki-content {
|
||||
flex: 1;
|
||||
margin-left: var(--sidebar-width);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.content-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: var(--bg-primary);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: var(--spacing-sm) var(--spacing-lg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.sidebar-toggle {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: var(--spacing-sm);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sidebar-toggle span {
|
||||
display: block;
|
||||
width: 20px;
|
||||
height: 2px;
|
||||
background-color: var(--text-primary);
|
||||
transition: transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.header-actions button {
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
font-size: var(--font-size-sm);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.header-actions button:hover {
|
||||
border-color: var(--accent-color);
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
/* Tiddler Container */
|
||||
.tiddler-container {
|
||||
flex: 1;
|
||||
max-width: var(--content-max-width);
|
||||
margin: 0 auto;
|
||||
padding: var(--spacing-lg);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ========== Tiddler (Content Block) ========== */
|
||||
.tiddler {
|
||||
background-color: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: box-shadow var(--transition-fast);
|
||||
}
|
||||
|
||||
.tiddler:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.tiddler-header {
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.tiddler-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.collapse-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
padding: var(--spacing-xs);
|
||||
transition: transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.tiddler.collapsed .collapse-toggle {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.tiddler-meta {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.difficulty-badge {
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
border-radius: 4px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.difficulty-badge.beginner {
|
||||
background-color: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.difficulty-badge.intermediate {
|
||||
background-color: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.difficulty-badge.advanced {
|
||||
background-color: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.tag-badge {
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
font-size: 0.75rem;
|
||||
background-color: var(--bg-tertiary);
|
||||
color: var(--text-secondary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tiddler-content {
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.tiddler.collapsed .tiddler-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ========== Content Typography ========== */
|
||||
.tiddler-content h1,
|
||||
.tiddler-content h2,
|
||||
.tiddler-content h3,
|
||||
.tiddler-content h4 {
|
||||
margin-top: var(--spacing-lg);
|
||||
margin-bottom: var(--spacing-md);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tiddler-content h1 { font-size: 1.75rem; }
|
||||
.tiddler-content h2 { font-size: 1.5rem; }
|
||||
.tiddler-content h3 { font-size: 1.25rem; }
|
||||
.tiddler-content h4 { font-size: 1.125rem; }
|
||||
|
||||
.tiddler-content p {
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
/* Lists - Enhanced Styling */
|
||||
.tiddler-content ul,
|
||||
.tiddler-content ol {
|
||||
margin: var(--spacing-md) 0;
|
||||
padding-left: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.tiddler-content ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.tiddler-content ul > li {
|
||||
position: relative;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.tiddler-content ul > li::before {
|
||||
content: "•";
|
||||
position: absolute;
|
||||
left: -16px;
|
||||
color: var(--accent-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tiddler-content ol {
|
||||
list-style: none;
|
||||
counter-reset: item;
|
||||
}
|
||||
|
||||
.tiddler-content ol > li {
|
||||
position: relative;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
padding-left: 8px;
|
||||
counter-increment: item;
|
||||
}
|
||||
|
||||
.tiddler-content ol > li::before {
|
||||
content: counter(item) ".";
|
||||
position: absolute;
|
||||
left: -24px;
|
||||
color: var(--accent-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Nested lists */
|
||||
.tiddler-content ul ul,
|
||||
.tiddler-content ol ol,
|
||||
.tiddler-content ul ol,
|
||||
.tiddler-content ol ul {
|
||||
margin: var(--spacing-xs) 0;
|
||||
}
|
||||
|
||||
.tiddler-content ul ul > li::before {
|
||||
content: "◦";
|
||||
}
|
||||
|
||||
.tiddler-content ul ul ul > li::before {
|
||||
content: "▪";
|
||||
}
|
||||
|
||||
.tiddler-content a {
|
||||
color: var(--accent-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.tiddler-content a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Inline Code - Red Highlight */
|
||||
.tiddler-content code {
|
||||
font-family: var(--font-family-mono);
|
||||
font-size: 0.875em;
|
||||
padding: 2px 6px;
|
||||
background-color: #fff5f5;
|
||||
color: #c92a2a;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ffc9c9;
|
||||
}
|
||||
|
||||
/* Code Blocks - Dark Background */
|
||||
.tiddler-content pre {
|
||||
position: relative;
|
||||
margin: var(--spacing-md) 0;
|
||||
padding: 0;
|
||||
background-color: #1e2128;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #3d4450;
|
||||
}
|
||||
|
||||
.tiddler-content pre::before {
|
||||
content: attr(data-language);
|
||||
display: block;
|
||||
padding: 8px 16px;
|
||||
background-color: #2d333b;
|
||||
color: #8b949e;
|
||||
font-size: 0.75rem;
|
||||
font-family: var(--font-family);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
border-bottom: 1px solid #3d4450;
|
||||
}
|
||||
|
||||
.tiddler-content pre code {
|
||||
display: block;
|
||||
padding: var(--spacing-md);
|
||||
background: none;
|
||||
color: #e6edf3;
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.6;
|
||||
overflow-x: auto;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.copy-code-btn {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 12px;
|
||||
padding: 4px 10px;
|
||||
font-size: 0.7rem;
|
||||
background-color: #3d4450;
|
||||
color: #8b949e;
|
||||
border: 1px solid #4d5566;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.copy-code-btn:hover {
|
||||
background-color: #4d5566;
|
||||
color: #e6edf3;
|
||||
}
|
||||
|
||||
.tiddler-content pre:hover .copy-code-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Tables - Blue Header Style */
|
||||
.tiddler-content table {
|
||||
width: 100%;
|
||||
margin: var(--spacing-md) 0;
|
||||
border-collapse: collapse;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tiddler-content th {
|
||||
padding: 12px 16px;
|
||||
background: linear-gradient(135deg, #1971c2, #228be6);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
border: none;
|
||||
border-bottom: 2px solid #1864ab;
|
||||
}
|
||||
|
||||
.tiddler-content td {
|
||||
padding: 10px 16px;
|
||||
border: 1px solid #e9ecef;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.tiddler-content tbody tr:nth-child(odd) {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.tiddler-content tbody tr:nth-child(even) {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.tiddler-content tbody tr:hover {
|
||||
background-color: #e7f5ff;
|
||||
}
|
||||
|
||||
/* Screenshots */
|
||||
.screenshot {
|
||||
margin: var(--spacing-lg) 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.screenshot img {
|
||||
max-width: 100%;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.screenshot figcaption {
|
||||
margin-top: var(--spacing-sm);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.screenshot-placeholder {
|
||||
padding: var(--spacing-xl);
|
||||
background-color: var(--bg-tertiary);
|
||||
border: 2px dashed var(--border-color);
|
||||
border-radius: 8px;
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ========== Footer ========== */
|
||||
.wiki-footer {
|
||||
padding: var(--spacing-lg);
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
border-top: 1px solid var(--border-color);
|
||||
background-color: var(--bg-primary);
|
||||
}
|
||||
|
||||
/* ========== Theme Toggle ========== */
|
||||
.theme-toggle {
|
||||
position: fixed;
|
||||
bottom: var(--spacing-lg);
|
||||
right: var(--spacing-lg);
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background-color: var(--bg-primary);
|
||||
box-shadow: var(--shadow-lg);
|
||||
cursor: pointer;
|
||||
font-size: 1.5rem;
|
||||
z-index: 100;
|
||||
transition: transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.theme-toggle:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
[data-theme="light"] .moon-icon { display: inline; }
|
||||
[data-theme="light"] .sun-icon { display: none; }
|
||||
[data-theme="dark"] .moon-icon { display: none; }
|
||||
[data-theme="dark"] .sun-icon { display: inline; }
|
||||
|
||||
/* ========== Back to Top ========== */
|
||||
.back-to-top {
|
||||
position: fixed;
|
||||
bottom: calc(var(--spacing-lg) + 60px);
|
||||
right: var(--spacing-lg);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background-color: var(--accent-color);
|
||||
color: white;
|
||||
font-size: 1.25rem;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all var(--transition-fast);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.back-to-top.visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.back-to-top:hover {
|
||||
background-color: var(--accent-hover);
|
||||
}
|
||||
|
||||
/* ========== Responsive ========== */
|
||||
@media (max-width: 1024px) {
|
||||
.wiki-sidebar {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.wiki-sidebar.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.wiki-content {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.sidebar-toggle {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.tiddler-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wiki-tags {
|
||||
overflow-x: auto;
|
||||
flex-wrap: nowrap;
|
||||
padding-bottom: var(--spacing-md);
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== Print Styles ========== */
|
||||
@media print {
|
||||
.wiki-sidebar,
|
||||
.theme-toggle,
|
||||
.back-to-top,
|
||||
.content-header,
|
||||
.collapse-toggle,
|
||||
.copy-code-btn {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.wiki-content {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.tiddler {
|
||||
break-inside: avoid;
|
||||
box-shadow: none;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.tiddler.collapsed .tiddler-content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tiddler-content pre {
|
||||
background-color: #f5f5f5 !important;
|
||||
color: #333 !important;
|
||||
}
|
||||
}
|
||||
278
.claude/skills/software-manual/templates/css/wiki-dark.css
Normal file
278
.claude/skills/software-manual/templates/css/wiki-dark.css
Normal file
@@ -0,0 +1,278 @@
|
||||
/* ========================================
|
||||
TiddlyWiki-Style Dark Theme
|
||||
Software Manual Skill
|
||||
======================================== */
|
||||
|
||||
[data-theme="dark"] {
|
||||
/* Dark Theme Colors */
|
||||
--bg-primary: #1a1a2e;
|
||||
--bg-secondary: #16213e;
|
||||
--bg-tertiary: #0f3460;
|
||||
--text-primary: #eaeaea;
|
||||
--text-secondary: #b8b8b8;
|
||||
--text-muted: #888888;
|
||||
--border-color: #2d3748;
|
||||
--accent-color: #4dabf7;
|
||||
--accent-hover: #339af0;
|
||||
--success-color: #51cf66;
|
||||
--warning-color: #ffd43b;
|
||||
--danger-color: #ff6b6b;
|
||||
--info-color: #22b8cf;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4);
|
||||
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Dark theme specific overrides */
|
||||
[data-theme="dark"] .wiki-logo .logo-placeholder {
|
||||
background: linear-gradient(135deg, var(--accent-color), #6741d9);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .wiki-search input {
|
||||
background-color: var(--bg-tertiary);
|
||||
border-color: var(--border-color);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .wiki-search input::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .search-results {
|
||||
background-color: var(--bg-secondary);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .search-result-item {
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .search-result-item:hover {
|
||||
background-color: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .result-excerpt mark {
|
||||
background-color: rgba(255, 212, 59, 0.3);
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .wiki-tags .tag {
|
||||
background-color: var(--bg-tertiary);
|
||||
border-color: var(--border-color);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .wiki-tags .tag:hover {
|
||||
border-color: var(--accent-color);
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .wiki-tags .tag.active {
|
||||
background-color: var(--accent-color);
|
||||
border-color: var(--accent-color);
|
||||
color: #1a1a2e;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .wiki-toc a:hover {
|
||||
background-color: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .content-header {
|
||||
background-color: var(--bg-primary);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .sidebar-toggle span {
|
||||
background-color: var(--text-primary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .header-actions button {
|
||||
background-color: var(--bg-secondary);
|
||||
border-color: var(--border-color);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .header-actions button:hover {
|
||||
border-color: var(--accent-color);
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .tiddler {
|
||||
background-color: var(--bg-primary);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .tiddler-header {
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .difficulty-badge.beginner {
|
||||
background-color: rgba(81, 207, 102, 0.2);
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .difficulty-badge.intermediate {
|
||||
background-color: rgba(255, 212, 59, 0.2);
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .difficulty-badge.advanced {
|
||||
background-color: rgba(255, 107, 107, 0.2);
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .tag-badge {
|
||||
background-color: var(--bg-tertiary);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .tiddler-content code {
|
||||
background-color: var(--bg-tertiary);
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .tiddler-content pre {
|
||||
background-color: #0d1117;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .tiddler-content pre code {
|
||||
color: #e6e6e6;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .copy-code-btn {
|
||||
background-color: var(--bg-tertiary);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .tiddler-content th {
|
||||
background-color: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .tiddler-content tr:nth-child(even) {
|
||||
background-color: var(--bg-secondary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .tiddler-content th,
|
||||
[data-theme="dark"] .tiddler-content td {
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .screenshot img {
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .screenshot-placeholder {
|
||||
background-color: var(--bg-tertiary);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .wiki-footer {
|
||||
background-color: var(--bg-primary);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .theme-toggle {
|
||||
background-color: var(--bg-secondary);
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .back-to-top {
|
||||
background-color: var(--accent-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .back-to-top:hover {
|
||||
background-color: var(--accent-hover);
|
||||
}
|
||||
|
||||
/* Scrollbar styling for dark theme */
|
||||
[data-theme="dark"] ::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
[data-theme="dark"] ::-webkit-scrollbar-track {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] ::-webkit-scrollbar-thumb {
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
[data-theme="dark"] ::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--border-color);
|
||||
}
|
||||
|
||||
/* Selection color */
|
||||
[data-theme="dark"] ::selection {
|
||||
background-color: rgba(77, 171, 247, 0.3);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Focus styles for accessibility */
|
||||
[data-theme="dark"] :focus {
|
||||
outline-color: var(--accent-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .wiki-search input:focus {
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: 0 0 0 3px rgba(77, 171, 247, 0.2);
|
||||
}
|
||||
|
||||
/* Link colors */
|
||||
[data-theme="dark"] .tiddler-content a {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .tiddler-content a:hover {
|
||||
color: var(--accent-hover);
|
||||
}
|
||||
|
||||
/* Blockquote styling */
|
||||
[data-theme="dark"] .tiddler-content blockquote {
|
||||
border-left: 4px solid var(--accent-color);
|
||||
background-color: var(--bg-tertiary);
|
||||
padding: var(--spacing-md);
|
||||
margin: var(--spacing-md) 0;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Horizontal rule */
|
||||
[data-theme="dark"] .tiddler-content hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--border-color);
|
||||
margin: var(--spacing-lg) 0;
|
||||
}
|
||||
|
||||
/* Alert/Note boxes */
|
||||
[data-theme="dark"] .note,
|
||||
[data-theme="dark"] .warning,
|
||||
[data-theme="dark"] .tip,
|
||||
[data-theme="dark"] .danger {
|
||||
padding: var(--spacing-md);
|
||||
border-radius: 6px;
|
||||
margin: var(--spacing-md) 0;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .note {
|
||||
background-color: rgba(34, 184, 207, 0.1);
|
||||
border-left: 4px solid var(--info-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .warning {
|
||||
background-color: rgba(255, 212, 59, 0.1);
|
||||
border-left: 4px solid var(--warning-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .tip {
|
||||
background-color: rgba(81, 207, 102, 0.1);
|
||||
border-left: 4px solid var(--success-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .danger {
|
||||
background-color: rgba(255, 107, 107, 0.1);
|
||||
border-left: 4px solid var(--danger-color);
|
||||
}
|
||||
466
.claude/skills/software-manual/templates/docsify-shell.html
Normal file
466
.claude/skills/software-manual/templates/docsify-shell.html
Normal file
@@ -0,0 +1,466 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="{{SOFTWARE_NAME}} - Interactive Software Manual">
|
||||
<meta name="generator" content="software-manual-skill">
|
||||
<title>{{SOFTWARE_NAME}} v{{VERSION}} - User Manual</title>
|
||||
<style>
|
||||
{{EMBEDDED_CSS}}
|
||||
</style>
|
||||
</head>
|
||||
<body class="docsify-container" data-theme="light">
|
||||
<!-- Sidebar Navigation -->
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<!-- Logo and Title -->
|
||||
<div class="sidebar-header">
|
||||
<div class="logo">
|
||||
<span class="logo-icon">{{LOGO_ICON}}</span>
|
||||
<div class="logo-text">
|
||||
<h1>{{SOFTWARE_NAME}}</h1>
|
||||
<span class="version">v{{VERSION}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search Box -->
|
||||
<div class="sidebar-search">
|
||||
<div class="search-box">
|
||||
<svg class="search-icon" viewBox="0 0 24 24" width="16" height="16">
|
||||
<circle cx="11" cy="11" r="8" fill="none" stroke="currentColor" stroke-width="2"/>
|
||||
<path d="M21 21l-4.35-4.35" fill="none" stroke="currentColor" stroke-width="2"/>
|
||||
</svg>
|
||||
<input type="text" id="searchInput" placeholder="搜索文档..." aria-label="Search">
|
||||
</div>
|
||||
<div id="searchResults" class="search-results"></div>
|
||||
</div>
|
||||
|
||||
<!-- Hierarchical Navigation -->
|
||||
<nav class="sidebar-nav" id="sidebarNav">
|
||||
{{SIDEBAR_NAV_HTML}}
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<main class="main-content" id="mainContent">
|
||||
<!-- Mobile Header -->
|
||||
<header class="mobile-header">
|
||||
<button class="sidebar-toggle" id="sidebarToggle" aria-label="Toggle sidebar">
|
||||
<svg viewBox="0 0 24 24" width="24" height="24">
|
||||
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z" fill="currentColor"/>
|
||||
</svg>
|
||||
</button>
|
||||
<span class="current-section" id="currentSection">{{SOFTWARE_NAME}}</span>
|
||||
<button class="theme-toggle-mobile" id="themeToggleMobile" aria-label="Toggle theme">
|
||||
<span class="sun-icon">☀</span>
|
||||
<span class="moon-icon">☾</span>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<!-- Content Sections (only one visible at a time) -->
|
||||
<div class="content-wrapper">
|
||||
{{SECTIONS_HTML}}
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="main-footer">
|
||||
<p>Generated by <strong>software-manual-skill</strong> | Last updated: {{TIMESTAMP}}</p>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
<!-- Theme Toggle (Desktop) -->
|
||||
<button class="theme-toggle" id="themeToggle" aria-label="Toggle theme">
|
||||
<span class="sun-icon">☀</span>
|
||||
<span class="moon-icon">☾</span>
|
||||
</button>
|
||||
|
||||
<!-- Back to Top -->
|
||||
<button class="back-to-top" id="backToTop" aria-label="Back to top">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20">
|
||||
<path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z" fill="currentColor"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Search Index Data -->
|
||||
<script id="search-index" type="application/json">
|
||||
{{SEARCH_INDEX_JSON}}
|
||||
</script>
|
||||
|
||||
<!-- Navigation Structure Data -->
|
||||
<script id="nav-structure" type="application/json">
|
||||
{{NAV_STRUCTURE_JSON}}
|
||||
</script>
|
||||
|
||||
<!-- Mermaid.js for diagram rendering -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
|
||||
<script>
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
theme: document.body.dataset.theme === 'dark' ? 'dark' : 'default',
|
||||
securityLevel: 'loose'
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Embedded JavaScript -->
|
||||
<script>
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// ========== State Management ==========
|
||||
let currentSectionId = null;
|
||||
const sections = document.querySelectorAll('.content-section');
|
||||
const navItems = document.querySelectorAll('.nav-item');
|
||||
|
||||
// ========== Section Navigation ==========
|
||||
function showSection(sectionId) {
|
||||
// Hide all sections
|
||||
sections.forEach(s => s.classList.remove('active'));
|
||||
|
||||
// Show target section
|
||||
const target = document.getElementById('section-' + sectionId);
|
||||
if (target) {
|
||||
target.classList.add('active');
|
||||
currentSectionId = sectionId;
|
||||
|
||||
// Update URL hash
|
||||
history.pushState(null, '', '#/' + sectionId);
|
||||
|
||||
// Update nav active state
|
||||
navItems.forEach(item => {
|
||||
item.classList.remove('active');
|
||||
if (item.dataset.section === sectionId) {
|
||||
item.classList.add('active');
|
||||
// Expand parent groups
|
||||
expandParentGroups(item);
|
||||
}
|
||||
});
|
||||
|
||||
// Update mobile header
|
||||
const currentSectionEl = document.getElementById('currentSection');
|
||||
if (currentSectionEl && target.dataset.title) {
|
||||
currentSectionEl.textContent = target.dataset.title;
|
||||
}
|
||||
|
||||
// Scroll to top
|
||||
document.getElementById('mainContent').scrollTop = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function expandParentGroups(item) {
|
||||
let parent = item.parentElement;
|
||||
while (parent) {
|
||||
if (parent.classList.contains('nav-group')) {
|
||||
parent.classList.add('expanded');
|
||||
const toggle = parent.querySelector('.nav-group-toggle');
|
||||
if (toggle) toggle.setAttribute('aria-expanded', 'true');
|
||||
}
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Navigation Click Handlers ==========
|
||||
navItems.forEach(item => {
|
||||
item.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const sectionId = this.dataset.section;
|
||||
if (sectionId) {
|
||||
showSection(sectionId);
|
||||
// Close sidebar on mobile
|
||||
document.getElementById('sidebar').classList.remove('open');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ========== Navigation Group Toggle ==========
|
||||
document.querySelectorAll('.nav-group-toggle').forEach(toggle => {
|
||||
toggle.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
const group = this.closest('.nav-group');
|
||||
group.classList.toggle('expanded');
|
||||
this.setAttribute('aria-expanded', group.classList.contains('expanded'));
|
||||
});
|
||||
});
|
||||
|
||||
// ========== Search Functionality ==========
|
||||
const indexData = JSON.parse(document.getElementById('search-index').textContent);
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
const searchResults = document.getElementById('searchResults');
|
||||
|
||||
function searchDocs(query) {
|
||||
if (!query || query.length < 2) return [];
|
||||
|
||||
const results = [];
|
||||
const lowerQuery = query.toLowerCase();
|
||||
|
||||
for (const [id, content] of Object.entries(indexData)) {
|
||||
let score = 0;
|
||||
const titleLower = content.title.toLowerCase();
|
||||
const bodyLower = content.body.toLowerCase();
|
||||
|
||||
if (titleLower.includes(lowerQuery)) score += 10;
|
||||
if (bodyLower.includes(lowerQuery)) score += 5;
|
||||
|
||||
if (score > 0) {
|
||||
results.push({
|
||||
id,
|
||||
title: content.title,
|
||||
excerpt: getExcerpt(content.body, query),
|
||||
score
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results.sort((a, b) => b.score - a.score).slice(0, 8);
|
||||
}
|
||||
|
||||
function getExcerpt(text, query) {
|
||||
const maxLength = 120;
|
||||
const lowerText = text.toLowerCase();
|
||||
const lowerQuery = query.toLowerCase();
|
||||
const index = lowerText.indexOf(lowerQuery);
|
||||
|
||||
if (index === -1) {
|
||||
return text.substring(0, maxLength) + (text.length > maxLength ? '...' : '');
|
||||
}
|
||||
|
||||
const start = Math.max(0, index - 30);
|
||||
const end = Math.min(text.length, index + query.length + 60);
|
||||
let excerpt = text.substring(start, end);
|
||||
|
||||
if (start > 0) excerpt = '...' + excerpt;
|
||||
if (end < text.length) excerpt += '...';
|
||||
|
||||
const regex = new RegExp('(' + query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi');
|
||||
return excerpt.replace(regex, '<mark>$1</mark>');
|
||||
}
|
||||
|
||||
searchInput.addEventListener('input', function() {
|
||||
const query = this.value.trim();
|
||||
const results = searchDocs(query);
|
||||
|
||||
if (results.length === 0) {
|
||||
searchResults.innerHTML = query.length >= 2
|
||||
? '<div class="no-results">未找到结果</div>'
|
||||
: '';
|
||||
searchResults.classList.toggle('visible', query.length >= 2);
|
||||
return;
|
||||
}
|
||||
|
||||
searchResults.innerHTML = results.map(r => `
|
||||
<a href="#/${r.id}" class="search-result-item" data-section="${r.id}">
|
||||
<div class="result-title">${r.title}</div>
|
||||
<div class="result-excerpt">${r.excerpt}</div>
|
||||
</a>
|
||||
`).join('');
|
||||
searchResults.classList.add('visible');
|
||||
});
|
||||
|
||||
searchResults.addEventListener('click', function(e) {
|
||||
const item = e.target.closest('.search-result-item');
|
||||
if (item) {
|
||||
e.preventDefault();
|
||||
searchInput.value = '';
|
||||
searchResults.innerHTML = '';
|
||||
searchResults.classList.remove('visible');
|
||||
showSection(item.dataset.section);
|
||||
}
|
||||
});
|
||||
|
||||
// Close search results when clicking outside
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!e.target.closest('.sidebar-search')) {
|
||||
searchResults.classList.remove('visible');
|
||||
}
|
||||
});
|
||||
|
||||
// ========== Theme Toggle ==========
|
||||
function setTheme(theme) {
|
||||
document.body.dataset.theme = theme;
|
||||
localStorage.setItem('docs-theme', theme);
|
||||
}
|
||||
|
||||
const savedTheme = localStorage.getItem('docs-theme') || 'light';
|
||||
setTheme(savedTheme);
|
||||
|
||||
document.getElementById('themeToggle').addEventListener('click', function() {
|
||||
setTheme(document.body.dataset.theme === 'dark' ? 'light' : 'dark');
|
||||
});
|
||||
|
||||
document.getElementById('themeToggleMobile').addEventListener('click', function() {
|
||||
setTheme(document.body.dataset.theme === 'dark' ? 'light' : 'dark');
|
||||
});
|
||||
|
||||
// ========== Sidebar Toggle (Mobile) ==========
|
||||
document.getElementById('sidebarToggle').addEventListener('click', function() {
|
||||
document.getElementById('sidebar').classList.toggle('open');
|
||||
});
|
||||
|
||||
// Close sidebar when clicking outside on mobile
|
||||
document.addEventListener('click', function(e) {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const toggle = document.getElementById('sidebarToggle');
|
||||
if (!sidebar.contains(e.target) && !toggle.contains(e.target)) {
|
||||
sidebar.classList.remove('open');
|
||||
}
|
||||
});
|
||||
|
||||
// ========== Back to Top ==========
|
||||
const backToTop = document.getElementById('backToTop');
|
||||
const mainContent = document.getElementById('mainContent');
|
||||
|
||||
mainContent.addEventListener('scroll', function() {
|
||||
backToTop.classList.toggle('visible', this.scrollTop > 300);
|
||||
});
|
||||
|
||||
backToTop.addEventListener('click', function() {
|
||||
mainContent.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
});
|
||||
|
||||
// ========== Code Block Copy ==========
|
||||
document.querySelectorAll('pre').forEach(pre => {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'code-block-wrapper';
|
||||
pre.parentNode.insertBefore(wrapper, pre);
|
||||
wrapper.appendChild(pre);
|
||||
|
||||
const copyBtn = document.createElement('button');
|
||||
copyBtn.className = 'copy-code-btn';
|
||||
copyBtn.innerHTML = '<svg viewBox="0 0 24 24" width="16" height="16"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z" fill="currentColor"/></svg>';
|
||||
copyBtn.addEventListener('click', function() {
|
||||
const code = pre.querySelector('code') || pre;
|
||||
navigator.clipboard.writeText(code.textContent).then(() => {
|
||||
copyBtn.innerHTML = '<svg viewBox="0 0 24 24" width="16" height="16"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" fill="currentColor"/></svg>';
|
||||
setTimeout(() => {
|
||||
copyBtn.innerHTML = '<svg viewBox="0 0 24 24" width="16" height="16"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z" fill="currentColor"/></svg>';
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
wrapper.appendChild(copyBtn);
|
||||
});
|
||||
|
||||
// ========== Mermaid Diagram Rendering ==========
|
||||
function renderMermaidDiagrams() {
|
||||
// Find all mermaid code blocks and convert them to diagrams
|
||||
document.querySelectorAll('pre code.language-mermaid, pre code.highlight-mermaid').forEach((codeBlock, index) => {
|
||||
const pre = codeBlock.parentElement;
|
||||
const wrapper = pre.parentElement;
|
||||
const code = codeBlock.textContent;
|
||||
|
||||
// Create mermaid container
|
||||
const mermaidDiv = document.createElement('div');
|
||||
mermaidDiv.className = 'mermaid';
|
||||
mermaidDiv.textContent = code;
|
||||
|
||||
// Replace code block with mermaid div
|
||||
if (wrapper && wrapper.classList.contains('code-block-wrapper')) {
|
||||
wrapper.parentElement.replaceChild(mermaidDiv, wrapper);
|
||||
} else {
|
||||
pre.parentElement.replaceChild(mermaidDiv, pre);
|
||||
}
|
||||
});
|
||||
|
||||
// Also handle codehilite blocks with mermaid
|
||||
document.querySelectorAll('.highlight').forEach((block) => {
|
||||
const code = block.querySelector('code, pre');
|
||||
if (code && code.textContent.trim().startsWith('graph ') ||
|
||||
code && code.textContent.trim().startsWith('sequenceDiagram') ||
|
||||
code && code.textContent.trim().startsWith('flowchart ') ||
|
||||
code && code.textContent.trim().startsWith('classDiagram') ||
|
||||
code && code.textContent.trim().startsWith('stateDiagram') ||
|
||||
code && code.textContent.trim().startsWith('erDiagram') ||
|
||||
code && code.textContent.trim().startsWith('gantt') ||
|
||||
code && code.textContent.trim().startsWith('pie') ||
|
||||
code && code.textContent.trim().startsWith('journey')) {
|
||||
const mermaidDiv = document.createElement('div');
|
||||
mermaidDiv.className = 'mermaid';
|
||||
mermaidDiv.textContent = code.textContent;
|
||||
block.parentElement.replaceChild(mermaidDiv, block);
|
||||
}
|
||||
});
|
||||
|
||||
// Render all mermaid diagrams
|
||||
if (typeof mermaid !== 'undefined') {
|
||||
mermaid.run();
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Internal Anchor Links Handler ==========
|
||||
// Handle clicks on internal anchor links (TOC links like #材料管理api)
|
||||
document.addEventListener('click', function(e) {
|
||||
const link = e.target.closest('a[href^="#"]');
|
||||
if (!link) return;
|
||||
|
||||
const href = link.getAttribute('href');
|
||||
// Skip section navigation links (handled by nav-item)
|
||||
if (link.classList.contains('nav-item')) return;
|
||||
// Skip search result links
|
||||
if (link.classList.contains('search-result-item')) return;
|
||||
|
||||
// Check if it's an internal anchor (not a section link)
|
||||
if (href && href.startsWith('#') && !href.startsWith('#/')) {
|
||||
e.preventDefault();
|
||||
const anchorId = href.substring(1);
|
||||
const targetElement = document.getElementById(anchorId);
|
||||
|
||||
if (targetElement) {
|
||||
// Scroll to the anchor within current section
|
||||
targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
// Update URL without triggering popstate
|
||||
history.pushState(null, '', '#/' + currentSectionId + '/' + anchorId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ========== Hash Parser ==========
|
||||
function parseHash(hash) {
|
||||
// Handle formats: #/sectionId, #/sectionId/anchorId, #anchorId
|
||||
if (!hash || hash === '#' || hash === '#/') return { section: null, anchor: null };
|
||||
|
||||
if (hash.startsWith('#/')) {
|
||||
const parts = hash.substring(2).split('/');
|
||||
return { section: parts[0] || null, anchor: parts[1] || null };
|
||||
} else {
|
||||
// Plain anchor like #材料管理api - stay on current section
|
||||
return { section: null, anchor: hash.substring(1) };
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Initial Load ==========
|
||||
// Check URL hash or show first section
|
||||
const initialHash = parseHash(window.location.hash);
|
||||
if (initialHash.section && document.getElementById('section-' + initialHash.section)) {
|
||||
showSection(initialHash.section);
|
||||
// Scroll to anchor if present
|
||||
if (initialHash.anchor) {
|
||||
setTimeout(() => {
|
||||
const anchor = document.getElementById(initialHash.anchor);
|
||||
if (anchor) anchor.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}, 100);
|
||||
}
|
||||
} else if (sections.length > 0) {
|
||||
const firstSection = sections[0].id.replace('section-', '');
|
||||
showSection(firstSection);
|
||||
}
|
||||
|
||||
// Render mermaid diagrams after initial load
|
||||
setTimeout(renderMermaidDiagrams, 100);
|
||||
|
||||
// Handle browser back/forward
|
||||
window.addEventListener('popstate', function() {
|
||||
const parsed = parseHash(window.location.hash);
|
||||
if (parsed.section) {
|
||||
showSection(parsed.section);
|
||||
if (parsed.anchor) {
|
||||
setTimeout(() => {
|
||||
const anchor = document.getElementById(parsed.anchor);
|
||||
if (anchor) anchor.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
327
.claude/skills/software-manual/templates/tiddlywiki-shell.html
Normal file
327
.claude/skills/software-manual/templates/tiddlywiki-shell.html
Normal file
@@ -0,0 +1,327 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="{{SOFTWARE_NAME}} - Interactive Software Manual">
|
||||
<meta name="generator" content="software-manual-skill">
|
||||
<title>{{SOFTWARE_NAME}} v{{VERSION}} - User Manual</title>
|
||||
<style>
|
||||
{{EMBEDDED_CSS}}
|
||||
</style>
|
||||
</head>
|
||||
<body class="wiki-container" data-theme="light">
|
||||
<!-- Sidebar Navigation -->
|
||||
<aside class="wiki-sidebar">
|
||||
<!-- Logo and Title -->
|
||||
<div class="wiki-logo">
|
||||
<div class="logo-placeholder">{{SOFTWARE_NAME}}</div>
|
||||
<h1>{{SOFTWARE_NAME}}</h1>
|
||||
<span class="version">v{{VERSION}}</span>
|
||||
</div>
|
||||
|
||||
<!-- Search Box -->
|
||||
<div class="wiki-search">
|
||||
<input type="text" id="searchInput" placeholder="Search documentation..." aria-label="Search">
|
||||
<div id="searchResults" class="search-results" aria-live="polite"></div>
|
||||
</div>
|
||||
|
||||
<!-- Tag Navigation (Dynamic) -->
|
||||
<nav class="wiki-tags" aria-label="Filter by category">
|
||||
<button class="tag active" data-tag="all">全部</button>
|
||||
{{TAG_BUTTONS_HTML}}
|
||||
</nav>
|
||||
|
||||
<!-- Table of Contents -->
|
||||
{{TOC_HTML}}
|
||||
</aside>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<main class="wiki-content">
|
||||
<!-- Header Bar -->
|
||||
<header class="content-header">
|
||||
<button class="sidebar-toggle" id="sidebarToggle" aria-label="Toggle sidebar">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</button>
|
||||
<div class="header-actions">
|
||||
<button class="expand-all" id="expandAll">Expand All</button>
|
||||
<button class="collapse-all" id="collapseAll">Collapse All</button>
|
||||
<button class="print-btn" id="printBtn">Print</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Tiddler Container -->
|
||||
<div class="tiddler-container">
|
||||
{{TIDDLERS_HTML}}
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="wiki-footer">
|
||||
<p>Generated by <strong>software-manual-skill</strong></p>
|
||||
<p>Last updated: <time datetime="{{TIMESTAMP}}">{{TIMESTAMP}}</time></p>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
<!-- Theme Toggle Button -->
|
||||
<button class="theme-toggle" id="themeToggle" aria-label="Toggle theme">
|
||||
<span class="sun-icon">☀</span>
|
||||
<span class="moon-icon">☾</span>
|
||||
</button>
|
||||
|
||||
<!-- Back to Top Button -->
|
||||
<button class="back-to-top" id="backToTop" aria-label="Back to top">↑</button>
|
||||
|
||||
<!-- Search Index Data -->
|
||||
<script id="search-index" type="application/json">
|
||||
{{SEARCH_INDEX_JSON}}
|
||||
</script>
|
||||
|
||||
<!-- Embedded JavaScript -->
|
||||
<script>
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// ========== Search Functionality ==========
|
||||
class WikiSearch {
|
||||
constructor(indexData) {
|
||||
this.index = indexData;
|
||||
}
|
||||
|
||||
search(query) {
|
||||
if (!query || query.length < 2) return [];
|
||||
|
||||
const results = [];
|
||||
const lowerQuery = query.toLowerCase();
|
||||
const queryWords = lowerQuery.split(/\s+/);
|
||||
|
||||
for (const [id, content] of Object.entries(this.index)) {
|
||||
let score = 0;
|
||||
|
||||
// Title match (higher weight)
|
||||
const titleLower = content.title.toLowerCase();
|
||||
if (titleLower.includes(lowerQuery)) {
|
||||
score += 10;
|
||||
}
|
||||
queryWords.forEach(word => {
|
||||
if (titleLower.includes(word)) score += 3;
|
||||
});
|
||||
|
||||
// Body match
|
||||
const bodyLower = content.body.toLowerCase();
|
||||
if (bodyLower.includes(lowerQuery)) {
|
||||
score += 5;
|
||||
}
|
||||
queryWords.forEach(word => {
|
||||
if (bodyLower.includes(word)) score += 1;
|
||||
});
|
||||
|
||||
// Tag match
|
||||
if (content.tags) {
|
||||
content.tags.forEach(tag => {
|
||||
if (tag.toLowerCase().includes(lowerQuery)) score += 4;
|
||||
});
|
||||
}
|
||||
|
||||
if (score > 0) {
|
||||
results.push({
|
||||
id,
|
||||
title: content.title,
|
||||
excerpt: this.highlight(content.body, query),
|
||||
score
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, 10);
|
||||
}
|
||||
|
||||
highlight(text, query) {
|
||||
const maxLength = 150;
|
||||
const lowerText = text.toLowerCase();
|
||||
const lowerQuery = query.toLowerCase();
|
||||
const index = lowerText.indexOf(lowerQuery);
|
||||
|
||||
if (index === -1) {
|
||||
return text.substring(0, maxLength) + (text.length > maxLength ? '...' : '');
|
||||
}
|
||||
|
||||
const start = Math.max(0, index - 40);
|
||||
const end = Math.min(text.length, index + query.length + 80);
|
||||
let excerpt = text.substring(start, end);
|
||||
|
||||
if (start > 0) excerpt = '...' + excerpt;
|
||||
if (end < text.length) excerpt += '...';
|
||||
|
||||
// Highlight matches
|
||||
const regex = new RegExp('(' + query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi');
|
||||
return excerpt.replace(regex, '<mark>$1</mark>');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize search
|
||||
const indexData = JSON.parse(document.getElementById('search-index').textContent);
|
||||
const search = new WikiSearch(indexData);
|
||||
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
const searchResults = document.getElementById('searchResults');
|
||||
|
||||
searchInput.addEventListener('input', function() {
|
||||
const query = this.value.trim();
|
||||
const results = search.search(query);
|
||||
|
||||
if (results.length === 0) {
|
||||
searchResults.innerHTML = query.length >= 2
|
||||
? '<div class="no-results">No results found</div>'
|
||||
: '';
|
||||
return;
|
||||
}
|
||||
|
||||
searchResults.innerHTML = results.map(r => `
|
||||
<a href="#${r.id}" class="search-result-item" data-tiddler="${r.id}">
|
||||
<div class="result-title">${r.title}</div>
|
||||
<div class="result-excerpt">${r.excerpt}</div>
|
||||
</a>
|
||||
`).join('');
|
||||
});
|
||||
|
||||
// Clear search on result click
|
||||
searchResults.addEventListener('click', function(e) {
|
||||
const item = e.target.closest('.search-result-item');
|
||||
if (item) {
|
||||
searchInput.value = '';
|
||||
searchResults.innerHTML = '';
|
||||
|
||||
// Expand target tiddler
|
||||
const tiddlerId = item.dataset.tiddler;
|
||||
const tiddler = document.getElementById(tiddlerId);
|
||||
if (tiddler) {
|
||||
tiddler.classList.remove('collapsed');
|
||||
const toggle = tiddler.querySelector('.collapse-toggle');
|
||||
if (toggle) toggle.textContent = '▼';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ========== Collapse/Expand ==========
|
||||
document.querySelectorAll('.collapse-toggle').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const tiddler = this.closest('.tiddler');
|
||||
tiddler.classList.toggle('collapsed');
|
||||
this.textContent = tiddler.classList.contains('collapsed') ? '▶' : '▼';
|
||||
});
|
||||
});
|
||||
|
||||
// Expand/Collapse All
|
||||
document.getElementById('expandAll').addEventListener('click', function() {
|
||||
document.querySelectorAll('.tiddler').forEach(t => {
|
||||
t.classList.remove('collapsed');
|
||||
const toggle = t.querySelector('.collapse-toggle');
|
||||
if (toggle) toggle.textContent = '▼';
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('collapseAll').addEventListener('click', function() {
|
||||
document.querySelectorAll('.tiddler').forEach(t => {
|
||||
t.classList.add('collapsed');
|
||||
const toggle = t.querySelector('.collapse-toggle');
|
||||
if (toggle) toggle.textContent = '▶';
|
||||
});
|
||||
});
|
||||
|
||||
// ========== Tag Filtering ==========
|
||||
document.querySelectorAll('.wiki-tags .tag').forEach(tag => {
|
||||
tag.addEventListener('click', function() {
|
||||
const filter = this.dataset.tag;
|
||||
|
||||
// Update active state
|
||||
document.querySelectorAll('.wiki-tags .tag').forEach(t => t.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
|
||||
// Filter tiddlers
|
||||
document.querySelectorAll('.tiddler').forEach(tiddler => {
|
||||
if (filter === 'all') {
|
||||
tiddler.style.display = '';
|
||||
} else {
|
||||
const tags = tiddler.dataset.tags || '';
|
||||
tiddler.style.display = tags.includes(filter) ? '' : 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ========== Theme Toggle ==========
|
||||
const themeToggle = document.getElementById('themeToggle');
|
||||
const savedTheme = localStorage.getItem('wiki-theme');
|
||||
|
||||
if (savedTheme) {
|
||||
document.body.dataset.theme = savedTheme;
|
||||
}
|
||||
|
||||
themeToggle.addEventListener('click', function() {
|
||||
const isDark = document.body.dataset.theme === 'dark';
|
||||
document.body.dataset.theme = isDark ? 'light' : 'dark';
|
||||
localStorage.setItem('wiki-theme', document.body.dataset.theme);
|
||||
});
|
||||
|
||||
// ========== Sidebar Toggle (Mobile) ==========
|
||||
document.getElementById('sidebarToggle').addEventListener('click', function() {
|
||||
document.querySelector('.wiki-sidebar').classList.toggle('open');
|
||||
});
|
||||
|
||||
// ========== Back to Top ==========
|
||||
const backToTop = document.getElementById('backToTop');
|
||||
|
||||
window.addEventListener('scroll', function() {
|
||||
backToTop.classList.toggle('visible', window.scrollY > 300);
|
||||
});
|
||||
|
||||
backToTop.addEventListener('click', function() {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
});
|
||||
|
||||
// ========== Print ==========
|
||||
document.getElementById('printBtn').addEventListener('click', function() {
|
||||
window.print();
|
||||
});
|
||||
|
||||
// ========== TOC Navigation ==========
|
||||
document.querySelectorAll('.wiki-toc a').forEach(link => {
|
||||
link.addEventListener('click', function(e) {
|
||||
const tiddlerId = this.getAttribute('href').substring(1);
|
||||
const tiddler = document.getElementById(tiddlerId);
|
||||
|
||||
if (tiddler) {
|
||||
// Expand if collapsed
|
||||
tiddler.classList.remove('collapsed');
|
||||
const toggle = tiddler.querySelector('.collapse-toggle');
|
||||
if (toggle) toggle.textContent = '▼';
|
||||
|
||||
// Close sidebar on mobile
|
||||
document.querySelector('.wiki-sidebar').classList.remove('open');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ========== Code Block Copy ==========
|
||||
document.querySelectorAll('pre').forEach(pre => {
|
||||
const copyBtn = document.createElement('button');
|
||||
copyBtn.className = 'copy-code-btn';
|
||||
copyBtn.textContent = 'Copy';
|
||||
copyBtn.addEventListener('click', function() {
|
||||
const code = pre.querySelector('code');
|
||||
navigator.clipboard.writeText(code.textContent).then(() => {
|
||||
copyBtn.textContent = 'Copied!';
|
||||
setTimeout(() => copyBtn.textContent = 'Copy', 2000);
|
||||
});
|
||||
});
|
||||
pre.appendChild(copyBtn);
|
||||
});
|
||||
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,219 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "discovery-finding-schema",
|
||||
"title": "Discovery Finding Schema",
|
||||
"description": "Schema for perspective-based issue discovery results",
|
||||
"type": "object",
|
||||
"required": ["perspective", "discovery_id", "analysis_timestamp", "cli_tool_used", "summary", "findings"],
|
||||
"properties": {
|
||||
"perspective": {
|
||||
"type": "string",
|
||||
"enum": ["bug", "ux", "test", "quality", "security", "performance", "maintainability", "best-practices"],
|
||||
"description": "Discovery perspective"
|
||||
},
|
||||
"discovery_id": {
|
||||
"type": "string",
|
||||
"pattern": "^DSC-\\d{8}-\\d{6}$",
|
||||
"description": "Parent discovery session ID"
|
||||
},
|
||||
"analysis_timestamp": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "ISO 8601 timestamp of analysis"
|
||||
},
|
||||
"cli_tool_used": {
|
||||
"type": "string",
|
||||
"enum": ["gemini", "qwen", "codex"],
|
||||
"description": "CLI tool that performed the analysis"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "Specific model version used",
|
||||
"examples": ["gemini-2.5-pro", "qwen-max"]
|
||||
},
|
||||
"analysis_duration_ms": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Analysis duration in milliseconds"
|
||||
},
|
||||
"summary": {
|
||||
"type": "object",
|
||||
"required": ["total_findings"],
|
||||
"properties": {
|
||||
"total_findings": { "type": "integer", "minimum": 0 },
|
||||
"critical": { "type": "integer", "minimum": 0 },
|
||||
"high": { "type": "integer", "minimum": 0 },
|
||||
"medium": { "type": "integer", "minimum": 0 },
|
||||
"low": { "type": "integer", "minimum": 0 },
|
||||
"files_analyzed": { "type": "integer", "minimum": 0 }
|
||||
},
|
||||
"description": "Summary statistics (FLAT structure, NOT nested)"
|
||||
},
|
||||
"findings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["id", "title", "perspective", "priority", "category", "description", "file", "line"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^dsc-[a-z]+-\\d{3}-[a-f0-9]{8}$",
|
||||
"description": "Unique finding ID: dsc-{perspective}-{seq}-{uuid8}",
|
||||
"examples": ["dsc-bug-001-a1b2c3d4"]
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"minLength": 10,
|
||||
"maxLength": 200,
|
||||
"description": "Concise finding title"
|
||||
},
|
||||
"perspective": {
|
||||
"type": "string",
|
||||
"enum": ["bug", "ux", "test", "quality", "security", "performance", "maintainability", "best-practices"]
|
||||
},
|
||||
"priority": {
|
||||
"type": "string",
|
||||
"enum": ["critical", "high", "medium", "low"],
|
||||
"description": "Priority level (lowercase only)"
|
||||
},
|
||||
"category": {
|
||||
"type": "string",
|
||||
"description": "Perspective-specific category",
|
||||
"examples": ["null-check", "edge-case", "missing-test", "complexity", "injection"]
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"minLength": 20,
|
||||
"description": "Detailed description of the finding"
|
||||
},
|
||||
"file": {
|
||||
"type": "string",
|
||||
"description": "File path relative to project root"
|
||||
},
|
||||
"line": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"description": "Line number of the finding"
|
||||
},
|
||||
"snippet": {
|
||||
"type": "string",
|
||||
"description": "Relevant code snippet"
|
||||
},
|
||||
"suggested_issue": {
|
||||
"type": "object",
|
||||
"required": ["title", "type", "priority"],
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Suggested issue title for export"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["bug", "feature", "enhancement", "refactor", "test", "docs"],
|
||||
"description": "Issue type"
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 5,
|
||||
"description": "Priority 1-5 (1=critical, 5=low)"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Suggested tags for the issue"
|
||||
}
|
||||
},
|
||||
"description": "Pre-filled issue suggestion for export"
|
||||
},
|
||||
"external_reference": {
|
||||
"type": ["object", "null"],
|
||||
"properties": {
|
||||
"source": { "type": "string" },
|
||||
"url": { "type": "string", "format": "uri" },
|
||||
"relevance": { "type": "string" }
|
||||
},
|
||||
"description": "External reference from Exa research (if applicable)"
|
||||
},
|
||||
"confidence": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"description": "Confidence score 0.0-1.0"
|
||||
},
|
||||
"impact": {
|
||||
"type": "string",
|
||||
"description": "Description of potential impact"
|
||||
},
|
||||
"recommendation": {
|
||||
"type": "string",
|
||||
"description": "Specific recommendation to address the finding"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"description": "Additional metadata (CWE ID, OWASP category, etc.)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Array of discovered findings"
|
||||
},
|
||||
"cross_references": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"finding_id": { "type": "string" },
|
||||
"related_perspectives": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"reason": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"description": "Cross-references to findings in other perspectives"
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"perspective": "bug",
|
||||
"discovery_id": "DSC-20250128-143022",
|
||||
"analysis_timestamp": "2025-01-28T14:35:00Z",
|
||||
"cli_tool_used": "gemini",
|
||||
"model": "gemini-2.5-pro",
|
||||
"analysis_duration_ms": 45000,
|
||||
"summary": {
|
||||
"total_findings": 8,
|
||||
"critical": 1,
|
||||
"high": 2,
|
||||
"medium": 3,
|
||||
"low": 2,
|
||||
"files_analyzed": 5
|
||||
},
|
||||
"findings": [
|
||||
{
|
||||
"id": "dsc-bug-001-a1b2c3d4",
|
||||
"title": "Missing null check in user validation",
|
||||
"perspective": "bug",
|
||||
"priority": "high",
|
||||
"category": "null-check",
|
||||
"description": "User object is accessed without null check after database query, which may fail if user doesn't exist",
|
||||
"file": "src/auth/validator.ts",
|
||||
"line": 45,
|
||||
"snippet": "const user = await db.findUser(id);\nreturn user.email; // user may be null",
|
||||
"suggested_issue": {
|
||||
"title": "Add null check in user validation",
|
||||
"type": "bug",
|
||||
"priority": 2,
|
||||
"tags": ["bug", "auth"]
|
||||
},
|
||||
"external_reference": null,
|
||||
"confidence": 0.85,
|
||||
"impact": "Runtime error when user not found",
|
||||
"recommendation": "Add null check: if (!user) throw new NotFoundError('User not found');"
|
||||
}
|
||||
],
|
||||
"cross_references": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "discovery-state-schema",
|
||||
"title": "Discovery State Schema (Merged)",
|
||||
"description": "Unified schema for issue discovery session (state + progress merged)",
|
||||
"type": "object",
|
||||
"required": ["discovery_id", "target_pattern", "phase", "created_at"],
|
||||
"properties": {
|
||||
"discovery_id": {
|
||||
"type": "string",
|
||||
"description": "Unique discovery session ID",
|
||||
"pattern": "^DSC-\\d{8}-\\d{6}$",
|
||||
"examples": ["DSC-20250128-143022"]
|
||||
},
|
||||
"target_pattern": {
|
||||
"type": "string",
|
||||
"description": "File/directory pattern being analyzed",
|
||||
"examples": ["src/auth/**", "codex-lens/**/*.py"]
|
||||
},
|
||||
"phase": {
|
||||
"type": "string",
|
||||
"enum": ["initialization", "parallel", "aggregation", "complete"],
|
||||
"description": "Current execution phase"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"target": {
|
||||
"type": "object",
|
||||
"description": "Target module information",
|
||||
"properties": {
|
||||
"files_count": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"source": { "type": "integer" },
|
||||
"tests": { "type": "integer" },
|
||||
"total": { "type": "integer" }
|
||||
}
|
||||
},
|
||||
"project": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"version": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"perspectives": {
|
||||
"type": "array",
|
||||
"description": "Perspective analysis status (merged from progress)",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["name", "status"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"enum": ["bug", "ux", "test", "quality", "security", "performance", "maintainability", "best-practices"]
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["pending", "in_progress", "completed", "failed"]
|
||||
},
|
||||
"findings": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"external_research": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean", "default": false },
|
||||
"completed": { "type": "boolean", "default": false }
|
||||
}
|
||||
},
|
||||
"results": {
|
||||
"type": "object",
|
||||
"description": "Aggregated results (final phase)",
|
||||
"properties": {
|
||||
"total_findings": { "type": "integer", "minimum": 0 },
|
||||
"issues_generated": { "type": "integer", "minimum": 0 },
|
||||
"priority_distribution": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"critical": { "type": "integer" },
|
||||
"high": { "type": "integer" },
|
||||
"medium": { "type": "integer" },
|
||||
"low": { "type": "integer" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"discovery_id": "DSC-20251228-182237",
|
||||
"target_pattern": "codex-lens/**/*.py",
|
||||
"phase": "complete",
|
||||
"created_at": "2025-12-28T18:22:37+08:00",
|
||||
"updated_at": "2025-12-28T18:35:00+08:00",
|
||||
"target": {
|
||||
"files_count": { "source": 48, "tests": 44, "total": 93 },
|
||||
"project": { "name": "codex-lens", "version": "0.1.0" }
|
||||
},
|
||||
"perspectives": [
|
||||
{ "name": "bug", "status": "completed", "findings": 15 },
|
||||
{ "name": "test", "status": "completed", "findings": 11 },
|
||||
{ "name": "quality", "status": "completed", "findings": 12 }
|
||||
],
|
||||
"external_research": { "enabled": false, "completed": false },
|
||||
"results": {
|
||||
"total_findings": 37,
|
||||
"issues_generated": 15,
|
||||
"priority_distribution": { "critical": 4, "high": 13, "medium": 16, "low": 6 }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
141
.claude/workflows/cli-templates/schemas/issues-jsonl-schema.json
Normal file
141
.claude/workflows/cli-templates/schemas/issues-jsonl-schema.json
Normal file
@@ -0,0 +1,141 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Issues JSONL Schema",
|
||||
"description": "Schema for each line in issues.jsonl (flat storage)",
|
||||
"type": "object",
|
||||
"required": ["id", "title", "status", "created_at"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Issue ID (GH-123, ISS-xxx, DSC-001)"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["registered", "planning", "planned", "queued", "executing", "completed", "failed", "paused"],
|
||||
"default": "registered"
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 5,
|
||||
"default": 3,
|
||||
"description": "1=critical, 2=high, 3=medium, 4=low, 5=trivial"
|
||||
},
|
||||
"context": {
|
||||
"type": "string",
|
||||
"description": "Issue context/description (markdown)"
|
||||
},
|
||||
"source": {
|
||||
"type": "string",
|
||||
"enum": ["github", "text", "discovery"],
|
||||
"description": "Source of the issue"
|
||||
},
|
||||
"source_url": {
|
||||
"type": "string",
|
||||
"description": "Original source URL (for GitHub issues)"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Issue tags"
|
||||
},
|
||||
"extended_context": {
|
||||
"type": "object",
|
||||
"description": "Minimal extended context for planning hints",
|
||||
"properties": {
|
||||
"location": {
|
||||
"type": "string",
|
||||
"description": "file:line format (e.g., 'src/auth.ts:42')"
|
||||
},
|
||||
"suggested_fix": {
|
||||
"type": "string",
|
||||
"description": "Suggested remediation"
|
||||
},
|
||||
"notes": {
|
||||
"type": "string",
|
||||
"description": "Additional notes (user clarifications or discovery hints)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"affected_components": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Files/modules affected"
|
||||
},
|
||||
"lifecycle_requirements": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"test_strategy": {
|
||||
"type": "string",
|
||||
"enum": ["unit", "integration", "e2e", "manual", "auto"]
|
||||
},
|
||||
"regression_scope": {
|
||||
"type": "string",
|
||||
"enum": ["affected", "related", "full"]
|
||||
},
|
||||
"acceptance_type": {
|
||||
"type": "string",
|
||||
"enum": ["automated", "manual", "both"]
|
||||
},
|
||||
"commit_strategy": {
|
||||
"type": "string",
|
||||
"enum": ["per-task", "squash", "atomic"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"bound_solution_id": {
|
||||
"type": "string",
|
||||
"description": "ID of the bound solution (null if none bound)"
|
||||
},
|
||||
"solution_count": {
|
||||
"type": "integer",
|
||||
"default": 0,
|
||||
"description": "Number of candidate solutions"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"planned_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"completed_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"id": "DSC-001",
|
||||
"title": "Fix: SQLite connection pool memory leak",
|
||||
"status": "registered",
|
||||
"priority": 1,
|
||||
"context": "Connection pool cleanup only happens when MAX_POOL_SIZE is reached...",
|
||||
"source": "discovery",
|
||||
"tags": ["bug", "resource-leak", "critical"],
|
||||
"extended_context": {
|
||||
"location": "storage/sqlite_store.py:59",
|
||||
"suggested_fix": "Implement periodic cleanup or weak references",
|
||||
"notes": null
|
||||
},
|
||||
"affected_components": ["storage/sqlite_store.py"],
|
||||
"lifecycle_requirements": {
|
||||
"test_strategy": "unit",
|
||||
"regression_scope": "affected",
|
||||
"acceptance_type": "automated",
|
||||
"commit_strategy": "per-task"
|
||||
},
|
||||
"bound_solution_id": null,
|
||||
"solution_count": 0,
|
||||
"created_at": "2025-12-28T18:22:37Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
248
.claude/workflows/cli-templates/schemas/queue-schema.json
Normal file
248
.claude/workflows/cli-templates/schemas/queue-schema.json
Normal file
@@ -0,0 +1,248 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Issue Execution Queue Schema",
|
||||
"description": "Execution queue supporting both task-level (T-N) and solution-level (S-N) granularity",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^QUE-[0-9]{8}-[0-9]{6}$",
|
||||
"description": "Queue ID in format QUE-YYYYMMDD-HHMMSS"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["active", "paused", "completed", "archived"],
|
||||
"default": "active"
|
||||
},
|
||||
"issue_ids": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Issues included in this queue"
|
||||
},
|
||||
"solutions": {
|
||||
"type": "array",
|
||||
"description": "Solution-level queue items (preferred for new queues)",
|
||||
"items": {
|
||||
"$ref": "#/definitions/solutionItem"
|
||||
}
|
||||
},
|
||||
"tasks": {
|
||||
"type": "array",
|
||||
"description": "Task-level queue items (legacy format)",
|
||||
"items": {
|
||||
"$ref": "#/definitions/taskItem"
|
||||
}
|
||||
},
|
||||
"conflicts": {
|
||||
"type": "array",
|
||||
"description": "Detected conflicts between items",
|
||||
"items": {
|
||||
"$ref": "#/definitions/conflict"
|
||||
}
|
||||
},
|
||||
"execution_groups": {
|
||||
"type": "array",
|
||||
"description": "Parallel/Sequential execution groups",
|
||||
"items": {
|
||||
"$ref": "#/definitions/executionGroup"
|
||||
}
|
||||
},
|
||||
"_metadata": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"version": { "type": "string", "default": "2.0" },
|
||||
"queue_type": {
|
||||
"type": "string",
|
||||
"enum": ["solution", "task"],
|
||||
"description": "Queue granularity level"
|
||||
},
|
||||
"total_solutions": { "type": "integer" },
|
||||
"total_tasks": { "type": "integer" },
|
||||
"pending_count": { "type": "integer" },
|
||||
"ready_count": { "type": "integer" },
|
||||
"executing_count": { "type": "integer" },
|
||||
"completed_count": { "type": "integer" },
|
||||
"failed_count": { "type": "integer" },
|
||||
"last_queue_formation": { "type": "string", "format": "date-time" },
|
||||
"last_updated": { "type": "string", "format": "date-time" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"solutionItem": {
|
||||
"type": "object",
|
||||
"required": ["item_id", "issue_id", "solution_id", "status", "task_count", "files_touched"],
|
||||
"properties": {
|
||||
"item_id": {
|
||||
"type": "string",
|
||||
"pattern": "^S-[0-9]+$",
|
||||
"description": "Solution-level queue item ID (S-1, S-2, ...)"
|
||||
},
|
||||
"issue_id": {
|
||||
"type": "string",
|
||||
"description": "Source issue ID"
|
||||
},
|
||||
"solution_id": {
|
||||
"type": "string",
|
||||
"description": "Bound solution ID"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["pending", "ready", "executing", "completed", "failed", "blocked"],
|
||||
"default": "pending"
|
||||
},
|
||||
"task_count": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"description": "Number of tasks in this solution"
|
||||
},
|
||||
"files_touched": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "All files modified by this solution"
|
||||
},
|
||||
"execution_order": {
|
||||
"type": "integer",
|
||||
"description": "Order in execution sequence"
|
||||
},
|
||||
"execution_group": {
|
||||
"type": "string",
|
||||
"description": "Parallel (P*) or Sequential (S*) group ID"
|
||||
},
|
||||
"depends_on": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Solution IDs this item depends on"
|
||||
},
|
||||
"semantic_priority": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"description": "Semantic importance score (0.0-1.0)"
|
||||
},
|
||||
"queued_at": { "type": "string", "format": "date-time" },
|
||||
"started_at": { "type": "string", "format": "date-time" },
|
||||
"completed_at": { "type": "string", "format": "date-time" },
|
||||
"result": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"summary": { "type": "string" },
|
||||
"files_modified": { "type": "array", "items": { "type": "string" } },
|
||||
"tasks_completed": { "type": "integer" },
|
||||
"commit_hashes": { "type": "array", "items": { "type": "string" } }
|
||||
}
|
||||
},
|
||||
"failure_reason": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"taskItem": {
|
||||
"type": "object",
|
||||
"required": ["item_id", "issue_id", "solution_id", "task_id", "status"],
|
||||
"properties": {
|
||||
"item_id": {
|
||||
"type": "string",
|
||||
"pattern": "^T-[0-9]+$",
|
||||
"description": "Task-level queue item ID (T-1, T-2, ...)"
|
||||
},
|
||||
"issue_id": { "type": "string" },
|
||||
"solution_id": { "type": "string" },
|
||||
"task_id": { "type": "string" },
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["pending", "ready", "executing", "completed", "failed", "blocked"],
|
||||
"default": "pending"
|
||||
},
|
||||
"execution_order": { "type": "integer" },
|
||||
"execution_group": { "type": "string" },
|
||||
"depends_on": { "type": "array", "items": { "type": "string" } },
|
||||
"semantic_priority": { "type": "number", "minimum": 0, "maximum": 1 },
|
||||
"queued_at": { "type": "string", "format": "date-time" },
|
||||
"started_at": { "type": "string", "format": "date-time" },
|
||||
"completed_at": { "type": "string", "format": "date-time" },
|
||||
"result": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"files_modified": { "type": "array", "items": { "type": "string" } },
|
||||
"files_created": { "type": "array", "items": { "type": "string" } },
|
||||
"summary": { "type": "string" },
|
||||
"commit_hash": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"failure_reason": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"conflict": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["file_conflict", "dependency_conflict", "resource_conflict"]
|
||||
},
|
||||
"file": {
|
||||
"type": "string",
|
||||
"description": "Conflicting file path"
|
||||
},
|
||||
"solutions": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Solution IDs involved (for solution-level queues)"
|
||||
},
|
||||
"tasks": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Task IDs involved (for task-level queues)"
|
||||
},
|
||||
"resolution": {
|
||||
"type": "string",
|
||||
"enum": ["sequential", "merge", "manual"]
|
||||
},
|
||||
"resolution_order": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Execution order to resolve conflict"
|
||||
},
|
||||
"rationale": {
|
||||
"type": "string",
|
||||
"description": "Explanation of resolution decision"
|
||||
},
|
||||
"resolved": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"executionGroup": {
|
||||
"type": "object",
|
||||
"required": ["id", "type"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^[PS][0-9]+$",
|
||||
"description": "Group ID (P1, P2 for parallel, S1, S2 for sequential)"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["parallel", "sequential"]
|
||||
},
|
||||
"solutions": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Solution IDs in this group"
|
||||
},
|
||||
"tasks": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Task IDs in this group (legacy)"
|
||||
},
|
||||
"solution_count": {
|
||||
"type": "integer",
|
||||
"description": "Number of solutions in group"
|
||||
},
|
||||
"task_count": {
|
||||
"type": "integer",
|
||||
"description": "Number of tasks in group (legacy)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
94
.claude/workflows/cli-templates/schemas/registry-schema.json
Normal file
94
.claude/workflows/cli-templates/schemas/registry-schema.json
Normal file
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Issue Registry Schema",
|
||||
"description": "Global registry of all issues and their solutions",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"issues": {
|
||||
"type": "array",
|
||||
"description": "List of registered issues",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["id", "title", "status", "created_at"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Issue ID (e.g., GH-123, TEXT-xxx)"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["registered", "planning", "planned", "queued", "executing", "completed", "failed", "paused"],
|
||||
"default": "registered"
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 5,
|
||||
"default": 3
|
||||
},
|
||||
"solution_count": {
|
||||
"type": "integer",
|
||||
"default": 0,
|
||||
"description": "Number of candidate solutions"
|
||||
},
|
||||
"bound_solution_id": {
|
||||
"type": "string",
|
||||
"description": "ID of the bound solution (null if none bound)"
|
||||
},
|
||||
"source": {
|
||||
"type": "string",
|
||||
"enum": ["github", "text", "file"],
|
||||
"description": "Source of the issue"
|
||||
},
|
||||
"source_url": {
|
||||
"type": "string",
|
||||
"description": "Original source URL (for GitHub issues)"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"planned_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"queued_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"completed_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"_metadata": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"version": { "type": "string", "default": "1.0" },
|
||||
"total_issues": { "type": "integer" },
|
||||
"by_status": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"registered": { "type": "integer" },
|
||||
"planning": { "type": "integer" },
|
||||
"planned": { "type": "integer" },
|
||||
"queued": { "type": "integer" },
|
||||
"executing": { "type": "integer" },
|
||||
"completed": { "type": "integer" },
|
||||
"failed": { "type": "integer" }
|
||||
}
|
||||
},
|
||||
"last_updated": { "type": "string", "format": "date-time" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
166
.claude/workflows/cli-templates/schemas/solution-schema.json
Normal file
166
.claude/workflows/cli-templates/schemas/solution-schema.json
Normal file
@@ -0,0 +1,166 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Issue Solution Schema",
|
||||
"description": "Schema for solution registered to an issue",
|
||||
"type": "object",
|
||||
"required": ["id", "tasks", "is_bound", "created_at"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Unique solution identifier: SOL-{issue-id}-{seq}",
|
||||
"pattern": "^SOL-.+-[0-9]+$",
|
||||
"examples": ["SOL-GH-123-1", "SOL-ISS-20251229-1"]
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "High-level summary of the solution"
|
||||
},
|
||||
"approach": {
|
||||
"type": "string",
|
||||
"description": "Technical approach or strategy"
|
||||
},
|
||||
"tasks": {
|
||||
"type": "array",
|
||||
"description": "Task breakdown for this solution",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["id", "title", "scope", "action", "implementation", "acceptance"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^T[0-9]+$"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Action verb + target"
|
||||
},
|
||||
"scope": {
|
||||
"type": "string",
|
||||
"description": "Module path or feature area"
|
||||
},
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": ["Create", "Update", "Implement", "Refactor", "Add", "Delete", "Configure", "Test", "Fix"]
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "1-2 sentences describing what to implement"
|
||||
},
|
||||
"modification_points": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file": { "type": "string" },
|
||||
"target": { "type": "string" },
|
||||
"change": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"implementation": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Step-by-step implementation guide"
|
||||
},
|
||||
"test": {
|
||||
"type": "object",
|
||||
"description": "Test requirements",
|
||||
"properties": {
|
||||
"unit": { "type": "array", "items": { "type": "string" } },
|
||||
"integration": { "type": "array", "items": { "type": "string" } },
|
||||
"commands": { "type": "array", "items": { "type": "string" } },
|
||||
"coverage_target": { "type": "number" }
|
||||
}
|
||||
},
|
||||
"regression": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Regression check points"
|
||||
},
|
||||
"acceptance": {
|
||||
"type": "object",
|
||||
"description": "Acceptance criteria & verification",
|
||||
"required": ["criteria", "verification"],
|
||||
"properties": {
|
||||
"criteria": { "type": "array", "items": { "type": "string" } },
|
||||
"verification": { "type": "array", "items": { "type": "string" } },
|
||||
"manual_checks": { "type": "array", "items": { "type": "string" } }
|
||||
}
|
||||
},
|
||||
"commit": {
|
||||
"type": "object",
|
||||
"description": "Commit specification",
|
||||
"properties": {
|
||||
"type": { "type": "string", "enum": ["feat", "fix", "refactor", "test", "docs", "chore"] },
|
||||
"scope": { "type": "string" },
|
||||
"message_template": { "type": "string" },
|
||||
"breaking": { "type": "boolean" }
|
||||
}
|
||||
},
|
||||
"depends_on": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"default": [],
|
||||
"description": "Task IDs this task depends on"
|
||||
},
|
||||
"estimated_minutes": {
|
||||
"type": "integer",
|
||||
"description": "Estimated time to complete"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"description": "Task status (optional, for tracking)"
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 5,
|
||||
"default": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"exploration_context": {
|
||||
"type": "object",
|
||||
"description": "ACE exploration results",
|
||||
"properties": {
|
||||
"project_structure": { "type": "string" },
|
||||
"relevant_files": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"patterns": { "type": "string" },
|
||||
"integration_points": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"analysis": {
|
||||
"type": "object",
|
||||
"description": "Solution risk assessment",
|
||||
"properties": {
|
||||
"risk": { "type": "string", "enum": ["low", "medium", "high"] },
|
||||
"impact": { "type": "string", "enum": ["low", "medium", "high"] },
|
||||
"complexity": { "type": "string", "enum": ["low", "medium", "high"] }
|
||||
}
|
||||
},
|
||||
"score": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"description": "Solution quality score (0.0-1.0)"
|
||||
},
|
||||
"is_bound": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Whether this solution is bound to the issue"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"bound_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "When this solution was bound to the issue"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
## MCP Tools Usage
|
||||
|
||||
### search_context (ACE) - Code Search (REQUIRED - HIGHEST PRIORITY)
|
||||
|
||||
**OVERRIDES**: All other search/discovery rules in other workflow files
|
||||
|
||||
**When**: ANY code discovery task, including:
|
||||
- Find code, understand codebase structure, locate implementations
|
||||
- Explore unknown locations
|
||||
- Verify file existence before reading
|
||||
- Pattern-based file discovery
|
||||
- Semantic code understanding
|
||||
|
||||
**Priority Rule**:
|
||||
1. **Always use mcp__ace-tool__search_context FIRST** for any code/file discovery
|
||||
2. Only use Built-in Grep for single-file exact line search (after location confirmed)
|
||||
3. Only use Built-in Read for known, confirmed file paths
|
||||
|
||||
**How**:
|
||||
```javascript
|
||||
// Natural language code search - best for understanding and exploration
|
||||
mcp__ace-tool__search_context({
|
||||
project_root_path: "/path/to/project",
|
||||
query: "authentication logic"
|
||||
})
|
||||
|
||||
// With keywords for better semantic matching
|
||||
mcp__ace-tool__search_context({
|
||||
project_root_path: "/path/to/project",
|
||||
query: "I want to find where the server handles user login. Keywords: auth, login, session"
|
||||
})
|
||||
```
|
||||
|
||||
**Good Query Examples**:
|
||||
- "Where is the function that handles user authentication?"
|
||||
- "What tests are there for the login functionality?"
|
||||
- "How is the database connected to the application?"
|
||||
- "I want to find where the server handles chunk merging. Keywords: upload chunk merge"
|
||||
- "Locate where the system refreshes cached data. Keywords: cache refresh, invalidation"
|
||||
|
||||
**Bad Query Examples** (use grep or file view instead):
|
||||
- "Find definition of constructor of class Foo" (use grep tool instead)
|
||||
- "Find all references to function bar" (use grep tool instead)
|
||||
- "Show me how Checkout class is used in services/payment.py" (use file view tool instead)
|
||||
|
||||
**Key Features**:
|
||||
- Real-time index of the codebase (always up-to-date)
|
||||
- Cross-language retrieval support
|
||||
- Semantic search with embeddings
|
||||
- No manual index initialization required
|
||||
|
||||
---
|
||||
|
||||
### read_file - Read File Contents
|
||||
|
||||
**When**: Read files found by search_context
|
||||
|
||||
**How**:
|
||||
```javascript
|
||||
read_file(path="/path/to/file.ts") // Single file
|
||||
read_file(path="/src/**/*.config.ts") // Pattern matching
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### edit_file - Modify Files
|
||||
|
||||
**When**: Built-in Edit tool fails or need advanced features
|
||||
|
||||
**How**:
|
||||
```javascript
|
||||
edit_file(path="/file.ts", old_string="...", new_string="...", mode="update")
|
||||
edit_file(path="/file.ts", line=10, content="...", mode="insert_after")
|
||||
```
|
||||
|
||||
**Modes**: `update` (replace text), `insert_after`, `insert_before`, `delete_line`
|
||||
|
||||
---
|
||||
|
||||
### write_file - Create/Overwrite Files
|
||||
|
||||
**When**: Create new files or completely replace content
|
||||
|
||||
**How**:
|
||||
```javascript
|
||||
write_file(path="/new-file.ts", content="...")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Exa - External Search
|
||||
|
||||
**When**: Find documentation/examples outside codebase
|
||||
|
||||
**How**:
|
||||
```javascript
|
||||
mcp__exa__search(query="React hooks 2025 documentation")
|
||||
mcp__exa__search(query="FastAPI auth example", numResults=10)
|
||||
mcp__exa__search(query="latest API docs", livecrawl="always")
|
||||
```
|
||||
|
||||
**Parameters**:
|
||||
- `query` (required): Search query string
|
||||
- `numResults` (optional): Number of results to return (default: 5)
|
||||
- `livecrawl` (optional): `"always"` or `"fallback"` for live crawling
|
||||
@@ -1,35 +1,27 @@
|
||||
## MCP Tools Usage
|
||||
## Context Acquisition (MCP Tools Priority)
|
||||
|
||||
### smart_search - Code Search (REQUIRED - HIGHEST PRIORITY)
|
||||
**For task context gathering and analysis, ALWAYS prefer MCP tools**:
|
||||
|
||||
**OVERRIDES**: All other search/discovery rules in other workflow files
|
||||
1. **mcp__ace-tool__search_context** - HIGHEST PRIORITY for code discovery
|
||||
- Semantic search with real-time codebase index
|
||||
- Use for: finding implementations, understanding architecture, locating patterns
|
||||
- Example: `mcp__ace-tool__search_context(project_root_path="/path", query="authentication logic")`
|
||||
|
||||
**When**: ANY code discovery task, including:
|
||||
- Find code, understand codebase structure, locate implementations
|
||||
- Explore unknown locations
|
||||
- Verify file existence before reading
|
||||
- Pattern-based file discovery
|
||||
2. **smart_search** - Fallback for structured search
|
||||
- Use `smart_search(query="...")` for keyword/regex search
|
||||
- Use `smart_search(action="find_files", pattern="*.ts")` for file discovery
|
||||
- Supports modes: `auto`, `hybrid`, `exact`, `ripgrep`
|
||||
|
||||
**Priority Rule**:
|
||||
1. **Always use smart_search FIRST** for any code/file discovery
|
||||
2. Only use Built-in Grep for single-file exact line search (after location confirmed)
|
||||
3. Only use Built-in Read for known, confirmed file paths
|
||||
3. **read_file** - Batch file reading
|
||||
- Read multiple files in parallel: `read_file(path="file1.ts")`, `read_file(path="file2.ts")`
|
||||
- Supports glob patterns: `read_file(path="src/**/*.config.ts")`
|
||||
|
||||
**Workflow** (search first, init if needed):
|
||||
```javascript
|
||||
// Step 1: Try search directly (works if index exists or uses ripgrep fallback)
|
||||
smart_search(query="authentication logic")
|
||||
|
||||
// Step 2: Only if search warns "No CodexLens index found", then init
|
||||
smart_search(action="init", path=".") // Creates FTS index only
|
||||
|
||||
// Note: For semantic/vector search, use "ccw view" dashboard to create vector index
|
||||
**Priority Order**:
|
||||
```
|
||||
ACE search_context (semantic) → smart_search (structured) → read_file (batch read) → shell commands (fallback)
|
||||
```
|
||||
|
||||
**Modes**: `auto` (intelligent routing), `hybrid` (semantic, needs vector index), `exact` (FTS), `ripgrep` (no index)
|
||||
|
||||
---
|
||||
|
||||
**NEVER** use shell commands (`cat`, `find`, `grep`) when MCP tools are available.
|
||||
### read_file - Read File Contents
|
||||
|
||||
**When**: Read files found by smart_search
|
||||
|
||||
@@ -76,18 +76,23 @@ chcp 65001 > $null
|
||||
|
||||
**For task context gathering and analysis, ALWAYS prefer MCP tools**:
|
||||
|
||||
1. **smart_search** - First choice for code discovery
|
||||
- Use `smart_search(query="...")` for semantic/keyword search
|
||||
1. **mcp__ace-tool__search_context** - HIGHEST PRIORITY for code discovery
|
||||
- Semantic search with real-time codebase index
|
||||
- Use for: finding implementations, understanding architecture, locating patterns
|
||||
- Example: `mcp__ace-tool__search_context(project_root_path="/path", query="authentication logic")`
|
||||
|
||||
2. **smart_search** - Fallback for structured search
|
||||
- Use `smart_search(query="...")` for keyword/regex search
|
||||
- Use `smart_search(action="find_files", pattern="*.ts")` for file discovery
|
||||
- Supports modes: `auto`, `hybrid`, `exact`, `ripgrep`
|
||||
|
||||
2. **read_file** - Batch file reading
|
||||
3. **read_file** - Batch file reading
|
||||
- Read multiple files in parallel: `read_file(path="file1.ts")`, `read_file(path="file2.ts")`
|
||||
- Supports glob patterns: `read_file(path="src/**/*.config.ts")`
|
||||
|
||||
**Priority Order**:
|
||||
```
|
||||
smart_search (discovery) → read_file (batch read) → shell commands (fallback)
|
||||
ACE search_context (semantic) → smart_search (structured) → read_file (batch read) → shell commands (fallback)
|
||||
```
|
||||
|
||||
**NEVER** use shell commands (`cat`, `find`, `grep`) when MCP tools are available.
|
||||
@@ -96,7 +101,7 @@ smart_search (discovery) → read_file (batch read) → shell commands (fallback
|
||||
|
||||
**Before**:
|
||||
- [ ] Understand PURPOSE and TASK clearly
|
||||
- [ ] Use smart_search to discover relevant files
|
||||
- [ ] Use ACE search_context first, fallback to smart_search for discovery
|
||||
- [ ] Use read_file to batch read context files, find 3+ patterns
|
||||
- [ ] Check RULES templates and constraints
|
||||
|
||||
|
||||
356
.codex/prompts/issue-execute.md
Normal file
356
.codex/prompts/issue-execute.md
Normal file
@@ -0,0 +1,356 @@
|
||||
---
|
||||
description: Execute all solutions from issue queue with git commit after each task
|
||||
argument-hint: ""
|
||||
---
|
||||
|
||||
# Issue Execute (Codex Version)
|
||||
|
||||
## Core Principle
|
||||
|
||||
**Serial Execution**: Execute solutions ONE BY ONE from the issue queue via `ccw issue next`. For each solution, complete all tasks sequentially (implement → test → commit per task). Continue autonomously until queue is empty.
|
||||
|
||||
## Execution Flow
|
||||
|
||||
```
|
||||
INIT: Fetch first solution via ccw issue next
|
||||
|
||||
WHILE solution exists:
|
||||
1. Receive solution JSON from ccw issue next
|
||||
2. Execute all tasks in solution.tasks sequentially:
|
||||
FOR each task:
|
||||
- IMPLEMENT: Follow task.implementation steps
|
||||
- TEST: Run task.test commands
|
||||
- VERIFY: Check task.acceptance criteria
|
||||
- COMMIT: Stage files, commit with task.commit.message_template
|
||||
3. Report completion via ccw issue done <item_id>
|
||||
4. Fetch next solution via ccw issue next
|
||||
|
||||
WHEN queue empty:
|
||||
Output final summary
|
||||
```
|
||||
|
||||
## Step 1: Fetch First Solution
|
||||
|
||||
Run this command to get your first solution:
|
||||
|
||||
```bash
|
||||
ccw issue next
|
||||
```
|
||||
|
||||
This returns JSON with the full solution definition:
|
||||
- `item_id`: Solution identifier in queue (e.g., "S-1")
|
||||
- `issue_id`: Parent issue ID (e.g., "ISS-20251227-001")
|
||||
- `solution_id`: Solution ID (e.g., "SOL-ISS-20251227-001-1")
|
||||
- `solution`: Full solution with all tasks
|
||||
- `execution_hints`: Timing and executor hints
|
||||
|
||||
If response contains `{ "status": "empty" }`, all solutions are complete - skip to final summary.
|
||||
|
||||
## Step 2: Parse Solution Response
|
||||
|
||||
Expected solution structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"item_id": "S-1",
|
||||
"issue_id": "ISS-20251227-001",
|
||||
"solution_id": "SOL-ISS-20251227-001-1",
|
||||
"status": "pending",
|
||||
"solution": {
|
||||
"id": "SOL-ISS-20251227-001-1",
|
||||
"description": "Description of solution approach",
|
||||
"tasks": [
|
||||
{
|
||||
"id": "T1",
|
||||
"title": "Task title",
|
||||
"scope": "src/module/",
|
||||
"action": "Create|Modify|Fix|Refactor|Add",
|
||||
"description": "What to do",
|
||||
"modification_points": [
|
||||
{ "file": "path/to/file.ts", "target": "function name", "change": "description" }
|
||||
],
|
||||
"implementation": [
|
||||
"Step 1: Do this",
|
||||
"Step 2: Do that"
|
||||
],
|
||||
"test": {
|
||||
"commands": ["npm test -- --filter=xxx"],
|
||||
"unit": ["Unit test requirement 1", "Unit test requirement 2"]
|
||||
},
|
||||
"regression": ["Verify existing tests still pass"],
|
||||
"acceptance": {
|
||||
"criteria": ["Criterion 1: Must pass", "Criterion 2: Must verify"],
|
||||
"verification": ["Run test command", "Manual verification step"]
|
||||
},
|
||||
"commit": {
|
||||
"type": "feat|fix|test|refactor",
|
||||
"scope": "module",
|
||||
"message_template": "feat(scope): description"
|
||||
},
|
||||
"depends_on": [],
|
||||
"estimated_minutes": 30,
|
||||
"priority": 1
|
||||
}
|
||||
],
|
||||
"exploration_context": {
|
||||
"relevant_files": ["path/to/reference.ts"],
|
||||
"patterns": "Follow existing pattern in xxx",
|
||||
"integration_points": "Used by other modules"
|
||||
},
|
||||
"analysis": {
|
||||
"risk": "low|medium|high",
|
||||
"impact": "low|medium|high",
|
||||
"complexity": "low|medium|high"
|
||||
},
|
||||
"score": 0.95,
|
||||
"is_bound": true
|
||||
},
|
||||
"execution_hints": {
|
||||
"executor": "codex",
|
||||
"estimated_minutes": 180
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 2.5: Initialize Todo Tracking
|
||||
|
||||
After parsing solution, create todo list to track each task:
|
||||
|
||||
```javascript
|
||||
// Create todos for all tasks in current solution
|
||||
TodoWrite({
|
||||
todos: solution.tasks.map(task => ({
|
||||
content: `${task.id}: ${task.title}`,
|
||||
activeForm: `Executing ${task.id}: ${task.title}`,
|
||||
status: "pending"
|
||||
}))
|
||||
})
|
||||
```
|
||||
|
||||
## Step 3: Execute Tasks Sequentially
|
||||
|
||||
Iterate through `solution.tasks` array and execute each task.
|
||||
|
||||
**Before starting each task**, mark it as in_progress:
|
||||
```javascript
|
||||
// Update current task status to in_progress
|
||||
TodoWrite({ todos: [...existingTodos.map(t =>
|
||||
t.content.startsWith(currentTask.id) ? {...t, status: "in_progress"} : t
|
||||
)] })
|
||||
```
|
||||
|
||||
**After completing each task** (commit done), mark it as completed:
|
||||
```javascript
|
||||
// Update completed task status
|
||||
TodoWrite({ todos: [...existingTodos.map(t =>
|
||||
t.content.startsWith(completedTask.id) ? {...t, status: "completed"} : t
|
||||
)] })
|
||||
```
|
||||
|
||||
### Phase A: IMPLEMENT
|
||||
|
||||
1. Read all `solution.exploration_context.relevant_files` to understand existing patterns
|
||||
2. Follow `task.implementation` steps in order
|
||||
3. Apply changes to `task.modification_points` files
|
||||
4. Follow `solution.exploration_context.patterns` for code style consistency
|
||||
5. Run `task.regression` checks if specified to ensure no breakage
|
||||
|
||||
**Output format:**
|
||||
```
|
||||
## Implementing: [task.title] (Task [N]/[Total])
|
||||
|
||||
**Scope**: [task.scope]
|
||||
**Action**: [task.action]
|
||||
|
||||
**Steps**:
|
||||
1. ✓ [implementation step 1]
|
||||
2. ✓ [implementation step 2]
|
||||
...
|
||||
|
||||
**Files Modified**:
|
||||
- path/to/file1.ts
|
||||
- path/to/file2.ts
|
||||
```
|
||||
|
||||
### Phase B: TEST
|
||||
|
||||
1. Run all commands in `task.test.commands`
|
||||
2. Verify unit tests pass (`task.test.unit`)
|
||||
3. Run integration tests if specified (`task.test.integration`)
|
||||
|
||||
**If tests fail**: Fix the code and re-run. Do NOT proceed until tests pass.
|
||||
|
||||
**Output format:**
|
||||
```
|
||||
## Testing: [task.title]
|
||||
|
||||
**Test Results**:
|
||||
- [x] Unit tests: PASSED
|
||||
- [x] Integration tests: PASSED (or N/A)
|
||||
```
|
||||
|
||||
### Phase C: VERIFY
|
||||
|
||||
Check all `task.acceptance.criteria` are met using `task.acceptance.verification` steps:
|
||||
|
||||
```
|
||||
## Verifying: [task.title]
|
||||
|
||||
**Acceptance Criteria**:
|
||||
- [x] Criterion 1: Verified
|
||||
- [x] Criterion 2: Verified
|
||||
...
|
||||
|
||||
**Verification Steps**:
|
||||
- [x] Run test command
|
||||
- [x] Manual verification step
|
||||
|
||||
All criteria met: YES
|
||||
```
|
||||
|
||||
**If any criterion fails**: Go back to IMPLEMENT phase and fix.
|
||||
|
||||
### Phase D: COMMIT
|
||||
|
||||
After all phases pass, commit the changes for this task:
|
||||
|
||||
```bash
|
||||
# Stage all modified files
|
||||
git add path/to/file1.ts path/to/file2.ts ...
|
||||
|
||||
# Commit with task message template
|
||||
git commit -m "$(cat <<'EOF'
|
||||
[task.commit.message_template]
|
||||
|
||||
Solution-ID: [solution_id]
|
||||
Issue-ID: [issue_id]
|
||||
Task-ID: [task.id]
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
**Output format:**
|
||||
```
|
||||
## Committed: [task.title]
|
||||
|
||||
**Commit**: [commit hash]
|
||||
**Message**: [commit message]
|
||||
**Files**: N files changed
|
||||
```
|
||||
|
||||
### Repeat for Next Task
|
||||
|
||||
Continue to next task in `solution.tasks` array until all tasks are complete.
|
||||
|
||||
## Step 4: Report Completion
|
||||
|
||||
After ALL tasks in the solution are complete, report to queue system:
|
||||
|
||||
```bash
|
||||
ccw issue done <item_id> --result '{
|
||||
"files_modified": ["path1", "path2"],
|
||||
"tests_passed": true,
|
||||
"acceptance_passed": true,
|
||||
"committed": true,
|
||||
"commits": [
|
||||
{ "task_id": "T1", "hash": "abc123" },
|
||||
{ "task_id": "T2", "hash": "def456" }
|
||||
],
|
||||
"summary": "[What was accomplished]"
|
||||
}'
|
||||
```
|
||||
|
||||
**If solution failed and cannot be fixed:**
|
||||
|
||||
```bash
|
||||
ccw issue done <item_id> --fail --reason "Task [task.id] failed: [details]"
|
||||
```
|
||||
|
||||
## Step 5: Continue to Next Solution
|
||||
|
||||
Clear current todo list and fetch next solution:
|
||||
|
||||
```javascript
|
||||
// Reset todos for next solution
|
||||
TodoWrite({ todos: [] })
|
||||
```
|
||||
|
||||
Then fetch next solution:
|
||||
|
||||
```bash
|
||||
ccw issue next
|
||||
```
|
||||
|
||||
**Output progress:**
|
||||
```
|
||||
✓ [N/M] Completed: [item_id] - [solution.approach]
|
||||
→ Fetching next solution...
|
||||
```
|
||||
|
||||
**DO NOT STOP.** Return to Step 2 and continue until queue is empty.
|
||||
|
||||
## Final Summary
|
||||
|
||||
When `ccw issue next` returns `{ "status": "empty" }`:
|
||||
|
||||
```markdown
|
||||
## Issue Queue Execution Complete
|
||||
|
||||
**Total Solutions Executed**: N
|
||||
**Total Tasks Executed**: M
|
||||
|
||||
**All Commits**:
|
||||
| # | Solution | Task | Commit |
|
||||
|---|----------|------|--------|
|
||||
| 1 | S-1 | T1 | abc123 |
|
||||
| 2 | S-1 | T2 | def456 |
|
||||
| 3 | S-2 | T1 | ghi789 |
|
||||
|
||||
**Files Modified**:
|
||||
- path/to/file1.ts
|
||||
- path/to/file2.ts
|
||||
|
||||
**Summary**:
|
||||
[Overall what was accomplished]
|
||||
```
|
||||
|
||||
## Execution Rules
|
||||
|
||||
1. **Never stop mid-queue** - Continue until queue is empty
|
||||
2. **One solution at a time** - Fully complete (all tasks + report) before moving on
|
||||
3. **Sequential within solution** - Complete each task (including commit) before next
|
||||
4. **Tests MUST pass** - Do not proceed to commit if tests fail
|
||||
5. **Commit after each task** - Each task gets its own commit
|
||||
6. **Self-verify** - All acceptance criteria must pass before commit
|
||||
7. **Report accurately** - Use `ccw issue done` after each solution
|
||||
8. **Handle failures gracefully** - If a solution fails, report via `ccw issue done --fail` and continue to next
|
||||
9. **Track with todos** - Use TodoWrite to track task progress within each solution
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Situation | Action |
|
||||
|-----------|--------|
|
||||
| `ccw issue next` returns empty | All done - output final summary |
|
||||
| Tests fail | Fix code, re-run tests |
|
||||
| Verification fails | Go back to implement phase |
|
||||
| Git commit fails | Check staging, retry commit |
|
||||
| `ccw issue done` fails | Log error, continue to next solution |
|
||||
| Unrecoverable error | Call `ccw issue done --fail`, continue to next |
|
||||
|
||||
## CLI Command Reference
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `ccw issue next` | Fetch next solution from queue |
|
||||
| `ccw issue done <id>` | Mark solution complete with result |
|
||||
| `ccw issue done <id> --fail` | Mark solution failed with reason |
|
||||
|
||||
## Start Execution
|
||||
|
||||
Begin by running:
|
||||
|
||||
```bash
|
||||
ccw issue next
|
||||
```
|
||||
|
||||
Then follow the solution lifecycle for each solution until queue is empty.
|
||||
103
.codex/prompts/issue-plan.md
Normal file
103
.codex/prompts/issue-plan.md
Normal file
@@ -0,0 +1,103 @@
|
||||
---
|
||||
description: Plan issue(s) into bound solutions (writes solutions JSONL via ccw issue bind)
|
||||
argument-hint: "<issue-id>[,<issue-id>,...] [--all-pending] [--batch-size 3]"
|
||||
---
|
||||
|
||||
# Issue Plan (Codex Version)
|
||||
|
||||
## Goal
|
||||
|
||||
Create executable solution(s) for issue(s) and bind the selected solution to each issue using `ccw issue bind`.
|
||||
|
||||
This workflow is **planning + registration** (no implementation): it explores the codebase just enough to produce a high-quality task breakdown that can be executed later (e.g., by `issue-execute.md`).
|
||||
|
||||
## Inputs
|
||||
|
||||
- **Explicit issues**: comma-separated IDs, e.g. `ISS-123,ISS-124`
|
||||
- **All pending**: `--all-pending` → plan all issues in `registered` status
|
||||
- **Batch size**: `--batch-size N` (default `3`) → max issues per batch
|
||||
|
||||
## Output Requirements
|
||||
|
||||
For each issue:
|
||||
- Register at least one solution and bind one solution to the issue (updates `.workflow/issues/issues.jsonl` and appends to `.workflow/issues/solutions/{issue-id}.jsonl`).
|
||||
- Ensure tasks conform to `.claude/workflows/cli-templates/schemas/solution-schema.json`.
|
||||
- Each task includes quantified `acceptance.criteria` and concrete `acceptance.verification`.
|
||||
|
||||
Return a final summary JSON:
|
||||
```json
|
||||
{
|
||||
"bound": [{ "issue_id": "...", "solution_id": "...", "task_count": 0 }],
|
||||
"pending_selection": [{ "issue_id": "...", "solutions": [{ "id": "...", "task_count": 0, "description": "..." }] }],
|
||||
"conflicts": [{ "file": "...", "issues": ["..."] }]
|
||||
}
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Resolve issue list
|
||||
|
||||
- If `--all-pending`:
|
||||
- Run `ccw issue list --status registered --json` and plan all returned issues.
|
||||
- Else:
|
||||
- Parse IDs from user input (split by `,`), and ensure each issue exists:
|
||||
- `ccw issue init <issue-id> --title "Issue <issue-id>"` (safe if already exists)
|
||||
|
||||
### Step 2: Load issue details
|
||||
|
||||
For each issue ID:
|
||||
- `ccw issue status <issue-id> --json`
|
||||
- Extract the issue title/context/labels and any discovery hints (affected files, snippets, etc. if present).
|
||||
|
||||
### Step 3: Minimal exploration (evidence-based)
|
||||
|
||||
- If issue context names specific files or symbols: open them first.
|
||||
- Otherwise:
|
||||
- Use `rg` to locate relevant code paths by keywords from the title/context.
|
||||
- Read 3+ similar patterns before proposing refactors or API changes.
|
||||
|
||||
### Step 4: Draft solutions and tasks (schema-driven)
|
||||
|
||||
Default to **one** solution per issue unless there are genuinely different approaches.
|
||||
|
||||
Task rules (from schema):
|
||||
- `id`: `T1`, `T2`, ...
|
||||
- `action`: one of `Create|Update|Implement|Refactor|Add|Delete|Configure|Test|Fix`
|
||||
- `implementation`: step-by-step, executable instructions
|
||||
- `test.commands`: include at least one command per task when feasible
|
||||
- `acceptance.criteria`: testable statements
|
||||
- `acceptance.verification`: concrete steps/commands mapping to criteria
|
||||
- Prefer small, independently testable tasks; encode dependencies in `depends_on`.
|
||||
|
||||
### Step 5: Register & bind solutions via CLI
|
||||
|
||||
**Create solution** (via CLI endpoint):
|
||||
```bash
|
||||
ccw issue solution <issue-id> --data '{"description":"...", "approach":"...", "tasks":[...]}'
|
||||
# Output: {"id":"SOL-{issue-id}-1", ...}
|
||||
```
|
||||
|
||||
**CLI Features:**
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| Auto-increment ID | `SOL-{issue-id}-{seq}` (e.g., `SOL-GH-123-1`) |
|
||||
| Multi-solution | Appends to existing JSONL, supports multiple per issue |
|
||||
| Trailing newline | Proper JSONL format, no corruption |
|
||||
|
||||
**Binding:**
|
||||
- **Single solution**: Auto-bind: `ccw issue bind <issue-id> <solution-id>`
|
||||
- **Multiple solutions**: Present alternatives in `pending_selection`, wait for user choice
|
||||
|
||||
### Step 6: Detect cross-issue file conflicts (best-effort)
|
||||
|
||||
Across the issues planned in this run:
|
||||
- Build a set of touched files from each solution’s `modification_points.file` (and/or task `scope` when explicit files are missing).
|
||||
- If the same file appears in multiple issues, add it to `conflicts` with all involved issue IDs.
|
||||
- Recommend a safe execution order (sequential) when conflicts exist.
|
||||
|
||||
## Done Criteria
|
||||
|
||||
- A bound solution exists for each issue unless explicitly deferred for user selection.
|
||||
- All tasks validate against the solution schema fields (especially acceptance criteria + verification).
|
||||
- The final summary JSON matches the required shape.
|
||||
|
||||
224
.codex/prompts/issue-queue.md
Normal file
224
.codex/prompts/issue-queue.md
Normal file
@@ -0,0 +1,224 @@
|
||||
---
|
||||
description: Form execution queue from bound solutions (orders solutions, detects conflicts, assigns groups)
|
||||
argument-hint: "[--issue <id>]"
|
||||
---
|
||||
|
||||
# Issue Queue (Codex Version)
|
||||
|
||||
## Goal
|
||||
|
||||
Create an ordered execution queue from all bound solutions. Analyze inter-solution file conflicts, calculate semantic priorities, and assign parallel/sequential execution groups.
|
||||
|
||||
This workflow is **ordering only** (no execution): it reads bound solutions, detects conflicts, and produces a queue file that `issue-execute.md` can consume.
|
||||
|
||||
## Inputs
|
||||
|
||||
- **All planned**: Default behavior → queue all issues with `planned` status and bound solutions
|
||||
- **Specific issue**: `--issue <id>` → queue only that issue's solution
|
||||
|
||||
## Output Requirements
|
||||
|
||||
**Generate Files (EXACTLY 2):**
|
||||
1. `.workflow/issues/queues/{queue-id}.json` - Full queue with solutions, conflicts, groups
|
||||
2. `.workflow/issues/queues/index.json` - Update with new queue entry
|
||||
|
||||
**Return Summary:**
|
||||
```json
|
||||
{
|
||||
"queue_id": "QUE-YYYYMMDD-HHMMSS",
|
||||
"total_solutions": 3,
|
||||
"total_tasks": 12,
|
||||
"execution_groups": [{ "id": "P1", "type": "parallel", "count": 2 }],
|
||||
"conflicts_resolved": 1,
|
||||
"issues_queued": ["ISS-xxx", "ISS-yyy"]
|
||||
}
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Generate Queue ID
|
||||
|
||||
Generate queue ID ONCE at start, reuse throughout:
|
||||
|
||||
```bash
|
||||
# Format: QUE-YYYYMMDD-HHMMSS (UTC)
|
||||
QUEUE_ID="QUE-$(date -u +%Y%m%d-%H%M%S)"
|
||||
```
|
||||
|
||||
### Step 2: Load Planned Issues
|
||||
|
||||
Get all issues with bound solutions:
|
||||
|
||||
```bash
|
||||
ccw issue list --status planned --json
|
||||
```
|
||||
|
||||
For each issue in the result:
|
||||
- Extract `id`, `bound_solution_id`, `priority`
|
||||
- Read solution from `.workflow/issues/solutions/{issue-id}.jsonl`
|
||||
- Find the bound solution by matching `solution.id === bound_solution_id`
|
||||
- Collect `files_touched` from all tasks' `modification_points.file`
|
||||
|
||||
Build solution list:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"issue_id": "ISS-xxx",
|
||||
"solution_id": "SOL-xxx",
|
||||
"task_count": 3,
|
||||
"files_touched": ["src/auth.ts", "src/utils.ts"],
|
||||
"priority": "medium"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Step 3: Detect File Conflicts
|
||||
|
||||
Build a file → solutions mapping:
|
||||
|
||||
```javascript
|
||||
fileModifications = {
|
||||
"src/auth.ts": ["SOL-ISS-001-1", "SOL-ISS-003-1"],
|
||||
"src/api.ts": ["SOL-ISS-002-1"]
|
||||
}
|
||||
```
|
||||
|
||||
Conflicts exist when a file has multiple solutions. For each conflict:
|
||||
- Record the file and involved solutions
|
||||
- Will be resolved in Step 4
|
||||
|
||||
### Step 4: Resolve Conflicts & Build DAG
|
||||
|
||||
**Resolution Rules (in priority order):**
|
||||
1. Higher issue priority first: `critical > high > medium > low`
|
||||
2. Foundation solutions first: fewer dependencies
|
||||
3. More tasks = higher priority: larger impact
|
||||
|
||||
For each file conflict:
|
||||
- Apply resolution rules to determine order
|
||||
- Add dependency edge: later solution `depends_on` earlier solution
|
||||
- Record rationale
|
||||
|
||||
**Semantic Priority Formula:**
|
||||
```
|
||||
Base: critical=0.9, high=0.7, medium=0.5, low=0.3
|
||||
Boost: task_count>=5 → +0.1, task_count>=3 → +0.05
|
||||
Final: clamp(base + boost, 0.0, 1.0)
|
||||
```
|
||||
|
||||
### Step 5: Assign Execution Groups
|
||||
|
||||
- **Parallel (P1, P2, ...)**: Solutions with NO file overlaps between them
|
||||
- **Sequential (S1, S2, ...)**: Solutions that share files must run in order
|
||||
|
||||
Group assignment:
|
||||
1. Start with all solutions in potential parallel group
|
||||
2. For each file conflict, move later solution to sequential group
|
||||
3. Assign group IDs: P1 for first parallel batch, S2 for first sequential, etc.
|
||||
|
||||
### Step 6: Generate Queue Files
|
||||
|
||||
**Queue file structure** (`.workflow/issues/queues/{QUEUE_ID}.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "QUE-20251228-120000",
|
||||
"status": "active",
|
||||
"issue_ids": ["ISS-001", "ISS-002"],
|
||||
"solutions": [
|
||||
{
|
||||
"item_id": "S-1",
|
||||
"issue_id": "ISS-001",
|
||||
"solution_id": "SOL-ISS-001-1",
|
||||
"status": "pending",
|
||||
"execution_order": 1,
|
||||
"execution_group": "P1",
|
||||
"depends_on": [],
|
||||
"semantic_priority": 0.8,
|
||||
"files_touched": ["src/auth.ts"],
|
||||
"task_count": 3
|
||||
}
|
||||
],
|
||||
"conflicts": [
|
||||
{
|
||||
"type": "file_conflict",
|
||||
"file": "src/auth.ts",
|
||||
"solutions": ["S-1", "S-3"],
|
||||
"resolution": "sequential",
|
||||
"resolution_order": ["S-1", "S-3"],
|
||||
"rationale": "S-1 creates auth module, S-3 extends it"
|
||||
}
|
||||
],
|
||||
"execution_groups": [
|
||||
{ "id": "P1", "type": "parallel", "solutions": ["S-1", "S-2"], "solution_count": 2 },
|
||||
{ "id": "S2", "type": "sequential", "solutions": ["S-3"], "solution_count": 1 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Update index** (`.workflow/issues/queues/index.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"active_queue_id": "QUE-20251228-120000",
|
||||
"queues": [
|
||||
{
|
||||
"id": "QUE-20251228-120000",
|
||||
"status": "active",
|
||||
"issue_ids": ["ISS-001", "ISS-002"],
|
||||
"total_solutions": 3,
|
||||
"completed_solutions": 0,
|
||||
"created_at": "2025-12-28T12:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Step 7: Update Issue Statuses
|
||||
|
||||
For each queued issue, update status to `queued`:
|
||||
|
||||
```bash
|
||||
ccw issue update <issue-id> --status queued
|
||||
```
|
||||
|
||||
## Queue Item ID Format
|
||||
|
||||
- Solution items: `S-1`, `S-2`, `S-3`, ...
|
||||
- Sequential numbering starting from 1
|
||||
|
||||
## Done Criteria
|
||||
|
||||
- [ ] Exactly 2 files generated: queue JSON + index update
|
||||
- [ ] Queue has valid DAG (no circular dependencies)
|
||||
- [ ] All file conflicts resolved with rationale
|
||||
- [ ] Semantic priority calculated for each solution (0.0-1.0)
|
||||
- [ ] Execution groups assigned (P* for parallel, S* for sequential)
|
||||
- [ ] Issue statuses updated to `queued`
|
||||
- [ ] Summary JSON returned with correct shape
|
||||
|
||||
## Validation Rules
|
||||
|
||||
1. **No cycles**: If resolution creates a cycle, abort and report
|
||||
2. **Parallel safety**: Solutions in same P* group must have NO file overlaps
|
||||
3. **Sequential order**: Solutions in S* group must be in correct dependency order
|
||||
4. **Single queue ID**: Use the same queue ID throughout (generated in Step 1)
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Situation | Action |
|
||||
|-----------|--------|
|
||||
| No planned issues | Return empty queue summary |
|
||||
| Circular dependency detected | Abort, report cycle details |
|
||||
| Missing solution file | Skip issue, log warning |
|
||||
| Index file missing | Create new index |
|
||||
|
||||
## Start Execution
|
||||
|
||||
Begin by listing planned issues:
|
||||
|
||||
```bash
|
||||
ccw issue list --status planned --json
|
||||
```
|
||||
|
||||
Then follow the workflow to generate the queue.
|
||||
41
.github/workflows/visual-tests.yml
vendored
Normal file
41
.github/workflows/visual-tests.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
name: Visual Regression Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
visual-tests:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Install Playwright browsers
|
||||
run: npx playwright install --with-deps chromium
|
||||
|
||||
- name: Run visual tests
|
||||
run: npm run test:visual
|
||||
|
||||
- name: Upload visual artifacts on failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: visual-regression-artifacts
|
||||
path: |
|
||||
ccw/tests/visual/snapshots/current
|
||||
ccw/tests/visual/snapshots/diff
|
||||
retention-days: 7
|
||||
46
CHANGELOG.md
46
CHANGELOG.md
@@ -5,6 +5,52 @@ All notable changes to Claude Code Workflow (CCW) will be documented in this fil
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [6.3.11] - 2025-12-28
|
||||
|
||||
### 🔧 Issue System Enhancements | Issue系统增强
|
||||
|
||||
#### CLI Improvements | CLI改进
|
||||
- **Added**: `ccw issue update <id> --status <status>` command for pure field updates
|
||||
- **Added**: Support for `--priority`, `--title`, `--description` in update command
|
||||
- **Added**: Auto-timestamp setting based on status (planned_at, queued_at, completed_at)
|
||||
|
||||
#### Issue Plan Command | Issue Plan命令
|
||||
- **Changed**: Agent execution from sequential to parallel (max 10 concurrent)
|
||||
- **Added**: Multi-solution user selection prompt with clear notification
|
||||
- **Added**: Explicit binding check (`solutions.length === 1`) before auto-bind
|
||||
|
||||
#### Issue Queue Command | Issue Queue命令
|
||||
- **Fixed**: Queue ID generation moved from agent to command (avoid duplicate IDs)
|
||||
- **Fixed**: Strict output file control (exactly 2 files per execution)
|
||||
- **Added**: Clear documentation for `update` vs `done`/`queue add` usage
|
||||
|
||||
#### Discovery System | Discovery系统
|
||||
- **Enhanced**: Discovery progress reading with new schema support
|
||||
- **Enhanced**: Discovery index reading and issue exporting
|
||||
|
||||
## [6.3.9] - 2025-12-27
|
||||
|
||||
### 🔧 Issue System Consistency | Issue系统一致性修复
|
||||
|
||||
#### Schema Unification | Schema统一
|
||||
- **Upgraded**: `solution-schema.json` to Rich Plan model with full lifecycle fields
|
||||
- **Added**: `test`, `regression`, `commit`, `lifecycle_status` objects to task schema
|
||||
- **Changed**: `acceptance` from string[] to object `{criteria[], verification[]}`
|
||||
- **Added**: `analysis` and `score` fields for multi-solution evaluation
|
||||
- **Removed**: Redundant `issue-task-jsonl-schema.json` and `solutions-jsonl-schema.json`
|
||||
- **Fixed**: `queue-schema.json` field naming (`queue_id` → `item_id`)
|
||||
|
||||
#### Agent Updates | Agent更新
|
||||
- **Added**: Multi-solution generation support based on complexity
|
||||
- **Added**: Search tool fallback chain (ACE → smart_search → Grep → rg → Glob)
|
||||
- **Added**: `lifecycle_requirements` propagation from issue to tasks
|
||||
- **Added**: Priority mapping formula (1-5 → 0.0-1.0 semantic priority)
|
||||
- **Fixed**: Task decomposition to match Rich Plan schema
|
||||
|
||||
#### Type Safety | 类型安全
|
||||
- **Added**: `QueueConflict` and `ExecutionGroup` interfaces to `issue.ts`
|
||||
- **Fixed**: `conflicts` array typing (from `any[]` to `QueueConflict[]`)
|
||||
|
||||
## [6.2.0] - 2025-12-21
|
||||
|
||||
### 🎯 Native CodexLens & Dashboard Revolution | 原生CodexLens与Dashboard革新
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
[](https://www.npmjs.com/package/claude-code-workflow)
|
||||
[](LICENSE)
|
||||
[]()
|
||||
[](https://github.com/catlog22/Claude-Code-Workflow/actions/workflows/visual-tests.yml)
|
||||
|
||||
**Languages:** [English](README.md) | [中文](README_CN.md)
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { cliCommand } from './commands/cli.js';
|
||||
import { memoryCommand } from './commands/memory.js';
|
||||
import { coreMemoryCommand } from './commands/core-memory.js';
|
||||
import { hookCommand } from './commands/hook.js';
|
||||
import { issueCommand } from './commands/issue.js';
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
@@ -260,5 +261,33 @@ export function run(argv: string[]): void {
|
||||
.option('--type <type>', 'Context type: session-start, context')
|
||||
.action((subcommand, args, options) => hookCommand(subcommand, args, options));
|
||||
|
||||
// Issue command - Issue lifecycle management with JSONL task tracking
|
||||
program
|
||||
.command('issue [subcommand] [args...]')
|
||||
.description('Issue lifecycle management with JSONL task tracking')
|
||||
.option('--title <title>', 'Task title')
|
||||
.option('--type <type>', 'Task type: feature, bug, refactor, test, chore, docs')
|
||||
.option('--status <status>', 'Task status')
|
||||
.option('--phase <phase>', 'Execution phase')
|
||||
.option('--description <desc>', 'Task description')
|
||||
.option('--depends-on <ids>', 'Comma-separated dependency task IDs')
|
||||
.option('--delivery-criteria <items>', 'Pipe-separated delivery criteria')
|
||||
.option('--pause-criteria <items>', 'Pipe-separated pause criteria')
|
||||
.option('--executor <type>', 'Executor: agent, codex, gemini, auto')
|
||||
.option('--priority <n>', 'Task priority (1-5)')
|
||||
.option('--format <fmt>', 'Output format: json, markdown')
|
||||
.option('--json', 'Output as JSON')
|
||||
.option('--brief', 'Brief JSON output (minimal fields)')
|
||||
.option('--force', 'Force operation')
|
||||
// New options for solution/queue management
|
||||
.option('--solution <path>', 'Solution JSON file path')
|
||||
.option('--solution-id <id>', 'Solution ID')
|
||||
.option('--data <json>', 'JSON data for create/solution')
|
||||
.option('--result <json>', 'Execution result JSON')
|
||||
.option('--reason <text>', 'Failure reason')
|
||||
.option('--fail', 'Mark task as failed')
|
||||
.option('--from-queue [queue-id]', 'Sync issue statuses from queue (default: active queue)')
|
||||
.action((subcommand, args, options) => issueCommand(subcommand, args, options));
|
||||
|
||||
program.parse(argv);
|
||||
}
|
||||
|
||||
2029
ccw/src/commands/issue.ts
Normal file
2029
ccw/src/commands/issue.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,7 @@ const MODULE_FILES = [
|
||||
'dashboard-js/components/tabs-other.js',
|
||||
'dashboard-js/components/carousel.js',
|
||||
'dashboard-js/components/notifications.js',
|
||||
'dashboard-js/components/cli-stream-viewer.js',
|
||||
'dashboard-js/components/global-notifications.js',
|
||||
'dashboard-js/components/cli-status.js',
|
||||
'dashboard-js/components/cli-history.js',
|
||||
|
||||
@@ -47,7 +47,8 @@ const MODULE_CSS_FILES = [
|
||||
'28-mcp-manager.css',
|
||||
'29-help.css',
|
||||
'30-core-memory.css',
|
||||
'31-api-settings.css'
|
||||
'31-api-settings.css',
|
||||
'34-discovery.css'
|
||||
];
|
||||
|
||||
const MODULE_FILES = [
|
||||
@@ -97,6 +98,8 @@ const MODULE_FILES = [
|
||||
'views/rules-manager.js',
|
||||
'views/claude-manager.js',
|
||||
'views/api-settings.js',
|
||||
'views/issue-manager.js',
|
||||
'views/issue-discovery.js',
|
||||
'views/help.js',
|
||||
'main.js'
|
||||
];
|
||||
|
||||
@@ -110,6 +110,34 @@ interface SessionReviewData {
|
||||
findings: Array<Finding & { dimension: string }>;
|
||||
}
|
||||
|
||||
interface ProjectGuidelines {
|
||||
conventions: {
|
||||
coding_style: string[];
|
||||
naming_patterns: string[];
|
||||
file_structure: string[];
|
||||
documentation: string[];
|
||||
};
|
||||
constraints: {
|
||||
architecture: string[];
|
||||
tech_stack: string[];
|
||||
performance: string[];
|
||||
security: string[];
|
||||
};
|
||||
quality_rules: Array<{ rule: string; scope: string; enforced_by?: string }>;
|
||||
learnings: Array<{
|
||||
date: string;
|
||||
session_id?: string;
|
||||
insight: string;
|
||||
context?: string;
|
||||
category?: string;
|
||||
}>;
|
||||
_metadata?: {
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
version: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ProjectOverview {
|
||||
projectName: string;
|
||||
description: string;
|
||||
@@ -144,6 +172,7 @@ interface ProjectOverview {
|
||||
analysis_timestamp: string | null;
|
||||
analysis_mode: string;
|
||||
};
|
||||
guidelines: ProjectGuidelines | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,11 +185,13 @@ export async function aggregateData(sessions: ScanSessionsResult, workflowDir: s
|
||||
// Initialize cache manager
|
||||
const cache = createDashboardCache(workflowDir);
|
||||
|
||||
// Prepare paths to watch for changes
|
||||
// Prepare paths to watch for changes (includes both new dual files and legacy)
|
||||
const watchPaths = [
|
||||
join(workflowDir, 'active'),
|
||||
join(workflowDir, 'archives'),
|
||||
join(workflowDir, 'project.json'),
|
||||
join(workflowDir, 'project-tech.json'),
|
||||
join(workflowDir, 'project-guidelines.json'),
|
||||
join(workflowDir, 'project.json'), // Legacy support
|
||||
...sessions.active.map(s => s.path),
|
||||
...sessions.archived.map(s => s.path)
|
||||
];
|
||||
@@ -516,12 +547,19 @@ function sortTaskIds(a: string, b: string): number {
|
||||
}
|
||||
|
||||
/**
|
||||
* Load project overview from project.json
|
||||
* Load project overview from project-tech.json and project-guidelines.json
|
||||
* Supports dual file structure with backward compatibility for legacy project.json
|
||||
* @param workflowDir - Path to .workflow directory
|
||||
* @returns Project overview data or null if not found
|
||||
*/
|
||||
function loadProjectOverview(workflowDir: string): ProjectOverview | null {
|
||||
const projectFile = join(workflowDir, 'project.json');
|
||||
const techFile = join(workflowDir, 'project-tech.json');
|
||||
const guidelinesFile = join(workflowDir, 'project-guidelines.json');
|
||||
const legacyFile = join(workflowDir, 'project.json');
|
||||
|
||||
// Check for new dual file structure first, fallback to legacy
|
||||
const useLegacy = !existsSync(techFile) && existsSync(legacyFile);
|
||||
const projectFile = useLegacy ? legacyFile : techFile;
|
||||
|
||||
if (!existsSync(projectFile)) {
|
||||
console.log(`Project file not found at: ${projectFile}`);
|
||||
@@ -532,15 +570,59 @@ function loadProjectOverview(workflowDir: string): ProjectOverview | null {
|
||||
const fileContent = readFileSync(projectFile, 'utf8');
|
||||
const projectData = JSON.parse(fileContent) as Record<string, unknown>;
|
||||
|
||||
console.log(`Successfully loaded project overview: ${projectData.project_name || 'Unknown'}`);
|
||||
console.log(`Successfully loaded project overview: ${projectData.project_name || 'Unknown'} (${useLegacy ? 'legacy' : 'tech'})`);
|
||||
|
||||
// Parse tech data (compatible with both legacy and new structure)
|
||||
const overview = projectData.overview as Record<string, unknown> | undefined;
|
||||
const technologyStack = overview?.technology_stack as Record<string, unknown[]> | undefined;
|
||||
const architecture = overview?.architecture as Record<string, unknown> | undefined;
|
||||
const developmentIndex = projectData.development_index as Record<string, unknown[]> | undefined;
|
||||
const statistics = projectData.statistics as Record<string, unknown> | undefined;
|
||||
const technologyAnalysis = projectData.technology_analysis as Record<string, unknown> | undefined;
|
||||
const developmentStatus = projectData.development_status as Record<string, unknown> | undefined;
|
||||
|
||||
// Support both old and new schema field names
|
||||
const technologyStack = (overview?.technology_stack || technologyAnalysis?.technology_stack) as Record<string, unknown[]> | undefined;
|
||||
const architecture = (overview?.architecture || technologyAnalysis?.architecture) as Record<string, unknown> | undefined;
|
||||
const developmentIndex = (projectData.development_index || developmentStatus?.development_index) as Record<string, unknown[]> | undefined;
|
||||
const statistics = (projectData.statistics || developmentStatus?.statistics) as Record<string, unknown> | undefined;
|
||||
const metadata = projectData._metadata as Record<string, unknown> | undefined;
|
||||
|
||||
// Load guidelines from separate file if exists
|
||||
let guidelines: ProjectGuidelines | null = null;
|
||||
if (existsSync(guidelinesFile)) {
|
||||
try {
|
||||
const guidelinesContent = readFileSync(guidelinesFile, 'utf8');
|
||||
const guidelinesData = JSON.parse(guidelinesContent) as Record<string, unknown>;
|
||||
|
||||
const conventions = guidelinesData.conventions as Record<string, string[]> | undefined;
|
||||
const constraints = guidelinesData.constraints as Record<string, string[]> | undefined;
|
||||
|
||||
guidelines = {
|
||||
conventions: {
|
||||
coding_style: conventions?.coding_style || [],
|
||||
naming_patterns: conventions?.naming_patterns || [],
|
||||
file_structure: conventions?.file_structure || [],
|
||||
documentation: conventions?.documentation || []
|
||||
},
|
||||
constraints: {
|
||||
architecture: constraints?.architecture || [],
|
||||
tech_stack: constraints?.tech_stack || [],
|
||||
performance: constraints?.performance || [],
|
||||
security: constraints?.security || []
|
||||
},
|
||||
quality_rules: (guidelinesData.quality_rules as Array<{ rule: string; scope: string; enforced_by?: string }>) || [],
|
||||
learnings: (guidelinesData.learnings as Array<{
|
||||
date: string;
|
||||
session_id?: string;
|
||||
insight: string;
|
||||
context?: string;
|
||||
category?: string;
|
||||
}>) || [],
|
||||
_metadata: guidelinesData._metadata as ProjectGuidelines['_metadata'] | undefined
|
||||
};
|
||||
console.log(`Successfully loaded project guidelines`);
|
||||
} catch (guidelinesErr) {
|
||||
console.error(`Failed to parse project-guidelines.json:`, (guidelinesErr as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
projectName: (projectData.project_name as string) || 'Unknown',
|
||||
description: (overview?.description as string) || '',
|
||||
@@ -574,10 +656,11 @@ function loadProjectOverview(workflowDir: string): ProjectOverview | null {
|
||||
initialized_by: (metadata?.initialized_by as string) || 'unknown',
|
||||
analysis_timestamp: (metadata?.analysis_timestamp as string) || null,
|
||||
analysis_mode: (metadata?.analysis_mode as string) || 'unknown'
|
||||
}
|
||||
},
|
||||
guidelines
|
||||
};
|
||||
} catch (err) {
|
||||
console.error(`Failed to parse project.json at ${projectFile}:`, (err as Error).message);
|
||||
console.error(`Failed to parse project file at ${projectFile}:`, (err as Error).message);
|
||||
console.error('Error stack:', (err as Error).stack);
|
||||
return null;
|
||||
}
|
||||
|
||||
607
ccw/src/core/routes/discovery-routes.ts
Normal file
607
ccw/src/core/routes/discovery-routes.ts
Normal file
@@ -0,0 +1,607 @@
|
||||
// @ts-nocheck
|
||||
/**
|
||||
* Discovery Routes Module
|
||||
*
|
||||
* Storage Structure:
|
||||
* .workflow/issues/discoveries/
|
||||
* ├── index.json # Discovery session index
|
||||
* └── {discovery-id}/
|
||||
* ├── discovery-state.json # State machine
|
||||
* ├── discovery-progress.json # Real-time progress
|
||||
* ├── perspectives/ # Per-perspective results
|
||||
* │ ├── bug.json
|
||||
* │ └── ...
|
||||
* ├── external-research.json # Exa research results
|
||||
* ├── discovery-issues.jsonl # Generated candidate issues
|
||||
* └── reports/
|
||||
*
|
||||
* API Endpoints:
|
||||
* - GET /api/discoveries - List all discovery sessions
|
||||
* - GET /api/discoveries/:id - Get discovery session detail
|
||||
* - GET /api/discoveries/:id/findings - Get all findings
|
||||
* - GET /api/discoveries/:id/progress - Get real-time progress
|
||||
* - POST /api/discoveries/:id/export - Export findings as issues
|
||||
* - PATCH /api/discoveries/:id/findings/:fid - Update finding status
|
||||
* - DELETE /api/discoveries/:id - Delete discovery session
|
||||
*/
|
||||
import type { IncomingMessage, ServerResponse } from 'http';
|
||||
import { readFileSync, existsSync, writeFileSync, mkdirSync, readdirSync, rmSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
export interface RouteContext {
|
||||
pathname: string;
|
||||
url: URL;
|
||||
req: IncomingMessage;
|
||||
res: ServerResponse;
|
||||
initialPath: string;
|
||||
handlePostRequest: (req: IncomingMessage, res: ServerResponse, handler: (body: unknown) => Promise<any>) => void;
|
||||
broadcastToClients: (data: unknown) => void;
|
||||
}
|
||||
|
||||
// ========== Helper Functions ==========
|
||||
|
||||
function getDiscoveriesDir(projectPath: string): string {
|
||||
return join(projectPath, '.workflow', 'issues', 'discoveries');
|
||||
}
|
||||
|
||||
function readDiscoveryIndex(discoveriesDir: string): { discoveries: any[]; total: number } {
|
||||
const indexPath = join(discoveriesDir, 'index.json');
|
||||
|
||||
// Try to read index.json first
|
||||
if (existsSync(indexPath)) {
|
||||
try {
|
||||
return JSON.parse(readFileSync(indexPath, 'utf8'));
|
||||
} catch {
|
||||
// Fall through to scan
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: scan directory for discovery folders
|
||||
if (!existsSync(discoveriesDir)) {
|
||||
return { discoveries: [], total: 0 };
|
||||
}
|
||||
|
||||
try {
|
||||
const entries = readdirSync(discoveriesDir, { withFileTypes: true });
|
||||
const discoveries: any[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory() && entry.name.startsWith('DSC-')) {
|
||||
const statePath = join(discoveriesDir, entry.name, 'discovery-state.json');
|
||||
if (existsSync(statePath)) {
|
||||
try {
|
||||
const state = JSON.parse(readFileSync(statePath, 'utf8'));
|
||||
discoveries.push({
|
||||
discovery_id: entry.name,
|
||||
target_pattern: state.target_pattern,
|
||||
perspectives: state.metadata?.perspectives || [],
|
||||
created_at: state.metadata?.created_at,
|
||||
completed_at: state.completed_at
|
||||
});
|
||||
} catch {
|
||||
// Skip invalid entries
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by creation time descending
|
||||
discoveries.sort((a, b) => {
|
||||
const timeA = new Date(a.created_at || 0).getTime();
|
||||
const timeB = new Date(b.created_at || 0).getTime();
|
||||
return timeB - timeA;
|
||||
});
|
||||
|
||||
return { discoveries, total: discoveries.length };
|
||||
} catch {
|
||||
return { discoveries: [], total: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
function writeDiscoveryIndex(discoveriesDir: string, index: any) {
|
||||
if (!existsSync(discoveriesDir)) {
|
||||
mkdirSync(discoveriesDir, { recursive: true });
|
||||
}
|
||||
writeFileSync(join(discoveriesDir, 'index.json'), JSON.stringify(index, null, 2));
|
||||
}
|
||||
|
||||
function readDiscoveryState(discoveriesDir: string, discoveryId: string): any | null {
|
||||
const statePath = join(discoveriesDir, discoveryId, 'discovery-state.json');
|
||||
if (!existsSync(statePath)) return null;
|
||||
try {
|
||||
return JSON.parse(readFileSync(statePath, 'utf8'));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function readDiscoveryProgress(discoveriesDir: string, discoveryId: string): any | null {
|
||||
// Try merged state first (new schema)
|
||||
const statePath = join(discoveriesDir, discoveryId, 'discovery-state.json');
|
||||
if (existsSync(statePath)) {
|
||||
try {
|
||||
const state = JSON.parse(readFileSync(statePath, 'utf8'));
|
||||
// New merged schema: perspectives array + results object
|
||||
if (state.perspectives && Array.isArray(state.perspectives)) {
|
||||
const completed = state.perspectives.filter((p: any) => p.status === 'completed').length;
|
||||
const total = state.perspectives.length;
|
||||
return {
|
||||
discovery_id: discoveryId,
|
||||
phase: state.phase,
|
||||
last_update: state.updated_at || state.created_at,
|
||||
progress: {
|
||||
perspective_analysis: {
|
||||
total,
|
||||
completed,
|
||||
in_progress: state.perspectives.filter((p: any) => p.status === 'in_progress').length,
|
||||
percent_complete: total > 0 ? Math.round((completed / total) * 100) : 0
|
||||
},
|
||||
external_research: state.external_research || { enabled: false, completed: false },
|
||||
aggregation: { completed: state.phase === 'aggregation' || state.phase === 'complete' },
|
||||
issue_generation: { completed: state.phase === 'complete', issues_count: state.results?.issues_generated || 0 }
|
||||
},
|
||||
agent_status: state.perspectives
|
||||
};
|
||||
}
|
||||
// Old schema: metadata.perspectives (backward compat)
|
||||
if (state.metadata?.perspectives) {
|
||||
return {
|
||||
discovery_id: discoveryId,
|
||||
phase: state.phase,
|
||||
progress: { perspective_analysis: { total: state.metadata.perspectives.length, completed: state.perspectives_completed?.length || 0 } }
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Fall through
|
||||
}
|
||||
}
|
||||
// Fallback: try legacy progress file
|
||||
const progressPath = join(discoveriesDir, discoveryId, 'discovery-progress.json');
|
||||
if (existsSync(progressPath)) {
|
||||
try { return JSON.parse(readFileSync(progressPath, 'utf8')); } catch { return null; }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function readPerspectiveFindings(discoveriesDir: string, discoveryId: string): any[] {
|
||||
const perspectivesDir = join(discoveriesDir, discoveryId, 'perspectives');
|
||||
if (!existsSync(perspectivesDir)) return [];
|
||||
|
||||
const allFindings: any[] = [];
|
||||
const files = readdirSync(perspectivesDir).filter(f => f.endsWith('.json'));
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
const content = JSON.parse(readFileSync(join(perspectivesDir, file), 'utf8'));
|
||||
const perspective = file.replace('.json', '');
|
||||
|
||||
if (content.findings && Array.isArray(content.findings)) {
|
||||
allFindings.push({
|
||||
perspective,
|
||||
summary: content.summary || {},
|
||||
findings: content.findings.map((f: any) => ({
|
||||
...f,
|
||||
perspective: f.perspective || perspective
|
||||
}))
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Skip invalid files
|
||||
}
|
||||
}
|
||||
|
||||
return allFindings;
|
||||
}
|
||||
|
||||
function readDiscoveryIssues(discoveriesDir: string, discoveryId: string): any[] {
|
||||
const issuesPath = join(discoveriesDir, discoveryId, 'discovery-issues.jsonl');
|
||||
if (!existsSync(issuesPath)) return [];
|
||||
try {
|
||||
const content = readFileSync(issuesPath, 'utf8');
|
||||
return content.split('\n').filter(line => line.trim()).map(line => JSON.parse(line));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function writeDiscoveryIssues(discoveriesDir: string, discoveryId: string, issues: any[]) {
|
||||
const issuesPath = join(discoveriesDir, discoveryId, 'discovery-issues.jsonl');
|
||||
writeFileSync(issuesPath, issues.map(i => JSON.stringify(i)).join('\n'));
|
||||
}
|
||||
|
||||
function flattenFindings(perspectiveResults: any[]): any[] {
|
||||
const allFindings: any[] = [];
|
||||
for (const result of perspectiveResults) {
|
||||
if (result.findings) {
|
||||
allFindings.push(...result.findings);
|
||||
}
|
||||
}
|
||||
return allFindings;
|
||||
}
|
||||
|
||||
function appendToIssuesJsonl(projectPath: string, issues: any[]): { added: number; skipped: number; skippedIds: string[] } {
|
||||
const issuesDir = join(projectPath, '.workflow', 'issues');
|
||||
const issuesPath = join(issuesDir, 'issues.jsonl');
|
||||
|
||||
if (!existsSync(issuesDir)) {
|
||||
mkdirSync(issuesDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Read existing issues
|
||||
let existingIssues: any[] = [];
|
||||
if (existsSync(issuesPath)) {
|
||||
try {
|
||||
const content = readFileSync(issuesPath, 'utf8');
|
||||
existingIssues = content.split('\n').filter(line => line.trim()).map(line => JSON.parse(line));
|
||||
} catch {
|
||||
// Start fresh
|
||||
}
|
||||
}
|
||||
|
||||
// Build set of existing IDs and source_finding combinations for deduplication
|
||||
const existingIds = new Set(existingIssues.map(i => i.id));
|
||||
const existingSourceFindings = new Set(
|
||||
existingIssues
|
||||
.filter(i => i.source === 'discovery' && i.source_finding_id)
|
||||
.map(i => `${i.source_discovery_id}:${i.source_finding_id}`)
|
||||
);
|
||||
|
||||
// Convert and filter duplicates
|
||||
const skippedIds: string[] = [];
|
||||
const newIssues: any[] = [];
|
||||
|
||||
for (const di of issues) {
|
||||
// Check for duplicate by ID
|
||||
if (existingIds.has(di.id)) {
|
||||
skippedIds.push(di.id);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for duplicate by source_discovery_id + source_finding_id
|
||||
const sourceKey = `${di.source_discovery_id}:${di.source_finding_id}`;
|
||||
if (di.source_finding_id && existingSourceFindings.has(sourceKey)) {
|
||||
skippedIds.push(di.id);
|
||||
continue;
|
||||
}
|
||||
|
||||
newIssues.push({
|
||||
id: di.id,
|
||||
title: di.title,
|
||||
status: 'registered',
|
||||
priority: di.priority || 3,
|
||||
context: di.context || di.description || '',
|
||||
source: 'discovery',
|
||||
source_discovery_id: di.source_discovery_id,
|
||||
source_finding_id: di.source_finding_id,
|
||||
perspective: di.perspective,
|
||||
file: di.file,
|
||||
line: di.line,
|
||||
labels: di.labels || [],
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
if (newIssues.length > 0) {
|
||||
const allIssues = [...existingIssues, ...newIssues];
|
||||
writeFileSync(issuesPath, allIssues.map(i => JSON.stringify(i)).join('\n'));
|
||||
}
|
||||
|
||||
return { added: newIssues.length, skipped: skippedIds.length, skippedIds };
|
||||
}
|
||||
|
||||
// ========== Route Handler ==========
|
||||
|
||||
export async function handleDiscoveryRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
const { pathname, url, req, res, initialPath, handlePostRequest } = ctx;
|
||||
const projectPath = url.searchParams.get('path') || initialPath;
|
||||
const discoveriesDir = getDiscoveriesDir(projectPath);
|
||||
|
||||
// GET /api/discoveries - List all discovery sessions
|
||||
if (pathname === '/api/discoveries' && req.method === 'GET') {
|
||||
const index = readDiscoveryIndex(discoveriesDir);
|
||||
|
||||
// Enrich with state info
|
||||
const enrichedDiscoveries = index.discoveries.map((d: any) => {
|
||||
const state = readDiscoveryState(discoveriesDir, d.discovery_id);
|
||||
const progress = readDiscoveryProgress(discoveriesDir, d.discovery_id);
|
||||
return {
|
||||
...d,
|
||||
phase: state?.phase || 'unknown',
|
||||
total_findings: state?.total_findings || 0,
|
||||
issues_generated: state?.issues_generated || 0,
|
||||
priority_distribution: state?.priority_distribution || {},
|
||||
progress: progress?.progress || null
|
||||
};
|
||||
});
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
discoveries: enrichedDiscoveries,
|
||||
total: enrichedDiscoveries.length,
|
||||
_metadata: { updated_at: new Date().toISOString() }
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
// GET /api/discoveries/:id - Get discovery detail
|
||||
const detailMatch = pathname.match(/^\/api\/discoveries\/([^/]+)$/);
|
||||
if (detailMatch && req.method === 'GET') {
|
||||
const discoveryId = detailMatch[1];
|
||||
const state = readDiscoveryState(discoveriesDir, discoveryId);
|
||||
|
||||
if (!state) {
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: `Discovery ${discoveryId} not found` }));
|
||||
return true;
|
||||
}
|
||||
|
||||
const progress = readDiscoveryProgress(discoveriesDir, discoveryId);
|
||||
const perspectiveResults = readPerspectiveFindings(discoveriesDir, discoveryId);
|
||||
const discoveryIssues = readDiscoveryIssues(discoveriesDir, discoveryId);
|
||||
|
||||
// Read external research if exists
|
||||
let externalResearch = null;
|
||||
const externalPath = join(discoveriesDir, discoveryId, 'external-research.json');
|
||||
if (existsSync(externalPath)) {
|
||||
try {
|
||||
externalResearch = JSON.parse(readFileSync(externalPath, 'utf8'));
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
...state,
|
||||
progress: progress?.progress || null,
|
||||
perspectives: perspectiveResults,
|
||||
external_research: externalResearch,
|
||||
discovery_issues: discoveryIssues
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
// GET /api/discoveries/:id/findings - Get all findings
|
||||
const findingsMatch = pathname.match(/^\/api\/discoveries\/([^/]+)\/findings$/);
|
||||
if (findingsMatch && req.method === 'GET') {
|
||||
const discoveryId = findingsMatch[1];
|
||||
|
||||
if (!existsSync(join(discoveriesDir, discoveryId))) {
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: `Discovery ${discoveryId} not found` }));
|
||||
return true;
|
||||
}
|
||||
|
||||
const perspectiveResults = readPerspectiveFindings(discoveriesDir, discoveryId);
|
||||
const allFindings = flattenFindings(perspectiveResults);
|
||||
|
||||
// Support filtering
|
||||
const perspectiveFilter = url.searchParams.get('perspective');
|
||||
const priorityFilter = url.searchParams.get('priority');
|
||||
|
||||
let filtered = allFindings;
|
||||
if (perspectiveFilter) {
|
||||
filtered = filtered.filter(f => f.perspective === perspectiveFilter);
|
||||
}
|
||||
if (priorityFilter) {
|
||||
filtered = filtered.filter(f => f.priority === priorityFilter);
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
findings: filtered,
|
||||
total: filtered.length,
|
||||
perspectives: [...new Set(allFindings.map(f => f.perspective))],
|
||||
_metadata: { discovery_id: discoveryId }
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
// GET /api/discoveries/:id/progress - Get real-time progress
|
||||
const progressMatch = pathname.match(/^\/api\/discoveries\/([^/]+)\/progress$/);
|
||||
if (progressMatch && req.method === 'GET') {
|
||||
const discoveryId = progressMatch[1];
|
||||
const progress = readDiscoveryProgress(discoveriesDir, discoveryId);
|
||||
|
||||
if (!progress) {
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: `Progress for ${discoveryId} not found` }));
|
||||
return true;
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(progress));
|
||||
return true;
|
||||
}
|
||||
|
||||
// POST /api/discoveries/:id/export - Export findings as issues
|
||||
const exportMatch = pathname.match(/^\/api\/discoveries\/([^/]+)\/export$/);
|
||||
if (exportMatch && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body: any) => {
|
||||
const discoveryId = exportMatch[1];
|
||||
const { finding_ids, export_all } = body as { finding_ids?: string[]; export_all?: boolean };
|
||||
|
||||
if (!existsSync(join(discoveriesDir, discoveryId))) {
|
||||
return { error: `Discovery ${discoveryId} not found` };
|
||||
}
|
||||
|
||||
const perspectiveResults = readPerspectiveFindings(discoveriesDir, discoveryId);
|
||||
const allFindings = flattenFindings(perspectiveResults);
|
||||
|
||||
let toExport: any[];
|
||||
if (export_all) {
|
||||
toExport = allFindings;
|
||||
} else if (finding_ids && finding_ids.length > 0) {
|
||||
toExport = allFindings.filter(f => finding_ids.includes(f.id));
|
||||
} else {
|
||||
return { error: 'Either finding_ids or export_all required' };
|
||||
}
|
||||
|
||||
if (toExport.length === 0) {
|
||||
return { error: 'No findings to export' };
|
||||
}
|
||||
|
||||
// Convert findings to issue format
|
||||
const issuesToExport = toExport.map((f, idx) => {
|
||||
const suggestedIssue = f.suggested_issue || {};
|
||||
return {
|
||||
id: `ISS-${Date.now()}-${idx}`,
|
||||
title: suggestedIssue.title || f.title,
|
||||
priority: suggestedIssue.priority || 3,
|
||||
context: f.description || '',
|
||||
source: 'discovery',
|
||||
source_discovery_id: discoveryId,
|
||||
source_finding_id: f.id, // Track original finding ID for deduplication
|
||||
perspective: f.perspective,
|
||||
file: f.file,
|
||||
line: f.line,
|
||||
labels: suggestedIssue.labels || [f.perspective]
|
||||
};
|
||||
});
|
||||
|
||||
// Append to main issues.jsonl (with deduplication)
|
||||
const result = appendToIssuesJsonl(projectPath, issuesToExport);
|
||||
|
||||
// Mark exported findings in perspective files
|
||||
if (result.added > 0) {
|
||||
const exportedFindingIds = new Set(
|
||||
issuesToExport
|
||||
.filter((_, idx) => !result.skippedIds.includes(issuesToExport[idx].id))
|
||||
.map(i => i.source_finding_id)
|
||||
);
|
||||
|
||||
// Update each perspective file to mark findings as exported
|
||||
const perspectivesDir = join(discoveriesDir, discoveryId, 'perspectives');
|
||||
if (existsSync(perspectivesDir)) {
|
||||
const files = readdirSync(perspectivesDir).filter(f => f.endsWith('.json'));
|
||||
for (const file of files) {
|
||||
const filePath = join(perspectivesDir, file);
|
||||
try {
|
||||
const content = JSON.parse(readFileSync(filePath, 'utf8'));
|
||||
if (content.findings) {
|
||||
let modified = false;
|
||||
for (const finding of content.findings) {
|
||||
if (exportedFindingIds.has(finding.id) && !finding.exported) {
|
||||
finding.exported = true;
|
||||
finding.exported_at = new Date().toISOString();
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
if (modified) {
|
||||
writeFileSync(filePath, JSON.stringify(content, null, 2));
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Skip invalid files
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update discovery state
|
||||
const state = readDiscoveryState(discoveriesDir, discoveryId);
|
||||
if (state) {
|
||||
state.issues_generated = (state.issues_generated || 0) + result.added;
|
||||
writeFileSync(
|
||||
join(discoveriesDir, discoveryId, 'discovery-state.json'),
|
||||
JSON.stringify(state, null, 2)
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
exported_count: result.added,
|
||||
skipped_count: result.skipped,
|
||||
skipped_ids: result.skippedIds,
|
||||
message: result.skipped > 0
|
||||
? `Exported ${result.added} issues, skipped ${result.skipped} duplicates`
|
||||
: `Exported ${result.added} issues`
|
||||
};
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// PATCH /api/discoveries/:id/findings/:fid - Update finding status
|
||||
const updateFindingMatch = pathname.match(/^\/api\/discoveries\/([^/]+)\/findings\/([^/]+)$/);
|
||||
if (updateFindingMatch && req.method === 'PATCH') {
|
||||
handlePostRequest(req, res, async (body: any) => {
|
||||
const [, discoveryId, findingId] = updateFindingMatch;
|
||||
const { status, dismissed } = body as { status?: string; dismissed?: boolean };
|
||||
|
||||
const perspectivesDir = join(discoveriesDir, discoveryId, 'perspectives');
|
||||
if (!existsSync(perspectivesDir)) {
|
||||
return { error: `Discovery ${discoveryId} not found` };
|
||||
}
|
||||
|
||||
// Find and update the finding
|
||||
const files = readdirSync(perspectivesDir).filter(f => f.endsWith('.json'));
|
||||
let updated = false;
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = join(perspectivesDir, file);
|
||||
try {
|
||||
const content = JSON.parse(readFileSync(filePath, 'utf8'));
|
||||
if (content.findings) {
|
||||
const findingIndex = content.findings.findIndex((f: any) => f.id === findingId);
|
||||
if (findingIndex !== -1) {
|
||||
if (status !== undefined) {
|
||||
content.findings[findingIndex].status = status;
|
||||
}
|
||||
if (dismissed !== undefined) {
|
||||
content.findings[findingIndex].dismissed = dismissed;
|
||||
}
|
||||
content.findings[findingIndex].updated_at = new Date().toISOString();
|
||||
writeFileSync(filePath, JSON.stringify(content, null, 2));
|
||||
updated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Skip invalid files
|
||||
}
|
||||
}
|
||||
|
||||
if (!updated) {
|
||||
return { error: `Finding ${findingId} not found` };
|
||||
}
|
||||
|
||||
return { success: true, finding_id: findingId };
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// DELETE /api/discoveries/:id - Delete discovery session
|
||||
const deleteMatch = pathname.match(/^\/api\/discoveries\/([^/]+)$/);
|
||||
if (deleteMatch && req.method === 'DELETE') {
|
||||
const discoveryId = deleteMatch[1];
|
||||
const discoveryPath = join(discoveriesDir, discoveryId);
|
||||
|
||||
if (!existsSync(discoveryPath)) {
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: `Discovery ${discoveryId} not found` }));
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
// Remove directory
|
||||
rmSync(discoveryPath, { recursive: true, force: true });
|
||||
|
||||
// Update index
|
||||
const index = readDiscoveryIndex(discoveriesDir);
|
||||
index.discoveries = index.discoveries.filter((d: any) => d.discovery_id !== discoveryId);
|
||||
index.total = index.discoveries.length;
|
||||
writeDiscoveryIndex(discoveriesDir, index);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true, deleted: discoveryId }));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Failed to delete discovery' }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Not handled
|
||||
return false;
|
||||
}
|
||||
717
ccw/src/core/routes/issue-routes.ts
Normal file
717
ccw/src/core/routes/issue-routes.ts
Normal file
@@ -0,0 +1,717 @@
|
||||
// @ts-nocheck
|
||||
/**
|
||||
* Issue Routes Module (Optimized - Flat JSONL Storage)
|
||||
*
|
||||
* Storage Structure:
|
||||
* .workflow/issues/
|
||||
* ├── issues.jsonl # All issues (one per line)
|
||||
* ├── queues/ # Queue history directory
|
||||
* │ ├── index.json # Queue index (active + history)
|
||||
* │ └── {queue-id}.json # Individual queue files
|
||||
* └── solutions/
|
||||
* ├── {issue-id}.jsonl # Solutions for issue (one per line)
|
||||
* └── ...
|
||||
*
|
||||
* API Endpoints (8 total):
|
||||
* - GET /api/issues - List all issues
|
||||
* - POST /api/issues - Create new issue
|
||||
* - GET /api/issues/:id - Get issue detail
|
||||
* - PATCH /api/issues/:id - Update issue (includes binding logic)
|
||||
* - DELETE /api/issues/:id - Delete issue
|
||||
* - POST /api/issues/:id/solutions - Add solution
|
||||
* - PATCH /api/issues/:id/tasks/:taskId - Update task
|
||||
* - GET /api/queue - Get execution queue
|
||||
* - POST /api/queue/reorder - Reorder queue items
|
||||
*/
|
||||
import type { IncomingMessage, ServerResponse } from 'http';
|
||||
import { readFileSync, existsSync, writeFileSync, mkdirSync, unlinkSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
export interface RouteContext {
|
||||
pathname: string;
|
||||
url: URL;
|
||||
req: IncomingMessage;
|
||||
res: ServerResponse;
|
||||
initialPath: string;
|
||||
handlePostRequest: (req: IncomingMessage, res: ServerResponse, handler: (body: unknown) => Promise<any>) => void;
|
||||
broadcastToClients: (data: unknown) => void;
|
||||
}
|
||||
|
||||
// ========== JSONL Helper Functions ==========
|
||||
|
||||
function readIssuesJsonl(issuesDir: string): any[] {
|
||||
const issuesPath = join(issuesDir, 'issues.jsonl');
|
||||
if (!existsSync(issuesPath)) return [];
|
||||
try {
|
||||
const content = readFileSync(issuesPath, 'utf8');
|
||||
return content.split('\n').filter(line => line.trim()).map(line => JSON.parse(line));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function writeIssuesJsonl(issuesDir: string, issues: any[]) {
|
||||
if (!existsSync(issuesDir)) mkdirSync(issuesDir, { recursive: true });
|
||||
const issuesPath = join(issuesDir, 'issues.jsonl');
|
||||
writeFileSync(issuesPath, issues.map(i => JSON.stringify(i)).join('\n'));
|
||||
}
|
||||
|
||||
function readSolutionsJsonl(issuesDir: string, issueId: string): any[] {
|
||||
const solutionsPath = join(issuesDir, 'solutions', `${issueId}.jsonl`);
|
||||
if (!existsSync(solutionsPath)) return [];
|
||||
try {
|
||||
const content = readFileSync(solutionsPath, 'utf8');
|
||||
return content.split('\n').filter(line => line.trim()).map(line => JSON.parse(line));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function readIssueHistoryJsonl(issuesDir: string): any[] {
|
||||
const historyPath = join(issuesDir, 'issue-history.jsonl');
|
||||
if (!existsSync(historyPath)) return [];
|
||||
try {
|
||||
const content = readFileSync(historyPath, 'utf8');
|
||||
return content.split('\n').filter(line => line.trim()).map(line => JSON.parse(line));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function writeSolutionsJsonl(issuesDir: string, issueId: string, solutions: any[]) {
|
||||
const solutionsDir = join(issuesDir, 'solutions');
|
||||
if (!existsSync(solutionsDir)) mkdirSync(solutionsDir, { recursive: true });
|
||||
writeFileSync(join(solutionsDir, `${issueId}.jsonl`), solutions.map(s => JSON.stringify(s)).join('\n'));
|
||||
}
|
||||
|
||||
function readQueue(issuesDir: string) {
|
||||
// Try new multi-queue structure first
|
||||
const queuesDir = join(issuesDir, 'queues');
|
||||
const indexPath = join(queuesDir, 'index.json');
|
||||
|
||||
if (existsSync(indexPath)) {
|
||||
try {
|
||||
const index = JSON.parse(readFileSync(indexPath, 'utf8'));
|
||||
const activeQueueId = index.active_queue_id;
|
||||
|
||||
if (activeQueueId) {
|
||||
const queueFilePath = join(queuesDir, `${activeQueueId}.json`);
|
||||
if (existsSync(queueFilePath)) {
|
||||
return JSON.parse(readFileSync(queueFilePath, 'utf8'));
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Fall through to legacy check
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to legacy queue.json
|
||||
const legacyQueuePath = join(issuesDir, 'queue.json');
|
||||
if (existsSync(legacyQueuePath)) {
|
||||
try {
|
||||
return JSON.parse(readFileSync(legacyQueuePath, 'utf8'));
|
||||
} catch {
|
||||
// Return empty queue
|
||||
}
|
||||
}
|
||||
|
||||
return { tasks: [], conflicts: [], execution_groups: [], _metadata: { version: '1.0', total_tasks: 0 } };
|
||||
}
|
||||
|
||||
function writeQueue(issuesDir: string, queue: any) {
|
||||
if (!existsSync(issuesDir)) mkdirSync(issuesDir, { recursive: true });
|
||||
|
||||
// Support both solution-based and task-based queues
|
||||
const items = queue.solutions || queue.tasks || [];
|
||||
const isSolutionBased = Array.isArray(queue.solutions) && queue.solutions.length > 0;
|
||||
|
||||
queue._metadata = {
|
||||
...queue._metadata,
|
||||
updated_at: new Date().toISOString(),
|
||||
...(isSolutionBased
|
||||
? { total_solutions: items.length }
|
||||
: { total_tasks: items.length })
|
||||
};
|
||||
|
||||
// Check if using new multi-queue structure
|
||||
const queuesDir = join(issuesDir, 'queues');
|
||||
const indexPath = join(queuesDir, 'index.json');
|
||||
|
||||
if (existsSync(indexPath) && queue.id) {
|
||||
// Write to new structure
|
||||
const queueFilePath = join(queuesDir, `${queue.id}.json`);
|
||||
writeFileSync(queueFilePath, JSON.stringify(queue, null, 2));
|
||||
|
||||
// Update index metadata
|
||||
try {
|
||||
const index = JSON.parse(readFileSync(indexPath, 'utf8'));
|
||||
const queueEntry = index.queues?.find((q: any) => q.id === queue.id);
|
||||
if (queueEntry) {
|
||||
if (isSolutionBased) {
|
||||
queueEntry.total_solutions = items.length;
|
||||
queueEntry.completed_solutions = items.filter((i: any) => i.status === 'completed').length;
|
||||
} else {
|
||||
queueEntry.total_tasks = items.length;
|
||||
queueEntry.completed_tasks = items.filter((i: any) => i.status === 'completed').length;
|
||||
}
|
||||
writeFileSync(indexPath, JSON.stringify(index, null, 2));
|
||||
}
|
||||
} catch {
|
||||
// Ignore index update errors
|
||||
}
|
||||
} else {
|
||||
// Fallback to legacy queue.json
|
||||
writeFileSync(join(issuesDir, 'queue.json'), JSON.stringify(queue, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
function getIssueDetail(issuesDir: string, issueId: string) {
|
||||
const issues = readIssuesJsonl(issuesDir);
|
||||
const issue = issues.find(i => i.id === issueId);
|
||||
if (!issue) return null;
|
||||
|
||||
const solutions = readSolutionsJsonl(issuesDir, issueId);
|
||||
let tasks: any[] = [];
|
||||
if (issue.bound_solution_id) {
|
||||
const boundSol = solutions.find(s => s.id === issue.bound_solution_id);
|
||||
if (boundSol?.tasks) tasks = boundSol.tasks;
|
||||
}
|
||||
return { ...issue, solutions, tasks };
|
||||
}
|
||||
|
||||
function enrichIssues(issues: any[], issuesDir: string) {
|
||||
return issues.map(issue => {
|
||||
const solutions = readSolutionsJsonl(issuesDir, issue.id);
|
||||
let taskCount = 0;
|
||||
|
||||
// Get task count from bound solution
|
||||
if (issue.bound_solution_id) {
|
||||
const boundSol = solutions.find(s => s.id === issue.bound_solution_id);
|
||||
if (boundSol?.tasks) {
|
||||
taskCount = boundSol.tasks.length;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...issue,
|
||||
solution_count: solutions.length,
|
||||
task_count: taskCount
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get queue items (supports both solution-based and task-based queues)
|
||||
*/
|
||||
function getQueueItems(queue: any): any[] {
|
||||
return queue.solutions || queue.tasks || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if queue is solution-based
|
||||
*/
|
||||
function isSolutionBasedQueue(queue: any): boolean {
|
||||
return Array.isArray(queue.solutions) && queue.solutions.length > 0;
|
||||
}
|
||||
|
||||
function groupQueueByExecutionGroup(queue: any) {
|
||||
const groups: { [key: string]: any[] } = {};
|
||||
const items = getQueueItems(queue);
|
||||
const isSolutionBased = isSolutionBasedQueue(queue);
|
||||
|
||||
for (const item of items) {
|
||||
const groupId = item.execution_group || 'ungrouped';
|
||||
if (!groups[groupId]) groups[groupId] = [];
|
||||
groups[groupId].push(item);
|
||||
}
|
||||
for (const groupId of Object.keys(groups)) {
|
||||
groups[groupId].sort((a, b) => (a.execution_order || 0) - (b.execution_order || 0));
|
||||
}
|
||||
const executionGroups = Object.entries(groups).map(([id, groupItems]) => ({
|
||||
id,
|
||||
type: id.startsWith('P') ? 'parallel' : id.startsWith('S') ? 'sequential' : 'unknown',
|
||||
// Use appropriate count field based on queue type
|
||||
...(isSolutionBased
|
||||
? { solution_count: groupItems.length, solutions: groupItems.map(i => i.item_id) }
|
||||
: { task_count: groupItems.length, tasks: groupItems.map(i => i.item_id) })
|
||||
})).sort((a, b) => {
|
||||
const aFirst = groups[a.id]?.[0]?.execution_order || 0;
|
||||
const bFirst = groups[b.id]?.[0]?.execution_order || 0;
|
||||
return aFirst - bFirst;
|
||||
});
|
||||
return { ...queue, execution_groups: executionGroups, grouped_items: groups };
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind solution to issue with proper side effects
|
||||
*/
|
||||
function bindSolutionToIssue(issuesDir: string, issueId: string, solutionId: string, issues: any[], issueIndex: number) {
|
||||
const solutions = readSolutionsJsonl(issuesDir, issueId);
|
||||
const solIndex = solutions.findIndex(s => s.id === solutionId);
|
||||
|
||||
if (solIndex === -1) return { error: `Solution ${solutionId} not found` };
|
||||
|
||||
// Unbind all, bind new
|
||||
solutions.forEach(s => { s.is_bound = false; });
|
||||
solutions[solIndex].is_bound = true;
|
||||
solutions[solIndex].bound_at = new Date().toISOString();
|
||||
writeSolutionsJsonl(issuesDir, issueId, solutions);
|
||||
|
||||
// Update issue
|
||||
issues[issueIndex].bound_solution_id = solutionId;
|
||||
issues[issueIndex].status = 'planned';
|
||||
issues[issueIndex].planned_at = new Date().toISOString();
|
||||
|
||||
return { success: true, bound: solutionId };
|
||||
}
|
||||
|
||||
// ========== Route Handler ==========
|
||||
|
||||
export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
const { pathname, url, req, res, initialPath, handlePostRequest } = ctx;
|
||||
const projectPath = url.searchParams.get('path') || initialPath;
|
||||
const issuesDir = join(projectPath, '.workflow', 'issues');
|
||||
|
||||
// ===== Queue Routes (top-level /api/queue) =====
|
||||
|
||||
// GET /api/queue - Get execution queue
|
||||
if (pathname === '/api/queue' && req.method === 'GET') {
|
||||
const queue = groupQueueByExecutionGroup(readQueue(issuesDir));
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(queue));
|
||||
return true;
|
||||
}
|
||||
|
||||
// GET /api/queue/history - Get queue history (all queues from index)
|
||||
if (pathname === '/api/queue/history' && req.method === 'GET') {
|
||||
const queuesDir = join(issuesDir, 'queues');
|
||||
const indexPath = join(queuesDir, 'index.json');
|
||||
|
||||
if (!existsSync(indexPath)) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ queues: [], active_queue_id: null }));
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const index = JSON.parse(readFileSync(indexPath, 'utf8'));
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(index));
|
||||
} catch {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ queues: [], active_queue_id: null }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// GET /api/queue/:id - Get specific queue by ID
|
||||
const queueDetailMatch = pathname.match(/^\/api\/queue\/([^/]+)$/);
|
||||
if (queueDetailMatch && req.method === 'GET' && queueDetailMatch[1] !== 'history' && queueDetailMatch[1] !== 'reorder') {
|
||||
const queueId = queueDetailMatch[1];
|
||||
const queuesDir = join(issuesDir, 'queues');
|
||||
const queueFilePath = join(queuesDir, `${queueId}.json`);
|
||||
|
||||
if (!existsSync(queueFilePath)) {
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: `Queue ${queueId} not found` }));
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const queue = JSON.parse(readFileSync(queueFilePath, 'utf8'));
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(groupQueueByExecutionGroup(queue)));
|
||||
} catch {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Failed to read queue' }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// POST /api/queue/switch - Switch active queue
|
||||
if (pathname === '/api/queue/switch' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body: any) => {
|
||||
const { queueId } = body;
|
||||
if (!queueId) return { error: 'queueId required' };
|
||||
|
||||
const queuesDir = join(issuesDir, 'queues');
|
||||
const indexPath = join(queuesDir, 'index.json');
|
||||
const queueFilePath = join(queuesDir, `${queueId}.json`);
|
||||
|
||||
if (!existsSync(queueFilePath)) {
|
||||
return { error: `Queue ${queueId} not found` };
|
||||
}
|
||||
|
||||
try {
|
||||
const index = existsSync(indexPath)
|
||||
? JSON.parse(readFileSync(indexPath, 'utf8'))
|
||||
: { active_queue_id: null, queues: [] };
|
||||
|
||||
index.active_queue_id = queueId;
|
||||
writeFileSync(indexPath, JSON.stringify(index, null, 2));
|
||||
|
||||
return { success: true, active_queue_id: queueId };
|
||||
} catch (err) {
|
||||
return { error: 'Failed to switch queue' };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// POST /api/queue/reorder - Reorder queue items (supports both solutions and tasks)
|
||||
if (pathname === '/api/queue/reorder' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body: any) => {
|
||||
const { groupId, newOrder } = body;
|
||||
if (!groupId || !Array.isArray(newOrder)) {
|
||||
return { error: 'groupId and newOrder (array) required' };
|
||||
}
|
||||
|
||||
const queue = readQueue(issuesDir);
|
||||
const items = getQueueItems(queue);
|
||||
const isSolutionBased = isSolutionBasedQueue(queue);
|
||||
|
||||
const groupItems = items.filter((item: any) => item.execution_group === groupId);
|
||||
const otherItems = items.filter((item: any) => item.execution_group !== groupId);
|
||||
|
||||
if (groupItems.length === 0) return { error: `No items in group ${groupId}` };
|
||||
|
||||
const groupItemIds = new Set(groupItems.map((i: any) => i.item_id));
|
||||
if (groupItemIds.size !== new Set(newOrder).size) {
|
||||
return { error: 'newOrder must contain all group items' };
|
||||
}
|
||||
for (const id of newOrder) {
|
||||
if (!groupItemIds.has(id)) return { error: `Invalid item_id: ${id}` };
|
||||
}
|
||||
|
||||
const itemMap = new Map(groupItems.map((i: any) => [i.item_id, i]));
|
||||
const reorderedItems = newOrder.map((qid: string, idx: number) => ({ ...itemMap.get(qid), _idx: idx }));
|
||||
const newQueueItems = [...otherItems, ...reorderedItems].sort((a, b) => {
|
||||
const aGroup = parseInt(a.execution_group?.match(/\d+/)?.[0] || '999');
|
||||
const bGroup = parseInt(b.execution_group?.match(/\d+/)?.[0] || '999');
|
||||
if (aGroup !== bGroup) return aGroup - bGroup;
|
||||
if (a.execution_group === b.execution_group) {
|
||||
return (a._idx ?? a.execution_order ?? 999) - (b._idx ?? b.execution_order ?? 999);
|
||||
}
|
||||
return (a.execution_order || 0) - (b.execution_order || 0);
|
||||
});
|
||||
|
||||
newQueueItems.forEach((item, idx) => { item.execution_order = idx + 1; delete item._idx; });
|
||||
|
||||
// Write back to appropriate array based on queue type
|
||||
if (isSolutionBased) {
|
||||
queue.solutions = newQueueItems;
|
||||
} else {
|
||||
queue.tasks = newQueueItems;
|
||||
}
|
||||
writeQueue(issuesDir, queue);
|
||||
|
||||
return { success: true, groupId, reordered: newOrder.length };
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// Legacy: GET /api/issues/queue (backward compat)
|
||||
if (pathname === '/api/issues/queue' && req.method === 'GET') {
|
||||
const queue = groupQueueByExecutionGroup(readQueue(issuesDir));
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(queue));
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===== Issue Routes =====
|
||||
|
||||
// GET /api/issues - List all issues
|
||||
if (pathname === '/api/issues' && req.method === 'GET') {
|
||||
const issues = enrichIssues(readIssuesJsonl(issuesDir), issuesDir);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
issues,
|
||||
_metadata: { version: '2.0', storage: 'jsonl', total_issues: issues.length, last_updated: new Date().toISOString() }
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
// GET /api/issues/history - List completed issues from history
|
||||
if (pathname === '/api/issues/history' && req.method === 'GET') {
|
||||
const history = readIssueHistoryJsonl(issuesDir);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
issues: history,
|
||||
_metadata: { version: '1.0', storage: 'jsonl', total_issues: history.length, last_updated: new Date().toISOString() }
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
// POST /api/issues - Create issue
|
||||
if (pathname === '/api/issues' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body: any) => {
|
||||
if (!body.id || !body.title) return { error: 'id and title required' };
|
||||
|
||||
const issues = readIssuesJsonl(issuesDir);
|
||||
if (issues.find(i => i.id === body.id)) return { error: `Issue ${body.id} exists` };
|
||||
|
||||
const newIssue = {
|
||||
id: body.id,
|
||||
title: body.title,
|
||||
status: body.status || 'registered',
|
||||
priority: body.priority || 3,
|
||||
context: body.context || '',
|
||||
source: body.source || 'text',
|
||||
source_url: body.source_url || null,
|
||||
tags: body.tags || [],
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
issues.push(newIssue);
|
||||
writeIssuesJsonl(issuesDir, issues);
|
||||
return { success: true, issue: newIssue };
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// GET /api/issues/:id - Get issue detail
|
||||
const detailMatch = pathname.match(/^\/api\/issues\/([^/]+)$/);
|
||||
if (detailMatch && req.method === 'GET') {
|
||||
const issueId = decodeURIComponent(detailMatch[1]);
|
||||
if (issueId === 'queue') return false;
|
||||
|
||||
const detail = getIssueDetail(issuesDir, issueId);
|
||||
if (!detail) {
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Issue not found' }));
|
||||
return true;
|
||||
}
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(detail));
|
||||
return true;
|
||||
}
|
||||
|
||||
// PATCH /api/issues/:id - Update issue (with binding support)
|
||||
const updateMatch = pathname.match(/^\/api\/issues\/([^/]+)$/);
|
||||
if (updateMatch && req.method === 'PATCH') {
|
||||
const issueId = decodeURIComponent(updateMatch[1]);
|
||||
if (issueId === 'queue') return false;
|
||||
|
||||
handlePostRequest(req, res, async (body: any) => {
|
||||
const issues = readIssuesJsonl(issuesDir);
|
||||
const issueIndex = issues.findIndex(i => i.id === issueId);
|
||||
if (issueIndex === -1) return { error: 'Issue not found' };
|
||||
|
||||
const updates: string[] = [];
|
||||
|
||||
// Handle binding if bound_solution_id provided
|
||||
if (body.bound_solution_id !== undefined) {
|
||||
if (body.bound_solution_id) {
|
||||
const bindResult = bindSolutionToIssue(issuesDir, issueId, body.bound_solution_id, issues, issueIndex);
|
||||
if (bindResult.error) return bindResult;
|
||||
updates.push('bound_solution_id');
|
||||
} else {
|
||||
// Unbind
|
||||
const solutions = readSolutionsJsonl(issuesDir, issueId);
|
||||
solutions.forEach(s => { s.is_bound = false; });
|
||||
writeSolutionsJsonl(issuesDir, issueId, solutions);
|
||||
issues[issueIndex].bound_solution_id = null;
|
||||
updates.push('bound_solution_id (unbound)');
|
||||
}
|
||||
}
|
||||
|
||||
// Update other fields
|
||||
for (const field of ['title', 'context', 'status', 'priority', 'tags']) {
|
||||
if (body[field] !== undefined) {
|
||||
issues[issueIndex][field] = body[field];
|
||||
updates.push(field);
|
||||
}
|
||||
}
|
||||
|
||||
issues[issueIndex].updated_at = new Date().toISOString();
|
||||
writeIssuesJsonl(issuesDir, issues);
|
||||
return { success: true, issueId, updated: updates };
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// DELETE /api/issues/:id
|
||||
const deleteMatch = pathname.match(/^\/api\/issues\/([^/]+)$/);
|
||||
if (deleteMatch && req.method === 'DELETE') {
|
||||
const issueId = decodeURIComponent(deleteMatch[1]);
|
||||
|
||||
const issues = readIssuesJsonl(issuesDir);
|
||||
const filtered = issues.filter(i => i.id !== issueId);
|
||||
if (filtered.length === issues.length) {
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Issue not found' }));
|
||||
return true;
|
||||
}
|
||||
|
||||
writeIssuesJsonl(issuesDir, filtered);
|
||||
|
||||
// Clean up solutions file
|
||||
const solPath = join(issuesDir, 'solutions', `${issueId}.jsonl`);
|
||||
if (existsSync(solPath)) {
|
||||
try { unlinkSync(solPath); } catch {}
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true, issueId }));
|
||||
return true;
|
||||
}
|
||||
|
||||
// POST /api/issues/:id/solutions - Add solution
|
||||
const addSolMatch = pathname.match(/^\/api\/issues\/([^/]+)\/solutions$/);
|
||||
if (addSolMatch && req.method === 'POST') {
|
||||
const issueId = decodeURIComponent(addSolMatch[1]);
|
||||
|
||||
handlePostRequest(req, res, async (body: any) => {
|
||||
if (!body.id || !body.tasks) return { error: 'id and tasks required' };
|
||||
|
||||
const solutions = readSolutionsJsonl(issuesDir, issueId);
|
||||
if (solutions.find(s => s.id === body.id)) return { error: `Solution ${body.id} exists` };
|
||||
|
||||
const newSolution = {
|
||||
id: body.id,
|
||||
description: body.description || '',
|
||||
tasks: body.tasks,
|
||||
exploration_context: body.exploration_context || {},
|
||||
analysis: body.analysis || {},
|
||||
score: body.score || 0,
|
||||
is_bound: false,
|
||||
created_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
solutions.push(newSolution);
|
||||
writeSolutionsJsonl(issuesDir, issueId, solutions);
|
||||
|
||||
// Update issue solution_count
|
||||
const issues = readIssuesJsonl(issuesDir);
|
||||
const idx = issues.findIndex(i => i.id === issueId);
|
||||
if (idx !== -1) {
|
||||
issues[idx].solution_count = solutions.length;
|
||||
issues[idx].updated_at = new Date().toISOString();
|
||||
writeIssuesJsonl(issuesDir, issues);
|
||||
}
|
||||
|
||||
return { success: true, solution: newSolution };
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// PATCH /api/issues/:id/tasks/:taskId - Update task
|
||||
const taskMatch = pathname.match(/^\/api\/issues\/([^/]+)\/tasks\/([^/]+)$/);
|
||||
if (taskMatch && req.method === 'PATCH') {
|
||||
const issueId = decodeURIComponent(taskMatch[1]);
|
||||
const taskId = decodeURIComponent(taskMatch[2]);
|
||||
|
||||
handlePostRequest(req, res, async (body: any) => {
|
||||
const issues = readIssuesJsonl(issuesDir);
|
||||
const issue = issues.find(i => i.id === issueId);
|
||||
if (!issue?.bound_solution_id) return { error: 'Issue or bound solution not found' };
|
||||
|
||||
const solutions = readSolutionsJsonl(issuesDir, issueId);
|
||||
const solIdx = solutions.findIndex(s => s.id === issue.bound_solution_id);
|
||||
if (solIdx === -1) return { error: 'Bound solution not found' };
|
||||
|
||||
const taskIdx = solutions[solIdx].tasks?.findIndex((t: any) => t.id === taskId);
|
||||
if (taskIdx === -1 || taskIdx === undefined) return { error: 'Task not found' };
|
||||
|
||||
const updates: string[] = [];
|
||||
for (const field of ['status', 'priority', 'result', 'error']) {
|
||||
if (body[field] !== undefined) {
|
||||
solutions[solIdx].tasks[taskIdx][field] = body[field];
|
||||
updates.push(field);
|
||||
}
|
||||
}
|
||||
solutions[solIdx].tasks[taskIdx].updated_at = new Date().toISOString();
|
||||
writeSolutionsJsonl(issuesDir, issueId, solutions);
|
||||
|
||||
return { success: true, issueId, taskId, updated: updates };
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// Legacy: PUT /api/issues/:id/task/:taskId (backward compat)
|
||||
const legacyTaskMatch = pathname.match(/^\/api\/issues\/([^/]+)\/task\/([^/]+)$/);
|
||||
if (legacyTaskMatch && req.method === 'PUT') {
|
||||
const issueId = decodeURIComponent(legacyTaskMatch[1]);
|
||||
const taskId = decodeURIComponent(legacyTaskMatch[2]);
|
||||
|
||||
handlePostRequest(req, res, async (body: any) => {
|
||||
const issues = readIssuesJsonl(issuesDir);
|
||||
const issue = issues.find(i => i.id === issueId);
|
||||
if (!issue?.bound_solution_id) return { error: 'Issue or bound solution not found' };
|
||||
|
||||
const solutions = readSolutionsJsonl(issuesDir, issueId);
|
||||
const solIdx = solutions.findIndex(s => s.id === issue.bound_solution_id);
|
||||
if (solIdx === -1) return { error: 'Bound solution not found' };
|
||||
|
||||
const taskIdx = solutions[solIdx].tasks?.findIndex((t: any) => t.id === taskId);
|
||||
if (taskIdx === -1 || taskIdx === undefined) return { error: 'Task not found' };
|
||||
|
||||
const updates: string[] = [];
|
||||
if (body.status !== undefined) { solutions[solIdx].tasks[taskIdx].status = body.status; updates.push('status'); }
|
||||
if (body.priority !== undefined) { solutions[solIdx].tasks[taskIdx].priority = body.priority; updates.push('priority'); }
|
||||
solutions[solIdx].tasks[taskIdx].updated_at = new Date().toISOString();
|
||||
writeSolutionsJsonl(issuesDir, issueId, solutions);
|
||||
|
||||
return { success: true, issueId, taskId, updated: updates };
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// Legacy: PUT /api/issues/:id/bind/:solutionId (backward compat)
|
||||
const legacyBindMatch = pathname.match(/^\/api\/issues\/([^/]+)\/bind\/([^/]+)$/);
|
||||
if (legacyBindMatch && req.method === 'PUT') {
|
||||
const issueId = decodeURIComponent(legacyBindMatch[1]);
|
||||
const solutionId = decodeURIComponent(legacyBindMatch[2]);
|
||||
|
||||
const issues = readIssuesJsonl(issuesDir);
|
||||
const issueIndex = issues.findIndex(i => i.id === issueId);
|
||||
if (issueIndex === -1) {
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Issue not found' }));
|
||||
return true;
|
||||
}
|
||||
|
||||
const result = bindSolutionToIssue(issuesDir, issueId, solutionId, issues, issueIndex);
|
||||
if (result.error) {
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(result));
|
||||
return true;
|
||||
}
|
||||
|
||||
issues[issueIndex].updated_at = new Date().toISOString();
|
||||
writeIssuesJsonl(issuesDir, issues);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true, issueId, solutionId }));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Legacy: PUT /api/issues/:id (backward compat for PATCH)
|
||||
const legacyUpdateMatch = pathname.match(/^\/api\/issues\/([^/]+)$/);
|
||||
if (legacyUpdateMatch && req.method === 'PUT') {
|
||||
const issueId = decodeURIComponent(legacyUpdateMatch[1]);
|
||||
if (issueId === 'queue') return false;
|
||||
|
||||
handlePostRequest(req, res, async (body: any) => {
|
||||
const issues = readIssuesJsonl(issuesDir);
|
||||
const issueIndex = issues.findIndex(i => i.id === issueId);
|
||||
if (issueIndex === -1) return { error: 'Issue not found' };
|
||||
|
||||
const updates: string[] = [];
|
||||
for (const field of ['title', 'context', 'status', 'priority', 'bound_solution_id', 'tags']) {
|
||||
if (body[field] !== undefined) {
|
||||
issues[issueIndex][field] = body[field];
|
||||
updates.push(field);
|
||||
}
|
||||
}
|
||||
|
||||
issues[issueIndex].updated_at = new Date().toISOString();
|
||||
writeIssuesJsonl(issuesDir, issues);
|
||||
return { success: true, issueId, updated: updates };
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -17,6 +17,8 @@ import { handleGraphRoutes } from './routes/graph-routes.js';
|
||||
import { handleSystemRoutes } from './routes/system-routes.js';
|
||||
import { handleFilesRoutes } from './routes/files-routes.js';
|
||||
import { handleSkillsRoutes } from './routes/skills-routes.js';
|
||||
import { handleIssueRoutes } from './routes/issue-routes.js';
|
||||
import { handleDiscoveryRoutes } from './routes/discovery-routes.js';
|
||||
import { handleRulesRoutes } from './routes/rules-routes.js';
|
||||
import { handleSessionRoutes } from './routes/session-routes.js';
|
||||
import { handleCcwRoutes } from './routes/ccw-routes.js';
|
||||
@@ -86,7 +88,10 @@ const MODULE_CSS_FILES = [
|
||||
'28-mcp-manager.css',
|
||||
'29-help.css',
|
||||
'30-core-memory.css',
|
||||
'31-api-settings.css'
|
||||
'31-api-settings.css',
|
||||
'32-issue-manager.css',
|
||||
'33-cli-stream-viewer.css',
|
||||
'34-discovery.css'
|
||||
];
|
||||
|
||||
// Modular JS files in dependency order
|
||||
@@ -107,6 +112,7 @@ const MODULE_FILES = [
|
||||
'components/flowchart.js',
|
||||
'components/carousel.js',
|
||||
'components/notifications.js',
|
||||
'components/cli-stream-viewer.js',
|
||||
'components/global-notifications.js',
|
||||
'components/task-queue-sidebar.js',
|
||||
'components/cli-status.js',
|
||||
@@ -142,6 +148,8 @@ const MODULE_FILES = [
|
||||
'views/claude-manager.js',
|
||||
'views/api-settings.js',
|
||||
'views/help.js',
|
||||
'views/issue-manager.js',
|
||||
'views/issue-discovery.js',
|
||||
'main.js'
|
||||
];
|
||||
|
||||
@@ -244,7 +252,7 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
|
||||
|
||||
// CORS headers for API requests
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
@@ -340,6 +348,21 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
|
||||
if (await handleSkillsRoutes(routeContext)) return;
|
||||
}
|
||||
|
||||
// Queue routes (/api/queue*) - top-level queue API
|
||||
if (pathname.startsWith('/api/queue')) {
|
||||
if (await handleIssueRoutes(routeContext)) return;
|
||||
}
|
||||
|
||||
// Issue routes (/api/issues*)
|
||||
if (pathname.startsWith('/api/issues')) {
|
||||
if (await handleIssueRoutes(routeContext)) return;
|
||||
}
|
||||
|
||||
// Discovery routes (/api/discoveries*)
|
||||
if (pathname.startsWith('/api/discoveries')) {
|
||||
if (await handleDiscoveryRoutes(routeContext)) return;
|
||||
}
|
||||
|
||||
// Rules routes (/api/rules*)
|
||||
if (pathname.startsWith('/api/rules')) {
|
||||
if (await handleRulesRoutes(routeContext)) return;
|
||||
|
||||
2853
ccw/src/templates/dashboard-css/32-issue-manager.css
Normal file
2853
ccw/src/templates/dashboard-css/32-issue-manager.css
Normal file
File diff suppressed because it is too large
Load Diff
580
ccw/src/templates/dashboard-css/33-cli-stream-viewer.css
Normal file
580
ccw/src/templates/dashboard-css/33-cli-stream-viewer.css
Normal file
@@ -0,0 +1,580 @@
|
||||
/**
|
||||
* CLI Stream Viewer Styles
|
||||
* Right-side popup panel for viewing CLI streaming output
|
||||
*/
|
||||
|
||||
/* ===== Overlay ===== */
|
||||
.cli-stream-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgb(0 0 0 / 0.3);
|
||||
z-index: 1050;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.cli-stream-overlay.open {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* ===== Main Panel ===== */
|
||||
.cli-stream-viewer {
|
||||
position: fixed;
|
||||
top: 60px;
|
||||
right: 16px;
|
||||
width: 650px;
|
||||
max-width: calc(100vw - 32px);
|
||||
max-height: calc(100vh - 80px);
|
||||
background: hsl(var(--card));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 32px rgb(0 0 0 / 0.2);
|
||||
z-index: 1100;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transform: translateX(calc(100% + 20px));
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.cli-stream-viewer.open {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* ===== Header ===== */
|
||||
.cli-stream-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
background: hsl(var(--muted) / 0.3);
|
||||
}
|
||||
|
||||
.cli-stream-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.cli-stream-title svg,
|
||||
.cli-stream-title i {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.cli-stream-count-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
padding: 0 6px;
|
||||
background: hsl(var(--muted));
|
||||
color: hsl(var(--muted-foreground));
|
||||
border-radius: 10px;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.cli-stream-count-badge.has-running {
|
||||
background: hsl(var(--warning));
|
||||
color: hsl(var(--warning-foreground, white));
|
||||
}
|
||||
|
||||
/* ===== Search Box ===== */
|
||||
.cli-stream-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 8px;
|
||||
background: hsl(var(--background));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.cli-stream-search:focus-within {
|
||||
border-color: hsl(var(--primary));
|
||||
box-shadow: 0 0 0 2px hsl(var(--primary) / 0.1);
|
||||
}
|
||||
|
||||
.cli-stream-search-input {
|
||||
width: 140px;
|
||||
padding: 2px 4px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.cli-stream-search-input::placeholder {
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.cli-stream-search-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cli-stream-search-clear {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.cli-stream-search:focus-within .cli-stream-search-clear,
|
||||
.cli-stream-search-input:not(:placeholder-shown) + .cli-stream-search-clear {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.cli-stream-search-clear:hover {
|
||||
background: hsl(var(--muted));
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.cli-stream-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.cli-stream-action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 10px;
|
||||
background: transparent;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.cli-stream-action-btn:hover {
|
||||
background: hsl(var(--hover));
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.cli-stream-close-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 1.25rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.cli-stream-close-btn:hover {
|
||||
background: hsl(var(--destructive) / 0.1);
|
||||
color: hsl(var(--destructive));
|
||||
}
|
||||
|
||||
/* ===== Tab Bar ===== */
|
||||
.cli-stream-tabs {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
background: hsl(var(--muted) / 0.2);
|
||||
overflow-x: auto;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.cli-stream-tabs::-webkit-scrollbar {
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.cli-stream-tabs::-webkit-scrollbar-thumb {
|
||||
background: hsl(var(--border));
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.cli-stream-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.cli-stream-tab:hover {
|
||||
background: hsl(var(--hover));
|
||||
color: hsl(var(--foreground));
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.cli-stream-tab.active {
|
||||
background: hsl(var(--card));
|
||||
border-color: hsl(var(--primary));
|
||||
color: hsl(var(--foreground));
|
||||
box-shadow: 0 2px 8px rgb(0 0 0 / 0.15);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.cli-stream-tab.active .cli-stream-tab-tool {
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.cli-stream-tab-status {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cli-stream-tab-status.running {
|
||||
background: hsl(var(--warning));
|
||||
animation: streamStatusPulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.cli-stream-tab-status.completed {
|
||||
background: hsl(var(--success));
|
||||
}
|
||||
|
||||
.cli-stream-tab-status.error {
|
||||
background: hsl(var(--destructive));
|
||||
}
|
||||
|
||||
@keyframes streamStatusPulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.6; transform: scale(1.2); }
|
||||
}
|
||||
|
||||
.cli-stream-tab-tool {
|
||||
font-weight: 500;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.cli-stream-tab-mode {
|
||||
font-size: 0.625rem;
|
||||
padding: 1px 4px;
|
||||
background: hsl(var(--muted));
|
||||
border-radius: 3px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.cli-stream-tab-close {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-left: 4px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.cli-stream-tab:hover .cli-stream-tab-close {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.cli-stream-tab-close:hover {
|
||||
background: hsl(var(--destructive) / 0.2);
|
||||
color: hsl(var(--destructive));
|
||||
}
|
||||
|
||||
.cli-stream-tab-close.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.3 !important;
|
||||
}
|
||||
|
||||
/* ===== Empty State ===== */
|
||||
.cli-stream-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px 24px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cli-stream-empty svg,
|
||||
.cli-stream-empty i {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.cli-stream-empty-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.cli-stream-empty-hint {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* ===== Terminal Content ===== */
|
||||
.cli-stream-content {
|
||||
flex: 1;
|
||||
min-height: 300px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
padding: 12px 16px;
|
||||
background: hsl(220 13% 8%);
|
||||
font-family: var(--font-mono, 'Consolas', 'Monaco', 'Courier New', monospace);
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.6;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.cli-stream-content::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.cli-stream-content::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.cli-stream-content::-webkit-scrollbar-thumb {
|
||||
background: hsl(0 0% 40%);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.cli-stream-line {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
margin: 0;
|
||||
padding: 2px 0;
|
||||
border-radius: 2px;
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
|
||||
.cli-stream-line:hover {
|
||||
background: hsl(0 0% 100% / 0.03);
|
||||
}
|
||||
|
||||
.cli-stream-line.stdout {
|
||||
color: hsl(0 0% 85%);
|
||||
}
|
||||
|
||||
.cli-stream-line.stderr {
|
||||
color: hsl(8 75% 65%);
|
||||
background: hsl(8 75% 65% / 0.05);
|
||||
}
|
||||
|
||||
.cli-stream-line.system {
|
||||
color: hsl(210 80% 65%);
|
||||
font-style: italic;
|
||||
padding-left: 8px;
|
||||
border-left: 2px solid hsl(210 80% 65% / 0.5);
|
||||
}
|
||||
|
||||
.cli-stream-line.info {
|
||||
color: hsl(200 80% 70%);
|
||||
}
|
||||
|
||||
/* JSON/Code syntax coloring in output */
|
||||
.cli-stream-line .json-key {
|
||||
color: hsl(200 80% 70%);
|
||||
}
|
||||
|
||||
.cli-stream-line .json-string {
|
||||
color: hsl(100 50% 60%);
|
||||
}
|
||||
|
||||
.cli-stream-line .json-number {
|
||||
color: hsl(40 80% 65%);
|
||||
}
|
||||
|
||||
/* Search highlight */
|
||||
.cli-stream-highlight {
|
||||
background: hsl(50 100% 50% / 0.4);
|
||||
color: inherit;
|
||||
padding: 0 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Filter result info */
|
||||
.cli-stream-filter-info {
|
||||
display: inline-block;
|
||||
padding: 4px 10px;
|
||||
margin-bottom: 8px;
|
||||
background: hsl(var(--primary) / 0.15);
|
||||
color: hsl(var(--primary));
|
||||
border-radius: 4px;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Auto-scroll indicator */
|
||||
.cli-stream-scroll-btn {
|
||||
position: sticky;
|
||||
bottom: 8px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 12px;
|
||||
background: hsl(var(--primary));
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-size: 0.625rem;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.cli-stream-content.has-new-content .cli-stream-scroll-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* ===== Status Bar ===== */
|
||||
.cli-stream-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 16px;
|
||||
border-top: 1px solid hsl(var(--border));
|
||||
background: hsl(var(--muted) / 0.3);
|
||||
font-size: 0.6875rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.cli-stream-status-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.cli-stream-status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.cli-stream-status-item svg,
|
||||
.cli-stream-status-item i {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.cli-stream-status-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.cli-stream-toggle-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
background: transparent;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 3px;
|
||||
font-size: 0.625rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.cli-stream-toggle-btn:hover {
|
||||
background: hsl(var(--hover));
|
||||
}
|
||||
|
||||
.cli-stream-toggle-btn.active {
|
||||
background: hsl(var(--primary) / 0.1);
|
||||
border-color: hsl(var(--primary));
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
/* ===== Header Button & Badge ===== */
|
||||
.cli-stream-btn {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.cli-stream-badge {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
right: -2px;
|
||||
min-width: 14px;
|
||||
height: 14px;
|
||||
padding: 0 4px;
|
||||
background: hsl(var(--warning));
|
||||
color: white;
|
||||
border-radius: 7px;
|
||||
font-size: 0.5625rem;
|
||||
font-weight: 600;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cli-stream-badge.has-running {
|
||||
display: flex;
|
||||
animation: streamBadgePulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes streamBadgePulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.15); }
|
||||
}
|
||||
|
||||
/* ===== Responsive ===== */
|
||||
@media (max-width: 768px) {
|
||||
.cli-stream-viewer {
|
||||
top: 56px;
|
||||
right: 8px;
|
||||
left: 8px;
|
||||
width: auto;
|
||||
max-height: calc(100vh - 72px);
|
||||
}
|
||||
|
||||
.cli-stream-content {
|
||||
min-height: 200px;
|
||||
max-height: 350px;
|
||||
}
|
||||
}
|
||||
783
ccw/src/templates/dashboard-css/34-discovery.css
Normal file
783
ccw/src/templates/dashboard-css/34-discovery.css
Normal file
@@ -0,0 +1,783 @@
|
||||
/* ==========================================
|
||||
ISSUE DISCOVERY STYLES
|
||||
========================================== */
|
||||
|
||||
/* Discovery Manager Container */
|
||||
.discovery-manager {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.discovery-manager.loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 300px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
/* Discovery Header */
|
||||
.discovery-header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.discovery-back-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--muted-foreground));
|
||||
background: hsl(var(--muted));
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.discovery-back-btn:hover {
|
||||
color: hsl(var(--foreground));
|
||||
background: hsl(var(--hover));
|
||||
}
|
||||
|
||||
/* Discovery List */
|
||||
.discovery-list-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* Discovery Card */
|
||||
.discovery-card {
|
||||
background: hsl(var(--card));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.75rem;
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.discovery-card:hover {
|
||||
border-color: hsl(var(--primary) / 0.5);
|
||||
box-shadow: 0 4px 12px hsl(var(--foreground) / 0.05);
|
||||
}
|
||||
|
||||
.discovery-card.running {
|
||||
border-color: hsl(var(--warning) / 0.5);
|
||||
}
|
||||
|
||||
.discovery-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.discovery-id {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.discovery-phase {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.discovery-phase.complete {
|
||||
background: hsl(var(--success) / 0.1);
|
||||
color: hsl(var(--success));
|
||||
}
|
||||
|
||||
.discovery-phase.parallel,
|
||||
.discovery-phase.external,
|
||||
.discovery-phase.aggregation {
|
||||
background: hsl(var(--warning) / 0.1);
|
||||
color: hsl(var(--warning));
|
||||
}
|
||||
|
||||
.discovery-phase.initialization {
|
||||
background: hsl(var(--primary) / 0.1);
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.discovery-card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.discovery-target {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Perspective Badges */
|
||||
.discovery-perspectives {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.perspective-badge {
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
|
||||
.perspective-badge.bug {
|
||||
background: hsl(0 84% 60% / 0.1);
|
||||
color: hsl(0 84% 60%);
|
||||
}
|
||||
|
||||
.perspective-badge.ux {
|
||||
background: hsl(262 84% 60% / 0.1);
|
||||
color: hsl(262 84% 60%);
|
||||
}
|
||||
|
||||
.perspective-badge.test {
|
||||
background: hsl(200 84% 50% / 0.1);
|
||||
color: hsl(200 84% 50%);
|
||||
}
|
||||
|
||||
.perspective-badge.quality {
|
||||
background: hsl(142 76% 36% / 0.1);
|
||||
color: hsl(142 76% 36%);
|
||||
}
|
||||
|
||||
.perspective-badge.security {
|
||||
background: hsl(0 84% 50% / 0.1);
|
||||
color: hsl(0 84% 50%);
|
||||
}
|
||||
|
||||
.perspective-badge.performance {
|
||||
background: hsl(38 92% 50% / 0.1);
|
||||
color: hsl(38 92% 50%);
|
||||
}
|
||||
|
||||
.perspective-badge.maintainability {
|
||||
background: hsl(280 60% 50% / 0.1);
|
||||
color: hsl(280 60% 50%);
|
||||
}
|
||||
|
||||
.perspective-badge.best-practices {
|
||||
background: hsl(170 60% 45% / 0.1);
|
||||
color: hsl(170 60% 45%);
|
||||
}
|
||||
|
||||
.perspective-badge.more {
|
||||
background: hsl(var(--muted));
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
/* Progress Bar */
|
||||
.discovery-progress-bar {
|
||||
height: 4px;
|
||||
background: hsl(var(--muted));
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.discovery-progress-bar .progress-fill {
|
||||
height: 100%;
|
||||
background: hsl(var(--primary));
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
/* Stats */
|
||||
.discovery-stats {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.discovery-stats .stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.discovery-stats .stat-value {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.discovery-stats .stat-label {
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
/* Priority Distribution Bar */
|
||||
.discovery-priority-bar {
|
||||
display: flex;
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.discovery-priority-bar .priority-segment {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.discovery-priority-bar .priority-segment.critical {
|
||||
background: hsl(0 84% 60%);
|
||||
}
|
||||
|
||||
.discovery-priority-bar .priority-segment.high {
|
||||
background: hsl(38 92% 50%);
|
||||
}
|
||||
|
||||
.discovery-priority-bar .priority-segment.medium {
|
||||
background: hsl(48 96% 53%);
|
||||
}
|
||||
|
||||
.discovery-priority-bar .priority-segment.low {
|
||||
background: hsl(142 76% 36%);
|
||||
}
|
||||
|
||||
.discovery-card-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 0.75rem;
|
||||
padding-top: 0.75rem;
|
||||
border-top: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.discovery-action-btn {
|
||||
padding: 0.375rem;
|
||||
border-radius: 0.375rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.discovery-action-btn:hover {
|
||||
color: hsl(var(--destructive));
|
||||
background: hsl(var(--destructive) / 0.1);
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.discovery-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 300px;
|
||||
text-align: center;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.discovery-empty .empty-icon {
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Discovery Detail Container */
|
||||
.discovery-detail-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 400px;
|
||||
gap: 1.5rem;
|
||||
height: calc(100vh - 200px);
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.discovery-detail-container {
|
||||
grid-template-columns: 1fr;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* Findings Panel */
|
||||
.discovery-findings-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: hsl(var(--card));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.discovery-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.toolbar-filters {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--foreground));
|
||||
background: hsl(var(--muted));
|
||||
border: 1px solid hsl(var(--border));
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toolbar-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
background: hsl(var(--muted));
|
||||
border: 1px solid hsl(var(--border));
|
||||
flex: 1;
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
.toolbar-search input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--foreground));
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.findings-count {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.findings-count-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.findings-count .selected-count {
|
||||
color: hsl(var(--primary));
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.findings-count-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.select-action-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--muted-foreground));
|
||||
background: transparent;
|
||||
border: 1px solid hsl(var(--border));
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.select-action-btn:hover {
|
||||
color: hsl(var(--foreground));
|
||||
background: hsl(var(--muted));
|
||||
border-color: hsl(var(--primary) / 0.3);
|
||||
}
|
||||
|
||||
/* Findings List */
|
||||
.findings-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.findings-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Finding Item */
|
||||
.finding-item {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.finding-item:hover {
|
||||
background: hsl(var(--muted) / 0.5);
|
||||
}
|
||||
|
||||
.finding-item.active {
|
||||
background: hsl(var(--primary) / 0.1);
|
||||
border: 1px solid hsl(var(--primary) / 0.3);
|
||||
}
|
||||
|
||||
.finding-item.selected {
|
||||
background: hsl(var(--primary) / 0.05);
|
||||
}
|
||||
|
||||
.finding-item.dismissed {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.finding-item.exported {
|
||||
opacity: 0.6;
|
||||
background: hsl(var(--success) / 0.05);
|
||||
border: 1px solid hsl(var(--success) / 0.2);
|
||||
}
|
||||
|
||||
.finding-item.exported:hover {
|
||||
background: hsl(var(--success) / 0.08);
|
||||
}
|
||||
|
||||
/* Exported Badge */
|
||||
.exported-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
background: hsl(var(--success) / 0.1);
|
||||
color: hsl(var(--success));
|
||||
}
|
||||
|
||||
.finding-checkbox input:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.finding-checkbox {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding-top: 0.125rem;
|
||||
}
|
||||
|
||||
.finding-checkbox input {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.finding-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.finding-header {
|
||||
display: flex;
|
||||
gap: 0.375rem;
|
||||
margin-bottom: 0.375rem;
|
||||
}
|
||||
|
||||
.finding-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--foreground));
|
||||
line-height: 1.3;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.finding-location {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
/* Priority Badge */
|
||||
.priority-badge {
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.priority-badge.critical {
|
||||
background: hsl(0 84% 60% / 0.1);
|
||||
color: hsl(0 84% 60%);
|
||||
}
|
||||
|
||||
.priority-badge.high {
|
||||
background: hsl(38 92% 50% / 0.1);
|
||||
color: hsl(38 92% 50%);
|
||||
}
|
||||
|
||||
.priority-badge.medium {
|
||||
background: hsl(48 96% 53% / 0.1);
|
||||
color: hsl(48 70% 40%);
|
||||
}
|
||||
|
||||
.priority-badge.low {
|
||||
background: hsl(142 76% 36% / 0.1);
|
||||
color: hsl(142 76% 36%);
|
||||
}
|
||||
|
||||
/* Bulk Actions */
|
||||
.bulk-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-top: 1px solid hsl(var(--border));
|
||||
background: hsl(var(--muted) / 0.3);
|
||||
}
|
||||
|
||||
.bulk-count {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.bulk-action-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.bulk-action-btn.export {
|
||||
background: hsl(var(--primary));
|
||||
color: hsl(var(--primary-foreground));
|
||||
}
|
||||
|
||||
.bulk-action-btn.export:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.bulk-action-btn.dismiss {
|
||||
background: hsl(var(--muted));
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.bulk-action-btn.dismiss:hover {
|
||||
background: hsl(var(--destructive) / 0.1);
|
||||
color: hsl(var(--destructive));
|
||||
}
|
||||
|
||||
/* Preview Panel */
|
||||
.discovery-preview-panel {
|
||||
background: hsl(var(--card));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
min-height: 300px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Finding Preview */
|
||||
.finding-preview {
|
||||
padding: 1.25rem;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.preview-badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.confidence-badge {
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 500;
|
||||
background: hsl(var(--muted));
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.preview-section {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.preview-section h4 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: hsl(var(--muted-foreground));
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.preview-location code {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
background: hsl(var(--muted));
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.preview-snippet {
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
background: hsl(var(--muted));
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.preview-snippet code {
|
||||
font-size: 0.75rem;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.preview-description,
|
||||
.preview-impact,
|
||||
.preview-recommendation {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
/* Suggested Issue */
|
||||
.preview-section.suggested-issue {
|
||||
padding: 0.75rem;
|
||||
background: hsl(var(--primary) / 0.05);
|
||||
border: 1px solid hsl(var(--primary) / 0.2);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.suggested-issue-content {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.suggested-issue-content .issue-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--foreground));
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.suggested-issue-content .issue-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.issue-type,
|
||||
.issue-priority,
|
||||
.issue-label {
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.issue-type {
|
||||
background: hsl(var(--primary) / 0.1);
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.issue-priority {
|
||||
background: hsl(var(--warning) / 0.1);
|
||||
color: hsl(var(--warning));
|
||||
}
|
||||
|
||||
.issue-label {
|
||||
background: hsl(var(--muted));
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
/* Preview Actions */
|
||||
.preview-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1.5rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.preview-action-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.preview-action-btn.primary {
|
||||
background: hsl(var(--primary));
|
||||
color: hsl(var(--primary-foreground));
|
||||
}
|
||||
|
||||
.preview-action-btn.primary:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.preview-action-btn.secondary {
|
||||
background: hsl(var(--muted));
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.preview-action-btn.secondary:hover {
|
||||
color: hsl(var(--destructive));
|
||||
background: hsl(var(--destructive) / 0.1);
|
||||
}
|
||||
@@ -21,24 +21,6 @@ let nativeResumeEnabled = localStorage.getItem('ccw-native-resume') !== 'false';
|
||||
// Recursive Query settings (for hierarchical storage aggregation)
|
||||
let recursiveQueryEnabled = localStorage.getItem('ccw-recursive-query') !== 'false'; // default true
|
||||
|
||||
// Code Index MCP provider (codexlens, ace, or none)
|
||||
let codeIndexMcpProvider = 'codexlens';
|
||||
|
||||
// ========== Helper Functions ==========
|
||||
/**
|
||||
* Get the context-tools filename based on provider
|
||||
*/
|
||||
function getContextToolsFileName(provider) {
|
||||
switch (provider) {
|
||||
case 'ace':
|
||||
return 'context-tools-ace.md';
|
||||
case 'none':
|
||||
return 'context-tools-none.md';
|
||||
default:
|
||||
return 'context-tools.md';
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Initialization ==========
|
||||
function initCliStatus() {
|
||||
// Load all statuses in one call using aggregated endpoint
|
||||
@@ -259,12 +241,7 @@ async function loadCliToolsConfig() {
|
||||
defaultCliTool = data.defaultTool;
|
||||
}
|
||||
|
||||
// Load Code Index MCP provider from config
|
||||
if (data.settings?.codeIndexMcp) {
|
||||
codeIndexMcpProvider = data.settings.codeIndexMcp;
|
||||
}
|
||||
|
||||
console.log('[CLI Config] Loaded from:', data._configInfo?.source || 'unknown', '| Default:', data.defaultTool, '| CodeIndexMCP:', codeIndexMcpProvider);
|
||||
console.log('[CLI Config] Loaded from:', data._configInfo?.source || 'unknown', '| Default:', data.defaultTool);
|
||||
return data;
|
||||
} catch (err) {
|
||||
console.error('Failed to load CLI tools config:', err);
|
||||
@@ -637,33 +614,6 @@ function renderCliStatus() {
|
||||
</div>
|
||||
<p class="cli-setting-desc">Cache prefix/suffix injection mode for prompts</p>
|
||||
</div>
|
||||
<div class="cli-setting-item">
|
||||
<label class="cli-setting-label">
|
||||
<i data-lucide="search" class="w-3 h-3"></i>
|
||||
Code Index MCP
|
||||
</label>
|
||||
<div class="cli-setting-control">
|
||||
<div class="flex items-center bg-muted rounded-lg p-0.5">
|
||||
<button class="code-mcp-btn px-3 py-1.5 text-xs font-medium rounded-md transition-all ${codeIndexMcpProvider === 'codexlens' ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}"
|
||||
onclick="setCodeIndexMcpProvider('codexlens')">
|
||||
CodexLens
|
||||
</button>
|
||||
<button class="code-mcp-btn px-3 py-1.5 text-xs font-medium rounded-md transition-all ${codeIndexMcpProvider === 'ace' ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}"
|
||||
onclick="setCodeIndexMcpProvider('ace')">
|
||||
ACE
|
||||
</button>
|
||||
<button class="code-mcp-btn px-3 py-1.5 text-xs font-medium rounded-md transition-all ${codeIndexMcpProvider === 'none' ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}"
|
||||
onclick="setCodeIndexMcpProvider('none')">
|
||||
None
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="cli-setting-desc">Code search provider (updates CLAUDE.md context-tools reference)</p>
|
||||
<p class="cli-setting-desc text-xs text-muted-foreground mt-1">
|
||||
<i data-lucide="file-text" class="w-3 h-3 inline-block mr-1"></i>
|
||||
Current: <code class="bg-muted px-1 rounded">${getContextToolsFileName(codeIndexMcpProvider)}</code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -786,33 +736,6 @@ async function setCacheInjectionMode(mode) {
|
||||
}
|
||||
}
|
||||
|
||||
async function setCodeIndexMcpProvider(provider) {
|
||||
try {
|
||||
const response = await fetch('/api/cli/code-index-mcp', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ provider: provider })
|
||||
});
|
||||
if (response.ok) {
|
||||
codeIndexMcpProvider = provider;
|
||||
if (window.claudeCliToolsConfig && window.claudeCliToolsConfig.settings) {
|
||||
window.claudeCliToolsConfig.settings.codeIndexMcp = provider;
|
||||
}
|
||||
const providerName = provider === 'ace' ? 'ACE (Augment)' : provider === 'none' ? 'None (Built-in only)' : 'CodexLens';
|
||||
showRefreshToast(`Code Index MCP switched to ${providerName}`, 'success');
|
||||
// Re-render both CLI status and settings section
|
||||
if (typeof renderCliStatus === 'function') renderCliStatus();
|
||||
if (typeof renderCliSettingsSection === 'function') renderCliSettingsSection();
|
||||
} else {
|
||||
const data = await response.json();
|
||||
showRefreshToast(`Failed to switch Code Index MCP: ${data.error}`, 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to switch Code Index MCP:', err);
|
||||
showRefreshToast('Failed to switch Code Index MCP', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAllCliStatus() {
|
||||
await loadAllStatuses();
|
||||
renderCliStatus();
|
||||
|
||||
532
ccw/src/templates/dashboard-js/components/cli-stream-viewer.js
Normal file
532
ccw/src/templates/dashboard-js/components/cli-stream-viewer.js
Normal file
@@ -0,0 +1,532 @@
|
||||
/**
|
||||
* CLI Stream Viewer Component
|
||||
* Real-time streaming output viewer for CLI executions
|
||||
*/
|
||||
|
||||
// ===== State Management =====
|
||||
let cliStreamExecutions = {}; // { executionId: { tool, mode, output, status, startTime, endTime } }
|
||||
let activeStreamTab = null;
|
||||
let autoScrollEnabled = true;
|
||||
let isCliStreamViewerOpen = false;
|
||||
let searchFilter = ''; // Search filter for output content
|
||||
|
||||
const MAX_OUTPUT_LINES = 5000; // Prevent memory issues
|
||||
|
||||
// ===== Initialization =====
|
||||
function initCliStreamViewer() {
|
||||
// Initialize keyboard shortcuts
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape' && isCliStreamViewerOpen) {
|
||||
if (searchFilter) {
|
||||
clearSearch();
|
||||
} else {
|
||||
toggleCliStreamViewer();
|
||||
}
|
||||
}
|
||||
// Ctrl+F to focus search when viewer is open
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'f' && isCliStreamViewerOpen) {
|
||||
e.preventDefault();
|
||||
const searchInput = document.getElementById('cliStreamSearchInput');
|
||||
if (searchInput) {
|
||||
searchInput.focus();
|
||||
searchInput.select();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize scroll detection for auto-scroll
|
||||
const content = document.getElementById('cliStreamContent');
|
||||
if (content) {
|
||||
content.addEventListener('scroll', handleStreamContentScroll);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Panel Control =====
|
||||
function toggleCliStreamViewer() {
|
||||
const viewer = document.getElementById('cliStreamViewer');
|
||||
const overlay = document.getElementById('cliStreamOverlay');
|
||||
|
||||
if (!viewer || !overlay) return;
|
||||
|
||||
isCliStreamViewerOpen = !isCliStreamViewerOpen;
|
||||
|
||||
if (isCliStreamViewerOpen) {
|
||||
viewer.classList.add('open');
|
||||
overlay.classList.add('open');
|
||||
|
||||
// If no active tab but have executions, select the first one
|
||||
if (!activeStreamTab && Object.keys(cliStreamExecutions).length > 0) {
|
||||
const firstId = Object.keys(cliStreamExecutions)[0];
|
||||
switchStreamTab(firstId);
|
||||
} else {
|
||||
renderStreamContent(activeStreamTab);
|
||||
}
|
||||
|
||||
// Re-init lucide icons
|
||||
if (typeof lucide !== 'undefined') {
|
||||
lucide.createIcons();
|
||||
}
|
||||
} else {
|
||||
viewer.classList.remove('open');
|
||||
overlay.classList.remove('open');
|
||||
}
|
||||
}
|
||||
|
||||
// ===== WebSocket Event Handlers =====
|
||||
function handleCliStreamStarted(payload) {
|
||||
const { executionId, tool, mode, timestamp } = payload;
|
||||
|
||||
// Create new execution record
|
||||
cliStreamExecutions[executionId] = {
|
||||
tool: tool || 'cli',
|
||||
mode: mode || 'analysis',
|
||||
output: [],
|
||||
status: 'running',
|
||||
startTime: timestamp ? new Date(timestamp).getTime() : Date.now(),
|
||||
endTime: null
|
||||
};
|
||||
|
||||
// Add system message
|
||||
cliStreamExecutions[executionId].output.push({
|
||||
type: 'system',
|
||||
content: `[${new Date().toLocaleTimeString()}] CLI execution started: ${tool} (${mode} mode)`,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// If this is the first execution or panel is open, select it
|
||||
if (!activeStreamTab || isCliStreamViewerOpen) {
|
||||
activeStreamTab = executionId;
|
||||
}
|
||||
|
||||
renderStreamTabs();
|
||||
renderStreamContent(activeStreamTab);
|
||||
updateStreamBadge();
|
||||
|
||||
// Auto-open panel if configured (optional)
|
||||
// if (!isCliStreamViewerOpen) toggleCliStreamViewer();
|
||||
}
|
||||
|
||||
function handleCliStreamOutput(payload) {
|
||||
const { executionId, chunkType, data } = payload;
|
||||
|
||||
const exec = cliStreamExecutions[executionId];
|
||||
if (!exec) return;
|
||||
|
||||
// Parse and add output lines
|
||||
const content = typeof data === 'string' ? data : JSON.stringify(data);
|
||||
const lines = content.split('\n');
|
||||
|
||||
lines.forEach(line => {
|
||||
if (line.trim() || lines.length === 1) { // Keep empty lines if it's the only content
|
||||
exec.output.push({
|
||||
type: chunkType || 'stdout',
|
||||
content: line,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Trim if too long
|
||||
if (exec.output.length > MAX_OUTPUT_LINES) {
|
||||
exec.output = exec.output.slice(-MAX_OUTPUT_LINES);
|
||||
}
|
||||
|
||||
// Update UI if this is the active tab
|
||||
if (activeStreamTab === executionId && isCliStreamViewerOpen) {
|
||||
requestAnimationFrame(() => {
|
||||
renderStreamContent(executionId);
|
||||
});
|
||||
}
|
||||
|
||||
// Update badge to show activity
|
||||
updateStreamBadge();
|
||||
}
|
||||
|
||||
function handleCliStreamCompleted(payload) {
|
||||
const { executionId, success, duration, timestamp } = payload;
|
||||
|
||||
const exec = cliStreamExecutions[executionId];
|
||||
if (!exec) return;
|
||||
|
||||
exec.status = success ? 'completed' : 'error';
|
||||
exec.endTime = timestamp ? new Date(timestamp).getTime() : Date.now();
|
||||
|
||||
// Add completion message
|
||||
const durationText = duration ? ` (${formatDuration(duration)})` : '';
|
||||
const statusText = success ? 'completed successfully' : 'failed';
|
||||
exec.output.push({
|
||||
type: 'system',
|
||||
content: `[${new Date().toLocaleTimeString()}] CLI execution ${statusText}${durationText}`,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
renderStreamTabs();
|
||||
if (activeStreamTab === executionId) {
|
||||
renderStreamContent(executionId);
|
||||
}
|
||||
updateStreamBadge();
|
||||
}
|
||||
|
||||
function handleCliStreamError(payload) {
|
||||
const { executionId, error, timestamp } = payload;
|
||||
|
||||
const exec = cliStreamExecutions[executionId];
|
||||
if (!exec) return;
|
||||
|
||||
exec.status = 'error';
|
||||
exec.endTime = timestamp ? new Date(timestamp).getTime() : Date.now();
|
||||
|
||||
// Add error message
|
||||
exec.output.push({
|
||||
type: 'stderr',
|
||||
content: `[ERROR] ${error || 'Unknown error occurred'}`,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
renderStreamTabs();
|
||||
if (activeStreamTab === executionId) {
|
||||
renderStreamContent(executionId);
|
||||
}
|
||||
updateStreamBadge();
|
||||
}
|
||||
|
||||
// ===== UI Rendering =====
|
||||
function renderStreamTabs() {
|
||||
const tabsContainer = document.getElementById('cliStreamTabs');
|
||||
if (!tabsContainer) return;
|
||||
|
||||
const execIds = Object.keys(cliStreamExecutions);
|
||||
|
||||
if (execIds.length === 0) {
|
||||
tabsContainer.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort: running first, then by start time (newest first)
|
||||
execIds.sort((a, b) => {
|
||||
const execA = cliStreamExecutions[a];
|
||||
const execB = cliStreamExecutions[b];
|
||||
|
||||
if (execA.status === 'running' && execB.status !== 'running') return -1;
|
||||
if (execA.status !== 'running' && execB.status === 'running') return 1;
|
||||
return execB.startTime - execA.startTime;
|
||||
});
|
||||
|
||||
tabsContainer.innerHTML = execIds.map(id => {
|
||||
const exec = cliStreamExecutions[id];
|
||||
const isActive = id === activeStreamTab;
|
||||
const canClose = exec.status !== 'running';
|
||||
|
||||
return `
|
||||
<div class="cli-stream-tab ${isActive ? 'active' : ''}"
|
||||
onclick="switchStreamTab('${id}')"
|
||||
data-execution-id="${id}">
|
||||
<span class="cli-stream-tab-status ${exec.status}"></span>
|
||||
<span class="cli-stream-tab-tool">${escapeHtml(exec.tool)}</span>
|
||||
<span class="cli-stream-tab-mode">${exec.mode}</span>
|
||||
<button class="cli-stream-tab-close ${canClose ? '' : 'disabled'}"
|
||||
onclick="event.stopPropagation(); closeStream('${id}')"
|
||||
title="${canClose ? _streamT('cliStream.close') : _streamT('cliStream.cannotCloseRunning')}"
|
||||
${canClose ? '' : 'disabled'}>×</button>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// Update count badge
|
||||
const countBadge = document.getElementById('cliStreamCountBadge');
|
||||
if (countBadge) {
|
||||
const runningCount = execIds.filter(id => cliStreamExecutions[id].status === 'running').length;
|
||||
countBadge.textContent = execIds.length;
|
||||
countBadge.classList.toggle('has-running', runningCount > 0);
|
||||
}
|
||||
}
|
||||
|
||||
function renderStreamContent(executionId) {
|
||||
const contentContainer = document.getElementById('cliStreamContent');
|
||||
if (!contentContainer) return;
|
||||
|
||||
const exec = executionId ? cliStreamExecutions[executionId] : null;
|
||||
|
||||
if (!exec) {
|
||||
// Show empty state
|
||||
contentContainer.innerHTML = `
|
||||
<div class="cli-stream-empty">
|
||||
<i data-lucide="terminal"></i>
|
||||
<div class="cli-stream-empty-title" data-i18n="cliStream.noStreams">${_streamT('cliStream.noStreams')}</div>
|
||||
<div class="cli-stream-empty-hint" data-i18n="cliStream.noStreamsHint">${_streamT('cliStream.noStreamsHint')}</div>
|
||||
</div>
|
||||
`;
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if should auto-scroll
|
||||
const wasAtBottom = contentContainer.scrollHeight - contentContainer.scrollTop <= contentContainer.clientHeight + 50;
|
||||
|
||||
// Filter output lines based on search
|
||||
let filteredOutput = exec.output;
|
||||
if (searchFilter.trim()) {
|
||||
const searchLower = searchFilter.toLowerCase();
|
||||
filteredOutput = exec.output.filter(line =>
|
||||
line.content.toLowerCase().includes(searchLower)
|
||||
);
|
||||
}
|
||||
|
||||
// Render output lines with search highlighting
|
||||
contentContainer.innerHTML = filteredOutput.map(line => {
|
||||
let content = escapeHtml(line.content);
|
||||
// Highlight search matches
|
||||
if (searchFilter.trim()) {
|
||||
const searchRegex = new RegExp(`(${escapeRegex(searchFilter)})`, 'gi');
|
||||
content = content.replace(searchRegex, '<mark class="cli-stream-highlight">$1</mark>');
|
||||
}
|
||||
return `<div class="cli-stream-line ${line.type}">${content}</div>`;
|
||||
}).join('');
|
||||
|
||||
// Show filter result count if filtering
|
||||
if (searchFilter.trim() && filteredOutput.length !== exec.output.length) {
|
||||
const filterInfo = document.createElement('div');
|
||||
filterInfo.className = 'cli-stream-filter-info';
|
||||
filterInfo.textContent = `${filteredOutput.length} / ${exec.output.length} lines`;
|
||||
contentContainer.insertBefore(filterInfo, contentContainer.firstChild);
|
||||
}
|
||||
|
||||
// Auto-scroll if enabled and was at bottom
|
||||
if (autoScrollEnabled && wasAtBottom) {
|
||||
contentContainer.scrollTop = contentContainer.scrollHeight;
|
||||
}
|
||||
|
||||
// Update status bar
|
||||
renderStreamStatus(executionId);
|
||||
}
|
||||
|
||||
function renderStreamStatus(executionId) {
|
||||
const statusContainer = document.getElementById('cliStreamStatus');
|
||||
if (!statusContainer) return;
|
||||
|
||||
const exec = executionId ? cliStreamExecutions[executionId] : null;
|
||||
|
||||
if (!exec) {
|
||||
statusContainer.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const duration = exec.endTime
|
||||
? formatDuration(exec.endTime - exec.startTime)
|
||||
: formatDuration(Date.now() - exec.startTime);
|
||||
|
||||
const statusLabel = exec.status === 'running'
|
||||
? _streamT('cliStream.running')
|
||||
: exec.status === 'completed'
|
||||
? _streamT('cliStream.completed')
|
||||
: _streamT('cliStream.error');
|
||||
|
||||
statusContainer.innerHTML = `
|
||||
<div class="cli-stream-status-info">
|
||||
<div class="cli-stream-status-item">
|
||||
<span class="cli-stream-tab-status ${exec.status}"></span>
|
||||
<span>${statusLabel}</span>
|
||||
</div>
|
||||
<div class="cli-stream-status-item">
|
||||
<i data-lucide="clock"></i>
|
||||
<span>${duration}</span>
|
||||
</div>
|
||||
<div class="cli-stream-status-item">
|
||||
<i data-lucide="file-text"></i>
|
||||
<span>${exec.output.length} ${_streamT('cliStream.lines') || 'lines'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cli-stream-status-actions">
|
||||
<button class="cli-stream-toggle-btn ${autoScrollEnabled ? 'active' : ''}"
|
||||
onclick="toggleAutoScroll()"
|
||||
title="${_streamT('cliStream.autoScroll')}">
|
||||
<i data-lucide="arrow-down-to-line"></i>
|
||||
<span data-i18n="cliStream.autoScroll">${_streamT('cliStream.autoScroll')}</span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
|
||||
// Update duration periodically for running executions
|
||||
if (exec.status === 'running') {
|
||||
setTimeout(() => {
|
||||
if (activeStreamTab === executionId && cliStreamExecutions[executionId]?.status === 'running') {
|
||||
renderStreamStatus(executionId);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function switchStreamTab(executionId) {
|
||||
if (!cliStreamExecutions[executionId]) return;
|
||||
|
||||
activeStreamTab = executionId;
|
||||
renderStreamTabs();
|
||||
renderStreamContent(executionId);
|
||||
}
|
||||
|
||||
function updateStreamBadge() {
|
||||
const badge = document.getElementById('cliStreamBadge');
|
||||
if (!badge) return;
|
||||
|
||||
const runningCount = Object.values(cliStreamExecutions).filter(e => e.status === 'running').length;
|
||||
|
||||
if (runningCount > 0) {
|
||||
badge.textContent = runningCount;
|
||||
badge.classList.add('has-running');
|
||||
} else {
|
||||
badge.textContent = '';
|
||||
badge.classList.remove('has-running');
|
||||
}
|
||||
}
|
||||
|
||||
// ===== User Actions =====
|
||||
function closeStream(executionId) {
|
||||
const exec = cliStreamExecutions[executionId];
|
||||
if (!exec || exec.status === 'running') return;
|
||||
|
||||
delete cliStreamExecutions[executionId];
|
||||
|
||||
// Switch to another tab if this was active
|
||||
if (activeStreamTab === executionId) {
|
||||
const remaining = Object.keys(cliStreamExecutions);
|
||||
activeStreamTab = remaining.length > 0 ? remaining[0] : null;
|
||||
}
|
||||
|
||||
renderStreamTabs();
|
||||
renderStreamContent(activeStreamTab);
|
||||
updateStreamBadge();
|
||||
}
|
||||
|
||||
function clearCompletedStreams() {
|
||||
const toRemove = Object.keys(cliStreamExecutions).filter(
|
||||
id => cliStreamExecutions[id].status !== 'running'
|
||||
);
|
||||
|
||||
toRemove.forEach(id => delete cliStreamExecutions[id]);
|
||||
|
||||
// Update active tab if needed
|
||||
if (activeStreamTab && !cliStreamExecutions[activeStreamTab]) {
|
||||
const remaining = Object.keys(cliStreamExecutions);
|
||||
activeStreamTab = remaining.length > 0 ? remaining[0] : null;
|
||||
}
|
||||
|
||||
renderStreamTabs();
|
||||
renderStreamContent(activeStreamTab);
|
||||
updateStreamBadge();
|
||||
}
|
||||
|
||||
function toggleAutoScroll() {
|
||||
autoScrollEnabled = !autoScrollEnabled;
|
||||
|
||||
if (autoScrollEnabled && activeStreamTab) {
|
||||
const content = document.getElementById('cliStreamContent');
|
||||
if (content) {
|
||||
content.scrollTop = content.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
renderStreamStatus(activeStreamTab);
|
||||
}
|
||||
|
||||
function handleStreamContentScroll() {
|
||||
const content = document.getElementById('cliStreamContent');
|
||||
if (!content) return;
|
||||
|
||||
// If user scrolls up, disable auto-scroll
|
||||
const isAtBottom = content.scrollHeight - content.scrollTop <= content.clientHeight + 50;
|
||||
if (!isAtBottom && autoScrollEnabled) {
|
||||
autoScrollEnabled = false;
|
||||
renderStreamStatus(activeStreamTab);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Helper Functions =====
|
||||
function formatDuration(ms) {
|
||||
if (ms < 1000) return `${ms}ms`;
|
||||
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
if (seconds < 60) return `${seconds}s`;
|
||||
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
if (minutes < 60) return `${minutes}m ${remainingSeconds}s`;
|
||||
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const remainingMinutes = minutes % 60;
|
||||
return `${hours}h ${remainingMinutes}m`;
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function escapeRegex(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
// ===== Search Functions =====
|
||||
function handleSearchInput(event) {
|
||||
searchFilter = event.target.value;
|
||||
renderStreamContent(activeStreamTab);
|
||||
}
|
||||
|
||||
function clearSearch() {
|
||||
searchFilter = '';
|
||||
const searchInput = document.getElementById('cliStreamSearchInput');
|
||||
if (searchInput) {
|
||||
searchInput.value = '';
|
||||
}
|
||||
renderStreamContent(activeStreamTab);
|
||||
}
|
||||
|
||||
// Translation helper with fallback (uses global t from i18n.js)
|
||||
function _streamT(key) {
|
||||
// First try global t() from i18n.js
|
||||
if (typeof t === 'function' && t !== _streamT) {
|
||||
try {
|
||||
return t(key);
|
||||
} catch (e) {
|
||||
// Fall through to fallbacks
|
||||
}
|
||||
}
|
||||
// Fallback values
|
||||
const fallbacks = {
|
||||
'cliStream.noStreams': 'No active CLI executions',
|
||||
'cliStream.noStreamsHint': 'Start a CLI command to see streaming output',
|
||||
'cliStream.running': 'Running',
|
||||
'cliStream.completed': 'Completed',
|
||||
'cliStream.error': 'Error',
|
||||
'cliStream.autoScroll': 'Auto-scroll',
|
||||
'cliStream.close': 'Close',
|
||||
'cliStream.cannotCloseRunning': 'Cannot close running execution',
|
||||
'cliStream.lines': 'lines',
|
||||
'cliStream.searchPlaceholder': 'Search output...',
|
||||
'cliStream.filterResults': 'results'
|
||||
};
|
||||
return fallbacks[key] || key;
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initCliStreamViewer);
|
||||
} else {
|
||||
initCliStreamViewer();
|
||||
}
|
||||
|
||||
// ===== Global Exposure =====
|
||||
window.toggleCliStreamViewer = toggleCliStreamViewer;
|
||||
window.handleCliStreamStarted = handleCliStreamStarted;
|
||||
window.handleCliStreamOutput = handleCliStreamOutput;
|
||||
window.handleCliStreamCompleted = handleCliStreamCompleted;
|
||||
window.handleCliStreamError = handleCliStreamError;
|
||||
window.switchStreamTab = switchStreamTab;
|
||||
window.closeStream = closeStream;
|
||||
window.clearCompletedStreams = clearCompletedStreams;
|
||||
window.toggleAutoScroll = toggleAutoScroll;
|
||||
window.handleSearchInput = handleSearchInput;
|
||||
window.clearSearch = clearSearch;
|
||||
@@ -155,6 +155,18 @@ function initNavigation() {
|
||||
} else {
|
||||
console.error('renderApiSettings not defined - please refresh the page');
|
||||
}
|
||||
} else if (currentView === 'issue-manager') {
|
||||
if (typeof renderIssueManager === 'function') {
|
||||
renderIssueManager();
|
||||
} else {
|
||||
console.error('renderIssueManager not defined - please refresh the page');
|
||||
}
|
||||
} else if (currentView === 'issue-discovery') {
|
||||
if (typeof renderIssueDiscovery === 'function') {
|
||||
renderIssueDiscovery();
|
||||
} else {
|
||||
console.error('renderIssueDiscovery not defined - please refresh the page');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -199,6 +211,10 @@ function updateContentTitle() {
|
||||
titleEl.textContent = t('title.codexLensManager');
|
||||
} else if (currentView === 'api-settings') {
|
||||
titleEl.textContent = t('title.apiSettings');
|
||||
} else if (currentView === 'issue-manager') {
|
||||
titleEl.textContent = t('title.issueManager');
|
||||
} else if (currentView === 'issue-discovery') {
|
||||
titleEl.textContent = t('title.issueDiscovery');
|
||||
} else if (currentView === 'liteTasks') {
|
||||
const names = { 'lite-plan': t('title.litePlanSessions'), 'lite-fix': t('title.liteFixSessions') };
|
||||
titleEl.textContent = names[currentLiteType] || t('title.liteTasks');
|
||||
|
||||
@@ -217,24 +217,40 @@ function handleNotification(data) {
|
||||
if (typeof handleCliExecutionStarted === 'function') {
|
||||
handleCliExecutionStarted(payload);
|
||||
}
|
||||
// Route to CLI Stream Viewer
|
||||
if (typeof handleCliStreamStarted === 'function') {
|
||||
handleCliStreamStarted(payload);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'CLI_OUTPUT':
|
||||
if (typeof handleCliOutput === 'function') {
|
||||
handleCliOutput(payload);
|
||||
}
|
||||
// Route to CLI Stream Viewer
|
||||
if (typeof handleCliStreamOutput === 'function') {
|
||||
handleCliStreamOutput(payload);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'CLI_EXECUTION_COMPLETED':
|
||||
if (typeof handleCliExecutionCompleted === 'function') {
|
||||
handleCliExecutionCompleted(payload);
|
||||
}
|
||||
// Route to CLI Stream Viewer
|
||||
if (typeof handleCliStreamCompleted === 'function') {
|
||||
handleCliStreamCompleted(payload);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'CLI_EXECUTION_ERROR':
|
||||
if (typeof handleCliExecutionError === 'function') {
|
||||
handleCliExecutionError(payload);
|
||||
}
|
||||
// Route to CLI Stream Viewer
|
||||
if (typeof handleCliStreamError === 'function') {
|
||||
handleCliStreamError(payload);
|
||||
}
|
||||
break;
|
||||
|
||||
// CLI Review Events
|
||||
|
||||
@@ -28,6 +28,8 @@ const i18n = {
|
||||
'common.deleteFailed': 'Delete failed',
|
||||
'common.retry': 'Retry',
|
||||
'common.refresh': 'Refresh',
|
||||
'common.back': 'Back',
|
||||
'common.search': 'Search...',
|
||||
'common.minutes': 'minutes',
|
||||
'common.enabled': 'Enabled',
|
||||
'common.disabled': 'Disabled',
|
||||
@@ -39,7 +41,23 @@ const i18n = {
|
||||
'header.refreshWorkspace': 'Refresh workspace',
|
||||
'header.toggleTheme': 'Toggle theme',
|
||||
'header.language': 'Language',
|
||||
|
||||
'header.cliStream': 'CLI Stream Viewer',
|
||||
|
||||
// CLI Stream Viewer
|
||||
'cliStream.title': 'CLI Stream',
|
||||
'cliStream.clearCompleted': 'Clear Completed',
|
||||
'cliStream.noStreams': 'No active CLI executions',
|
||||
'cliStream.noStreamsHint': 'Start a CLI command to see streaming output',
|
||||
'cliStream.running': 'Running',
|
||||
'cliStream.completed': 'Completed',
|
||||
'cliStream.error': 'Error',
|
||||
'cliStream.autoScroll': 'Auto-scroll',
|
||||
'cliStream.close': 'Close',
|
||||
'cliStream.cannotCloseRunning': 'Cannot close running execution',
|
||||
'cliStream.lines': 'lines',
|
||||
'cliStream.searchPlaceholder': 'Search output...',
|
||||
'cliStream.filterResults': 'results',
|
||||
|
||||
// Sidebar - Project section
|
||||
'nav.project': 'Project',
|
||||
'nav.overview': 'Overview',
|
||||
@@ -546,8 +564,6 @@ const i18n = {
|
||||
'cli.recursiveQueryDesc': 'Aggregate CLI history and memory data from parent and child projects',
|
||||
'cli.maxContextFiles': 'Max Context Files',
|
||||
'cli.maxContextFilesDesc': 'Maximum files to include in smart context',
|
||||
'cli.codeIndexMcp': 'Code Index MCP',
|
||||
'cli.codeIndexMcpDesc': 'Code search provider (updates CLAUDE.md context-tools reference)',
|
||||
|
||||
// CCW Install
|
||||
'ccw.install': 'CCW Install',
|
||||
@@ -1711,6 +1727,203 @@ const i18n = {
|
||||
'coreMemory.belongsToClusters': 'Belongs to Clusters',
|
||||
'coreMemory.relationsError': 'Failed to load relations',
|
||||
|
||||
// Issue Manager
|
||||
'nav.issues': 'Issues',
|
||||
'nav.issueManager': 'Manager',
|
||||
'nav.issueDiscovery': 'Discovery',
|
||||
'title.issueManager': 'Issue Manager',
|
||||
'title.issueDiscovery': 'Issue Discovery',
|
||||
|
||||
// Issue Discovery
|
||||
'discovery.title': 'Issue Discovery',
|
||||
'discovery.description': 'Discover potential issues from multiple perspectives',
|
||||
'discovery.noSessions': 'No discovery sessions',
|
||||
'discovery.noDiscoveries': 'No discoveries yet',
|
||||
'discovery.runHint': 'Run /issue:discover to start discovering issues',
|
||||
'discovery.runCommand': 'Run /issue:discover to start discovering issues',
|
||||
'discovery.sessions': 'Sessions',
|
||||
'discovery.findings': 'Findings',
|
||||
'discovery.phase': 'Phase',
|
||||
'discovery.perspectives': 'Perspectives',
|
||||
'discovery.progress': 'Progress',
|
||||
'discovery.total': 'Total',
|
||||
'discovery.exported': 'Exported',
|
||||
'discovery.dismissed': 'Dismissed',
|
||||
'discovery.pending': 'Pending',
|
||||
'discovery.external': 'External Research',
|
||||
'discovery.selectAll': 'Select All',
|
||||
'discovery.deselectAll': 'Deselect All',
|
||||
'discovery.exportSelected': 'Export Selected',
|
||||
'discovery.dismissSelected': 'Dismiss Selected',
|
||||
'discovery.exportAsIssue': 'Export as Issue',
|
||||
'discovery.dismiss': 'Dismiss',
|
||||
'discovery.keep': 'Keep',
|
||||
'discovery.priority.critical': 'Critical',
|
||||
'discovery.priority.high': 'High',
|
||||
'discovery.priority.medium': 'Medium',
|
||||
'discovery.priority.low': 'Low',
|
||||
'discovery.perspective.bug': 'Bug',
|
||||
'discovery.perspective.ux': 'UX',
|
||||
'discovery.perspective.test': 'Test',
|
||||
'discovery.perspective.quality': 'Quality',
|
||||
'discovery.perspective.security': 'Security',
|
||||
'discovery.perspective.performance': 'Performance',
|
||||
'discovery.perspective.maintainability': 'Maintainability',
|
||||
'discovery.perspective.best-practices': 'Best Practices',
|
||||
'discovery.file': 'File',
|
||||
'discovery.line': 'Line',
|
||||
'discovery.confidence': 'Confidence',
|
||||
'discovery.suggestedIssue': 'Suggested Issue',
|
||||
'discovery.externalRef': 'External Reference',
|
||||
'discovery.noFindings': 'No findings match your filters',
|
||||
'discovery.filterPerspective': 'Filter by Perspective',
|
||||
'discovery.filterPriority': 'Filter by Priority',
|
||||
'discovery.filterAll': 'All',
|
||||
'discovery.allPerspectives': 'All Perspectives',
|
||||
'discovery.allPriorities': 'All Priorities',
|
||||
'discovery.selectFinding': 'Select a finding to preview',
|
||||
'discovery.location': 'Location',
|
||||
'discovery.code': 'Code',
|
||||
'discovery.impact': 'Impact',
|
||||
'discovery.recommendation': 'Recommendation',
|
||||
'discovery.exportAsIssues': 'Export as Issues',
|
||||
'discovery.selectAll': 'Select All',
|
||||
'discovery.deselectAll': 'Deselect All',
|
||||
'discovery.deleteSession': 'Delete Session',
|
||||
'discovery.confirmDelete': 'Are you sure you want to delete this discovery session?',
|
||||
'discovery.deleted': 'Discovery session deleted',
|
||||
'discovery.exportSuccess': 'Findings exported as issues',
|
||||
'discovery.dismissSuccess': 'Findings dismissed',
|
||||
'discovery.backToList': 'Back to Sessions',
|
||||
'discovery.viewDetails': 'View Details',
|
||||
'discovery.inProgress': 'In Progress',
|
||||
'discovery.completed': 'Completed',
|
||||
// issues.* keys (used by issue-manager.js)
|
||||
'issues.title': 'Issue Manager',
|
||||
'issues.description': 'Manage issues, solutions, and execution queue',
|
||||
'issues.viewIssues': 'Issues',
|
||||
'issues.viewQueue': 'Queue',
|
||||
'issues.filterStatus': 'Status',
|
||||
'issues.filterAll': 'All',
|
||||
'issues.noIssues': 'No issues found',
|
||||
'issues.createHint': 'Click "Create" to add your first issue',
|
||||
'issues.priority': 'Priority',
|
||||
'issues.tasks': 'tasks',
|
||||
'issues.solutions': 'solutions',
|
||||
'issues.boundSolution': 'Bound',
|
||||
'issues.queueEmpty': 'Queue is empty',
|
||||
'issues.reorderHint': 'Drag items within a group to reorder',
|
||||
'issues.parallelGroup': 'Parallel',
|
||||
'issues.sequentialGroup': 'Sequential',
|
||||
'issues.dependsOn': 'Depends on',
|
||||
// Create & Search
|
||||
'issues.create': 'Create',
|
||||
'issues.createTitle': 'Create New Issue',
|
||||
'issues.issueId': 'Issue ID',
|
||||
'issues.issueTitle': 'Title',
|
||||
'issues.issueContext': 'Context',
|
||||
'issues.issuePriority': 'Priority',
|
||||
'issues.titlePlaceholder': 'Brief description of the issue',
|
||||
'issues.contextPlaceholder': 'Detailed description, requirements, etc.',
|
||||
'issues.priorityLowest': 'Lowest',
|
||||
'issues.priorityLow': 'Low',
|
||||
'issues.priorityMedium': 'Medium',
|
||||
'issues.priorityHigh': 'High',
|
||||
'issues.priorityCritical': 'Critical',
|
||||
'issues.searchPlaceholder': 'Search issues...',
|
||||
'issues.showing': 'Showing',
|
||||
'issues.of': 'of',
|
||||
'issues.issues': 'issues',
|
||||
'issues.tryDifferentFilter': 'Try adjusting your search or filters',
|
||||
'issues.createFirst': 'Create First Issue',
|
||||
'issues.idRequired': 'Issue ID is required',
|
||||
'issues.titleRequired': 'Title is required',
|
||||
'issues.created': 'Issue created successfully',
|
||||
'issues.confirmDelete': 'Are you sure you want to delete this issue?',
|
||||
'issues.deleted': 'Issue deleted',
|
||||
'issues.idAutoGenerated': 'Auto-generated',
|
||||
'issues.regenerateId': 'Regenerate ID',
|
||||
// Solution detail
|
||||
'issues.solutionDetail': 'Solution Details',
|
||||
'issues.bind': 'Bind',
|
||||
'issues.unbind': 'Unbind',
|
||||
'issues.bound': 'Bound',
|
||||
'issues.totalTasks': 'Total Tasks',
|
||||
'issues.bindStatus': 'Bind Status',
|
||||
'issues.createdAt': 'Created',
|
||||
'issues.taskList': 'Task List',
|
||||
'issues.noTasks': 'No tasks in this solution',
|
||||
'issues.noSolutions': 'No solutions',
|
||||
'issues.viewJson': 'View Raw JSON',
|
||||
'issues.scope': 'Scope',
|
||||
'issues.modificationPoints': 'Modification Points',
|
||||
'issues.implementationSteps': 'Implementation Steps',
|
||||
'issues.acceptanceCriteria': 'Acceptance Criteria',
|
||||
'issues.dependencies': 'Dependencies',
|
||||
'issues.solutionBound': 'Solution bound successfully',
|
||||
'issues.solutionUnbound': 'Solution unbound',
|
||||
// Queue operations
|
||||
'issues.queueEmptyHint': 'Generate execution queue from bound solutions',
|
||||
'issues.createQueue': 'Create Queue',
|
||||
'issues.regenerate': 'Regenerate',
|
||||
'issues.regenerateQueue': 'Regenerate Queue',
|
||||
'issues.refreshQueue': 'Refresh',
|
||||
'issues.executionGroups': 'groups',
|
||||
'issues.totalItems': 'items',
|
||||
'issues.queueRefreshed': 'Queue refreshed',
|
||||
'issues.confirmCreateQueue': 'This will execute /issue:queue command via Claude Code CLI to generate execution queue from bound solutions.\n\nContinue?',
|
||||
'issues.creatingQueue': 'Creating execution queue...',
|
||||
'issues.queueExecutionStarted': 'Queue generation started',
|
||||
'issues.queueCreated': 'Queue created successfully',
|
||||
'issues.queueCreationFailed': 'Queue creation failed',
|
||||
'issues.queueCommandHint': 'Run one of the following commands in your terminal to generate the execution queue from bound solutions:',
|
||||
'issues.queueCommandInfo': 'After running the command, click "Refresh" to see the updated queue.',
|
||||
'issues.alternative': 'Alternative',
|
||||
'issues.refreshAfter': 'Refresh Queue',
|
||||
// issue.* keys (legacy)
|
||||
'issue.viewIssues': 'Issues',
|
||||
'issue.viewQueue': 'Queue',
|
||||
'issue.filterAll': 'All',
|
||||
'issue.filterStatus': 'Status',
|
||||
'issue.filterPriority': 'Priority',
|
||||
'issue.noIssues': 'No issues found',
|
||||
'issue.noIssuesHint': 'Issues will appear here when created via /issue:plan command',
|
||||
'issue.noQueue': 'No tasks in queue',
|
||||
'issue.noQueueHint': 'Run /issue:queue to form execution queue from bound solutions',
|
||||
'issue.tasks': 'tasks',
|
||||
'issue.solutions': 'solutions',
|
||||
'issue.parallel': 'Parallel',
|
||||
'issue.sequential': 'Sequential',
|
||||
'issue.status.registered': 'Registered',
|
||||
'issue.status.planned': 'Planned',
|
||||
'issue.status.queued': 'Queued',
|
||||
'issue.status.executing': 'Executing',
|
||||
'issue.status.completed': 'Completed',
|
||||
'issue.status.failed': 'Failed',
|
||||
'issue.priority.critical': 'Critical',
|
||||
'issue.priority.high': 'High',
|
||||
'issue.priority.medium': 'Medium',
|
||||
'issue.priority.low': 'Low',
|
||||
'issue.detail.context': 'Context',
|
||||
'issue.detail.solutions': 'Solutions',
|
||||
'issue.detail.tasks': 'Tasks',
|
||||
'issue.detail.noSolutions': 'No solutions available',
|
||||
'issue.detail.noTasks': 'No tasks available',
|
||||
'issue.detail.bound': 'Bound',
|
||||
'issue.detail.modificationPoints': 'Modification Points',
|
||||
'issue.detail.implementation': 'Implementation Steps',
|
||||
'issue.detail.acceptance': 'Acceptance Criteria',
|
||||
'issue.queue.reordered': 'Queue reordered',
|
||||
'issue.queue.reorderFailed': 'Failed to reorder queue',
|
||||
'issue.saved': 'Issue saved',
|
||||
'issue.saveFailed': 'Failed to save issue',
|
||||
'issue.taskUpdated': 'Task updated',
|
||||
'issue.taskUpdateFailed': 'Failed to update task',
|
||||
'issue.conflicts': 'Conflicts',
|
||||
'issue.noConflicts': 'No conflicts detected',
|
||||
'issue.conflict.resolved': 'Resolved',
|
||||
'issue.conflict.pending': 'Pending',
|
||||
|
||||
// Common additions
|
||||
'common.copyId': 'Copy ID',
|
||||
'common.copied': 'Copied!',
|
||||
@@ -1737,6 +1950,8 @@ const i18n = {
|
||||
'common.deleteFailed': '删除失败',
|
||||
'common.retry': '重试',
|
||||
'common.refresh': '刷新',
|
||||
'common.back': '返回',
|
||||
'common.search': '搜索...',
|
||||
'common.minutes': '分钟',
|
||||
'common.enabled': '已启用',
|
||||
'common.disabled': '已禁用',
|
||||
@@ -1748,7 +1963,23 @@ const i18n = {
|
||||
'header.refreshWorkspace': '刷新工作区',
|
||||
'header.toggleTheme': '切换主题',
|
||||
'header.language': '语言',
|
||||
|
||||
'header.cliStream': 'CLI 流式输出',
|
||||
|
||||
// CLI Stream Viewer
|
||||
'cliStream.title': 'CLI 流式输出',
|
||||
'cliStream.clearCompleted': '清除已完成',
|
||||
'cliStream.noStreams': '没有活动的 CLI 执行',
|
||||
'cliStream.noStreamsHint': '启动 CLI 命令以查看流式输出',
|
||||
'cliStream.running': '运行中',
|
||||
'cliStream.completed': '已完成',
|
||||
'cliStream.error': '错误',
|
||||
'cliStream.autoScroll': '自动滚动',
|
||||
'cliStream.close': '关闭',
|
||||
'cliStream.cannotCloseRunning': '无法关闭运行中的执行',
|
||||
'cliStream.lines': '行',
|
||||
'cliStream.searchPlaceholder': '搜索输出...',
|
||||
'cliStream.filterResults': '条结果',
|
||||
|
||||
// Sidebar - Project section
|
||||
'nav.project': '项目',
|
||||
'nav.overview': '概览',
|
||||
@@ -2256,8 +2487,6 @@ const i18n = {
|
||||
'cli.recursiveQueryDesc': '聚合显示父项目和子项目的 CLI 历史与内存数据',
|
||||
'cli.maxContextFiles': '最大上下文文件数',
|
||||
'cli.maxContextFilesDesc': '智能上下文包含的最大文件数',
|
||||
'cli.codeIndexMcp': '代码索引 MCP',
|
||||
'cli.codeIndexMcpDesc': '代码搜索提供者 (更新 CLAUDE.md 的 context-tools 引用)',
|
||||
|
||||
// CCW Install
|
||||
'ccw.install': 'CCW 安装',
|
||||
@@ -3308,6 +3537,8 @@ const i18n = {
|
||||
'common.edit': '编辑',
|
||||
'common.close': '关闭',
|
||||
'common.refresh': '刷新',
|
||||
'common.back': '返回',
|
||||
'common.search': '搜索...',
|
||||
'common.refreshed': '已刷新',
|
||||
'common.refreshing': '刷新中...',
|
||||
'common.loading': '加载中...',
|
||||
@@ -3429,6 +3660,203 @@ const i18n = {
|
||||
'coreMemory.belongsToClusters': '所属聚类',
|
||||
'coreMemory.relationsError': '加载关联失败',
|
||||
|
||||
// Issue Manager
|
||||
'nav.issues': '议题',
|
||||
'nav.issueManager': '管理器',
|
||||
'nav.issueDiscovery': '发现',
|
||||
'title.issueManager': '议题管理器',
|
||||
'title.issueDiscovery': '议题发现',
|
||||
|
||||
// Issue Discovery
|
||||
'discovery.title': '议题发现',
|
||||
'discovery.description': '从多个视角发现潜在问题',
|
||||
'discovery.noSessions': '暂无发现会话',
|
||||
'discovery.noDiscoveries': '暂无发现',
|
||||
'discovery.runHint': '运行 /issue:discover 开始发现问题',
|
||||
'discovery.runCommand': '运行 /issue:discover 开始发现问题',
|
||||
'discovery.sessions': '会话',
|
||||
'discovery.findings': '发现',
|
||||
'discovery.phase': '阶段',
|
||||
'discovery.perspectives': '视角',
|
||||
'discovery.progress': '进度',
|
||||
'discovery.total': '总计',
|
||||
'discovery.exported': '已导出',
|
||||
'discovery.dismissed': '已忽略',
|
||||
'discovery.pending': '待处理',
|
||||
'discovery.external': '外部研究',
|
||||
'discovery.selectAll': '全选',
|
||||
'discovery.deselectAll': '取消全选',
|
||||
'discovery.exportSelected': '导出选中',
|
||||
'discovery.dismissSelected': '忽略选中',
|
||||
'discovery.exportAsIssue': '导出为议题',
|
||||
'discovery.dismiss': '忽略',
|
||||
'discovery.keep': '保留',
|
||||
'discovery.priority.critical': '紧急',
|
||||
'discovery.priority.high': '高',
|
||||
'discovery.priority.medium': '中',
|
||||
'discovery.priority.low': '低',
|
||||
'discovery.perspective.bug': 'Bug',
|
||||
'discovery.perspective.ux': '用户体验',
|
||||
'discovery.perspective.test': '测试',
|
||||
'discovery.perspective.quality': '代码质量',
|
||||
'discovery.perspective.security': '安全',
|
||||
'discovery.perspective.performance': '性能',
|
||||
'discovery.perspective.maintainability': '可维护性',
|
||||
'discovery.perspective.best-practices': '最佳实践',
|
||||
'discovery.file': '文件',
|
||||
'discovery.line': '行号',
|
||||
'discovery.confidence': '置信度',
|
||||
'discovery.suggestedIssue': '建议议题',
|
||||
'discovery.externalRef': '外部参考',
|
||||
'discovery.noFindings': '没有匹配的发现',
|
||||
'discovery.filterPerspective': '按视角筛选',
|
||||
'discovery.filterPriority': '按优先级筛选',
|
||||
'discovery.filterAll': '全部',
|
||||
'discovery.allPerspectives': '所有视角',
|
||||
'discovery.allPriorities': '所有优先级',
|
||||
'discovery.selectFinding': '选择一个发现以预览',
|
||||
'discovery.location': '位置',
|
||||
'discovery.code': '代码',
|
||||
'discovery.impact': '影响',
|
||||
'discovery.recommendation': '建议',
|
||||
'discovery.exportAsIssues': '导出为议题',
|
||||
'discovery.selectAll': '全选',
|
||||
'discovery.deselectAll': '取消全选',
|
||||
'discovery.deleteSession': '删除会话',
|
||||
'discovery.confirmDelete': '确定要删除此发现会话吗?',
|
||||
'discovery.deleted': '发现会话已删除',
|
||||
'discovery.exportSuccess': '发现已导出为议题',
|
||||
'discovery.dismissSuccess': '发现已忽略',
|
||||
'discovery.backToList': '返回列表',
|
||||
'discovery.viewDetails': '查看详情',
|
||||
'discovery.inProgress': '进行中',
|
||||
'discovery.completed': '已完成',
|
||||
// issues.* keys (used by issue-manager.js)
|
||||
'issues.title': '议题管理器',
|
||||
'issues.description': '管理议题、解决方案和执行队列',
|
||||
'issues.viewIssues': '议题',
|
||||
'issues.viewQueue': '队列',
|
||||
'issues.filterStatus': '状态',
|
||||
'issues.filterAll': '全部',
|
||||
'issues.noIssues': '暂无议题',
|
||||
'issues.createHint': '点击"创建"添加您的第一个议题',
|
||||
'issues.priority': '优先级',
|
||||
'issues.tasks': '任务',
|
||||
'issues.solutions': '解决方案',
|
||||
'issues.boundSolution': '已绑定',
|
||||
'issues.queueEmpty': '队列为空',
|
||||
'issues.reorderHint': '在组内拖拽项目以重新排序',
|
||||
'issues.parallelGroup': '并行',
|
||||
'issues.sequentialGroup': '顺序',
|
||||
'issues.dependsOn': '依赖于',
|
||||
// Create & Search
|
||||
'issues.create': '创建',
|
||||
'issues.createTitle': '创建新议题',
|
||||
'issues.issueId': '议题ID',
|
||||
'issues.issueTitle': '标题',
|
||||
'issues.issueContext': '上下文',
|
||||
'issues.issuePriority': '优先级',
|
||||
'issues.titlePlaceholder': '简要描述议题',
|
||||
'issues.contextPlaceholder': '详细描述、需求等',
|
||||
'issues.priorityLowest': '最低',
|
||||
'issues.priorityLow': '低',
|
||||
'issues.priorityMedium': '中',
|
||||
'issues.priorityHigh': '高',
|
||||
'issues.priorityCritical': '紧急',
|
||||
'issues.searchPlaceholder': '搜索议题...',
|
||||
'issues.showing': '显示',
|
||||
'issues.of': '共',
|
||||
'issues.issues': '条议题',
|
||||
'issues.tryDifferentFilter': '尝试调整搜索或筛选条件',
|
||||
'issues.createFirst': '创建第一个议题',
|
||||
'issues.idRequired': '议题ID为必填',
|
||||
'issues.titleRequired': '标题为必填',
|
||||
'issues.created': '议题创建成功',
|
||||
'issues.confirmDelete': '确定要删除此议题吗?',
|
||||
'issues.deleted': '议题已删除',
|
||||
'issues.idAutoGenerated': '自动生成',
|
||||
'issues.regenerateId': '重新生成ID',
|
||||
// Solution detail
|
||||
'issues.solutionDetail': '解决方案详情',
|
||||
'issues.bind': '绑定',
|
||||
'issues.unbind': '解绑',
|
||||
'issues.bound': '已绑定',
|
||||
'issues.totalTasks': '任务总数',
|
||||
'issues.bindStatus': '绑定状态',
|
||||
'issues.createdAt': '创建时间',
|
||||
'issues.taskList': '任务列表',
|
||||
'issues.noTasks': '此解决方案无任务',
|
||||
'issues.noSolutions': '暂无解决方案',
|
||||
'issues.viewJson': '查看原始JSON',
|
||||
'issues.scope': '作用域',
|
||||
'issues.modificationPoints': '修改点',
|
||||
'issues.implementationSteps': '实现步骤',
|
||||
'issues.acceptanceCriteria': '验收标准',
|
||||
'issues.dependencies': '依赖项',
|
||||
'issues.solutionBound': '解决方案已绑定',
|
||||
'issues.solutionUnbound': '解决方案已解绑',
|
||||
// Queue operations
|
||||
'issues.queueEmptyHint': '从绑定的解决方案生成执行队列',
|
||||
'issues.createQueue': '创建队列',
|
||||
'issues.regenerate': '重新生成',
|
||||
'issues.regenerateQueue': '重新生成队列',
|
||||
'issues.refreshQueue': '刷新',
|
||||
'issues.executionGroups': '个执行组',
|
||||
'issues.totalItems': '个任务',
|
||||
'issues.queueRefreshed': '队列已刷新',
|
||||
'issues.confirmCreateQueue': '这将通过 Claude Code CLI 执行 /issue:queue 命令,从绑定的解决方案生成执行队列。\n\n是否继续?',
|
||||
'issues.creatingQueue': '正在创建执行队列...',
|
||||
'issues.queueExecutionStarted': '队列生成已启动',
|
||||
'issues.queueCreated': '队列创建成功',
|
||||
'issues.queueCreationFailed': '队列创建失败',
|
||||
'issues.queueCommandHint': '在终端中运行以下命令之一,从绑定的解决方案生成执行队列:',
|
||||
'issues.queueCommandInfo': '运行命令后,点击"刷新"查看更新后的队列。',
|
||||
'issues.alternative': '或者',
|
||||
'issues.refreshAfter': '刷新队列',
|
||||
// issue.* keys (legacy)
|
||||
'issue.viewIssues': '议题',
|
||||
'issue.viewQueue': '队列',
|
||||
'issue.filterAll': '全部',
|
||||
'issue.filterStatus': '状态',
|
||||
'issue.filterPriority': '优先级',
|
||||
'issue.noIssues': '暂无议题',
|
||||
'issue.noIssuesHint': '通过 /issue:plan 命令创建的议题将显示在此处',
|
||||
'issue.noQueue': '队列中暂无任务',
|
||||
'issue.noQueueHint': '运行 /issue:queue 从绑定的解决方案生成执行队列',
|
||||
'issue.tasks': '任务',
|
||||
'issue.solutions': '解决方案',
|
||||
'issue.parallel': '并行',
|
||||
'issue.sequential': '顺序',
|
||||
'issue.status.registered': '已注册',
|
||||
'issue.status.planned': '已规划',
|
||||
'issue.status.queued': '已入队',
|
||||
'issue.status.executing': '执行中',
|
||||
'issue.status.completed': '已完成',
|
||||
'issue.status.failed': '失败',
|
||||
'issue.priority.critical': '紧急',
|
||||
'issue.priority.high': '高',
|
||||
'issue.priority.medium': '中',
|
||||
'issue.priority.low': '低',
|
||||
'issue.detail.context': '上下文',
|
||||
'issue.detail.solutions': '解决方案',
|
||||
'issue.detail.tasks': '任务',
|
||||
'issue.detail.noSolutions': '暂无解决方案',
|
||||
'issue.detail.noTasks': '暂无任务',
|
||||
'issue.detail.bound': '已绑定',
|
||||
'issue.detail.modificationPoints': '修改点',
|
||||
'issue.detail.implementation': '实现步骤',
|
||||
'issue.detail.acceptance': '验收标准',
|
||||
'issue.queue.reordered': '队列已重排',
|
||||
'issue.queue.reorderFailed': '队列重排失败',
|
||||
'issue.saved': '议题已保存',
|
||||
'issue.saveFailed': '保存议题失败',
|
||||
'issue.taskUpdated': '任务已更新',
|
||||
'issue.taskUpdateFailed': '更新任务失败',
|
||||
'issue.conflicts': '冲突',
|
||||
'issue.noConflicts': '未检测到冲突',
|
||||
'issue.conflict.resolved': '已解决',
|
||||
'issue.conflict.pending': '待处理',
|
||||
|
||||
// Common additions
|
||||
'common.copyId': '复制 ID',
|
||||
'common.copied': '已复制!',
|
||||
|
||||
@@ -987,24 +987,6 @@ function renderCliSettingsSection() {
|
||||
'</div>' +
|
||||
'<p class="cli-setting-desc">' + t('cli.maxContextFilesDesc') + '</p>' +
|
||||
'</div>' +
|
||||
'<div class="cli-setting-item">' +
|
||||
'<label class="cli-setting-label">' +
|
||||
'<i data-lucide="search" class="w-3 h-3"></i>' +
|
||||
t('cli.codeIndexMcp') +
|
||||
'</label>' +
|
||||
'<div class="cli-setting-control">' +
|
||||
'<select class="cli-setting-select" onchange="setCodeIndexMcpProvider(this.value)">' +
|
||||
'<option value="codexlens"' + (codeIndexMcpProvider === 'codexlens' ? ' selected' : '') + '>CodexLens</option>' +
|
||||
'<option value="ace"' + (codeIndexMcpProvider === 'ace' ? ' selected' : '') + '>ACE (Augment)</option>' +
|
||||
'<option value="none"' + (codeIndexMcpProvider === 'none' ? ' selected' : '') + '>None (Built-in)</option>' +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
'<p class="cli-setting-desc">' + t('cli.codeIndexMcpDesc') + '</p>' +
|
||||
'<p class="cli-setting-desc text-xs text-muted-foreground">' +
|
||||
'<i data-lucide="file-text" class="w-3 h-3 inline-block mr-1"></i>' +
|
||||
'Current: <code class="bg-muted px-1 rounded">' + getContextToolsFileName(codeIndexMcpProvider) + '</code>' +
|
||||
'</p>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
container.innerHTML = settingsHtml;
|
||||
|
||||
@@ -446,6 +446,12 @@ async function loadSemanticDepsStatus() {
|
||||
* Build GPU mode selector HTML
|
||||
*/
|
||||
function buildGpuModeSelector(gpuInfo) {
|
||||
// Check if DirectML is unavailable due to Python environment
|
||||
var directmlUnavailableReason = null;
|
||||
if (!gpuInfo.available.includes('directml') && gpuInfo.pythonEnv && gpuInfo.pythonEnv.error) {
|
||||
directmlUnavailableReason = gpuInfo.pythonEnv.error;
|
||||
}
|
||||
|
||||
var modes = [
|
||||
{
|
||||
id: 'cpu',
|
||||
@@ -457,10 +463,13 @@ function buildGpuModeSelector(gpuInfo) {
|
||||
{
|
||||
id: 'directml',
|
||||
label: 'DirectML',
|
||||
desc: t('codexlens.directmlModeDesc') || 'Windows GPU (NVIDIA/AMD/Intel)',
|
||||
desc: directmlUnavailableReason
|
||||
? directmlUnavailableReason
|
||||
: (t('codexlens.directmlModeDesc') || 'Windows GPU (NVIDIA/AMD/Intel)'),
|
||||
icon: 'cpu',
|
||||
available: gpuInfo.available.includes('directml'),
|
||||
recommended: gpuInfo.mode === 'directml'
|
||||
recommended: gpuInfo.mode === 'directml',
|
||||
warning: directmlUnavailableReason
|
||||
},
|
||||
{
|
||||
id: 'cuda',
|
||||
@@ -487,6 +496,7 @@ function buildGpuModeSelector(gpuInfo) {
|
||||
var isDisabled = !mode.available;
|
||||
var isRecommended = mode.recommended;
|
||||
var isDefault = mode.id === gpuInfo.mode;
|
||||
var hasWarning = mode.warning;
|
||||
|
||||
html +=
|
||||
'<label class="flex items-center gap-3 p-2 rounded border cursor-pointer hover:bg-muted/50 transition-colors ' +
|
||||
@@ -502,7 +512,7 @@ function buildGpuModeSelector(gpuInfo) {
|
||||
(isRecommended ? '<span class="text-xs bg-primary/20 text-primary px-1.5 py-0.5 rounded">' + (t('common.recommended') || 'Recommended') + '</span>' : '') +
|
||||
(isDisabled ? '<span class="text-xs text-muted-foreground">(' + (t('common.unavailable') || 'Unavailable') + ')</span>' : '') +
|
||||
'</div>' +
|
||||
'<div class="text-xs text-muted-foreground">' + mode.desc + '</div>' +
|
||||
'<div class="text-xs ' + (hasWarning ? 'text-warning' : 'text-muted-foreground') + '">' + mode.desc + '</div>' +
|
||||
'</div>' +
|
||||
'</label>';
|
||||
});
|
||||
|
||||
@@ -168,16 +168,22 @@ async function loadAvailableSkills() {
|
||||
if (!response.ok) throw new Error('Failed to load skills');
|
||||
const data = await response.json();
|
||||
|
||||
// Combine project and user skills (API returns { projectSkills: [], userSkills: [] })
|
||||
const allSkills = [
|
||||
...(data.projectSkills || []).map(s => ({ ...s, scope: 'project' })),
|
||||
...(data.userSkills || []).map(s => ({ ...s, scope: 'user' }))
|
||||
];
|
||||
|
||||
const container = document.getElementById('skill-discovery-skill-context');
|
||||
if (container && data.skills) {
|
||||
if (data.skills.length === 0) {
|
||||
if (container) {
|
||||
if (allSkills.length === 0) {
|
||||
container.innerHTML = `
|
||||
<span class="font-mono bg-muted px-1.5 py-0.5 rounded">${t('hook.wizard.availableSkills')}</span>
|
||||
<span class="text-muted-foreground ml-2">${t('hook.wizard.noSkillsFound').split('.')[0]}</span>
|
||||
`;
|
||||
} else {
|
||||
const skillBadges = data.skills.map(skill => `
|
||||
<span class="px-2 py-0.5 bg-emerald-500/10 text-emerald-500 rounded" title="${escapeHtml(skill.description)}">${escapeHtml(skill.name)}</span>
|
||||
const skillBadges = allSkills.map(skill => `
|
||||
<span class="px-2 py-0.5 bg-emerald-500/10 text-emerald-500 rounded" title="${escapeHtml(skill.description || '')}">${escapeHtml(skill.name)}</span>
|
||||
`).join('');
|
||||
container.innerHTML = `
|
||||
<span class="font-mono bg-muted px-1.5 py-0.5 rounded">${t('hook.wizard.availableSkills')}</span>
|
||||
@@ -187,7 +193,7 @@ async function loadAvailableSkills() {
|
||||
}
|
||||
|
||||
// Store skills for wizard use
|
||||
window.availableSkills = data.skills || [];
|
||||
window.availableSkills = allSkills;
|
||||
} catch (err) {
|
||||
console.error('Failed to load skills:', err);
|
||||
const container = document.getElementById('skill-discovery-skill-context');
|
||||
|
||||
730
ccw/src/templates/dashboard-js/views/issue-discovery.js
Normal file
730
ccw/src/templates/dashboard-js/views/issue-discovery.js
Normal file
@@ -0,0 +1,730 @@
|
||||
// ==========================================
|
||||
// ISSUE DISCOVERY VIEW
|
||||
// Manages discovery sessions and findings
|
||||
// ==========================================
|
||||
|
||||
// ========== Discovery State ==========
|
||||
var discoveryData = {
|
||||
discoveries: [],
|
||||
selectedDiscovery: null,
|
||||
selectedFinding: null,
|
||||
findings: [],
|
||||
perspectiveFilter: 'all',
|
||||
priorityFilter: 'all',
|
||||
searchQuery: '',
|
||||
selectedFindings: new Set(),
|
||||
viewMode: 'list' // 'list' | 'detail'
|
||||
};
|
||||
var discoveryLoading = false;
|
||||
var discoveryPollingInterval = null;
|
||||
|
||||
// ========== Helper Functions ==========
|
||||
function getFilteredFindings() {
|
||||
const findings = discoveryData.findings || [];
|
||||
let filtered = findings;
|
||||
|
||||
if (discoveryData.perspectiveFilter !== 'all') {
|
||||
filtered = filtered.filter(f => f.perspective === discoveryData.perspectiveFilter);
|
||||
}
|
||||
if (discoveryData.priorityFilter !== 'all') {
|
||||
filtered = filtered.filter(f => f.priority === discoveryData.priorityFilter);
|
||||
}
|
||||
if (discoveryData.searchQuery) {
|
||||
const q = discoveryData.searchQuery.toLowerCase();
|
||||
filtered = filtered.filter(f =>
|
||||
(f.title && f.title.toLowerCase().includes(q)) ||
|
||||
(f.file && f.file.toLowerCase().includes(q)) ||
|
||||
(f.description && f.description.toLowerCase().includes(q))
|
||||
);
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
// ========== Main Render Function ==========
|
||||
async function renderIssueDiscovery() {
|
||||
const container = document.getElementById('mainContent');
|
||||
if (!container) return;
|
||||
|
||||
// Hide stats grid and carousel
|
||||
hideStatsAndCarousel();
|
||||
|
||||
// Show loading state
|
||||
container.innerHTML = '<div class="discovery-manager loading">' +
|
||||
'<div class="loading-spinner"><i data-lucide="loader-2" class="w-8 h-8 animate-spin"></i></div>' +
|
||||
'<p>' + t('common.loading') + '</p>' +
|
||||
'</div>';
|
||||
lucide.createIcons();
|
||||
|
||||
// Load data
|
||||
await loadDiscoveryData();
|
||||
|
||||
// Render the main view
|
||||
renderDiscoveryView();
|
||||
}
|
||||
|
||||
// ========== Data Loading ==========
|
||||
async function loadDiscoveryData() {
|
||||
discoveryLoading = true;
|
||||
try {
|
||||
const response = await fetch('/api/discoveries?path=' + encodeURIComponent(projectPath));
|
||||
if (!response.ok) throw new Error('Failed to load discoveries');
|
||||
const data = await response.json();
|
||||
discoveryData.discoveries = data.discoveries || [];
|
||||
updateDiscoveryBadge();
|
||||
} catch (err) {
|
||||
console.error('Failed to load discoveries:', err);
|
||||
discoveryData.discoveries = [];
|
||||
} finally {
|
||||
discoveryLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDiscoveryDetail(discoveryId) {
|
||||
try {
|
||||
const response = await fetch('/api/discoveries/' + encodeURIComponent(discoveryId) + '?path=' + encodeURIComponent(projectPath));
|
||||
if (!response.ok) throw new Error('Failed to load discovery detail');
|
||||
return await response.json();
|
||||
} catch (err) {
|
||||
console.error('Failed to load discovery detail:', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDiscoveryFindings(discoveryId) {
|
||||
try {
|
||||
let url = '/api/discoveries/' + encodeURIComponent(discoveryId) + '/findings?path=' + encodeURIComponent(projectPath);
|
||||
if (discoveryData.perspectiveFilter !== 'all') {
|
||||
url += '&perspective=' + encodeURIComponent(discoveryData.perspectiveFilter);
|
||||
}
|
||||
if (discoveryData.priorityFilter !== 'all') {
|
||||
url += '&priority=' + encodeURIComponent(discoveryData.priorityFilter);
|
||||
}
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error('Failed to load findings');
|
||||
const data = await response.json();
|
||||
return data.findings || [];
|
||||
} catch (err) {
|
||||
console.error('Failed to load findings:', err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDiscoveryProgress(discoveryId) {
|
||||
try {
|
||||
const response = await fetch('/api/discoveries/' + encodeURIComponent(discoveryId) + '/progress?path=' + encodeURIComponent(projectPath));
|
||||
if (!response.ok) return null;
|
||||
return await response.json();
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function updateDiscoveryBadge() {
|
||||
const badge = document.getElementById('badgeDiscovery');
|
||||
if (badge) {
|
||||
badge.textContent = discoveryData.discoveries.length;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Main View Render ==========
|
||||
function renderDiscoveryView() {
|
||||
const container = document.getElementById('mainContent');
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="discovery-manager">
|
||||
<!-- Header -->
|
||||
<div class="discovery-header mb-6">
|
||||
<div class="flex items-center justify-between flex-wrap gap-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-primary/10 rounded-lg flex items-center justify-center">
|
||||
<i data-lucide="search-code" class="w-5 h-5 text-primary"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-foreground">${t('discovery.title') || 'Issue Discovery'}</h2>
|
||||
<p class="text-sm text-muted-foreground">${t('discovery.description') || 'Discover potential issues from multiple perspectives'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
${discoveryData.viewMode === 'detail' ? `
|
||||
<button class="discovery-back-btn" onclick="backToDiscoveryList()">
|
||||
<i data-lucide="arrow-left" class="w-4 h-4"></i>
|
||||
<span>${t('common.back') || 'Back'}</span>
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${discoveryData.viewMode === 'list' ? renderDiscoveryListSection() : renderDiscoveryDetailSection()}
|
||||
</div>
|
||||
`;
|
||||
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
// ========== Discovery List Section ==========
|
||||
function renderDiscoveryListSection() {
|
||||
const discoveries = discoveryData.discoveries || [];
|
||||
|
||||
if (discoveries.length === 0) {
|
||||
return `
|
||||
<div class="discovery-empty">
|
||||
<div class="empty-icon">
|
||||
<i data-lucide="search-x" class="w-12 h-12 text-muted-foreground"></i>
|
||||
</div>
|
||||
<h3 class="text-lg font-medium text-foreground mt-4">${t('discovery.noDiscoveries') || 'No discoveries yet'}</h3>
|
||||
<p class="text-sm text-muted-foreground mt-2">${t('discovery.runCommand') || 'Run /issue:discover to start discovering issues'}</p>
|
||||
<div class="mt-4 p-3 bg-muted/50 rounded-lg">
|
||||
<code class="text-sm text-primary">/issue:discover src/auth/**</code>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="discovery-list-container">
|
||||
${discoveries.map(d => renderDiscoveryCard(d)).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderDiscoveryCard(discovery) {
|
||||
const { discovery_id, target_pattern, perspectives, phase, total_findings, issues_generated, priority_distribution, progress } = discovery;
|
||||
|
||||
const isComplete = phase === 'complete';
|
||||
const isRunning = phase && phase !== 'complete' && phase !== 'failed';
|
||||
|
||||
// Calculate progress percentage
|
||||
let progressPercent = 0;
|
||||
if (progress && progress.perspective_analysis) {
|
||||
progressPercent = progress.perspective_analysis.percent_complete || 0;
|
||||
} else if (isComplete) {
|
||||
progressPercent = 100;
|
||||
}
|
||||
|
||||
// Priority distribution bar
|
||||
const critical = priority_distribution?.critical || 0;
|
||||
const high = priority_distribution?.high || 0;
|
||||
const medium = priority_distribution?.medium || 0;
|
||||
const low = priority_distribution?.low || 0;
|
||||
const total = critical + high + medium + low || 1;
|
||||
|
||||
return `
|
||||
<div class="discovery-card ${isComplete ? 'complete' : ''} ${isRunning ? 'running' : ''}" onclick="viewDiscoveryDetail('${discovery_id}')">
|
||||
<div class="discovery-card-header">
|
||||
<div class="discovery-id">
|
||||
<i data-lucide="search" class="w-4 h-4"></i>
|
||||
<span>${discovery_id}</span>
|
||||
</div>
|
||||
<span class="discovery-phase ${phase}">${phase || 'unknown'}</span>
|
||||
</div>
|
||||
|
||||
<div class="discovery-card-body">
|
||||
<div class="discovery-target">
|
||||
<i data-lucide="folder" class="w-4 h-4 text-muted-foreground"></i>
|
||||
<span class="text-sm text-foreground">${target_pattern || 'N/A'}</span>
|
||||
</div>
|
||||
|
||||
${perspectives && perspectives.length > 0 ? `
|
||||
<div class="discovery-perspectives">
|
||||
${perspectives.slice(0, 5).map(p => `<span class="perspective-badge ${p}">${p}</span>`).join('')}
|
||||
${perspectives.length > 5 ? `<span class="perspective-badge more">+${perspectives.length - 5}</span>` : ''}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${isRunning ? `
|
||||
<div class="discovery-progress-bar">
|
||||
<div class="progress-fill" style="width: ${progressPercent}%"></div>
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground mt-1">${progressPercent}% complete</div>
|
||||
` : ''}
|
||||
|
||||
<div class="discovery-stats">
|
||||
<div class="stat">
|
||||
<span class="stat-value">${total_findings || 0}</span>
|
||||
<span class="stat-label">${t('discovery.findings') || 'Findings'}</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-value">${issues_generated || 0}</span>
|
||||
<span class="stat-label">${t('discovery.exported') || 'Exported'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${total_findings > 0 ? `
|
||||
<div class="discovery-priority-bar">
|
||||
<div class="priority-segment critical" style="width: ${(critical / total) * 100}%" title="Critical: ${critical}"></div>
|
||||
<div class="priority-segment high" style="width: ${(high / total) * 100}%" title="High: ${high}"></div>
|
||||
<div class="priority-segment medium" style="width: ${(medium / total) * 100}%" title="Medium: ${medium}"></div>
|
||||
<div class="priority-segment low" style="width: ${(low / total) * 100}%" title="Low: ${low}"></div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<div class="discovery-card-footer">
|
||||
<button class="discovery-action-btn" onclick="event.stopPropagation(); deleteDiscovery('${discovery_id}')">
|
||||
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ========== Discovery Detail Section ==========
|
||||
function renderDiscoveryDetailSection() {
|
||||
const discovery = discoveryData.selectedDiscovery;
|
||||
if (!discovery) {
|
||||
return '<div class="loading-spinner"><i data-lucide="loader-2" class="w-8 h-8 animate-spin"></i></div>';
|
||||
}
|
||||
|
||||
const findings = discoveryData.findings || [];
|
||||
const perspectives = [...new Set(findings.map(f => f.perspective))];
|
||||
const filteredFindings = getFilteredFindings();
|
||||
|
||||
return `
|
||||
<div class="discovery-detail-container">
|
||||
<!-- Left Panel: Findings List -->
|
||||
<div class="discovery-findings-panel">
|
||||
<!-- Toolbar -->
|
||||
<div class="discovery-toolbar">
|
||||
<div class="toolbar-filters">
|
||||
<select class="filter-select" onchange="filterDiscoveryByPerspective(this.value)">
|
||||
<option value="all" ${discoveryData.perspectiveFilter === 'all' ? 'selected' : ''}>${t('discovery.allPerspectives') || 'All Perspectives'}</option>
|
||||
${perspectives.map(p => `<option value="${p}" ${discoveryData.perspectiveFilter === p ? 'selected' : ''}>${p}</option>`).join('')}
|
||||
</select>
|
||||
<select class="filter-select" onchange="filterDiscoveryByPriority(this.value)">
|
||||
<option value="all" ${discoveryData.priorityFilter === 'all' ? 'selected' : ''}>${t('discovery.allPriorities') || 'All Priorities'}</option>
|
||||
<option value="critical" ${discoveryData.priorityFilter === 'critical' ? 'selected' : ''}>Critical</option>
|
||||
<option value="high" ${discoveryData.priorityFilter === 'high' ? 'selected' : ''}>High</option>
|
||||
<option value="medium" ${discoveryData.priorityFilter === 'medium' ? 'selected' : ''}>Medium</option>
|
||||
<option value="low" ${discoveryData.priorityFilter === 'low' ? 'selected' : ''}>Low</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="toolbar-search">
|
||||
<i data-lucide="search" class="w-4 h-4"></i>
|
||||
<input type="text" placeholder="${t('common.search') || 'Search...'}"
|
||||
value="${discoveryData.searchQuery}"
|
||||
oninput="searchDiscoveryFindings(this.value)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Findings Count -->
|
||||
<div class="findings-count">
|
||||
<div class="findings-count-left">
|
||||
<span>${filteredFindings.length} ${t('discovery.findings') || 'findings'}</span>
|
||||
${discoveryData.selectedFindings.size > 0 ? `
|
||||
<span class="selected-count">(${discoveryData.selectedFindings.size} selected)</span>
|
||||
` : ''}
|
||||
</div>
|
||||
<div class="findings-count-actions">
|
||||
<button class="select-action-btn" onclick="selectAllFindings()">
|
||||
<i data-lucide="check-square" class="w-3 h-3"></i>
|
||||
<span>${t('discovery.selectAll') || 'Select All'}</span>
|
||||
</button>
|
||||
<button class="select-action-btn" onclick="deselectAllFindings()">
|
||||
<i data-lucide="square" class="w-3 h-3"></i>
|
||||
<span>${t('discovery.deselectAll') || 'Deselect All'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Findings List -->
|
||||
<div class="findings-list">
|
||||
${filteredFindings.length === 0 ? `
|
||||
<div class="findings-empty">
|
||||
<i data-lucide="inbox" class="w-8 h-8 text-muted-foreground"></i>
|
||||
<p>${t('discovery.noFindings') || 'No findings match your filters'}</p>
|
||||
</div>
|
||||
` : filteredFindings.map(f => renderFindingItem(f)).join('')}
|
||||
</div>
|
||||
|
||||
<!-- Bulk Actions -->
|
||||
${discoveryData.selectedFindings.size > 0 ? `
|
||||
<div class="bulk-actions">
|
||||
<span class="bulk-count">${discoveryData.selectedFindings.size} selected</span>
|
||||
<button class="bulk-action-btn export" onclick="exportSelectedFindings()">
|
||||
<i data-lucide="upload" class="w-4 h-4"></i>
|
||||
<span>${t('discovery.exportAsIssues') || 'Export as Issues'}</span>
|
||||
</button>
|
||||
<button class="bulk-action-btn dismiss" onclick="dismissSelectedFindings()">
|
||||
<i data-lucide="x" class="w-4 h-4"></i>
|
||||
<span>${t('discovery.dismiss') || 'Dismiss'}</span>
|
||||
</button>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<!-- Right Panel: Finding Preview -->
|
||||
<div class="discovery-preview-panel">
|
||||
${discoveryData.selectedFinding ? renderFindingPreview(discoveryData.selectedFinding) : `
|
||||
<div class="preview-empty">
|
||||
<i data-lucide="mouse-pointer-click" class="w-12 h-12 text-muted-foreground"></i>
|
||||
<p>${t('discovery.selectFinding') || 'Select a finding to preview'}</p>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderFindingItem(finding) {
|
||||
const isSelected = discoveryData.selectedFindings.has(finding.id);
|
||||
const isActive = discoveryData.selectedFinding?.id === finding.id;
|
||||
const isExported = finding.exported === true;
|
||||
|
||||
return `
|
||||
<div class="finding-item ${isActive ? 'active' : ''} ${isSelected ? 'selected' : ''} ${finding.dismissed ? 'dismissed' : ''} ${isExported ? 'exported' : ''}"
|
||||
onclick="selectFinding('${finding.id}')">
|
||||
<div class="finding-checkbox" onclick="event.stopPropagation(); toggleFindingSelection('${finding.id}')">
|
||||
<input type="checkbox" ${isSelected ? 'checked' : ''} ${isExported ? 'disabled' : ''}>
|
||||
</div>
|
||||
<div class="finding-content">
|
||||
<div class="finding-header">
|
||||
<span class="perspective-badge ${finding.perspective}">${finding.perspective}</span>
|
||||
<span class="priority-badge ${finding.priority}">${finding.priority}</span>
|
||||
${isExported ? '<span class="exported-badge">' + (t('discovery.exported') || 'Exported') + '</span>' : ''}
|
||||
</div>
|
||||
<div class="finding-title">${finding.title || 'Untitled'}</div>
|
||||
<div class="finding-location">
|
||||
<i data-lucide="file" class="w-3 h-3"></i>
|
||||
<span>${finding.file || 'Unknown'}${finding.line ? ':' + finding.line : ''}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderFindingPreview(finding) {
|
||||
return `
|
||||
<div class="finding-preview">
|
||||
<div class="preview-header">
|
||||
<div class="preview-badges">
|
||||
<span class="perspective-badge ${finding.perspective}">${finding.perspective}</span>
|
||||
<span class="priority-badge ${finding.priority}">${finding.priority}</span>
|
||||
${finding.confidence ? `<span class="confidence-badge">${Math.round(finding.confidence * 100)}% confidence</span>` : ''}
|
||||
</div>
|
||||
<h3 class="preview-title">${finding.title || 'Untitled'}</h3>
|
||||
</div>
|
||||
|
||||
<div class="preview-section">
|
||||
<h4><i data-lucide="file-code" class="w-4 h-4"></i> ${t('discovery.location') || 'Location'}</h4>
|
||||
<div class="preview-location">
|
||||
<code>${finding.file || 'Unknown'}${finding.line ? ':' + finding.line : ''}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${finding.snippet ? `
|
||||
<div class="preview-section">
|
||||
<h4><i data-lucide="code" class="w-4 h-4"></i> ${t('discovery.code') || 'Code'}</h4>
|
||||
<pre class="preview-snippet"><code>${escapeHtml(finding.snippet)}</code></pre>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="preview-section">
|
||||
<h4><i data-lucide="info" class="w-4 h-4"></i> ${t('discovery.description') || 'Description'}</h4>
|
||||
<p class="preview-description">${finding.description || 'No description'}</p>
|
||||
</div>
|
||||
|
||||
${finding.impact ? `
|
||||
<div class="preview-section">
|
||||
<h4><i data-lucide="alert-triangle" class="w-4 h-4"></i> ${t('discovery.impact') || 'Impact'}</h4>
|
||||
<p class="preview-impact">${finding.impact}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${finding.recommendation ? `
|
||||
<div class="preview-section">
|
||||
<h4><i data-lucide="lightbulb" class="w-4 h-4"></i> ${t('discovery.recommendation') || 'Recommendation'}</h4>
|
||||
<p class="preview-recommendation">${finding.recommendation}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${finding.suggested_issue ? `
|
||||
<div class="preview-section suggested-issue">
|
||||
<h4><i data-lucide="clipboard-list" class="w-4 h-4"></i> ${t('discovery.suggestedIssue') || 'Suggested Issue'}</h4>
|
||||
<div class="suggested-issue-content">
|
||||
<div class="issue-title">${finding.suggested_issue.title || finding.title}</div>
|
||||
<div class="issue-meta">
|
||||
<span class="issue-type">${finding.suggested_issue.type || 'bug'}</span>
|
||||
<span class="issue-priority">P${finding.suggested_issue.priority || 3}</span>
|
||||
${finding.suggested_issue.labels ? finding.suggested_issue.labels.map(l => `<span class="issue-label">${l}</span>`).join('') : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="preview-actions">
|
||||
<button class="preview-action-btn primary" onclick="exportSingleFinding('${finding.id}')">
|
||||
<i data-lucide="upload" class="w-4 h-4"></i>
|
||||
<span>${t('discovery.exportAsIssue') || 'Export as Issue'}</span>
|
||||
</button>
|
||||
<button class="preview-action-btn secondary" onclick="dismissFinding('${finding.id}')">
|
||||
<i data-lucide="x" class="w-4 h-4"></i>
|
||||
<span>${t('discovery.dismiss') || 'Dismiss'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ========== Actions ==========
|
||||
async function viewDiscoveryDetail(discoveryId) {
|
||||
discoveryData.viewMode = 'detail';
|
||||
discoveryData.selectedFinding = null;
|
||||
discoveryData.selectedFindings.clear();
|
||||
discoveryData.perspectiveFilter = 'all';
|
||||
discoveryData.priorityFilter = 'all';
|
||||
discoveryData.searchQuery = '';
|
||||
|
||||
// Show loading
|
||||
renderDiscoveryView();
|
||||
|
||||
// Load detail
|
||||
const detail = await loadDiscoveryDetail(discoveryId);
|
||||
if (detail) {
|
||||
discoveryData.selectedDiscovery = detail;
|
||||
// Flatten findings from perspectives
|
||||
const allFindings = [];
|
||||
if (detail.perspectives) {
|
||||
for (const p of detail.perspectives) {
|
||||
if (p.findings) {
|
||||
allFindings.push(...p.findings);
|
||||
}
|
||||
}
|
||||
}
|
||||
discoveryData.findings = allFindings;
|
||||
}
|
||||
|
||||
// Start polling if running
|
||||
if (detail && detail.phase && detail.phase !== 'complete' && detail.phase !== 'failed') {
|
||||
startDiscoveryPolling(discoveryId);
|
||||
}
|
||||
|
||||
renderDiscoveryView();
|
||||
}
|
||||
|
||||
function backToDiscoveryList() {
|
||||
stopDiscoveryPolling();
|
||||
discoveryData.viewMode = 'list';
|
||||
discoveryData.selectedDiscovery = null;
|
||||
discoveryData.selectedFinding = null;
|
||||
discoveryData.findings = [];
|
||||
discoveryData.selectedFindings.clear();
|
||||
renderDiscoveryView();
|
||||
}
|
||||
|
||||
function selectFinding(findingId) {
|
||||
const finding = discoveryData.findings.find(f => f.id === findingId);
|
||||
discoveryData.selectedFinding = finding || null;
|
||||
renderDiscoveryView();
|
||||
}
|
||||
|
||||
function toggleFindingSelection(findingId) {
|
||||
if (discoveryData.selectedFindings.has(findingId)) {
|
||||
discoveryData.selectedFindings.delete(findingId);
|
||||
} else {
|
||||
discoveryData.selectedFindings.add(findingId);
|
||||
}
|
||||
renderDiscoveryView();
|
||||
}
|
||||
|
||||
function selectAllFindings() {
|
||||
// Get filtered findings (respecting current filters)
|
||||
const filteredFindings = getFilteredFindings();
|
||||
// Select only non-exported findings
|
||||
for (const finding of filteredFindings) {
|
||||
if (!finding.exported) {
|
||||
discoveryData.selectedFindings.add(finding.id);
|
||||
}
|
||||
}
|
||||
renderDiscoveryView();
|
||||
}
|
||||
|
||||
function deselectAllFindings() {
|
||||
discoveryData.selectedFindings.clear();
|
||||
renderDiscoveryView();
|
||||
}
|
||||
|
||||
function filterDiscoveryByPerspective(perspective) {
|
||||
discoveryData.perspectiveFilter = perspective;
|
||||
renderDiscoveryView();
|
||||
}
|
||||
|
||||
function filterDiscoveryByPriority(priority) {
|
||||
discoveryData.priorityFilter = priority;
|
||||
renderDiscoveryView();
|
||||
}
|
||||
|
||||
function searchDiscoveryFindings(query) {
|
||||
discoveryData.searchQuery = query;
|
||||
renderDiscoveryView();
|
||||
}
|
||||
|
||||
async function exportSelectedFindings() {
|
||||
if (discoveryData.selectedFindings.size === 0) return;
|
||||
|
||||
const discoveryId = discoveryData.selectedDiscovery?.discovery_id;
|
||||
if (!discoveryId) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/discoveries/' + encodeURIComponent(discoveryId) + '/export?path=' + encodeURIComponent(projectPath), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ finding_ids: Array.from(discoveryData.selectedFindings) })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
// Show detailed message if duplicates were skipped
|
||||
const msg = result.skipped_count > 0
|
||||
? `Exported ${result.exported_count} issues, skipped ${result.skipped_count} duplicates`
|
||||
: `Exported ${result.exported_count} issues`;
|
||||
showNotification('success', msg);
|
||||
discoveryData.selectedFindings.clear();
|
||||
// Reload discovery data to reflect exported status
|
||||
await loadDiscoveryData();
|
||||
if (discoveryData.selectedDiscovery) {
|
||||
await viewDiscoveryDetail(discoveryData.selectedDiscovery.discovery_id);
|
||||
} else {
|
||||
renderDiscoveryView();
|
||||
}
|
||||
} else {
|
||||
showNotification('error', result.error || 'Export failed');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Export failed:', err);
|
||||
showNotification('error', 'Export failed');
|
||||
}
|
||||
}
|
||||
|
||||
async function exportSingleFinding(findingId) {
|
||||
const discoveryId = discoveryData.selectedDiscovery?.discovery_id;
|
||||
if (!discoveryId) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/discoveries/' + encodeURIComponent(discoveryId) + '/export?path=' + encodeURIComponent(projectPath), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ finding_ids: [findingId] })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
showNotification('success', 'Exported 1 issue');
|
||||
// Reload discovery data
|
||||
await loadDiscoveryData();
|
||||
renderDiscoveryView();
|
||||
} else {
|
||||
showNotification('error', result.error || 'Export failed');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Export failed:', err);
|
||||
showNotification('error', 'Export failed');
|
||||
}
|
||||
}
|
||||
|
||||
async function dismissFinding(findingId) {
|
||||
const discoveryId = discoveryData.selectedDiscovery?.discovery_id;
|
||||
if (!discoveryId) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/discoveries/' + encodeURIComponent(discoveryId) + '/findings/' + encodeURIComponent(findingId) + '?path=' + encodeURIComponent(projectPath), {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ dismissed: true })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
// Update local state
|
||||
const finding = discoveryData.findings.find(f => f.id === findingId);
|
||||
if (finding) {
|
||||
finding.dismissed = true;
|
||||
}
|
||||
if (discoveryData.selectedFinding?.id === findingId) {
|
||||
discoveryData.selectedFinding = null;
|
||||
}
|
||||
renderDiscoveryView();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Dismiss failed:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function dismissSelectedFindings() {
|
||||
for (const findingId of discoveryData.selectedFindings) {
|
||||
await dismissFinding(findingId);
|
||||
}
|
||||
discoveryData.selectedFindings.clear();
|
||||
renderDiscoveryView();
|
||||
}
|
||||
|
||||
async function deleteDiscovery(discoveryId) {
|
||||
if (!confirm(`Delete discovery ${discoveryId}? This cannot be undone.`)) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/discoveries/' + encodeURIComponent(discoveryId) + '?path=' + encodeURIComponent(projectPath), {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
showNotification('success', 'Discovery deleted');
|
||||
await loadDiscoveryData();
|
||||
renderDiscoveryView();
|
||||
} else {
|
||||
showNotification('error', result.error || 'Delete failed');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Delete failed:', err);
|
||||
showNotification('error', 'Delete failed');
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Progress Polling ==========
|
||||
function startDiscoveryPolling(discoveryId) {
|
||||
stopDiscoveryPolling();
|
||||
|
||||
discoveryPollingInterval = setInterval(async () => {
|
||||
const progress = await loadDiscoveryProgress(discoveryId);
|
||||
if (progress) {
|
||||
// Update progress in UI
|
||||
if (discoveryData.selectedDiscovery) {
|
||||
discoveryData.selectedDiscovery.progress = progress.progress;
|
||||
discoveryData.selectedDiscovery.phase = progress.phase;
|
||||
}
|
||||
|
||||
// Stop polling if complete
|
||||
if (progress.phase === 'complete' || progress.phase === 'failed') {
|
||||
stopDiscoveryPolling();
|
||||
// Reload full detail
|
||||
viewDiscoveryDetail(discoveryId);
|
||||
}
|
||||
}
|
||||
}, 3000); // Poll every 3 seconds
|
||||
}
|
||||
|
||||
function stopDiscoveryPolling() {
|
||||
if (discoveryPollingInterval) {
|
||||
clearInterval(discoveryPollingInterval);
|
||||
discoveryPollingInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Utilities ==========
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// ========== Cleanup ==========
|
||||
function cleanupDiscoveryView() {
|
||||
stopDiscoveryPolling();
|
||||
discoveryData.selectedDiscovery = null;
|
||||
discoveryData.selectedFinding = null;
|
||||
discoveryData.findings = [];
|
||||
discoveryData.selectedFindings.clear();
|
||||
discoveryData.viewMode = 'list';
|
||||
}
|
||||
1829
ccw/src/templates/dashboard-js/views/issue-manager.js
Normal file
1829
ccw/src/templates/dashboard-js/views/issue-manager.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -169,6 +169,9 @@ function renderProjectOverview() {
|
||||
${renderDevelopmentIndex(project.developmentIndex)}
|
||||
</div>
|
||||
|
||||
<!-- Project Guidelines -->
|
||||
${renderProjectGuidelines(project.guidelines)}
|
||||
|
||||
<!-- Statistics -->
|
||||
<div class="bg-card border border-border rounded-lg p-6">
|
||||
<h3 class="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
|
||||
@@ -248,3 +251,153 @@ function renderDevelopmentIndex(devIndex) {
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderProjectGuidelines(guidelines) {
|
||||
if (!guidelines) {
|
||||
return `
|
||||
<div class="bg-card border border-border rounded-lg p-6 mb-6">
|
||||
<h3 class="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
|
||||
<i data-lucide="scroll-text" class="w-5 h-5"></i> Project Guidelines
|
||||
</h3>
|
||||
<p class="text-muted-foreground text-sm">
|
||||
No guidelines configured. Run <code class="px-2 py-1 bg-muted rounded text-xs font-mono">/session:solidify</code> to add project constraints and conventions.
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Count total items
|
||||
const conventionCount = Object.values(guidelines.conventions || {}).flat().length;
|
||||
const constraintCount = Object.values(guidelines.constraints || {}).flat().length;
|
||||
const rulesCount = (guidelines.quality_rules || []).length;
|
||||
const learningsCount = (guidelines.learnings || []).length;
|
||||
const totalCount = conventionCount + constraintCount + rulesCount + learningsCount;
|
||||
|
||||
if (totalCount === 0) {
|
||||
return `
|
||||
<div class="bg-card border border-border rounded-lg p-6 mb-6">
|
||||
<h3 class="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
|
||||
<i data-lucide="scroll-text" class="w-5 h-5"></i> Project Guidelines
|
||||
</h3>
|
||||
<p class="text-muted-foreground text-sm">
|
||||
Guidelines file exists but is empty. Run <code class="px-2 py-1 bg-muted rounded text-xs font-mono">/session:solidify</code> to add project constraints and conventions.
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="bg-card border border-border rounded-lg p-6 mb-6">
|
||||
<h3 class="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
|
||||
<i data-lucide="scroll-text" class="w-5 h-5"></i> Project Guidelines
|
||||
<span class="text-xs px-2 py-0.5 bg-primary-light text-primary rounded-full">${totalCount} items</span>
|
||||
</h3>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Conventions -->
|
||||
${renderGuidelinesSection('Conventions', guidelines.conventions, 'book-marked', 'bg-success-light text-success', [
|
||||
{ key: 'coding_style', label: 'Coding Style' },
|
||||
{ key: 'naming_patterns', label: 'Naming Patterns' },
|
||||
{ key: 'file_structure', label: 'File Structure' },
|
||||
{ key: 'documentation', label: 'Documentation' }
|
||||
])}
|
||||
|
||||
<!-- Constraints -->
|
||||
${renderGuidelinesSection('Constraints', guidelines.constraints, 'shield-alert', 'bg-destructive/10 text-destructive', [
|
||||
{ key: 'architecture', label: 'Architecture' },
|
||||
{ key: 'tech_stack', label: 'Tech Stack' },
|
||||
{ key: 'performance', label: 'Performance' },
|
||||
{ key: 'security', label: 'Security' }
|
||||
])}
|
||||
|
||||
<!-- Quality Rules -->
|
||||
${renderQualityRules(guidelines.quality_rules)}
|
||||
|
||||
<!-- Learnings -->
|
||||
${renderLearnings(guidelines.learnings)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderGuidelinesSection(title, data, icon, badgeClass, categories) {
|
||||
if (!data) return '';
|
||||
|
||||
const items = categories.flatMap(cat => (data[cat.key] || []).map(item => ({ category: cat.label, value: item })));
|
||||
if (items.length === 0) return '';
|
||||
|
||||
return `
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
|
||||
<i data-lucide="${icon}" class="w-4 h-4"></i>
|
||||
<span>${title}</span>
|
||||
<span class="text-xs px-2 py-0.5 ${badgeClass} rounded-full">${items.length}</span>
|
||||
</h4>
|
||||
<div class="space-y-2">
|
||||
${items.slice(0, 8).map(item => `
|
||||
<div class="flex items-start gap-3 p-3 bg-background border border-border rounded-lg">
|
||||
<span class="text-xs px-2 py-0.5 bg-muted text-muted-foreground rounded whitespace-nowrap">${escapeHtml(item.category)}</span>
|
||||
<span class="text-sm text-foreground">${escapeHtml(item.value)}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
${items.length > 8 ? `<div class="text-sm text-muted-foreground text-center py-2">... and ${items.length - 8} more</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderQualityRules(rules) {
|
||||
if (!rules || rules.length === 0) return '';
|
||||
|
||||
return `
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
|
||||
<i data-lucide="check-square" class="w-4 h-4"></i>
|
||||
<span>Quality Rules</span>
|
||||
<span class="text-xs px-2 py-0.5 bg-warning-light text-warning rounded-full">${rules.length}</span>
|
||||
</h4>
|
||||
<div class="space-y-2">
|
||||
${rules.slice(0, 6).map(rule => `
|
||||
<div class="p-3 bg-background border border-border rounded-lg">
|
||||
<div class="flex items-start justify-between mb-1">
|
||||
<span class="text-sm text-foreground font-medium">${escapeHtml(rule.rule)}</span>
|
||||
${rule.enforced_by ? `<span class="text-xs px-2 py-0.5 bg-muted text-muted-foreground rounded">${escapeHtml(rule.enforced_by)}</span>` : ''}
|
||||
</div>
|
||||
<span class="text-xs text-muted-foreground">Scope: ${escapeHtml(rule.scope)}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
${rules.length > 6 ? `<div class="text-sm text-muted-foreground text-center py-2">... and ${rules.length - 6} more</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderLearnings(learnings) {
|
||||
if (!learnings || learnings.length === 0) return '';
|
||||
|
||||
return `
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
|
||||
<i data-lucide="lightbulb" class="w-4 h-4"></i>
|
||||
<span>Session Learnings</span>
|
||||
<span class="text-xs px-2 py-0.5 bg-accent text-accent-foreground rounded-full">${learnings.length}</span>
|
||||
</h4>
|
||||
<div class="space-y-2">
|
||||
${learnings.slice(0, 5).map(learning => `
|
||||
<div class="p-3 bg-background border border-border rounded-lg border-l-4 border-l-primary">
|
||||
<div class="flex items-start justify-between mb-2">
|
||||
<span class="text-sm text-foreground">${escapeHtml(learning.insight)}</span>
|
||||
<span class="text-xs text-muted-foreground whitespace-nowrap ml-2">${formatDate(learning.date)}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
${learning.category ? `<span class="px-2 py-0.5 bg-muted text-muted-foreground rounded">${escapeHtml(learning.category)}</span>` : ''}
|
||||
${learning.session_id ? `<span class="px-2 py-0.5 bg-primary-light text-primary rounded font-mono">${escapeHtml(learning.session_id)}</span>` : ''}
|
||||
</div>
|
||||
${learning.context ? `<p class="text-xs text-muted-foreground mt-2">${escapeHtml(learning.context)}</p>` : ''}
|
||||
</div>
|
||||
`).join('')}
|
||||
${learnings.length > 5 ? `<div class="text-sm text-muted-foreground text-center py-2">... and ${learnings.length - 5} more</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -275,6 +275,18 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- CLI Stream Viewer Button -->
|
||||
<button class="cli-stream-btn p-1.5 text-muted-foreground hover:text-foreground hover:bg-hover rounded relative"
|
||||
id="cliStreamBtn"
|
||||
onclick="toggleCliStreamViewer()"
|
||||
data-i18n-title="header.cliStream"
|
||||
title="CLI Stream Viewer">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="4 17 10 11 4 5"/>
|
||||
<line x1="12" y1="19" x2="20" y2="19"/>
|
||||
</svg>
|
||||
<span class="cli-stream-badge" id="cliStreamBadge"></span>
|
||||
</button>
|
||||
<!-- Refresh Button -->
|
||||
<button class="refresh-btn p-1.5 text-muted-foreground hover:text-foreground hover:bg-hover rounded" id="refreshWorkspace" data-i18n-title="header.refreshWorkspace" title="Refresh workspace">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
@@ -394,6 +406,26 @@
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Issues Section -->
|
||||
<div class="mb-2" id="issuesNav">
|
||||
<div class="flex items-center px-4 py-2 text-sm font-semibold text-muted-foreground uppercase tracking-wide">
|
||||
<i data-lucide="clipboard-list" class="nav-section-icon mr-2"></i>
|
||||
<span class="nav-section-title" data-i18n="nav.issues">Issues</span>
|
||||
</div>
|
||||
<ul class="space-y-0.5">
|
||||
<li class="nav-item flex items-center gap-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-view="issue-manager" data-tooltip="Issue Manager">
|
||||
<i data-lucide="list-checks" class="nav-icon"></i>
|
||||
<span class="nav-text flex-1" data-i18n="nav.issueManager">Manager</span>
|
||||
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-hover text-muted-foreground" id="badgeIssues">0</span>
|
||||
</li>
|
||||
<li class="nav-item flex items-center gap-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-view="issue-discovery" data-tooltip="Issue Discovery">
|
||||
<i data-lucide="search-code" class="nav-icon"></i>
|
||||
<span class="nav-text flex-1" data-i18n="nav.issueDiscovery">Discovery</span>
|
||||
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-hover text-muted-foreground" id="badgeDiscovery">0</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- MCP Servers Section -->
|
||||
<div class="mb-2" id="mcpServersNav">
|
||||
<div class="flex items-center px-4 py-2 text-sm font-semibold text-muted-foreground uppercase tracking-wide">
|
||||
@@ -578,6 +610,44 @@
|
||||
<div class="drawer-overlay hidden fixed inset-0 bg-black/50 z-40" id="drawerOverlay" onclick="closeTaskDrawer()"></div>
|
||||
</div>
|
||||
|
||||
<!-- CLI Stream Viewer Panel -->
|
||||
<div class="cli-stream-viewer" id="cliStreamViewer">
|
||||
<div class="cli-stream-header">
|
||||
<div class="cli-stream-title">
|
||||
<i data-lucide="terminal"></i>
|
||||
<span data-i18n="cliStream.title">CLI Stream</span>
|
||||
<span class="cli-stream-count-badge" id="cliStreamCountBadge">0</span>
|
||||
</div>
|
||||
<div class="cli-stream-search">
|
||||
<i data-lucide="search" class="cli-stream-search-icon"></i>
|
||||
<input type="text"
|
||||
id="cliStreamSearchInput"
|
||||
class="cli-stream-search-input"
|
||||
placeholder="Search output..."
|
||||
oninput="handleSearchInput(event)"
|
||||
data-i18n-placeholder="cliStream.searchPlaceholder">
|
||||
<button class="cli-stream-search-clear" onclick="clearSearch()" title="Clear search">×</button>
|
||||
</div>
|
||||
<div class="cli-stream-actions">
|
||||
<button class="cli-stream-action-btn" onclick="clearCompletedStreams()" data-i18n="cliStream.clearCompleted">
|
||||
<i data-lucide="trash-2"></i>
|
||||
<span>Clear</span>
|
||||
</button>
|
||||
<button class="cli-stream-close-btn" onclick="toggleCliStreamViewer()" title="Close">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cli-stream-tabs" id="cliStreamTabs">
|
||||
<!-- Dynamic tabs -->
|
||||
</div>
|
||||
<div class="cli-stream-content" id="cliStreamContent">
|
||||
<!-- Terminal output -->
|
||||
</div>
|
||||
<div class="cli-stream-status" id="cliStreamStatus">
|
||||
<!-- Status bar -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="cli-stream-overlay" id="cliStreamOverlay" onclick="toggleCliStreamViewer()"></div>
|
||||
|
||||
<!-- Markdown Preview Modal -->
|
||||
<div id="markdownModal" class="markdown-modal hidden fixed inset-0 z-[100] flex items-center justify-center">
|
||||
<div class="markdown-modal-backdrop absolute inset-0 bg-black/60" onclick="closeMarkdownModal()"></div>
|
||||
|
||||
@@ -596,7 +596,7 @@ async function executeCliTool(
|
||||
ensureHistoryDir(workingDir); // Ensure history directory exists
|
||||
|
||||
// NEW: Check if model is a custom LiteLLM endpoint ID
|
||||
if (model && !['gemini', 'qwen', 'codex'].includes(tool)) {
|
||||
if (model) {
|
||||
const endpoint = findEndpointById(workingDir, model);
|
||||
if (endpoint) {
|
||||
// Route to LiteLLM executor
|
||||
|
||||
@@ -379,11 +379,63 @@ async function ensureLiteLLMEmbedderReady(): Promise<BootstrapResult> {
|
||||
*/
|
||||
type GpuMode = 'cpu' | 'cuda' | 'directml';
|
||||
|
||||
/**
|
||||
* Python environment info for compatibility checks
|
||||
*/
|
||||
interface PythonEnvInfo {
|
||||
version: string; // e.g., "3.11.5"
|
||||
majorMinor: string; // e.g., "3.11"
|
||||
architecture: number; // 32 or 64
|
||||
compatible: boolean; // true if 64-bit and Python 3.8-3.12
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check Python environment in venv for DirectML compatibility
|
||||
* DirectML requires: 64-bit Python, version 3.8-3.12
|
||||
*/
|
||||
async function checkPythonEnvForDirectML(): Promise<PythonEnvInfo> {
|
||||
const pythonPath =
|
||||
process.platform === 'win32'
|
||||
? join(CODEXLENS_VENV, 'Scripts', 'python.exe')
|
||||
: join(CODEXLENS_VENV, 'bin', 'python');
|
||||
|
||||
if (!existsSync(pythonPath)) {
|
||||
return { version: '', majorMinor: '', architecture: 0, compatible: false, error: 'Python not found in venv' };
|
||||
}
|
||||
|
||||
try {
|
||||
// Get Python version and architecture in one call
|
||||
const checkScript = `import sys, struct; print(f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}|{struct.calcsize('P') * 8}")`;
|
||||
const result = execSync(`"${pythonPath}" -c "${checkScript}"`, { encoding: 'utf-8', timeout: 10000 }).trim();
|
||||
const [version, archStr] = result.split('|');
|
||||
const architecture = parseInt(archStr, 10);
|
||||
const [major, minor] = version.split('.').map(Number);
|
||||
const majorMinor = `${major}.${minor}`;
|
||||
|
||||
// DirectML wheels available for Python 3.8-3.12, 64-bit only
|
||||
const versionCompatible = major === 3 && minor >= 8 && minor <= 12;
|
||||
const archCompatible = architecture === 64;
|
||||
const compatible = versionCompatible && archCompatible;
|
||||
|
||||
let error: string | undefined;
|
||||
if (!archCompatible) {
|
||||
error = `Python is ${architecture}-bit. onnxruntime-directml requires 64-bit Python. Please reinstall Python as 64-bit.`;
|
||||
} else if (!versionCompatible) {
|
||||
error = `Python ${majorMinor} is not supported. onnxruntime-directml requires Python 3.8-3.12.`;
|
||||
}
|
||||
|
||||
return { version, majorMinor, architecture, compatible, error };
|
||||
} catch (e) {
|
||||
return { version: '', majorMinor: '', architecture: 0, compatible: false, error: `Failed to check Python: ${(e as Error).message}` };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect available GPU acceleration
|
||||
* @returns Detected GPU mode and info
|
||||
*/
|
||||
async function detectGpuSupport(): Promise<{ mode: GpuMode; available: GpuMode[]; info: string }> {
|
||||
async function detectGpuSupport(): Promise<{ mode: GpuMode; available: GpuMode[]; info: string; pythonEnv?: PythonEnvInfo }> {
|
||||
const available: GpuMode[] = ['cpu'];
|
||||
let detectedInfo = 'CPU only';
|
||||
|
||||
@@ -402,19 +454,20 @@ async function detectGpuSupport(): Promise<{ mode: GpuMode; available: GpuMode[]
|
||||
// NVIDIA not available
|
||||
}
|
||||
|
||||
// On Windows, DirectML is always available if DirectX 12 is supported
|
||||
// On Windows, DirectML requires 64-bit Python 3.8-3.12
|
||||
let pythonEnv: PythonEnvInfo | undefined;
|
||||
if (process.platform === 'win32') {
|
||||
try {
|
||||
// Check for DirectX 12 support via dxdiag or registry
|
||||
// DirectML works on most modern Windows 10/11 systems
|
||||
pythonEnv = await checkPythonEnvForDirectML();
|
||||
if (pythonEnv.compatible) {
|
||||
available.push('directml');
|
||||
if (available.includes('cuda')) {
|
||||
detectedInfo = 'NVIDIA GPU detected (CUDA & DirectML available)';
|
||||
} else {
|
||||
detectedInfo = 'DirectML available (Windows GPU acceleration)';
|
||||
}
|
||||
} catch {
|
||||
// DirectML check failed
|
||||
} else if (pythonEnv.error) {
|
||||
// DirectML not available due to Python environment
|
||||
console.log(`[CodexLens] DirectML unavailable: ${pythonEnv.error}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,7 +479,7 @@ async function detectGpuSupport(): Promise<{ mode: GpuMode; available: GpuMode[]
|
||||
recommendedMode = 'cuda';
|
||||
}
|
||||
|
||||
return { mode: recommendedMode, available, info: detectedInfo };
|
||||
return { mode: recommendedMode, available, info: detectedInfo, pythonEnv };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -441,6 +494,19 @@ async function installSemantic(gpuMode: GpuMode = 'cpu'): Promise<BootstrapResul
|
||||
return { success: false, error: 'CodexLens not installed. Install CodexLens first.' };
|
||||
}
|
||||
|
||||
// Check Python environment compatibility for DirectML
|
||||
if (gpuMode === 'directml') {
|
||||
const pythonEnv = await checkPythonEnvForDirectML();
|
||||
if (!pythonEnv.compatible) {
|
||||
const errorDetails = pythonEnv.error || 'Unknown compatibility issue';
|
||||
return {
|
||||
success: false,
|
||||
error: `DirectML installation failed: ${errorDetails}\n\nTo fix this:\n1. Uninstall current Python\n2. Install 64-bit Python 3.10, 3.11, or 3.12 from python.org\n3. Delete ~/.codexlens/venv folder\n4. Reinstall CodexLens`
|
||||
};
|
||||
}
|
||||
console.log(`[CodexLens] Python ${pythonEnv.version} (${pythonEnv.architecture}-bit) - DirectML compatible`);
|
||||
}
|
||||
|
||||
const pipPath =
|
||||
process.platform === 'win32'
|
||||
? join(CODEXLENS_VENV, 'Scripts', 'pip.exe')
|
||||
@@ -1411,7 +1477,7 @@ export {
|
||||
cancelIndexing,
|
||||
isIndexingInProgress,
|
||||
};
|
||||
export type { GpuMode };
|
||||
export type { GpuMode, PythonEnvInfo };
|
||||
|
||||
// Backward-compatible export for tests
|
||||
export const codexLensTool = {
|
||||
|
||||
@@ -19,6 +19,10 @@ export interface LiteLLMExecutionOptions {
|
||||
includeDirs?: string[]; // Additional directories for @patterns
|
||||
enableCache?: boolean; // Override endpoint cache setting
|
||||
onOutput?: (data: { type: string; data: string }) => void;
|
||||
/** Number of retries after the initial attempt (default: 0) */
|
||||
maxRetries?: number;
|
||||
/** Base delay for exponential backoff in milliseconds (default: 1000) */
|
||||
retryBaseDelayMs?: number;
|
||||
}
|
||||
|
||||
export interface LiteLLMExecutionResult {
|
||||
@@ -180,7 +184,15 @@ export async function executeLiteLLMEndpoint(
|
||||
}
|
||||
|
||||
// Use litellm-client to call chat
|
||||
const response = await client.chat(finalPrompt, endpoint.model);
|
||||
const response = await callWithRetries(
|
||||
() => client.chat(finalPrompt, endpoint.model),
|
||||
{
|
||||
maxRetries: options.maxRetries ?? 0,
|
||||
baseDelayMs: options.retryBaseDelayMs ?? 1000,
|
||||
onOutput,
|
||||
rateLimitKey: `${provider.type}:${endpoint.model}`,
|
||||
},
|
||||
);
|
||||
|
||||
if (onOutput) {
|
||||
onOutput({ type: 'stdout', data: response });
|
||||
@@ -239,3 +251,74 @@ function getProviderBaseUrlEnvVarName(providerType: string): string | null {
|
||||
|
||||
return envVarMap[providerType] || null;
|
||||
}
|
||||
|
||||
const rateLimitRetryQueueNextAt = new Map<string, number>();
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function isRateLimitError(errorMessage: string): boolean {
|
||||
return /429|rate limit|too many requests/i.test(errorMessage);
|
||||
}
|
||||
|
||||
function isRetryableError(errorMessage: string): boolean {
|
||||
// Never retry auth/config errors
|
||||
if (/401|403|unauthorized|forbidden/i.test(errorMessage)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Retry rate limits, transient server errors, and network timeouts
|
||||
return /(429|500|502|503|504|timeout|timed out|econnreset|enotfound|econnrefused|socket hang up)/i.test(
|
||||
errorMessage,
|
||||
);
|
||||
}
|
||||
|
||||
async function callWithRetries(
|
||||
call: () => Promise<string>,
|
||||
options: {
|
||||
maxRetries: number;
|
||||
baseDelayMs: number;
|
||||
onOutput?: (data: { type: string; data: string }) => void;
|
||||
rateLimitKey: string;
|
||||
},
|
||||
): Promise<string> {
|
||||
const { maxRetries, baseDelayMs, onOutput, rateLimitKey } = options;
|
||||
let attempt = 0;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
return await call();
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||
|
||||
if (attempt >= maxRetries || !isRetryableError(errorMessage)) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
const delayMs = baseDelayMs * 2 ** attempt;
|
||||
|
||||
if (onOutput) {
|
||||
onOutput({
|
||||
type: 'stderr',
|
||||
data: `[LiteLLM retry ${attempt + 1}/${maxRetries}: waiting ${delayMs}ms] ${errorMessage}\n`,
|
||||
});
|
||||
}
|
||||
|
||||
attempt += 1;
|
||||
|
||||
if (isRateLimitError(errorMessage)) {
|
||||
const now = Date.now();
|
||||
const earliestAt = now + delayMs;
|
||||
const queuedAt = rateLimitRetryQueueNextAt.get(rateLimitKey) ?? 0;
|
||||
const scheduledAt = Math.max(queuedAt, earliestAt);
|
||||
rateLimitRetryQueueNextAt.set(rateLimitKey, scheduledAt + delayMs);
|
||||
|
||||
await sleep(scheduledAt - now);
|
||||
continue;
|
||||
}
|
||||
|
||||
await sleep(delayMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,7 +247,13 @@ function generatePreviewMd(metadata) {
|
||||
* Main execute function
|
||||
*/
|
||||
async function execute(params) {
|
||||
const { prototypesDir = '.', template: templatePath } = params;
|
||||
const {
|
||||
prototypesDir = '.',
|
||||
template: templatePath,
|
||||
runId: runIdParam,
|
||||
sessionId: sessionIdParam,
|
||||
timestamp: timestampParam,
|
||||
} = params;
|
||||
|
||||
const targetPath = resolve(process.cwd(), prototypesDir);
|
||||
|
||||
@@ -262,11 +268,16 @@ async function execute(params) {
|
||||
throw new Error('No prototype files found matching pattern {target}-style-{s}-layout-{l}.html');
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const runId = runIdParam || `run-${now.toISOString().replace(/[:.]/g, '-').slice(0, -5)}`;
|
||||
const sessionId = sessionIdParam || 'standalone';
|
||||
const timestamp = timestampParam || now.toISOString();
|
||||
|
||||
// Generate metadata
|
||||
const metadata = {
|
||||
runId: `run-${new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5)}`,
|
||||
sessionId: 'standalone',
|
||||
timestamp: new Date().toISOString(),
|
||||
runId,
|
||||
sessionId,
|
||||
timestamp,
|
||||
styles,
|
||||
layouts,
|
||||
targets
|
||||
@@ -319,6 +330,18 @@ Auto-detects matrix dimensions from file pattern: {target}-style-{s}-layout-{l}.
|
||||
template: {
|
||||
type: 'string',
|
||||
description: 'Optional path to compare.html template'
|
||||
},
|
||||
runId: {
|
||||
type: 'string',
|
||||
description: 'Optional run identifier to inject into compare.html (defaults to generated timestamp-based run id)'
|
||||
},
|
||||
sessionId: {
|
||||
type: 'string',
|
||||
description: 'Optional session identifier to inject into compare.html (default: standalone)'
|
||||
},
|
||||
timestamp: {
|
||||
type: 'string',
|
||||
description: 'Optional ISO timestamp to inject into compare.html (defaults to current time)'
|
||||
}
|
||||
},
|
||||
required: []
|
||||
|
||||
182
ccw/tests/browser-launcher.test.ts
Normal file
182
ccw/tests/browser-launcher.test.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* Unit tests for browser-launcher utility module.
|
||||
*
|
||||
* Notes:
|
||||
* - Targets the runtime implementation shipped in `ccw/dist`.
|
||||
* - Prevents real browser launches by stubbing `child_process.spawn` used by the `open` package.
|
||||
* - Stubs `os.platform` and `path.resolve` to exercise platform-specific URL formatting.
|
||||
*/
|
||||
|
||||
import { after, beforeEach, describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { Buffer } from 'node:buffer';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const os = require('node:os') as typeof import('node:os');
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const childProcess = require('node:child_process') as typeof import('node:child_process');
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const pathModule = require('node:path') as typeof import('node:path');
|
||||
|
||||
const ORIGINAL_ENV = { ...process.env };
|
||||
|
||||
type SpawnCall = { command: string; args: string[] };
|
||||
|
||||
const spawnCalls: SpawnCall[] = [];
|
||||
const spawnPlan: Array<{ type: 'return' } | { type: 'throw'; error: Error }> = [];
|
||||
|
||||
const originalPlatform = os.platform;
|
||||
const originalSpawn = childProcess.spawn;
|
||||
const originalResolve = pathModule.resolve;
|
||||
|
||||
let platformValue: NodeJS.Platform = originalPlatform();
|
||||
let resolveImpl = (...args: string[]) => originalResolve(...args);
|
||||
|
||||
os.platform = (() => platformValue) as any;
|
||||
pathModule.resolve = ((...args: string[]) => resolveImpl(...args)) as any;
|
||||
|
||||
childProcess.spawn = ((command: string, args: string[] = []) => {
|
||||
spawnCalls.push({ command: String(command), args: args.map(String) });
|
||||
|
||||
const next = spawnPlan.shift();
|
||||
if (next?.type === 'throw') {
|
||||
throw next.error;
|
||||
}
|
||||
|
||||
return { unref() {} } as any;
|
||||
}) as any;
|
||||
|
||||
const browserLauncherUrl = new URL('../dist/utils/browser-launcher.js', import.meta.url).href;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let mod: any;
|
||||
|
||||
function extractOpenTargets(): string[] {
|
||||
const targets: string[] = [];
|
||||
|
||||
for (const call of spawnCalls) {
|
||||
const encodedIndex = call.args.indexOf('-EncodedCommand');
|
||||
if (encodedIndex !== -1) {
|
||||
const base64 = call.args[encodedIndex + 1];
|
||||
if (!base64) continue;
|
||||
|
||||
const decoded = Buffer.from(base64, 'base64').toString('utf16le');
|
||||
const match = decoded.match(/Start\\s+\"([^\"]+)\"/);
|
||||
targets.push(match?.[1] ?? decoded);
|
||||
continue;
|
||||
}
|
||||
|
||||
targets.push([call.command, ...call.args].join(' '));
|
||||
}
|
||||
|
||||
return targets;
|
||||
}
|
||||
|
||||
function normalizedTargets(): string[] {
|
||||
return extractOpenTargets().map((t) => t.replace(/\\/g, '/'));
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
spawnCalls.length = 0;
|
||||
spawnPlan.length = 0;
|
||||
|
||||
for (const key of Object.keys(process.env)) {
|
||||
if (!(key in ORIGINAL_ENV)) delete process.env[key];
|
||||
}
|
||||
for (const [key, value] of Object.entries(ORIGINAL_ENV)) {
|
||||
process.env[key] = value;
|
||||
}
|
||||
|
||||
platformValue = originalPlatform();
|
||||
resolveImpl = (...args: string[]) => originalResolve(...args);
|
||||
});
|
||||
|
||||
describe('browser-launcher utility module', async () => {
|
||||
mod = await import(browserLauncherUrl);
|
||||
|
||||
it('launchBrowser opens HTTP/HTTPS URLs', async () => {
|
||||
await mod.launchBrowser('http://example.com');
|
||||
await mod.launchBrowser('https://example.com');
|
||||
|
||||
const targets = normalizedTargets().join('\n');
|
||||
assert.ok(targets.includes('http://example.com'));
|
||||
assert.ok(targets.includes('https://example.com'));
|
||||
});
|
||||
|
||||
it('launchBrowser converts file path to file:// URL (Windows)', async () => {
|
||||
platformValue = 'win32';
|
||||
resolveImpl = () => 'C:\\tmp\\file.html';
|
||||
|
||||
await mod.launchBrowser('file.html');
|
||||
assert.ok(normalizedTargets().join('\n').includes('file:///C:/tmp/file.html'));
|
||||
});
|
||||
|
||||
it('launchBrowser converts file path to file:// URL (Unix)', async () => {
|
||||
platformValue = 'linux';
|
||||
resolveImpl = () => '/tmp/file.html';
|
||||
|
||||
await mod.launchBrowser('file.html');
|
||||
assert.ok(normalizedTargets().join('\n').includes('file:///tmp/file.html'));
|
||||
});
|
||||
|
||||
it('isHeadlessEnvironment detects common CI env vars', () => {
|
||||
for (const key of ['CI', 'CONTINUOUS_INTEGRATION', 'GITHUB_ACTIONS', 'GITLAB_CI', 'JENKINS_URL']) {
|
||||
delete process.env[key];
|
||||
}
|
||||
assert.equal(mod.isHeadlessEnvironment(), false);
|
||||
|
||||
process.env.CI = '1';
|
||||
assert.equal(mod.isHeadlessEnvironment(), true);
|
||||
|
||||
delete process.env.CI;
|
||||
process.env.GITHUB_ACTIONS = 'true';
|
||||
assert.equal(mod.isHeadlessEnvironment(), true);
|
||||
});
|
||||
|
||||
it('wraps browser launch errors for URLs', async () => {
|
||||
spawnPlan.push({ type: 'throw', error: new Error('boom') });
|
||||
await assert.rejects(
|
||||
mod.launchBrowser('https://example.com'),
|
||||
(err: any) => err instanceof Error && err.message.includes('Failed to open browser: boom'),
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to opening file path directly when URL open fails', async () => {
|
||||
platformValue = 'win32';
|
||||
resolveImpl = () => 'C:\\tmp\\file.html';
|
||||
spawnPlan.push({ type: 'throw', error: new Error('primary fail') });
|
||||
spawnPlan.push({ type: 'return' });
|
||||
|
||||
await mod.launchBrowser('file.html');
|
||||
const targets = normalizedTargets().join('\n');
|
||||
assert.ok(targets.includes('file:///C:/tmp/file.html'));
|
||||
assert.ok(targets.includes('C:/tmp/file.html'));
|
||||
});
|
||||
|
||||
it('throws when both primary and fallback file opens fail', async () => {
|
||||
platformValue = 'win32';
|
||||
resolveImpl = () => 'C:\\tmp\\file.html';
|
||||
spawnPlan.push({ type: 'throw', error: new Error('primary fail') });
|
||||
spawnPlan.push({ type: 'throw', error: new Error('fallback fail') });
|
||||
|
||||
await assert.rejects(
|
||||
mod.launchBrowser('file.html'),
|
||||
(err: any) =>
|
||||
err instanceof Error && err.message.includes('Failed to open browser: primary fail'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
after(() => {
|
||||
os.platform = originalPlatform;
|
||||
childProcess.spawn = originalSpawn;
|
||||
pathModule.resolve = originalResolve;
|
||||
|
||||
for (const key of Object.keys(process.env)) {
|
||||
if (!(key in ORIGINAL_ENV)) delete process.env[key];
|
||||
}
|
||||
for (const [key, value] of Object.entries(ORIGINAL_ENV)) {
|
||||
process.env[key] = value;
|
||||
}
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user