Compare commits

...

13 Commits

Author SHA1 Message Date
catlog22
efbbaff834 chore: bump version to 7.2.6
- Fix: ccw install --force now properly skips all interactive prompts
- Fix: Backup confirmation prompt respects --force flag
- Fix: Git Bash fix prompt respects --force flag
2026-03-11 11:05:33 +08:00
catlog22
1ada08f073 refactor: 移除调试日志相关代码,简化历史保存逻辑 2026-03-11 10:35:57 +08:00
catlog22
65ff5f54cb refactor: update version check logic and response handling 2026-03-10 23:52:00 +08:00
catlog22
c50d9b21dc refactor: update workflow commands to use /workflow:spec:setup for initialization 2026-03-10 23:47:27 +08:00
catlog22
38d1987f41 改进交互推荐审查流程,支持批量确认和修改记录 2026-03-10 23:46:19 +08:00
catlog22
d29dabf0a9 Refactor code structure for improved readability and maintainability 2026-03-10 22:09:35 +08:00
catlog22
2d723644ea chore: remove unused subproject test-ddd-project 2026-03-10 21:43:44 +08:00
catlog22
9fb13ed6b0 Implement phases for skill iteration tuning: Evaluation, Improvement, and Reporting
- Added Phase 3: Evaluate Quality with steps for preparing context, constructing evaluation prompts, executing evaluation via CLI, parsing scores, and checking termination conditions.
- Introduced Phase 4: Apply Improvements to implement targeted changes based on evaluation suggestions, including agent execution and change documentation.
- Created Phase 5: Final Report to generate a comprehensive report of the iteration process, including score progression and remaining weaknesses.
- Established evaluation criteria in a new document to guide the evaluation process.
- Developed templates for evaluation and execution prompts to standardize input for the evaluation and execution phases.
2026-03-10 21:42:58 +08:00
catlog22
b4ad8c7b80 fix(frontend): fix TS2352 type cast in AdvancedTab test mock 2026-03-09 23:28:36 +08:00
catlog22
6f9dc836c3 chore: release v7.2.5 2026-03-09 23:24:04 +08:00
catlog22
663620955c chore: update commands, specs, and ccw tools
Update DDD commands (doc-generate, doc-refresh, sync), workflow commands
(session/sync, spec/add, spec/setup, spec/load), ccw specs, personal
preferences, and add generate-ddd-docs tool.
2026-03-09 23:20:39 +08:00
catlog22
cbd1813ea7 feat(team-coordinate): align with team-skill-designer and team-lifecycle-v4 standards
Add quality-gates.md and knowledge-transfer.md specs. Update SKILL.md
with Shared Constants, Specs Reference, and new user commands. Enhance
coordinator role.md with frontmatter, Message Types, Message Bus Protocol,
and Toolbox. Add When to Use + Strategy sections to all command files.
Update role-spec-template with Identity/Boundaries blocks and validation
checklist.
2026-03-09 23:20:17 +08:00
catlog22
b2fc2f60f1 feat: implement ignore patterns and extension filters in CodexLens
- Added tests to ensure loading of ignore patterns and extension filters from settings.
- Implemented functionality to respect ignore patterns and extension filters during file indexing.
- Created integration tests for CodexLens ignore-pattern configuration routes.
- Added a new AdvancedTab component with tests for managing ignore patterns and extension filters.
- Established a comprehensive branding naming system for the Maestro project, including guidelines for package names, CLI commands, and directory structure.
2026-03-09 14:43:21 +08:00
91 changed files with 5669 additions and 721 deletions

View File

@@ -0,0 +1,27 @@
---
title: "Personal Coding Style"
dimension: personal
category: general
keywords:
- style
- preference
readMode: optional
priority: medium
---
# Personal Coding Style
## Preferences
- Describe your preferred coding style here
- Example: verbose variable names vs terse, functional vs imperative
## Patterns I Prefer
- List patterns you reach for most often
- Example: builder pattern, factory functions, tagged unions
## Things I Avoid
- List anti-patterns or approaches you dislike
- Example: deep inheritance hierarchies, magic strings

View File

@@ -0,0 +1,25 @@
---
title: "Tool Preferences"
dimension: personal
category: general
keywords:
- tool
- cli
- editor
readMode: optional
priority: low
---
# Tool Preferences
## Editor
- Preferred editor and key extensions/plugins
## CLI Tools
- Preferred shell, package manager, build tools
## Debugging
- Preferred debugging approach and tools

View File

@@ -1,3 +1,13 @@
---
title: Architecture Constraints
readMode: optional
priority: medium
category: general
scope: project
dimension: specs
keywords: [architecture, constraint, schema, compatibility, portability, design, arch]
---
# Architecture Constraints
## Schema Evolution

View File

@@ -1,3 +1,13 @@
---
title: Coding Conventions
readMode: optional
priority: medium
category: general
scope: project
dimension: specs
keywords: [coding, convention, style, naming, pattern, navigation, schema, error-handling, implementation, validation, clarity, doc]
---
# Coding Conventions
## Navigation & Path Handling
@@ -9,6 +19,7 @@
## Document Generation
- [architecture] For document generation systems, adopt Layer 3→2→1 pattern (components → features → indexes) for efficient incremental updates. (learned: 2026-03-07)
- [tools] When commands need to generate files with deterministic paths and frontmatter, use dedicated ccw tool endpoints (`ccw tool exec`) instead of raw `ccw cli -p` calls. Endpoints control output path, file naming, and structural metadata; CLI tools only generate prose content. (learned: 2026-03-09)
## Implementation Quality

View File

@@ -48,8 +48,9 @@ doc-index.json → tech-registry/*.md (L3) → feature-maps/*.md (L2) → _index
├── tech-registry/ ← Component documentation (Layer 3)
│ ├── _index.md
│ └── {component-slug}.md
└── sessions/
── _index.md ← Planning sessions index (Layer 1)
└── planning/ ← Planning sessions (Layer 1)
── _index.md ← Planning sessions index
└── {task-slug}-{date}/ ← Individual session folders
```
## Phase 1: Load & Validate
@@ -87,147 +88,82 @@ IF docs already exist AND NOT --force:
Ask user (unless -y → overwrite)
```
## Phase 2: Layer 3 Component Documentation
## Phase 2: Layer 3 -- Component Documentation
For each component in `technicalComponents[]`:
For each component in `technicalComponents[]`, call the generate_ddd_docs endpoint:
```bash
ccw cli -p "PURPOSE: Generate component documentation for {component.name}
TASK:
• Document component purpose and responsibility
• List exported symbols (classes, functions, types)
• Document dependencies (internal and external)
• Include code examples for key APIs
• Document integration points with other components
MODE: write
CONTEXT: @{component.codeLocations[].path}
EXPECTED: Markdown file with: Overview, API Reference, Dependencies, Usage Examples
CONSTRAINTS: Focus on public API | Include type signatures
" --tool gemini --mode write --cd .workflow/.doc-index/tech-registry/
for COMPONENT_ID in "${technicalComponents[@]}"; do
ccw tool exec generate_ddd_docs '{"strategy":"component","entityId":"'"$COMPONENT_ID"'","tool":"gemini"}'
done
```
The endpoint handles:
- Loading the component entity from doc-index.json
- Building YAML frontmatter (layer: 3, component_id, name, type, features, code_locations, generated_at)
- Constructing the CLI prompt with code context paths
- **Including Change History section**: Pull related entries from `doc-index.json.actions[]` where `affectedComponents` includes this component ID. Display as timeline (date, action type, description)
- Writing output to `.workflow/.doc-index/tech-registry/{slug}.md`
- Tool fallback (gemini -> qwen -> codex) on failure
Output: `.workflow/.doc-index/tech-registry/{component-slug}.md`
Frontmatter:
```markdown
---
layer: 3
component_id: tech-{slug}
name: ComponentName
type: service|controller|model|...
features: [feat-auth]
code_locations:
- path: src/services/auth.ts
symbols: [AuthService, AuthService.login]
generated_at: ISO8601
---
```
## Phase 3: Layer 2 -- Feature Documentation
Sections: Responsibility, Code Locations, Related Requirements, Architecture Decisions, Dependencies (in/out)
## Phase 3: Layer 2 — Feature Documentation
For each feature in `features[]`:
For each feature in `features[]`, call the generate_ddd_docs endpoint:
```bash
ccw cli -p "PURPOSE: Generate feature documentation for {feature.name}
TASK:
• Describe feature purpose and business value
• List requirements (from requirementIds)
• Document components involved (from techComponentIds)
• Include architecture decisions (from adrIds)
• Provide integration guide
MODE: write
CONTEXT: @.workflow/.doc-index/tech-registry/{related-components}.md
EXPECTED: Markdown file with: Overview, Requirements, Components, Architecture, Integration
CONSTRAINTS: Reference Layer 3 component docs | Business-focused language
" --tool gemini --mode write --cd .workflow/.doc-index/feature-maps/
for FEATURE_ID in "${features[@]}"; do
ccw tool exec generate_ddd_docs '{"strategy":"feature","entityId":"'"$FEATURE_ID"'","tool":"gemini"}'
done
```
The endpoint handles:
- Loading the feature entity from doc-index.json
- Building YAML frontmatter (layer: 2, feature_id, name, epic_id, status, requirements, components, tags, generated_at)
- Constructing the CLI prompt referencing Layer 3 component docs
- **Including Change History section**: Pull related entries from `doc-index.json.actions[]` where `affectedFeatures` includes this feature ID. Display as timeline (date, action type, description)
- Writing output to `.workflow/.doc-index/feature-maps/{slug}.md`
- Tool fallback (gemini -> qwen -> codex) on failure
Output: `.workflow/.doc-index/feature-maps/{feature-slug}.md`
Frontmatter:
```markdown
---
layer: 2
feature_id: feat-{slug}
name: Feature Name
epic_id: EPIC-NNN|null
status: implemented|in-progress|planned|partial
requirements: [REQ-001, REQ-002]
components: [tech-auth-service, tech-user-model]
depends_on_layer3: [tech-auth-service, tech-user-model]
tags: [auth, security]
generated_at: ISO8601
---
```
Sections: Overview, Requirements (with mapping status), Technical Components, Architecture Decisions, Change History
## Phase 4: Layer 1 — Index & Overview Documentation
## Phase 4: Layer 1 -- Index & Overview Documentation
### 4.1 Index Documents
Generate catalog files:
Generate catalog files for each subdirectory:
- **feature-maps/_index.md** — Feature overview table with status
- **tech-registry/_index.md** — Component registry table with types
- **action-logs/_index.md** — Action history table (empty initially for new projects)
```bash
# Feature maps index
ccw tool exec generate_ddd_docs '{"strategy":"index","entityId":"feature-maps","tool":"gemini"}'
# Tech registry index
ccw tool exec generate_ddd_docs '{"strategy":"index","entityId":"tech-registry","tool":"gemini"}'
# Action logs index
ccw tool exec generate_ddd_docs '{"strategy":"index","entityId":"action-logs","tool":"gemini"}'
# Planning sessions index
ccw tool exec generate_ddd_docs '{"strategy":"index","entityId":"planning","tool":"gemini"}'
```
Or generate all indexes at once (omit entityId):
```bash
ccw tool exec generate_ddd_docs '{"strategy":"index","tool":"gemini"}'
```
### 4.2 README.md (unless --skip-overview)
```bash
ccw cli -p "PURPOSE: Generate project README with overview and navigation
TASK:
• Project summary and purpose
• Quick start guide
• Navigation to features, components, and architecture
• Link to doc-index.json
MODE: write
CONTEXT: @.workflow/.doc-index/doc-index.json @.workflow/.doc-index/feature-maps/_index.md
EXPECTED: README.md with: Overview, Quick Start, Navigation, Links
CONSTRAINTS: High-level only | Entry point for new developers
" --tool gemini --mode write --cd .workflow/.doc-index/
ccw tool exec generate_ddd_docs '{"strategy":"overview","tool":"gemini"}'
```
### 4.3 ARCHITECTURE.md (unless --skip-overview)
```bash
ccw cli -p "PURPOSE: Generate architecture overview document
TASK:
• System design overview
• Component relationships and dependencies
• Key architecture decisions (from ADRs)
• Technology stack
MODE: write
CONTEXT: @.workflow/.doc-index/doc-index.json @.workflow/.doc-index/tech-registry/*.md
EXPECTED: ARCHITECTURE.md with: System Design, Component Diagram, ADRs, Tech Stack
CONSTRAINTS: Architecture-focused | Reference component docs for details
" --tool gemini --mode write --cd .workflow/.doc-index/
```
### 4.4 sessions/_index.md (unless --skip-overview)
```bash
ccw cli -p "PURPOSE: Generate planning sessions index
TASK:
• List all planning session folders chronologically
• Link to each session's plan.json
• Show session status and task count
MODE: write
CONTEXT: @.workflow/.doc-index/planning/*/plan.json
EXPECTED: sessions/_index.md with: Session List, Links, Status
CONSTRAINTS: Chronological order | Link to session folders
" --tool gemini --mode write --cd .workflow/.doc-index/sessions/
```
Layer 1 frontmatter:
```markdown
---
layer: 1
depends_on_layer2: [feat-auth, feat-orders]
generated_at: ISO8601
---
ccw tool exec generate_ddd_docs '{"strategy":"overview","entityId":"architecture","tool":"gemini"}'
```
## Phase 5: SCHEMA.md (unless --skip-schema)
@@ -235,17 +171,7 @@ generated_at: ISO8601
### 5.1 Generate Schema Documentation
```bash
ccw cli -p "PURPOSE: Document doc-index.json schema structure and versioning
TASK:
• Document current schema structure (all fields)
• Define versioning policy (semver: major.minor)
• Document migration protocol for version upgrades
• Provide examples for each schema section
MODE: write
CONTEXT: @.workflow/.doc-index/doc-index.json
EXPECTED: SCHEMA.md with: Schema Structure, Versioning Policy, Migration Protocol, Examples
CONSTRAINTS: Complete field documentation | Clear migration steps
" --tool gemini --mode write --cd .workflow/.doc-index/
ccw tool exec generate_ddd_docs '{"strategy":"schema","tool":"gemini"}'
```
### 5.2 Versioning Policy
@@ -284,7 +210,7 @@ Total: {N} documents generated
| `-y, --yes` | Auto-confirm all decisions |
| `--layer <3\|2\|1\|all>` | Generate specific layer only (default: all) |
| `--force` | Overwrite existing documents |
| `--skip-overview` | Skip README.md, ARCHITECTURE.md, sessions/_index.md |
| `--skip-overview` | Skip README.md, ARCHITECTURE.md, planning/_index.md |
| `--skip-schema` | Skip SCHEMA.md generation |
## Integration Points
@@ -293,3 +219,4 @@ Total: {N} documents generated
- **Called by**: `/ddd:scan` (after index assembly), `/ddd:index-build` (after index assembly)
- **Standalone**: Can be run independently on any project with existing doc-index.json
- **Output**: Complete document tree in `.workflow/.doc-index/`
- **Endpoint**: `ccw tool exec generate_ddd_docs` handles prompt construction, frontmatter, tool fallback, and file creation

View File

@@ -163,7 +163,7 @@ ccw cli -p "PURPOSE: Update project overview docs after feature changes
TASK:
• Update README.md feature list
• Update ARCHITECTURE.md if new components added
• Update sessions/_index.md with new planning sessions
• Update planning/_index.md with new planning sessions
MODE: write
CONTEXT: @.workflow/.doc-index/feature-maps/*.md @.workflow/.doc-index/doc-index.json
EXPECTED: Updated overview docs with current project state

View File

@@ -37,11 +37,42 @@ After completing a development task, synchronize the document index with actual
- `doc-index.json` must exist
- Git repository with committed or staged changes
## Phase 0: Consistency Validation
Before processing changes, verify that `doc-index.json` entries are consistent with actual code state.
### 0.1 Validate Code Locations
For each `technicalComponents[].codeLocations[]`:
- Verify file exists on disk
- If file was deleted/moved → flag for removal or update
- If file exists → verify listed `symbols[]` still exist (quick grep/AST check)
### 0.2 Validate Symbols
For components with `codeLocations[].symbols[]`:
- Check each symbol still exists in the referenced file
- Detect new exported symbols not yet tracked
- Report: `{N} stale symbols, {N} untracked symbols`
### 0.3 Validation Report
```
Consistency Check:
Components validated: {N}
Files verified: {N}
Stale references: {N} (files missing or symbols removed)
Untracked symbols: {N} (new exports not in index)
```
If stale references found: warn and auto-fix during Phase 3 updates.
If `--dry-run`: report only, no fixes.
## Phase 1: Change Detection
### 0.1 Schema Version Check (TASK-006)
### 1.0.1 Schema Version Check
Before processing changes, verify doc-index schema compatibility:
Before processing changes, verify doc-index.json schema compatibility:
```javascript
const docIndex = JSON.parse(Read('.workflow/.doc-index/doc-index.json'));
@@ -201,6 +232,7 @@ For each affected component in `doc-index.json`:
- Update `codeLocations` if file paths or line ranges changed
- Update `symbols` if new exports were added
- Add new `actionIds` entry
- **Auto-update `responsibility`**: If symbols changed (new methods/exports added or removed), re-infer responsibility from current symbols list using Gemini analysis. This prevents stale descriptions (e.g., responsibility still says "登录、注册" after adding logout support)
### 3.2 Register New Components

View File

@@ -332,21 +332,22 @@ CONSTRAINTS: Focus on ${dimensions.join(', ')}
5. **Interactive Recommendation Review** (skip in auto mode):
Walk through each recommendation one-by-one for user confirmation:
Present all recommendations, then batch-confirm via **single AskUserQuestion call** (up to 4 questions):
```
For each recommendation (ordered by priority high→medium→low):
1. Present: action, rationale, priority, steps[] (numbered sub-steps)
2. AskUserQuestion (single-select, header: "建议#N"):
1. Display all recommendations with numbering (action, rationale, priority, steps[])
2. Single AskUserQuestion call — one question per recommendation (max 4, ordered by priority high→medium→low):
Each question (single-select, header: "建议#N"):
- **确认** (label: "确认", desc: "Accept as-is") → review_status = "accepted"
- **修改** (label: "修改", desc: "Adjust scope/steps") → record modification → review_status = "modified"
- **删除** (label: "删除", desc: "Not needed") → record reason → review_status = "rejected"
- **跳过审议** (label: "跳过审议", desc: "Accept all remaining") → break loop
3. Record review decision to discussion.md Decision Log
4. Update conclusions.json recommendation.review_status
- **修改** (label: "修改", desc: "Adjust scope/steps") → review_status = "modified"
- **删除** (label: "删除", desc: "Not needed") → review_status = "rejected"
3. If >4 recommendations: batch in groups of 4 with additional AskUserQuestion calls
4. For "修改" selections: follow up to capture modification details
5. Record all review decisions to discussion.md Decision Log
6. Update conclusions.json recommendation.review_status for each
```
**After review loop**: Display summary of reviewed recommendations:
**After review**: Display summary of reviewed recommendations:
- Accepted: N items | Modified: N items | Rejected: N items
- Only accepted/modified recommendations proceed to next step

View File

@@ -65,11 +65,14 @@ Analyze context and produce two update payloads. Use LLM reasoning (current agen
```javascript
// ── Guidelines extraction ──
// Scan git diff + session for:
// - New patterns adopted → convention
// - Restrictions discovered → constraint
// - Surprises / gotchas → learning
// - Debugging experiences → bug
// - Reusable code patterns → pattern
// - Architecture/design decisions → decision
// - Conventions, constraints, insights → rule
//
// Output: array of { type, category, text }
// Output: array of { type, tag, text }
// type: 'bug' | 'pattern' | 'decision' | 'rule'
// tag: domain tag (api, routing, schema, security, etc.)
// RULE: Only extract genuinely reusable insights. Skip trivial/obvious items.
// RULE: Deduplicate against existing guidelines before adding.
@@ -118,7 +121,7 @@ console.log(`
── Sync Preview ──
Guidelines (${guidelineUpdates.length} items):
${guidelineUpdates.map(g => ` [${g.type}/${g.category}] ${g.text}`).join('\n') || ' (none)'}
${guidelineUpdates.map(g => ` [${g.type}:${g.tag}] ${g.text}`).join('\n') || ' (none)'}
Tech [${detectCategory(summary)}]:
${techEntry.title}
@@ -137,26 +140,102 @@ if (!autoYes) {
## Step 4: Write
```javascript
// ── Update specs/*.md ──
// Uses .ccw/specs/ directory (same as frontend/backend spec-index-builder)
if (guidelineUpdates.length > 0) {
// Map guideline types to spec files
const specFileMap = {
convention: '.ccw/specs/coding-conventions.md',
constraint: '.ccw/specs/architecture-constraints.md',
learning: '.ccw/specs/coding-conventions.md' // learnings appended to conventions
const matter = require('gray-matter') // YAML frontmatter parser
// ── Frontmatter check & repair helper ──
// Ensures target spec file has valid YAML frontmatter with keywords
// Uses gray-matter for robust parsing (handles malformed frontmatter, missing fields)
function ensureFrontmatter(filePath, tag, type) {
const titleMap = {
'coding-conventions': 'Coding Conventions',
'architecture-constraints': 'Architecture Constraints',
'learnings': 'Learnings',
'quality-rules': 'Quality Rules'
}
const basename = filePath.split('/').pop().replace('.md', '')
const title = titleMap[basename] || basename
const defaultFm = {
title,
readMode: 'optional',
priority: 'medium',
scope: 'project',
dimension: 'specs',
keywords: [tag, type]
}
if (!file_exists(filePath)) {
// Case A: Create new file with frontmatter
Write(filePath, matter.stringify(`\n# ${title}\n\n`, defaultFm))
return
}
const raw = Read(filePath)
let parsed
try {
parsed = matter(raw)
} catch {
parsed = { data: {}, content: raw }
}
const hasFrontmatter = raw.trimStart().startsWith('---')
if (!hasFrontmatter) {
// Case B: File exists but no frontmatter → prepend
Write(filePath, matter.stringify(raw, defaultFm))
return
}
// Case C: Frontmatter exists → ensure keywords include current tag
const existingKeywords = parsed.data.keywords || []
const newKeywords = [...new Set([...existingKeywords, tag, type])]
if (newKeywords.length !== existingKeywords.length) {
parsed.data.keywords = newKeywords
Write(filePath, matter.stringify(parsed.content, parsed.data))
}
}
// ── Update specs/*.md ──
// Uses .ccw/specs/ directory - unified [type:tag] entry format
if (guidelineUpdates.length > 0) {
// Map knowledge types to spec files
const specFileMap = {
bug: '.ccw/specs/learnings.md',
pattern: '.ccw/specs/coding-conventions.md',
decision: '.ccw/specs/architecture-constraints.md',
rule: null // determined by content below
}
const date = new Date().toISOString().split('T')[0]
const needsDate = { bug: true, pattern: true, decision: true, rule: false }
for (const g of guidelineUpdates) {
const targetFile = specFileMap[g.type]
// For rule type, route by content and tag
let targetFile = specFileMap[g.type]
if (!targetFile) {
const isQuality = /\b(test|coverage|lint|eslint|质量|测试覆盖|pre-commit|tsc|type.check)\b/i.test(g.text)
|| ['testing', 'quality', 'lint'].includes(g.tag)
const isConstraint = /\b(禁止|no|never|must not|forbidden|不得|不允许)\b/i.test(g.text)
if (isQuality) {
targetFile = '.ccw/specs/quality-rules.md'
} else if (isConstraint) {
targetFile = '.ccw/specs/architecture-constraints.md'
} else {
targetFile = '.ccw/specs/coding-conventions.md'
}
}
// Ensure frontmatter exists and keywords are up-to-date
ensureFrontmatter(targetFile, g.tag, g.type)
const existing = Read(targetFile)
const ruleText = g.type === 'learning'
? `- [${g.category}] ${g.text} (learned: ${new Date().toISOString().split('T')[0]})`
: `- [${g.category}] ${g.text}`
const entryLine = needsDate[g.type]
? `- [${g.type}:${g.tag}] ${g.text} (${date})`
: `- [${g.type}:${g.tag}] ${g.text}`
// Deduplicate: skip if text already in file
if (!existing.includes(g.text)) {
const newContent = existing.trimEnd() + '\n' + ruleText + '\n'
const newContent = existing.trimEnd() + '\n' + entryLine + '\n'
Write(targetFile, newContent)
}
}
@@ -198,4 +277,5 @@ Write(techPath, JSON.stringify(tech, null, 2))
## Related Commands
- `/workflow:spec:setup` - Initialize project with specs scaffold
- `/workflow:spec:add` - Interactive wizard to create individual specs with scope selection
- `/workflow:spec:add` - Add knowledge entries (bug/pattern/decision/rule) with unified [type:tag] format
- `/workflow:spec:load` - Interactive spec loader with keyword/type/tag filtering

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,392 @@
---
name: load
description: Interactive spec loader - ask what user needs, then load relevant specs by keyword routing
argument-hint: "[--all] [--type <bug|pattern|decision|rule>] [--tag <tag>] [\"keyword query\"]"
examples:
- /workflow:spec:load
- /workflow:spec:load "api routing"
- /workflow:spec:load --type bug
- /workflow:spec:load --all
- /workflow:spec:load --tag security
---
# Spec Load Command (/workflow:spec:load)
## Overview
Interactive entry point for loading and browsing project specs. Asks the user what they need, then routes to the appropriate spec content based on keywords, type filters, or tag filters.
**Design**: Menu-driven → keyword match → load & display. No file modifications.
**Note**: This command may be called by other workflow commands. Upon completion, return immediately to continue the calling workflow.
## Usage
```bash
/workflow:spec:load # Interactive menu
/workflow:spec:load "api routing" # Direct keyword search
/workflow:spec:load --type bug # Filter by knowledge type
/workflow:spec:load --tag security # Filter by domain tag
/workflow:spec:load --all # Load all specs
```
## Execution Process
```
Input Parsing:
├─ Parse --all flag → loadAll = true | false
├─ Parse --type (bug|pattern|decision|rule)
├─ Parse --tag (domain tag)
└─ Parse keyword query (positional text)
Decision:
├─ --all → Load all specs (Path C)
├─ --type or --tag or keyword → Direct filter (Path B)
└─ No args → Interactive menu (Path A)
Path A: Interactive Menu
├─ Step A1: Ask user intent
├─ Step A2: Route to action
└─ Step A3: Display results
Path B: Direct Filter
├─ Step B1: Build filter from args
├─ Step B2: Search specs
└─ Step B3: Display results
Path C: Load All
└─ Display all spec contents
Output:
└─ Formatted spec entries matching user query
```
## Implementation
### Step 1: Parse Input
```javascript
const args = $ARGUMENTS
const argsLower = args.toLowerCase()
const loadAll = argsLower.includes('--all')
const hasType = argsLower.includes('--type')
const hasTag = argsLower.includes('--tag')
let type = hasType ? args.match(/--type\s+(\w+)/i)?.[1]?.toLowerCase() : null
let tag = hasTag ? args.match(/--tag\s+([\w-]+)/i)?.[1]?.toLowerCase() : null
// Extract keyword query (everything that's not a flag)
let keyword = args
.replace(/--type\s+\w+/gi, '')
.replace(/--tag\s+[\w-]+/gi, '')
.replace(/--all/gi, '')
.replace(/^["']|["']$/g, '')
.trim()
// Validate type
if (type && !['bug', 'pattern', 'decision', 'rule'].includes(type)) {
console.log("Invalid type. Use 'bug', 'pattern', 'decision', or 'rule'.")
return
}
```
### Step 2: Determine Mode
```javascript
const useInteractive = !loadAll && !hasType && !hasTag && !keyword
```
### Path A: Interactive Menu
```javascript
if (useInteractive) {
const answer = AskUserQuestion({
questions: [{
question: "What specs would you like to load?",
header: "Action",
multiSelect: false,
options: [
{
label: "Browse all specs",
description: "Load and display all project spec entries"
},
{
label: "Search by keyword",
description: "Find specs matching a keyword (e.g., api, security, routing)"
},
{
label: "View bug experiences",
description: "Load all [bug:*] debugging experience entries"
},
{
label: "View code patterns",
description: "Load all [pattern:*] reusable code pattern entries"
}
]
}]
})
const choice = answer.answers["Action"]
if (choice === "Browse all specs") {
loadAll = true
} else if (choice === "View bug experiences") {
type = "bug"
} else if (choice === "View code patterns") {
type = "pattern"
} else if (choice === "Search by keyword") {
// Ask for keyword
const kwAnswer = AskUserQuestion({
questions: [{
question: "Enter keyword(s) to search for:",
header: "Keyword",
multiSelect: false,
options: [
{ label: "api", description: "API endpoints, HTTP, REST, routing" },
{ label: "security", description: "Authentication, authorization, input validation" },
{ label: "arch", description: "Architecture, design patterns, module structure" },
{ label: "perf", description: "Performance, caching, optimization" }
]
}]
})
keyword = kwAnswer.answers["Keyword"].toLowerCase()
} else {
// "Other" — user typed custom input, use as keyword
keyword = choice.toLowerCase()
}
}
```
### Step 3: Load Spec Files
```javascript
// Discover all spec files
const specFiles = [
'.ccw/specs/coding-conventions.md',
'.ccw/specs/architecture-constraints.md',
'.ccw/specs/learnings.md',
'.ccw/specs/quality-rules.md'
]
// Also check personal specs
const personalFiles = [
'~/.ccw/personal/conventions.md',
'~/.ccw/personal/constraints.md',
'~/.ccw/personal/learnings.md',
'.ccw/personal/conventions.md',
'.ccw/personal/constraints.md',
'.ccw/personal/learnings.md'
]
// Read all existing spec files
const allEntries = []
for (const file of [...specFiles, ...personalFiles]) {
if (!file_exists(file)) continue
const content = Read(file)
// Extract entries using unified format regex
// Entry line: - [type:tag] summary (date)
// Extended: - key: value
const lines = content.split('\n')
let currentEntry = null
for (const line of lines) {
const entryMatch = line.match(/^- \[(\w+):([\w-]+)\] (.*?)(?:\s+\((\d{4}-\d{2}-\d{2})\))?$/)
if (entryMatch) {
if (currentEntry) allEntries.push(currentEntry)
currentEntry = {
type: entryMatch[1],
tag: entryMatch[2],
summary: entryMatch[3],
date: entryMatch[4] || null,
extended: {},
source: file,
raw: line
}
} else if (currentEntry && /^\s{4}- ([\w-]+):\s?(.*)/.test(line)) {
const fieldMatch = line.match(/^\s{4}- ([\w-]+):\s?(.*)/)
currentEntry.extended[fieldMatch[1]] = fieldMatch[2]
} else if (currentEntry && !/^\s{4}/.test(line) && line.trim() !== '') {
// Non-indented non-empty line = end of current entry
allEntries.push(currentEntry)
currentEntry = null
}
// Also handle legacy format: - [tag] text (learned: date)
const legacyMatch = line.match(/^- \[([\w-]+)\] (.+?)(?:\s+\(learned: (\d{4}-\d{2}-\d{2})\))?$/)
if (!entryMatch && legacyMatch) {
if (currentEntry) allEntries.push(currentEntry)
currentEntry = {
type: 'rule',
tag: legacyMatch[1],
summary: legacyMatch[2],
date: legacyMatch[3] || null,
extended: {},
source: file,
raw: line,
legacy: true
}
}
}
if (currentEntry) allEntries.push(currentEntry)
}
```
### Step 4: Filter Entries
```javascript
let filtered = allEntries
// Filter by type
if (type) {
filtered = filtered.filter(e => e.type === type)
}
// Filter by tag
if (tag) {
filtered = filtered.filter(e => e.tag === tag)
}
// Filter by keyword (search in tag, summary, and extended fields)
if (keyword) {
const kw = keyword.toLowerCase()
const kwTerms = kw.split(/\s+/)
filtered = filtered.filter(e => {
const searchText = [
e.type, e.tag, e.summary,
...Object.values(e.extended)
].join(' ').toLowerCase()
return kwTerms.every(term => searchText.includes(term))
})
}
// If --all, keep everything (no filter)
```
### Step 5: Display Results
```javascript
if (filtered.length === 0) {
const filterDesc = []
if (type) filterDesc.push(`type=${type}`)
if (tag) filterDesc.push(`tag=${tag}`)
if (keyword) filterDesc.push(`keyword="${keyword}"`)
console.log(`
No specs found matching: ${filterDesc.join(', ') || '(all)'}
Available spec files:
${specFiles.filter(f => file_exists(f)).map(f => ` - ${f}`).join('\n') || ' (none)'}
Suggestions:
- Use /workflow:spec:setup to initialize specs
- Use /workflow:spec:add to add new entries
- Use /workflow:spec:load --all to see everything
`)
return
}
// Group by source file
const grouped = {}
for (const entry of filtered) {
if (!grouped[entry.source]) grouped[entry.source] = []
grouped[entry.source].push(entry)
}
// Display
console.log(`
## Specs Loaded (${filtered.length} entries)
${type ? `Type: ${type}` : ''}${tag ? ` Tag: ${tag}` : ''}${keyword ? ` Keyword: "${keyword}"` : ''}
`)
for (const [source, entries] of Object.entries(grouped)) {
console.log(`### ${source}`)
console.log('')
for (const entry of entries) {
// Render entry
const datePart = entry.date ? ` (${entry.date})` : ''
console.log(`- [${entry.type}:${entry.tag}] ${entry.summary}${datePart}`)
// Render extended fields
for (const [key, value] of Object.entries(entry.extended)) {
console.log(` - ${key}: ${value}`)
}
}
console.log('')
}
// Summary footer
const typeCounts = {}
for (const e of filtered) {
typeCounts[e.type] = (typeCounts[e.type] || 0) + 1
}
const typeBreakdown = Object.entries(typeCounts)
.map(([t, c]) => `${t}: ${c}`)
.join(', ')
console.log(`---`)
console.log(`Total: ${filtered.length} entries (${typeBreakdown})`)
console.log(`Sources: ${Object.keys(grouped).join(', ')}`)
```
## Examples
### Interactive Browse
```bash
/workflow:spec:load
# → Menu: "What specs would you like to load?"
# → User selects "Browse all specs"
# → Displays all entries grouped by file
```
### Keyword Search
```bash
/workflow:spec:load "api routing"
# → Filters entries where tag/summary/extended contains "api" AND "routing"
# → Displays matching entries
```
### Type Filter
```bash
/workflow:spec:load --type bug
# → Shows all [bug:*] entries from learnings.md
```
### Tag Filter
```bash
/workflow:spec:load --tag security
# → Shows all [*:security] entries across all spec files
```
### Combined Filters
```bash
/workflow:spec:load --type rule --tag api
# → Shows all [rule:api] entries
```
### Load All
```bash
/workflow:spec:load --all
# → Displays every entry from every spec file
```
## Error Handling
| Error | Resolution |
|-------|------------|
| No spec files found | Suggest `/workflow:spec:setup` to initialize |
| No matching entries | Show available files and suggest alternatives |
| Invalid type | Exit with valid type list |
| Corrupt entry format | Skip unparseable lines, continue loading |
## Related Commands
- `/workflow:spec:setup` - Initialize project with specs scaffold
- `/workflow:spec:add` - Add knowledge entries (bug/pattern/decision/rule) with unified [type:tag] format
- `/workflow:session:sync` - Quick-sync session work to specs and project-tech
- `ccw spec list` - View spec file index
- `ccw spec load` - CLI-level spec loading (used by hooks)

View File

@@ -471,70 +471,129 @@ For each category of collected answers, append rules to the corresponding spec M
- Round 5 (quality): `category: execution` (testing phase)
```javascript
const matter = require('gray-matter') // YAML frontmatter parser
// ── Frontmatter check & repair helper ──
// Ensures target spec file has valid YAML frontmatter with keywords
// Uses gray-matter for robust parsing (handles malformed frontmatter, missing fields)
function ensureSpecFrontmatter(filePath, extraKeywords = []) {
const titleMap = {
'coding-conventions': 'Coding Conventions',
'architecture-constraints': 'Architecture Constraints',
'learnings': 'Learnings',
'quality-rules': 'Quality Rules'
}
const basename = filePath.split('/').pop().replace('.md', '')
const title = titleMap[basename] || basename
const defaultKw = filePath.includes('conventions') ? 'convention'
: filePath.includes('constraints') ? 'constraint' : 'quality'
const defaultFm = {
title,
readMode: 'optional',
priority: 'medium',
category: 'general',
scope: 'project',
dimension: 'specs',
keywords: [...new Set([defaultKw, ...extraKeywords])]
}
if (!file_exists(filePath)) {
// Case A: Create new file with frontmatter
const specDir = path.dirname(filePath)
if (!fs.existsSync(specDir)) {
fs.mkdirSync(specDir, { recursive: true })
}
Write(filePath, matter.stringify(`\n# ${title}\n\n`, defaultFm))
return
}
const raw = Read(filePath)
let parsed
try {
parsed = matter(raw)
} catch {
parsed = { data: {}, content: raw }
}
const hasFrontmatter = raw.trimStart().startsWith('---')
if (!hasFrontmatter) {
// Case B: File exists but no frontmatter → prepend
Write(filePath, matter.stringify(raw, defaultFm))
return
}
// Case C: Frontmatter exists → ensure keywords include extras
const existingKeywords = parsed.data.keywords || []
const newKeywords = [...new Set([...existingKeywords, defaultKw, ...extraKeywords])]
if (newKeywords.length !== existingKeywords.length) {
parsed.data.keywords = newKeywords
Write(filePath, matter.stringify(parsed.content, parsed.data))
}
}
// Helper: append rules to a spec MD file with category support
// Uses .ccw/specs/ directory (same as frontend/backend spec-index-builder)
function appendRulesToSpecFile(filePath, rules, defaultCategory = 'general') {
if (rules.length === 0) return
// Ensure .ccw/specs/ directory exists
const specDir = path.dirname(filePath)
if (!fs.existsSync(specDir)) {
fs.mkdirSync(specDir, { recursive: true })
}
// Extract domain tags from rules for keyword accumulation
const ruleTags = rules
.map(r => r.match(/\[[\w]+:([\w-]+)\]/)?.[1])
.filter(Boolean)
// Check if file exists
if (!file_exists(filePath)) {
// Create file with frontmatter including category
const frontmatter = `---
title: ${filePath.includes('conventions') ? 'Coding Conventions' : filePath.includes('constraints') ? 'Architecture Constraints' : 'Quality Rules'}
readMode: optional
priority: medium
category: ${defaultCategory}
scope: project
dimension: specs
keywords: [${defaultCategory}, ${filePath.includes('conventions') ? 'convention' : filePath.includes('constraints') ? 'constraint' : 'quality'}]
---
# ${filePath.includes('conventions') ? 'Coding Conventions' : filePath.includes('constraints') ? 'Architecture Constraints' : 'Quality Rules'}
`
Write(filePath, frontmatter)
}
// Ensure frontmatter exists and keywords include rule tags
ensureSpecFrontmatter(filePath, [...new Set(ruleTags)])
const existing = Read(filePath)
// Append new rules as markdown list items after existing content
const newContent = existing.trimEnd() + '\n' + rules.map(r => `- ${r}`).join('\n') + '\n'
// Append new rules as markdown list items - rules are already in [type:tag] format from caller
const newContent = existing.trimEnd() + '\n' + rules.map(r => {
// If rule already has - prefix or [type:tag] format, use as-is
if (/^- /.test(r)) return r
if (/^\[[\w]+:[\w-]+\]/.test(r)) return `- ${r}`
return `- [rule:${defaultCategory}] ${r}`
}).join('\n') + '\n'
Write(filePath, newContent)
}
// Write conventions (general category) - use .ccw/specs/ (same as frontend/backend)
appendRulesToSpecFile('.ccw/specs/coding-conventions.md',
[...newCodingStyle, ...newNamingPatterns, ...newFileStructure, ...newDocumentation],
'general')
// Helper: infer domain tag from rule content
function inferTag(text) {
const t = text.toLowerCase()
if (/\b(api|http|rest|endpoint|routing)\b/.test(t)) return 'api'
if (/\b(security|auth|permission|xss|sql|sanitize)\b/.test(t)) return 'security'
if (/\b(database|db|sql|postgres|mysql)\b/.test(t)) return 'db'
if (/\b(react|component|hook|jsx|tsx)\b/.test(t)) return 'react'
if (/\b(performance|cache|lazy|async|slow)\b/.test(t)) return 'perf'
if (/\b(test|coverage|mock|jest|vitest)\b/.test(t)) return 'testing'
if (/\b(architecture|layer|module|dependency)\b/.test(t)) return 'arch'
if (/\b(naming|camel|pascal|prefix|suffix)\b/.test(t)) return 'naming'
if (/\b(file|folder|directory|structure)\b/.test(t)) return 'file'
if (/\b(doc|comment|jsdoc|readme)\b/.test(t)) return 'doc'
if (/\b(build|webpack|vite|compile)\b/.test(t)) return 'build'
if (/\b(deploy|ci|cd|docker)\b/.test(t)) return 'deploy'
if (/\b(lint|eslint|prettier|format)\b/.test(t)) return 'lint'
if (/\b(type|typescript|strict|any)\b/.test(t)) return 'typing'
return 'style' // fallback for coding conventions
}
// Write constraints (planning category)
// Write conventions - infer domain tags from content
appendRulesToSpecFile('.ccw/specs/coding-conventions.md',
[...newCodingStyle, ...newNamingPatterns, ...newFileStructure, ...newDocumentation]
.map(r => /^\[[\w]+:[\w-]+\]/.test(r) ? r : `[rule:${inferTag(r)}] ${r}`),
'style')
// Write constraints - infer domain tags from content
appendRulesToSpecFile('.ccw/specs/architecture-constraints.md',
[...newArchitecture, ...newTechStack, ...newPerformance, ...newSecurity],
'planning')
[...newArchitecture, ...newTechStack, ...newPerformance, ...newSecurity]
.map(r => /^\[[\w]+:[\w-]+\]/.test(r) ? r : `[rule:${inferTag(r)}] ${r}`),
'arch')
// Write quality rules (execution category)
if (newQualityRules.length > 0) {
const qualityPath = '.ccw/specs/quality-rules.md'
if (!file_exists(qualityPath)) {
Write(qualityPath, `---
title: Quality Rules
readMode: required
priority: high
category: execution
scope: project
dimension: specs
keywords: [execution, quality, testing, coverage, lint]
---
# Quality Rules
`)
}
// ensureSpecFrontmatter handles create/repair/keyword-update
ensureSpecFrontmatter(qualityPath, ['quality', 'testing', 'coverage', 'lint'])
appendRulesToSpecFile(qualityPath,
newQualityRules.map(q => `${q.rule} (scope: ${q.scope}, enforced by: ${q.enforced_by})`),
'execution')
@@ -644,7 +703,8 @@ Next steps:
## Related Commands
- `/workflow:spec:add` - Interactive wizard to create individual specs with scope selection
- `/workflow:spec:add` - Add knowledge entries (bug/pattern/decision/rule) with unified [type:tag] format
- `/workflow:spec:load` - Interactive spec loader with keyword/type/tag filtering
- `/workflow:session:sync` - Quick-sync session work to specs and project-tech
- `workflow-plan` skill - Start planning with initialized project context
- `/workflow:status --project` - View project state and guidelines

View File

@@ -289,7 +289,7 @@
},
{
"name": "init-guidelines",
"command": "/workflow:init-guidelines",
"command": "/workflow:spec:setup -guidelines",
"description": "Interactive wizard to fill specs/*.md based on project analysis",
"arguments": "[--reset]",
"category": "workflow",
@@ -300,7 +300,7 @@
},
{
"name": "init-specs",
"command": "/workflow:init-specs",
"command": "/workflow:spec:setup -specs",
"description": "Interactive wizard to create individual specs or personal constraints with scope selection",
"arguments": "[--scope <global|project>] [--dimension <specs|personal>] [--category <general|exploration|planning|execution>]",
"category": "workflow",
@@ -311,7 +311,7 @@
},
{
"name": "init",
"command": "/workflow:init",
"command": "/workflow:spec:setup ",
"description": "Initialize project-level state with intelligent project analysis using cli-explore-agent",
"arguments": "[--regenerate] [--skip-specs]",
"category": "workflow",

View File

@@ -276,7 +276,7 @@
},
{
"name": "init-guidelines",
"command": "/workflow:init-guidelines",
"command": "/workflow:spec:setup -guidelines",
"description": "Interactive wizard to fill specs/*.md based on project analysis",
"arguments": "[--reset]",
"category": "workflow",
@@ -287,7 +287,7 @@
},
{
"name": "init-specs",
"command": "/workflow:init-specs",
"command": "/workflow:spec:setup -specs",
"description": "Interactive wizard to create individual specs or personal constraints with scope selection",
"arguments": "[--scope <global|project>] [--dimension <specs|personal>] [--category <general|exploration|planning|execution>]",
"category": "workflow",
@@ -298,7 +298,7 @@
},
{
"name": "init",
"command": "/workflow:init",
"command": "/workflow:spec:setup ",
"description": "Initialize project-level state with intelligent project analysis using cli-explore-agent",
"arguments": "[--regenerate] [--skip-specs]",
"category": "workflow",

View File

@@ -298,7 +298,7 @@
},
{
"name": "init-guidelines",
"command": "/workflow:init-guidelines",
"command": "/workflow:spec:setup -guidelines",
"description": "Interactive wizard to fill specs/*.md based on project analysis",
"arguments": "[--reset]",
"category": "workflow",
@@ -309,7 +309,7 @@
},
{
"name": "init-specs",
"command": "/workflow:init-specs",
"command": "/workflow:spec:setup -specs",
"description": "Interactive wizard to create individual specs or personal constraints with scope selection",
"arguments": "[--scope <global|project>] [--dimension <specs|personal>] [--category <general|exploration|planning|execution>]",
"category": "workflow",
@@ -320,7 +320,7 @@
},
{
"name": "init",
"command": "/workflow:init",
"command": "/workflow:spec:setup ",
"description": "Initialize project-level state with intelligent project analysis using cli-explore-agent",
"arguments": "[--regenerate] [--skip-specs]",
"category": "workflow",

View File

@@ -145,7 +145,7 @@
},
{
"name": "init-guidelines",
"command": "/workflow:init-guidelines",
"command": "/workflow:spec:setup -guidelines",
"description": "Interactive wizard to fill specs/*.md based on project analysis",
"arguments": "[--reset]",
"category": "workflow",
@@ -156,7 +156,7 @@
},
{
"name": "init-specs",
"command": "/workflow:init-specs",
"command": "/workflow:spec:setup -specs",
"description": "Interactive wizard to create individual specs or personal constraints with scope selection",
"arguments": "[--scope <global|project>] [--dimension <specs|personal>] [--category <general|exploration|planning|execution>]",
"category": "workflow",
@@ -167,7 +167,7 @@
},
{
"name": "init",
"command": "/workflow:init",
"command": "/workflow:spec:setup ",
"description": "Initialize project-level state with intelligent project analysis using cli-explore-agent",
"arguments": "[--regenerate] [--skip-specs]",
"category": "workflow",

View File

@@ -0,0 +1,382 @@
---
name: skill-iter-tune
description: Iterative skill tuning via execute-evaluate-improve feedback loop. Uses ccw cli Claude to execute skill, Gemini to evaluate quality, and Agent to apply improvements. Iterates until quality threshold or max iterations. Triggers on "skill iter tune", "iterative skill tuning", "tune skill".
allowed-tools: Skill, Agent, AskUserQuestion, TaskCreate, TaskUpdate, TaskList, Read, Write, Edit, Bash, Glob, Grep
---
# Skill Iter Tune
Iterative skill refinement through execute-evaluate-improve feedback loops. Each iteration runs the skill via Claude, evaluates output via Gemini, and applies improvements via Agent.
## Architecture Overview
```
┌──────────────────────────────────────────────────────────────────────────┐
│ Skill Iter Tune Orchestrator (SKILL.md) │
│ → Parse input → Setup workspace → Iteration Loop → Final Report │
└────────────────────────────┬─────────────────────────────────────────────┘
┌───────────────────┼───────────────────────────────────┐
↓ ↓ ↓
┌──────────┐ ┌─────────────────────────────┐ ┌──────────┐
│ Phase 1 │ │ Iteration Loop (2→3→4) │ │ Phase 5 │
│ Setup │ │ ┌─────┐ ┌─────┐ ┌─────┐ │ │ Report │
│ │─────→│ │ P2 │→ │ P3 │→ │ P4 │ │────→│ │
│ Backup + │ │ │Exec │ │Eval │ │Impr │ │ │ History │
│ Init │ │ └─────┘ └─────┘ └─────┘ │ │ Summary │
└──────────┘ │ ↑ │ │ └──────────┘
│ └───────────────┘ │
│ (if score < threshold │
│ AND iter < max) │
└─────────────────────────────┘
```
### Chain Mode Extension
```
Chain Mode (execution_mode === "chain"):
Phase 2 runs per-skill in chain_order:
Skill A → ccw cli → artifacts/skill-A/
↓ (artifacts as input)
Skill B → ccw cli → artifacts/skill-B/
↓ (artifacts as input)
Skill C → ccw cli → artifacts/skill-C/
Phase 3 evaluates entire chain output + per-skill scores
Phase 4 improves weakest skill(s) in chain
```
## Key Design Principles
1. **Iteration Loop**: Phases 2-3-4 repeat until quality threshold, max iterations, or convergence
2. **Two-Tool Pipeline**: Claude (write/execute) + Gemini (analyze/evaluate) = complementary perspectives
3. **Pure Orchestrator**: SKILL.md coordinates only — execution detail lives in phase files
4. **Progressive Phase Loading**: Phase docs read only when that phase executes
5. **Skill Versioning**: Each iteration snapshots skill state before execution
6. **Convergence Detection**: Stop early if score stalls (no improvement in 2 consecutive iterations)
## Interactive Preference Collection
```javascript
// ★ Auto mode detection
const autoYes = /\b(-y|--yes)\b/.test($ARGUMENTS)
if (autoYes) {
workflowPreferences = {
autoYes: true,
maxIterations: 5,
qualityThreshold: 80,
executionMode: 'single'
}
} else {
const prefResponse = AskUserQuestion({
questions: [
{
question: "选择迭代调优配置:",
header: "Tune Config",
multiSelect: false,
options: [
{ label: "Quick (3 iter, 70)", description: "快速迭代,适合小幅改进" },
{ label: "Standard (5 iter, 80) (Recommended)", description: "平衡方案,适合多数场景" },
{ label: "Thorough (8 iter, 90)", description: "深度优化,适合生产级 skill" }
]
}
]
})
const configMap = {
"Quick": { maxIterations: 3, qualityThreshold: 70 },
"Standard": { maxIterations: 5, qualityThreshold: 80 },
"Thorough": { maxIterations: 8, qualityThreshold: 90 }
}
const selected = Object.keys(configMap).find(k =>
prefResponse["Tune Config"].startsWith(k)
) || "Standard"
workflowPreferences = { autoYes: false, ...configMap[selected] }
// ★ Mode selection: chain vs single
const modeResponse = AskUserQuestion({
questions: [{
question: "选择调优模式:",
header: "Tune Mode",
multiSelect: false,
options: [
{ label: "Single Skill (Recommended)", description: "独立调优每个 skill适合单一 skill 优化" },
{ label: "Skill Chain", description: "按链序执行,前一个 skill 的产出作为后一个的输入" }
]
}]
});
workflowPreferences.executionMode = modeResponse["Tune Mode"].startsWith("Skill Chain")
? "chain" : "single";
}
```
## Input Processing
```
$ARGUMENTS → Parse:
├─ Skill path(s): first arg, comma-separated for multiple
│ e.g., ".claude/skills/my-skill" or "my-skill" (auto-prefixed)
│ Chain mode: order preserved as chain_order
├─ Test scenario: --scenario "description" or remaining text
└─ Flags: --max-iterations=N, --threshold=N, -y/--yes
```
## Execution Flow
> **⚠️ COMPACT DIRECTIVE**: Context compression MUST check TodoWrite phase status.
> The phase currently marked `in_progress` is the active execution phase — preserve its FULL content.
> Only compress phases marked `completed` or `pending`.
### Phase 1: Setup (one-time)
Read and execute: `Ref: phases/01-setup.md`
- Parse skill paths, validate existence
- Create workspace at `.workflow/.scratchpad/skill-iter-tune-{ts}/`
- Backup original skill files
- Initialize iteration-state.json
Output: `workDir`, `targetSkills[]`, `testScenario`, initialized state
### Iteration Loop
```javascript
// Orchestrator iteration loop
while (true) {
// Increment iteration
state.current_iteration++;
state.iterations.push({
round: state.current_iteration,
status: 'pending',
execution: null,
evaluation: null,
improvement: null
});
// Update TodoWrite
TaskUpdate(iterationTask, {
subject: `Iteration ${state.current_iteration}/${state.max_iterations}`,
status: 'in_progress',
activeForm: `Running iteration ${state.current_iteration}`
});
// === Phase 2: Execute ===
// Read: phases/02-execute.md
// Single mode: one ccw cli call for all skills
// Chain mode: sequential ccw cli per skill in chain_order, passing artifacts
// Snapshot skill → construct prompt → ccw cli --tool claude --mode write
// Collect artifacts
// === Phase 3: Evaluate ===
// Read: phases/03-evaluate.md
// Construct eval prompt → ccw cli --tool gemini --mode analysis
// Parse score → write iteration-N-eval.md → check termination
// Check termination
if (shouldTerminate(state)) {
break; // → Phase 5
}
// === Phase 4: Improve ===
// Read: phases/04-improve.md
// Agent applies suggestions → write iteration-N-changes.md
// Update TodoWrite with score
// Continue loop
}
```
### Phase 2: Execute Skill (per iteration)
Read and execute: `Ref: phases/02-execute.md`
- Snapshot skill → `iteration-{N}/skill-snapshot/`
- Build execution prompt from skill content + test scenario
- Execute: `ccw cli -p "..." --tool claude --mode write --cd "${iterDir}/artifacts"`
- Collect artifacts
### Phase 3: Evaluate Quality (per iteration)
Read and execute: `Ref: phases/03-evaluate.md`
- Build evaluation prompt with skill + artifacts + criteria + history
- Execute: `ccw cli -p "..." --tool gemini --mode analysis`
- Parse 5-dimension score (Clarity, Completeness, Correctness, Effectiveness, Efficiency)
- Write `iteration-{N}-eval.md`
- Check termination: score >= threshold | iter >= max | convergence | error limit
### Phase 4: Apply Improvements (per iteration, skipped on termination)
Read and execute: `Ref: phases/04-improve.md`
- Read evaluation suggestions
- Launch general-purpose Agent to apply changes
- Write `iteration-{N}-changes.md`
- Update state
### Phase 5: Final Report (one-time)
Read and execute: `Ref: phases/05-report.md`
- Generate comprehensive report with score progression table
- Write `final-report.md`
- Display summary to user
**Phase Reference Documents** (read on-demand when phase executes):
| Phase | Document | Purpose | Compact |
|-------|----------|---------|---------|
| 1 | [phases/01-setup.md](phases/01-setup.md) | Initialize workspace and state | TodoWrite 驱动 |
| 2 | [phases/02-execute.md](phases/02-execute.md) | Execute skill via ccw cli Claude | TodoWrite 驱动 + 🔄 sentinel |
| 3 | [phases/03-evaluate.md](phases/03-evaluate.md) | Evaluate via ccw cli Gemini | TodoWrite 驱动 + 🔄 sentinel |
| 4 | [phases/04-improve.md](phases/04-improve.md) | Apply improvements via Agent | TodoWrite 驱动 + 🔄 sentinel |
| 5 | [phases/05-report.md](phases/05-report.md) | Generate final report | TodoWrite 驱动 |
**Compact Rules**:
1. **TodoWrite `in_progress`** → 保留完整内容,禁止压缩
2. **TodoWrite `completed`** → 可压缩为摘要
3. **🔄 sentinel fallback** → 若 compact 后仅存 sentinel 而无完整 Step 协议,立即 `Read()` 恢复
## Core Rules
1. **Start Immediately**: First action is preference collection → Phase 1 setup
2. **Progressive Loading**: Read phase doc ONLY when that phase is about to execute
3. **Snapshot Before Execute**: Always snapshot skill state before each iteration
4. **Background CLI**: ccw cli runs in background, wait for hook callback before proceeding
5. **Parse Every Output**: Extract structured JSON from CLI outputs for state updates
6. **DO NOT STOP**: Continuous iteration until termination condition met
7. **Single State Source**: `iteration-state.json` is the only source of truth
## Data Flow
```
User Input (skill paths + test scenario)
↓ (+ execution_mode + chain_order if chain mode)
Phase 1: Setup
↓ workDir, targetSkills[], testScenario, iteration-state.json
┌─→ Phase 2: Execute (ccw cli claude)
│ ↓ artifacts/ (skill execution output)
│ ↓
│ Phase 3: Evaluate (ccw cli gemini)
│ ↓ score, dimensions[], suggestions[], iteration-N-eval.md
│ ↓
│ [Terminate?]─── YES ──→ Phase 5: Report → final-report.md
│ ↓ NO
│ ↓
│ Phase 4: Improve (Agent)
│ ↓ modified skill files, iteration-N-changes.md
│ ↓
└───┘ next iteration
```
## TodoWrite Pattern
```javascript
// Initial state
TaskCreate({ subject: "Phase 1: Setup workspace", activeForm: "Setting up workspace" })
TaskCreate({ subject: "Iteration Loop", activeForm: "Running iterations" })
TaskCreate({ subject: "Phase 5: Final Report", activeForm: "Generating report" })
// Chain mode: create per-skill tracking tasks
if (state.execution_mode === 'chain') {
for (const skillName of state.chain_order) {
TaskCreate({
subject: `Chain: ${skillName}`,
activeForm: `Tracking ${skillName}`,
description: `Skill chain member position ${state.chain_order.indexOf(skillName) + 1}`
})
}
}
// During iteration N
// Single mode: one score per iteration (existing behavior)
// Chain mode: per-skill status updates
if (state.execution_mode === 'chain') {
// After each skill executes in Phase 2:
TaskUpdate(chainSkillTask, {
subject: `Chain: ${skillName} — Iter ${N} executed`,
activeForm: `${skillName} iteration ${N}`
})
// After Phase 3 evaluates:
TaskUpdate(chainSkillTask, {
subject: `Chain: ${skillName} — Score ${chainScores[skillName]}/100`,
activeForm: `${skillName} scored`
})
} else {
// Single mode (existing)
TaskCreate({
subject: `Iteration ${N}: Score ${score}/100`,
activeForm: `Iteration ${N} complete`,
description: `Strengths: ... | Weaknesses: ... | Suggestions: ${count}`
})
}
// Completed — collapse
TaskUpdate(iterLoop, {
subject: `Iteration Loop (${totalIters} iters, final: ${finalScore})`,
status: 'completed'
})
```
## Termination Logic
```javascript
function shouldTerminate(state) {
// 1. Quality threshold met
if (state.latest_score >= state.quality_threshold) {
return { terminate: true, reason: 'quality_threshold_met' };
}
// 2. Max iterations reached
if (state.current_iteration >= state.max_iterations) {
return { terminate: true, reason: 'max_iterations_reached' };
}
// 3. Convergence: ≤2 points improvement over last 2 iterations
if (state.score_trend.length >= 3) {
const last3 = state.score_trend.slice(-3);
if (last3[2] - last3[0] <= 2) {
state.converged = true;
return { terminate: true, reason: 'convergence_detected' };
}
}
// 4. Error limit
if (state.error_count >= state.max_errors) {
return { terminate: true, reason: 'error_limit_reached' };
}
return { terminate: false };
}
```
## Error Handling
| Phase | Error | Recovery |
|-------|-------|----------|
| 2: Execute | CLI timeout/crash | Retry once with simplified prompt, then skip |
| 3: Evaluate | CLI fails | Retry once, then use score 50 with warning |
| 3: Evaluate | JSON parse fails | Extract score heuristically, save raw output |
| 4: Improve | Agent fails | Rollback from `iteration-{N}/skill-snapshot/` |
| Any | 3+ consecutive errors | Terminate with error report |
**Error Budget**: Each phase gets 1 retry. 3 consecutive failed iterations triggers termination.
## Coordinator Checklist
### Pre-Phase Actions
- [ ] Read iteration-state.json for current state
- [ ] Verify workspace directory exists
- [ ] Check error count hasn't exceeded limit
### Per-Iteration Actions
- [ ] Increment current_iteration in state
- [ ] Create iteration-{N} subdirectory
- [ ] Update TodoWrite with iteration status
- [ ] After Phase 3: check termination before Phase 4
- [ ] After Phase 4: write state, proceed to next iteration
### Post-Workflow Actions
- [ ] Execute Phase 5 (Report)
- [ ] Display final summary to user
- [ ] Update all TodoWrite tasks to completed

View File

@@ -0,0 +1,144 @@
# Phase 1: Setup
Initialize workspace, backup skills, parse inputs.
## Objective
- Parse skill path(s) and test scenario from user input
- Validate all skill paths exist and contain SKILL.md
- Create isolated workspace directory structure
- Backup original skill files
- Initialize iteration-state.json
## Execution
### Step 1.1: Parse Input
Parse `$ARGUMENTS` to extract skill paths and test scenario.
```javascript
// Parse skill paths (first argument or comma-separated)
const args = $ARGUMENTS.trim();
const pathMatch = args.match(/^([^\s]+)/);
const rawPaths = pathMatch ? pathMatch[1].split(',') : [];
// Parse test scenario
const scenarioMatch = args.match(/(?:--scenario|--test)\s+"([^"]+)"/);
const scenarioText = scenarioMatch ? scenarioMatch[1] : args.replace(rawPaths.join(','), '').trim();
// Record chain order (preserves input order for chain mode)
const chainOrder = rawPaths.map(p => p.startsWith('.claude/') ? p.split('/').pop() : p);
// If no scenario, ask user
if (!scenarioText) {
const response = AskUserQuestion({
questions: [{
question: "Please describe the test scenario for evaluating this skill:",
header: "Test Scenario",
multiSelect: false,
options: [
{ label: "General quality test", description: "Evaluate overall skill quality with a generic task" },
{ label: "Specific scenario", description: "I'll describe a specific test case" }
]
}]
});
// Use response to construct testScenario
}
```
### Step 1.2: Validate Skill Paths
```javascript
const targetSkills = [];
for (const rawPath of rawPaths) {
const skillPath = rawPath.startsWith('.claude/') ? rawPath : `.claude/skills/${rawPath}`;
// Validate SKILL.md exists
const skillFiles = Glob(`${skillPath}/SKILL.md`);
if (skillFiles.length === 0) {
throw new Error(`Skill not found at: ${skillPath} -- SKILL.md missing`);
}
// Collect all skill files
const allFiles = Glob(`${skillPath}/**/*.md`);
targetSkills.push({
name: skillPath.split('/').pop(),
path: skillPath,
files: allFiles.map(f => f.replace(skillPath + '/', '')),
primary_file: 'SKILL.md'
});
}
```
### Step 1.3: Create Workspace
```javascript
const ts = Date.now();
const workDir = `.workflow/.scratchpad/skill-iter-tune-${ts}`;
Bash(`mkdir -p "${workDir}/backups" "${workDir}/iterations"`);
```
### Step 1.4: Backup Original Skills
```javascript
for (const skill of targetSkills) {
Bash(`cp -r "${skill.path}" "${workDir}/backups/${skill.name}"`);
}
```
### Step 1.5: Initialize State
Write `iteration-state.json` with initial state:
```javascript
const initialState = {
status: 'running',
started_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
target_skills: targetSkills,
test_scenario: {
description: scenarioText,
// Parse --requirements and --input-args from $ARGUMENTS if provided
// e.g., --requirements "clear output,no errors" --input-args "my-skill --scenario test"
requirements: parseListArg(args, '--requirements') || [],
input_args: parseStringArg(args, '--input-args') || '',
success_criteria: parseStringArg(args, '--success-criteria') || 'Produces correct, high-quality output'
},
execution_mode: workflowPreferences.executionMode || 'single',
chain_order: workflowPreferences.executionMode === 'chain'
? targetSkills.map(s => s.name)
: [],
current_iteration: 0,
max_iterations: workflowPreferences.maxIterations,
quality_threshold: workflowPreferences.qualityThreshold,
latest_score: 0,
score_trend: [],
converged: false,
iterations: [],
errors: [],
error_count: 0,
max_errors: 3,
work_dir: workDir,
backup_dir: `${workDir}/backups`
};
Write(`${workDir}/iteration-state.json`, JSON.stringify(initialState, null, 2));
// Chain mode: create per-skill tracking tasks
if (initialState.execution_mode === 'chain') {
for (const skill of targetSkills) {
TaskCreate({
subject: `Chain: ${skill.name}`,
activeForm: `Tracking ${skill.name}`,
description: `Skill chain member: ${skill.path} | Position: ${targetSkills.indexOf(skill) + 1}/${targetSkills.length}`
});
}
}
```
## Output
- **Variables**: `workDir`, `targetSkills[]`, `testScenario`, `chainOrder` (chain mode)
- **Files**: `iteration-state.json`, `backups/` directory with skill copies
- **TodoWrite**: Mark Phase 1 completed, start Iteration Loop. Chain mode: per-skill tracking tasks created

View File

@@ -0,0 +1,292 @@
# Phase 2: Execute Skill
> **COMPACT SENTINEL [Phase 2: Execute]**
> This phase contains 4 execution steps (Step 2.1 -- 2.4).
> If you can read this sentinel but cannot find the full Step protocol below, context has been compressed.
> Recovery: `Read("phases/02-execute.md")`
Execute the target skill against the test scenario using `ccw cli --tool claude --mode write`. Claude receives the full skill definition and simulates producing its expected output artifacts.
## Objective
- Snapshot current skill version before execution
- Construct execution prompt with full skill content + test scenario
- Execute via ccw cli Claude
- Collect output artifacts
## Execution
### Step 2.1: Snapshot Current Skill
```javascript
const N = state.current_iteration;
const iterDir = `${state.work_dir}/iterations/iteration-${N}`;
Bash(`mkdir -p "${iterDir}/skill-snapshot" "${iterDir}/artifacts"`);
// Chain mode: create per-skill artifact directories
if (state.execution_mode === 'chain') {
for (const skillName of state.chain_order) {
Bash(`mkdir -p "${iterDir}/artifacts/${skillName}"`);
}
}
// Snapshot current skill state (so we can compare/rollback)
for (const skill of state.target_skills) {
Bash(`cp -r "${skill.path}" "${iterDir}/skill-snapshot/${skill.name}"`);
}
```
### Step 2.2: Construct Execution Prompt (Single Mode)
Read the execute-prompt template and substitute variables.
> Skip to Step 2.2b if `state.execution_mode === 'chain'`.
```javascript
// Ref: templates/execute-prompt.md
// Build skillContent by reading only executable skill files (SKILL.md, phases/, specs/)
// Exclude README.md, docs/, and other non-executable files to save tokens
const skillContent = state.target_skills.map(skill => {
const skillMd = Read(`${skill.path}/SKILL.md`);
const phaseFiles = Glob(`${skill.path}/phases/*.md`).sort().map(f => ({
relativePath: f.replace(skill.path + '/', ''),
content: Read(f)
}));
const specFiles = Glob(`${skill.path}/specs/*.md`).map(f => ({
relativePath: f.replace(skill.path + '/', ''),
content: Read(f)
}));
return `### File: SKILL.md\n${skillMd}\n\n` +
phaseFiles.map(f => `### File: ${f.relativePath}\n${f.content}`).join('\n\n') +
(specFiles.length > 0 ? '\n\n' + specFiles.map(f => `### File: ${f.relativePath}\n${f.content}`).join('\n\n') : '');
}).join('\n\n---\n\n');
// Construct full prompt using template
const executePrompt = `PURPOSE: Simulate executing the following workflow skill against a test scenario. Produce all expected output artifacts as if the skill were invoked with the given input.
SKILL CONTENT:
${skillContent}
TEST SCENARIO:
Description: ${state.test_scenario.description}
Input Arguments: ${state.test_scenario.input_args}
Requirements: ${state.test_scenario.requirements.join('; ')}
Success Criteria: ${state.test_scenario.success_criteria}
TASK:
1. Study the complete skill structure (SKILL.md + all phase files)
2. Follow the skill execution flow sequentially
3. For each phase, produce the artifacts that phase would generate
4. Write all output artifacts to the current working directory
5. Create a manifest.json listing all produced artifacts
MODE: write
CONTEXT: @**/*
EXPECTED: All artifacts written to disk + manifest.json
CONSTRAINTS: Follow skill flow exactly, produce realistic output, not placeholders`;
```
### Step 2.3: Execute via ccw cli
> **CHECKPOINT**: Before executing CLI, verify:
> 1. This phase is TodoWrite `in_progress`
> 2. `iterDir/artifacts/` directory exists
> 3. Prompt is properly escaped
```javascript
function escapeForShell(str) {
return str.replace(/"/g, '\\"').replace(/\$/g, '\\$').replace(/`/g, '\\`');
}
const cliCommand = `ccw cli -p "${escapeForShell(executePrompt)}" --tool claude --mode write --cd "${iterDir}/artifacts"`;
// Execute in background, wait for hook callback
Bash({
command: cliCommand,
run_in_background: true,
timeout: 600000 // 10 minutes max
});
// STOP HERE -- wait for hook callback to resume
// After callback, verify artifacts were produced
```
### Step 2.2b: Chain Execution Path
> Skip this step if `state.execution_mode === 'single'`.
In chain mode, execute each skill sequentially. Each skill receives the previous skill's artifacts as input context.
```javascript
// Chain execution: iterate through chain_order
let previousArtifacts = ''; // Accumulates upstream output
for (let i = 0; i < state.chain_order.length; i++) {
const skillName = state.chain_order[i];
const skill = state.target_skills.find(s => s.name === skillName);
const skillArtifactDir = `${iterDir}/artifacts/${skillName}`;
// Build this skill's content
const skillMd = Read(`${skill.path}/SKILL.md`);
const phaseFiles = Glob(`${skill.path}/phases/*.md`).sort().map(f => ({
relativePath: f.replace(skill.path + '/', ''),
content: Read(f)
}));
const specFiles = Glob(`${skill.path}/specs/*.md`).map(f => ({
relativePath: f.replace(skill.path + '/', ''),
content: Read(f)
}));
const singleSkillContent = `### File: SKILL.md\n${skillMd}\n\n` +
phaseFiles.map(f => `### File: ${f.relativePath}\n${f.content}`).join('\n\n') +
(specFiles.length > 0 ? '\n\n' + specFiles.map(f => `### File: ${f.relativePath}\n${f.content}`).join('\n\n') : '');
// Build chain context from previous skill's artifacts
const chainInputContext = previousArtifacts
? `\nPREVIOUS CHAIN OUTPUT (from upstream skill "${state.chain_order[i - 1]}"):\n${previousArtifacts}\n\nIMPORTANT: Use the above output as input context for this skill's execution.\n`
: '';
// Construct per-skill execution prompt
// Ref: templates/execute-prompt.md
const chainPrompt = `PURPOSE: Simulate executing the following workflow skill against a test scenario. Produce all expected output artifacts.
SKILL CONTENT (${skillName} — chain position ${i + 1}/${state.chain_order.length}):
${singleSkillContent}
${chainInputContext}
TEST SCENARIO:
Description: ${state.test_scenario.description}
Input Arguments: ${state.test_scenario.input_args}
Requirements: ${state.test_scenario.requirements.join('; ')}
Success Criteria: ${state.test_scenario.success_criteria}
TASK:
1. Study the complete skill structure
2. Follow the skill execution flow sequentially
3. Produce all expected artifacts
4. Write output to the current working directory
5. Create manifest.json listing all produced artifacts
MODE: write
CONTEXT: @**/*
CONSTRAINTS: Follow skill flow exactly, produce realistic output`;
function escapeForShell(str) {
return str.replace(/"/g, '\\"').replace(/\$/g, '\\$').replace(/`/g, '\\`');
}
const cliCommand = `ccw cli -p "${escapeForShell(chainPrompt)}" --tool claude --mode write --cd "${skillArtifactDir}"`;
// Execute in background
Bash({
command: cliCommand,
run_in_background: true,
timeout: 600000
});
// STOP -- wait for hook callback
// After callback: collect artifacts for next skill in chain
const artifacts = Glob(`${skillArtifactDir}/**/*`);
const skillSuccess = artifacts.length > 0;
if (skillSuccess) {
previousArtifacts = artifacts.slice(0, 10).map(f => {
const relPath = f.replace(skillArtifactDir + '/', '');
const content = Read(f, { limit: 100 });
return `--- ${relPath} ---\n${content}`;
}).join('\n\n');
} else {
// Mid-chain failure: keep previous artifacts for downstream skills
// Log warning but continue chain — downstream skills receive last successful output
state.errors.push({
phase: 'execute',
message: `Chain skill "${skillName}" (position ${i + 1}) produced no artifacts. Downstream skills will receive upstream output from "${state.chain_order[i - 1] || 'none'}" instead.`,
timestamp: new Date().toISOString()
});
state.error_count++;
// previousArtifacts remains from last successful skill (or empty if first)
}
// Update per-skill TodoWrite
// TaskUpdate chain skill task with execution status
// Record per-skill execution
if (!state.iterations[N - 1].execution.chain_executions) {
state.iterations[N - 1].execution.chain_executions = [];
}
state.iterations[N - 1].execution.chain_executions.push({
skill_name: skillName,
cli_command: cliCommand,
artifacts_dir: skillArtifactDir,
success: skillSuccess
});
// Check error budget: abort chain if too many consecutive failures
if (state.error_count >= 3) {
state.errors.push({
phase: 'execute',
message: `Chain execution aborted at skill "${skillName}" — error limit reached (${state.error_count} errors).`,
timestamp: new Date().toISOString()
});
break;
}
}
```
### Step 2.4: Collect Artifacts
After CLI completes (hook callback received):
```javascript
// List produced artifacts
const artifactFiles = Glob(`${iterDir}/artifacts/**/*`);
// Chain mode: check per-skill artifacts
if (state.execution_mode === 'chain') {
const chainSuccess = state.iterations[N - 1].execution.chain_executions?.every(e => e.success) ?? false;
state.iterations[N - 1].execution.success = chainSuccess;
state.iterations[N - 1].execution.artifacts_dir = `${iterDir}/artifacts`;
} else {
if (artifactFiles.length === 0) {
// Execution produced nothing -- record error
state.iterations[N - 1].execution = {
cli_command: cliCommand,
started_at: new Date().toISOString(),
completed_at: new Date().toISOString(),
artifacts_dir: `${iterDir}/artifacts`,
success: false
};
state.error_count++;
// Continue to Phase 3 anyway -- Gemini can evaluate the skill even without artifacts
} else {
state.iterations[N - 1].execution = {
cli_command: cliCommand,
started_at: new Date().toISOString(),
completed_at: new Date().toISOString(),
artifacts_dir: `${iterDir}/artifacts`,
success: true
};
}
} // end single mode branch
// Update state
Write(`${state.work_dir}/iteration-state.json`, JSON.stringify(state, null, 2));
```
## Error Handling
| Error | Recovery |
|-------|----------|
| CLI timeout (10min) | Record failure, continue to Phase 3 without artifacts |
| CLI crash | Retry once with simplified prompt (SKILL.md only, no phase files) |
| No artifacts produced | Continue to Phase 3, evaluation focuses on skill definition quality |
## Output
- **Files**: `iteration-{N}/skill-snapshot/`, `iteration-{N}/artifacts/`
- **State**: `iterations[N-1].execution` updated
- **Next**: Phase 3 (Evaluate)

View File

@@ -0,0 +1,312 @@
# Phase 3: Evaluate Quality
> **COMPACT SENTINEL [Phase 3: Evaluate]**
> This phase contains 5 execution steps (Step 3.1 -- 3.5).
> If you can read this sentinel but cannot find the full Step protocol below, context has been compressed.
> Recovery: `Read("phases/03-evaluate.md")`
Evaluate skill quality using `ccw cli --tool gemini --mode analysis`. Gemini scores the skill across 5 dimensions and provides improvement suggestions.
## Objective
- Construct evaluation prompt with skill + artifacts + criteria
- Execute via ccw cli Gemini
- Parse multi-dimensional score
- Write iteration-{N}-eval.md
- Check termination conditions
## Execution
### Step 3.1: Prepare Evaluation Context
```javascript
const N = state.current_iteration;
const iterDir = `${state.work_dir}/iterations/iteration-${N}`;
// Read evaluation criteria
// Ref: specs/evaluation-criteria.md
const evaluationCriteria = Read('.claude/skills/skill-iter-tune/specs/evaluation-criteria.md');
// Build skillContent (same pattern as Phase 02 — only executable files)
const skillContent = state.target_skills.map(skill => {
const skillMd = Read(`${skill.path}/SKILL.md`);
const phaseFiles = Glob(`${skill.path}/phases/*.md`).sort().map(f => ({
relativePath: f.replace(skill.path + '/', ''),
content: Read(f)
}));
const specFiles = Glob(`${skill.path}/specs/*.md`).map(f => ({
relativePath: f.replace(skill.path + '/', ''),
content: Read(f)
}));
return `### File: SKILL.md\n${skillMd}\n\n` +
phaseFiles.map(f => `### File: ${f.relativePath}\n${f.content}`).join('\n\n') +
(specFiles.length > 0 ? '\n\n' + specFiles.map(f => `### File: ${f.relativePath}\n${f.content}`).join('\n\n') : '');
}).join('\n\n---\n\n');
// Build artifacts summary
let artifactsSummary = 'No artifacts produced (execution may have failed)';
if (state.execution_mode === 'chain') {
// Chain mode: group artifacts by skill
const chainSummaries = state.chain_order.map(skillName => {
const skillArtifactDir = `${iterDir}/artifacts/${skillName}`;
const files = Glob(`${skillArtifactDir}/**/*`);
if (files.length === 0) return `### ${skillName} (no artifacts)`;
const filesSummary = files.map(f => {
const relPath = f.replace(`${skillArtifactDir}/`, '');
const content = Read(f, { limit: 200 });
return `--- ${relPath} ---\n${content}`;
}).join('\n\n');
return `### ${skillName} (chain position ${state.chain_order.indexOf(skillName) + 1})\n${filesSummary}`;
});
artifactsSummary = chainSummaries.join('\n\n---\n\n');
} else {
// Single mode (existing)
const artifactFiles = Glob(`${iterDir}/artifacts/**/*`);
if (artifactFiles.length > 0) {
artifactsSummary = artifactFiles.map(f => {
const relPath = f.replace(`${iterDir}/artifacts/`, '');
const content = Read(f, { limit: 200 });
return `--- ${relPath} ---\n${content}`;
}).join('\n\n');
}
}
// Build previous evaluation context
const previousEvalContext = state.iterations.filter(i => i.evaluation).length > 0
? `PREVIOUS ITERATIONS:\n` + state.iterations.filter(i => i.evaluation).map(iter =>
`Iteration ${iter.round}: Score ${iter.evaluation.score}\n` +
` Applied: ${iter.improvement?.changes_applied?.map(c => c.summary).join('; ') || 'none'}\n` +
` Weaknesses: ${iter.evaluation.weaknesses?.slice(0, 3).join('; ') || 'none'}`
).join('\n') + '\nIMPORTANT: Focus on NEW issues not yet addressed.'
: '';
```
### Step 3.2: Construct Evaluation Prompt
```javascript
// Ref: templates/eval-prompt.md
const evalPrompt = `PURPOSE: Evaluate the quality of a workflow skill by examining its definition and produced artifacts.
SKILL DEFINITION:
${skillContent}
TEST SCENARIO:
${state.test_scenario.description}
Requirements: ${state.test_scenario.requirements.join('; ')}
Success Criteria: ${state.test_scenario.success_criteria}
ARTIFACTS PRODUCED:
${artifactsSummary}
EVALUATION CRITERIA:
${evaluationCriteria}
${previousEvalContext}
${state.execution_mode === 'chain' ? `
CHAIN CONTEXT:
This skill chain contains ${state.chain_order.length} skills executed in order:
${state.chain_order.map((s, i) => `${i+1}. ${s}`).join('\n')}
Current evaluation covers the entire chain output.
Please provide per-skill quality scores in an additional "chain_scores" field: { "${state.chain_order[0]}": <score>, ... }
` : ''}
TASK:
1. Score each dimension (Clarity 0.20, Completeness 0.25, Correctness 0.25, Effectiveness 0.20, Efficiency 0.10) on 0-100
2. Calculate weighted composite score
3. List top 3 strengths
4. List top 3-5 weaknesses with file:section references
5. Provide 3-5 prioritized improvement suggestions with concrete changes
EXPECTED OUTPUT (strict JSON, no markdown):
{
"composite_score": <0-100>,
"dimensions": [
{"name":"Clarity","id":"clarity","score":<0-100>,"weight":0.20,"feedback":"..."},
{"name":"Completeness","id":"completeness","score":<0-100>,"weight":0.25,"feedback":"..."},
{"name":"Correctness","id":"correctness","score":<0-100>,"weight":0.25,"feedback":"..."},
{"name":"Effectiveness","id":"effectiveness","score":<0-100>,"weight":0.20,"feedback":"..."},
{"name":"Efficiency","id":"efficiency","score":<0-100>,"weight":0.10,"feedback":"..."}
],
"strengths": ["...", "...", "..."],
"weaknesses": ["...with file:section ref...", "..."],
"suggestions": [
{"priority":"high|medium|low","target_file":"...","description":"...","rationale":"...","code_snippet":"..."}
]
}
CONSTRAINTS: Be rigorous, reference exact files, focus on highest-impact changes, output ONLY JSON`;
```
### Step 3.3: Execute via ccw cli Gemini
> **CHECKPOINT**: Verify evaluation prompt is properly constructed before CLI execution.
```javascript
// Shell escape utility (same as Phase 02)
function escapeForShell(str) {
return str.replace(/"/g, '\\"').replace(/\$/g, '\\$').replace(/`/g, '\\`');
}
const skillPath = state.target_skills[0].path; // Primary skill for --cd
const cliCommand = `ccw cli -p "${escapeForShell(evalPrompt)}" --tool gemini --mode analysis --cd "${skillPath}"`;
// Execute in background
Bash({
command: cliCommand,
run_in_background: true,
timeout: 300000 // 5 minutes
});
// STOP -- wait for hook callback
```
### Step 3.4: Parse Score and Write Eval File
After CLI completes:
```javascript
// Parse JSON from Gemini output
// The output may contain markdown wrapping -- extract JSON
const rawOutput = /* CLI output from callback */;
const jsonMatch = rawOutput.match(/\{[\s\S]*\}/);
let evaluation;
if (jsonMatch) {
try {
evaluation = JSON.parse(jsonMatch[0]);
// Extract chain_scores if present
if (state.execution_mode === 'chain' && evaluation.chain_scores) {
state.iterations[N - 1].evaluation.chain_scores = evaluation.chain_scores;
}
} catch (e) {
// Fallback: try to extract score heuristically
const scoreMatch = rawOutput.match(/"composite_score"\s*:\s*(\d+)/);
evaluation = {
composite_score: scoreMatch ? parseInt(scoreMatch[1]) : 50,
dimensions: [],
strengths: [],
weaknesses: ['Evaluation output parsing failed -- raw output saved'],
suggestions: []
};
}
} else {
evaluation = {
composite_score: 50,
dimensions: [],
strengths: [],
weaknesses: ['No structured evaluation output -- defaulting to 50'],
suggestions: []
};
}
// Write iteration-N-eval.md
const evalReport = `# Iteration ${N} Evaluation
**Composite Score**: ${evaluation.composite_score}/100
**Date**: ${new Date().toISOString()}
## Dimension Scores
| Dimension | Score | Weight | Feedback |
|-----------|-------|--------|----------|
${(evaluation.dimensions || []).map(d =>
`| ${d.name} | ${d.score} | ${d.weight} | ${d.feedback} |`
).join('\n')}
${(state.execution_mode === 'chain' && evaluation.chain_scores) ? `
## Chain Scores
| Skill | Score | Chain Position |
|-------|-------|----------------|
${state.chain_order.map((s, i) => `| ${s} | ${evaluation.chain_scores[s] || '-'} | ${i + 1} |`).join('\n')}
` : ''}
## Strengths
${(evaluation.strengths || []).map(s => `- ${s}`).join('\n')}
## Weaknesses
${(evaluation.weaknesses || []).map(w => `- ${w}`).join('\n')}
## Improvement Suggestions
${(evaluation.suggestions || []).map((s, i) =>
`### ${i + 1}. [${s.priority}] ${s.description}\n- **Target**: ${s.target_file}\n- **Rationale**: ${s.rationale}\n${s.code_snippet ? `- **Suggested**:\n\`\`\`\n${s.code_snippet}\n\`\`\`` : ''}`
).join('\n\n')}
`;
Write(`${iterDir}/iteration-${N}-eval.md`, evalReport);
// Update state
state.iterations[N - 1].evaluation = {
score: evaluation.composite_score,
dimensions: evaluation.dimensions || [],
strengths: evaluation.strengths || [],
weaknesses: evaluation.weaknesses || [],
suggestions: evaluation.suggestions || [],
chain_scores: evaluation.chain_scores || null,
eval_file: `${iterDir}/iteration-${N}-eval.md`
};
state.latest_score = evaluation.composite_score;
state.score_trend.push(evaluation.composite_score);
Write(`${state.work_dir}/iteration-state.json`, JSON.stringify(state, null, 2));
```
### Step 3.5: Check Termination
```javascript
function shouldTerminate(state) {
// 1. Quality threshold met
if (state.latest_score >= state.quality_threshold) {
return { terminate: true, reason: 'quality_threshold_met' };
}
// 2. Max iterations reached
if (state.current_iteration >= state.max_iterations) {
return { terminate: true, reason: 'max_iterations_reached' };
}
// 3. Convergence: no improvement in last 2 iterations
if (state.score_trend.length >= 3) {
const last3 = state.score_trend.slice(-3);
const improvement = last3[2] - last3[0];
if (improvement <= 2) {
state.converged = true;
return { terminate: true, reason: 'convergence_detected' };
}
}
// 4. Error limit
if (state.error_count >= state.max_errors) {
return { terminate: true, reason: 'error_limit_reached' };
}
return { terminate: false };
}
const termination = shouldTerminate(state);
if (termination.terminate) {
state.termination_reason = termination.reason;
Write(`${state.work_dir}/iteration-state.json`, JSON.stringify(state, null, 2));
// Skip Phase 4, go directly to Phase 5 (Report)
} else {
// Continue to Phase 4 (Improve)
}
```
## Error Handling
| Error | Recovery |
|-------|----------|
| CLI timeout | Retry once, if still fails use score 50 with warning |
| JSON parse failure | Extract score heuristically, save raw output |
| No output | Default score 50, note in weaknesses |
## Output
- **Files**: `iteration-{N}-eval.md`
- **State**: `iterations[N-1].evaluation`, `latest_score`, `score_trend` updated
- **Decision**: terminate -> Phase 5, continue -> Phase 4
- **TodoWrite**: Update current iteration score display

View File

@@ -0,0 +1,186 @@
# Phase 4: Apply Improvements
> **COMPACT SENTINEL [Phase 4: Improve]**
> This phase contains 4 execution steps (Step 4.1 -- 4.4).
> If you can read this sentinel but cannot find the full Step protocol below, context has been compressed.
> Recovery: `Read("phases/04-improve.md")`
Apply targeted improvements to skill files based on evaluation suggestions. Uses a general-purpose Agent to make changes, ensuring only suggested modifications are applied.
## Objective
- Read evaluation suggestions from current iteration
- Launch Agent to apply improvements in priority order
- Document all changes made
- Update iteration state
## Execution
### Step 4.1: Prepare Improvement Context
```javascript
const N = state.current_iteration;
const iterDir = `${state.work_dir}/iterations/iteration-${N}`;
const evaluation = state.iterations[N - 1].evaluation;
// Verify we have suggestions to apply
if (!evaluation.suggestions || evaluation.suggestions.length === 0) {
// No suggestions -- skip improvement, mark iteration complete
state.iterations[N - 1].improvement = {
changes_applied: [],
changes_file: null,
improvement_rationale: 'No suggestions provided by evaluation'
};
state.iterations[N - 1].status = 'completed';
Write(`${state.work_dir}/iteration-state.json`, JSON.stringify(state, null, 2));
// -> Return to orchestrator for next iteration
return;
}
// Build file inventory for agent context
const skillFileInventory = state.target_skills.map(skill => {
return `Skill: ${skill.name} (${skill.path})\nFiles:\n` +
skill.files.map(f => ` - ${f}`).join('\n');
}).join('\n\n');
// Chain mode: add chain relationship context
const chainContext = state.execution_mode === 'chain'
? `\nChain Order: ${state.chain_order.join(' -> ')}\n` +
`Chain Scores: ${state.chain_order.map(s =>
`${s}: ${state.iterations[N-1].evaluation?.chain_scores?.[s] || 'N/A'}`
).join(', ')}\n` +
`Weakest Link: ${state.chain_order.reduce((min, s) => {
const score = state.iterations[N-1].evaluation?.chain_scores?.[s] || 100;
return score < (state.iterations[N-1].evaluation?.chain_scores?.[min] || 100) ? s : min;
}, state.chain_order[0])}`
: '';
```
### Step 4.2: Launch Improvement Agent
> **CHECKPOINT**: Before launching agent, verify:
> 1. evaluation.suggestions is non-empty
> 2. All target_file paths in suggestions are valid
```javascript
const suggestionsText = evaluation.suggestions.map((s, i) =>
`${i + 1}. [${s.priority.toUpperCase()}] ${s.description}\n` +
` Target: ${s.target_file}\n` +
` Rationale: ${s.rationale}\n` +
(s.code_snippet ? ` Suggested change:\n ${s.code_snippet}\n` : '')
).join('\n');
Agent({
subagent_type: 'general-purpose',
run_in_background: false,
description: `Apply skill improvements iteration ${N}`,
prompt: `## Task: Apply Targeted Improvements to Skill Files
You are improving a workflow skill based on evaluation feedback. Apply ONLY the suggested changes -- do not refactor, add features, or "improve" beyond what is explicitly suggested.
## Current Score: ${evaluation.score}/100
Dimension breakdown:
${evaluation.dimensions.map(d => `- ${d.name}: ${d.score}/100`).join('\n')}
## Skill File Inventory
${skillFileInventory}
${chainContext ? `## Chain Context\n${chainContext}\n\nPrioritize improvements on the weakest skill in the chain. Also consider interface compatibility between adjacent skills in the chain.\n` : ''}
## Improvement Suggestions (apply in priority order)
${suggestionsText}
## Rules
1. Read each target file BEFORE modifying it
2. Apply ONLY the suggested changes -- no unsolicited modifications
3. If a suggestion's target_file doesn't exist, skip it and note in summary
4. If a suggestion conflicts with existing patterns, adapt it to fit (note adaptation)
5. Preserve existing code style, naming conventions, and structure
6. After all changes, write a change summary to: ${iterDir}/iteration-${N}-changes.md
## Changes Summary Format (write to ${iterDir}/iteration-${N}-changes.md)
# Iteration ${N} Changes
## Applied Suggestions
- [high] description: what was changed in which file
- [medium] description: what was changed in which file
## Files Modified
- path/to/file.md: brief description of changes
## Skipped Suggestions (if any)
- description: reason for skipping
## Notes
- Any adaptations or considerations
## Success Criteria
- All high-priority suggestions applied
- Medium-priority suggestions applied if feasible
- Low-priority suggestions applied if trivial
- Changes summary written to ${iterDir}/iteration-${N}-changes.md
`
});
```
### Step 4.3: Verify Changes
After agent completes:
```javascript
// Verify changes summary was written
const changesFile = `${iterDir}/iteration-${N}-changes.md`;
const changesExist = Glob(changesFile).length > 0;
if (!changesExist) {
// Agent didn't write summary -- create a minimal one
Write(changesFile, `# Iteration ${N} Changes\n\n## Notes\nAgent completed but did not produce changes summary.\n`);
}
// Read changes summary to extract applied changes
const changesContent = Read(changesFile);
// Parse applied changes (heuristic: count lines starting with "- [")
const appliedMatches = changesContent.match(/^- \[.+?\]/gm) || [];
const changes_applied = appliedMatches.map(m => ({
summary: m.replace(/^- /, ''),
file: '' // Extracted from context
}));
```
### Step 4.4: Update State
```javascript
state.iterations[N - 1].improvement = {
changes_applied: changes_applied,
changes_file: changesFile,
improvement_rationale: `Applied ${changes_applied.length} improvements based on evaluation score ${evaluation.score}`
};
state.iterations[N - 1].status = 'completed';
state.updated_at = new Date().toISOString();
// Also update the skill files list in case new files were created
for (const skill of state.target_skills) {
skill.files = Glob(`${skill.path}/**/*.md`).map(f => f.replace(skill.path + '/', ''));
}
Write(`${state.work_dir}/iteration-state.json`, JSON.stringify(state, null, 2));
// -> Return to orchestrator for next iteration (Phase 2) or termination check
```
## Error Handling
| Error | Recovery |
|-------|----------|
| Agent fails to complete | Rollback from skill-snapshot: `cp -r "${iterDir}/skill-snapshot/${skill.name}/*" "${skill.path}/"` |
| Agent corrupts files | Same rollback from snapshot |
| Changes summary missing | Create minimal summary, continue |
| target_file not found | Agent skips suggestion, notes in summary |
## Output
- **Files**: `iteration-{N}-changes.md`, modified skill files
- **State**: `iterations[N-1].improvement` and `.status` updated
- **Next**: Return to orchestrator, begin next iteration (Phase 2) or terminate

View File

@@ -0,0 +1,166 @@
# Phase 5: Final Report
> **COMPACT SENTINEL [Phase 5: Report]**
> This phase contains 4 execution steps (Step 5.1 -- 5.4).
> If you can read this sentinel but cannot find the full Step protocol below, context has been compressed.
> Recovery: `Read("phases/05-report.md")`
Generate comprehensive iteration history report and display results to user.
## Objective
- Read complete iteration state
- Generate formatted final report with score progression
- Write final-report.md
- Display summary to user
## Execution
### Step 5.1: Read Complete State
```javascript
const state = JSON.parse(Read(`${state.work_dir}/iteration-state.json`));
state.status = 'completed';
state.updated_at = new Date().toISOString();
```
### Step 5.2: Generate Report
```javascript
// Determine outcome
const outcomeMap = {
quality_threshold_met: 'PASSED -- Quality threshold reached',
max_iterations_reached: 'MAX ITERATIONS -- Threshold not reached',
convergence_detected: 'CONVERGED -- Score stopped improving',
error_limit_reached: 'FAILED -- Too many errors'
};
const outcome = outcomeMap[state.termination_reason] || 'COMPLETED';
// Build score progression table
const scoreTable = state.iterations
.filter(i => i.evaluation)
.map(i => {
const dims = i.evaluation.dimensions || [];
const dimScores = ['clarity', 'completeness', 'correctness', 'effectiveness', 'efficiency']
.map(id => {
const dim = dims.find(d => d.id === id);
return dim ? dim.score : '-';
});
return `| ${i.round} | ${i.evaluation.score} | ${dimScores.join(' | ')} |`;
}).join('\n');
// Build iteration details
const iterationDetails = state.iterations.map(iter => {
const evalSection = iter.evaluation
? `**Score**: ${iter.evaluation.score}/100\n` +
`**Strengths**: ${iter.evaluation.strengths?.join(', ') || 'N/A'}\n` +
`**Weaknesses**: ${iter.evaluation.weaknesses?.slice(0, 3).join(', ') || 'N/A'}`
: '**Evaluation**: Skipped or failed';
const changesSection = iter.improvement
? `**Changes Applied**: ${iter.improvement.changes_applied?.length || 0}\n` +
(iter.improvement.changes_applied?.map(c => ` - ${c.summary}`).join('\n') || ' None')
: '**Improvements**: None';
return `### Iteration ${iter.round}\n${evalSection}\n${changesSection}`;
}).join('\n\n');
const report = `# Skill Iter Tune -- Final Report
## Summary
| Field | Value |
|-------|-------|
| **Target Skills** | ${state.target_skills.map(s => s.name).join(', ')} |
| **Execution Mode** | ${state.execution_mode} |
${state.execution_mode === 'chain' ? `| **Chain Order** | ${state.chain_order.join(' -> ')} |` : ''}
| **Test Scenario** | ${state.test_scenario.description} |
| **Iterations** | ${state.iterations.length} |
| **Initial Score** | ${state.score_trend[0] || 'N/A'} |
| **Final Score** | ${state.latest_score}/100 |
| **Quality Threshold** | ${state.quality_threshold} |
| **Outcome** | ${outcome} |
| **Started** | ${state.started_at} |
| **Completed** | ${state.updated_at} |
## Score Progression
| Iter | Composite | Clarity | Completeness | Correctness | Effectiveness | Efficiency |
|------|-----------|---------|--------------|-------------|---------------|------------|
${scoreTable}
**Trend**: ${state.score_trend.join(' -> ')}
${state.execution_mode === 'chain' ? `
## Chain Score Progression
| Iter | ${state.chain_order.join(' | ')} |
|------|${state.chain_order.map(() => '------').join('|')}|
${state.iterations.filter(i => i.evaluation?.chain_scores).map(i => {
const scores = state.chain_order.map(s => i.evaluation.chain_scores[s] || '-');
return `| ${i.round} | ${scores.join(' | ')} |`;
}).join('\n')}
` : ''}
## Iteration Details
${iterationDetails}
## Remaining Weaknesses
${state.iterations.length > 0 && state.iterations[state.iterations.length - 1].evaluation
? state.iterations[state.iterations.length - 1].evaluation.weaknesses?.map(w => `- ${w}`).join('\n') || 'None identified'
: 'No evaluation data available'}
## Artifact Locations
| Path | Description |
|------|-------------|
| \`${state.work_dir}/iteration-state.json\` | Complete state history |
| \`${state.work_dir}/iterations/iteration-{N}/iteration-{N}-eval.md\` | Per-iteration evaluations |
| \`${state.work_dir}/iterations/iteration-{N}/iteration-{N}-changes.md\` | Per-iteration change logs |
| \`${state.work_dir}/final-report.md\` | This report |
| \`${state.backup_dir}/\` | Original skill backups |
## Restore Original
To revert all changes and restore the original skill files:
\`\`\`bash
${state.target_skills.map(s => `cp -r "${state.backup_dir}/${s.name}"/* "${s.path}/"`).join('\n')}
\`\`\`
`;
```
### Step 5.3: Write Report and Update State
```javascript
Write(`${state.work_dir}/final-report.md`, report);
state.status = 'completed';
Write(`${state.work_dir}/iteration-state.json`, JSON.stringify(state, null, 2));
```
### Step 5.4: Display Summary to User
Output to user:
```
Skill Iter Tune Complete!
Target: {skill names}
Iterations: {count}
Score: {initial} -> {final} ({outcome})
Threshold: {threshold}
Score trend: {score1} -> {score2} -> ... -> {scoreN}
Full report: {workDir}/final-report.md
Backups: {backupDir}/
```
## Output
- **Files**: `final-report.md`
- **State**: `status = completed`
- **Next**: Workflow complete. Return control to user.

View File

@@ -0,0 +1,63 @@
# Evaluation Criteria
Skill 质量评估标准,由 Phase 03 (Evaluate) 引用。Gemini 按此标准对 skill 产出物进行多维度评分。
## Dimensions
| Dimension | Weight | ID | Description |
|-----------|--------|----|-------------|
| Clarity | 0.20 | clarity | 指令清晰无歧义结构良好易于遵循。Phase 文件有明确的 Step 划分、输入输出说明 |
| Completeness | 0.25 | completeness | 覆盖所有必要阶段、边界情况、错误处理。没有遗漏关键执行路径 |
| Correctness | 0.25 | correctness | 逻辑正确数据流一致Phase 间无矛盾。State schema 与实际使用匹配 |
| Effectiveness | 0.20 | effectiveness | 在给定测试场景下能产出高质量输出。产物满足用户需求和成功标准 |
| Efficiency | 0.10 | efficiency | 无冗余内容,上下文使用合理,不浪费 token。Phase 职责清晰无重叠 |
## Scoring Guide
| Range | Level | Description |
|-------|-------|-------------|
| 90-100 | Excellent | 生产级别,几乎无改进空间 |
| 80-89 | Good | 可投入使用,仅需微调 |
| 70-79 | Adequate | 功能可用,有明显可改进区域 |
| 60-69 | Needs Work | 存在影响产出质量的显著问题 |
| 0-59 | Poor | 结构或逻辑存在根本性问题 |
## Composite Score Calculation
```
composite = sum(dimension.score * dimension.weight)
```
## Output JSON Schema
```json
{
"composite_score": 75,
"dimensions": [
{ "name": "Clarity", "id": "clarity", "score": 80, "weight": 0.20, "feedback": "..." },
{ "name": "Completeness", "id": "completeness", "score": 70, "weight": 0.25, "feedback": "..." },
{ "name": "Correctness", "id": "correctness", "score": 78, "weight": 0.25, "feedback": "..." },
{ "name": "Effectiveness", "id": "effectiveness", "score": 72, "weight": 0.20, "feedback": "..." },
{ "name": "Efficiency", "id": "efficiency", "score": 85, "weight": 0.10, "feedback": "..." }
],
"strengths": ["...", "...", "..."],
"weaknesses": ["...", "...", "..."],
"suggestions": [
{
"priority": "high",
"target_file": "phases/02-execute.md",
"description": "Add explicit error handling for CLI timeout",
"rationale": "Current phase has no recovery path when CLI execution exceeds timeout",
"code_snippet": "optional suggested replacement code"
}
]
}
```
## Evaluation Focus by Iteration
| Iteration | Primary Focus |
|-----------|--------------|
| 1 | 全面评估,建立 baseline |
| 2-3 | 重点关注上一轮 weaknesses 是否改善,避免重复已解决的问题 |
| 4+ | 精细化改进,关注 Effectiveness 和 Efficiency |

View File

@@ -0,0 +1,134 @@
# Evaluation Prompt Template
Phase 03 使用此模板构造 ccw cli 提示词,让 Gemini 按多维度评估 skill 质量。
## Template
```
PURPOSE: Evaluate the quality of a workflow skill by examining both its definition files and the artifacts it produced when executed against a test scenario. Provide a structured multi-dimensional score with actionable improvement suggestions.
SKILL DEFINITION:
${skillContent}
TEST SCENARIO:
${testScenario.description}
Requirements: ${testScenario.requirements}
Success Criteria: ${testScenario.success_criteria}
ARTIFACTS PRODUCED:
${artifactsSummary}
EVALUATION CRITERIA:
${evaluationCriteria}
${previousEvalContext}
TASK:
1. Read all skill definition files and produced artifacts carefully
2. Score each dimension on 0-100 based on the evaluation criteria:
- Clarity (weight 0.20): Instructions unambiguous, well-structured, easy to follow
- Completeness (weight 0.25): All phases, edge cases, error handling covered
- Correctness (weight 0.25): Logic sound, data flow consistent, no contradictions
- Effectiveness (weight 0.20): Produces high-quality output for the test scenario
- Efficiency (weight 0.10): Minimal redundancy, appropriate context usage
3. Calculate weighted composite score
4. List top 3 strengths
5. List top 3-5 weaknesses with specific file:section references
6. Provide 3-5 prioritized improvement suggestions with concrete changes
MODE: analysis
EXPECTED OUTPUT FORMAT (strict JSON, no markdown wrapping):
{
"composite_score": <number 0-100>,
"dimensions": [
{ "name": "Clarity", "id": "clarity", "score": <0-100>, "weight": 0.20, "feedback": "<specific feedback>" },
{ "name": "Completeness", "id": "completeness", "score": <0-100>, "weight": 0.25, "feedback": "<specific feedback>" },
{ "name": "Correctness", "id": "correctness", "score": <0-100>, "weight": 0.25, "feedback": "<specific feedback>" },
{ "name": "Effectiveness", "id": "effectiveness", "score": <0-100>, "weight": 0.20, "feedback": "<specific feedback>" },
{ "name": "Efficiency", "id": "efficiency", "score": <0-100>, "weight": 0.10, "feedback": "<specific feedback>" }
],
"strengths": ["<strength 1>", "<strength 2>", "<strength 3>"],
"weaknesses": ["<weakness 1 with file:section reference>", "..."],
"suggestions": [
{
"priority": "high|medium|low",
"target_file": "<relative path to skill file>",
"description": "<what to change>",
"rationale": "<why this improves quality>",
"code_snippet": "<optional: suggested replacement content>"
}
],
"chain_scores": {
"<skill_name>": "<number 0-100, per-skill score — only present in chain mode>"
}
}
CONSTRAINTS:
- Be rigorous and specific — reference exact file paths and sections
- Each suggestion MUST include a target_file that maps to a skill file
- Focus suggestions on highest-impact changes first
- Do NOT suggest changes already addressed in previous iterations
- Output ONLY the JSON object, no surrounding text or markdown
```
## Variable Substitution
| Variable | Source | Description |
|----------|--------|-------------|
| `${skillContent}` | Same as execute-prompt.md | 完整 skill 文件内容 |
| `${testScenario.*}` | iteration-state.json | 测试场景信息 |
| `${artifactsSummary}` | Phase 03 reads artifacts/ dir | 产出物文件列表 + 内容摘要 |
| `${evaluationCriteria}` | specs/evaluation-criteria.md | 评分标准全文 |
| `${previousEvalContext}` | 历史迭代记录 | 前几轮评估摘要(避免重复建议) |
| `${chainContext}` | Phase 03 constructs | chain 模式下的链上下文信息 |
## previousEvalContext Construction
```javascript
// Build context from prior iterations to avoid repeating suggestions
const previousEvalContext = state.iterations.length > 0
? `PREVIOUS ITERATIONS (context for avoiding duplicate suggestions):
${state.iterations.map(iter => `
Iteration ${iter.round}: Score ${iter.evaluation?.score || 'N/A'}
Applied changes: ${iter.improvement?.changes_applied?.map(c => c.summary).join('; ') || 'none'}
Remaining weaknesses: ${iter.evaluation?.weaknesses?.slice(0, 3).join('; ') || 'none'}
`).join('')}
IMPORTANT: Focus on NEW issues or issues NOT adequately addressed in previous improvements.`
: '';
```
## chainContext Construction
```javascript
// Build chain context for evaluation (chain mode only)
const chainContext = state.execution_mode === 'chain'
? `CHAIN CONTEXT:
This skill chain contains ${state.chain_order.length} skills executed in order:
${state.chain_order.map((s, i) => `${i+1}. ${s}`).join('\n')}
Current evaluation covers the entire chain output.
Please provide per-skill quality scores in an additional "chain_scores" field.`
: '';
```
## artifactsSummary Construction
```javascript
// Read manifest.json if available, otherwise list files
const manifestPath = `${iterDir}/artifacts/manifest.json`;
let artifactsSummary;
if (fileExists(manifestPath)) {
const manifest = JSON.parse(Read(manifestPath));
artifactsSummary = manifest.artifacts.map(a =>
`- ${a.path}: ${a.description} (Phase ${a.phase})`
).join('\n');
} else {
// Fallback: list all files with first 200 lines each
const files = Glob(`${iterDir}/artifacts/**/*`);
artifactsSummary = files.map(f => {
const content = Read(f, { limit: 200 });
return `--- ${f.replace(iterDir + '/artifacts/', '')} ---\n${content}`;
}).join('\n\n');
}
```

View File

@@ -0,0 +1,97 @@
# Execute Prompt Template
Phase 02 使用此模板构造 ccw cli 提示词,让 Claude 模拟执行 skill 并产出所有预期产物。
## Template
```
PURPOSE: Simulate executing the following workflow skill against a test scenario. Produce all expected output artifacts as if the skill were invoked with the given input. This is for evaluating skill quality.
SKILL CONTENT:
${skillContent}
TEST SCENARIO:
Description: ${testScenario.description}
Input Arguments: ${testScenario.input_args}
Requirements: ${testScenario.requirements}
Success Criteria: ${testScenario.success_criteria}
TASK:
1. Study the complete skill structure (SKILL.md + all phase files)
2. Follow the skill's execution flow sequentially (Phase 1 → Phase N)
3. For each phase, produce the artifacts that phase would generate
4. Write all output artifacts to the current working directory
5. Create a manifest.json listing all produced artifacts with descriptions
MODE: write
CONTEXT: @**/*
EXPECTED:
- All artifacts the skill would produce for this test scenario
- Each artifact in its correct relative path
- A manifest.json at root: { "artifacts": [{ "path": "...", "description": "...", "phase": N }] }
CONSTRAINTS:
- Follow the skill execution flow exactly — do not skip or reorder phases
- Produce realistic, high-quality output (not placeholder content)
- If the skill requires user interaction (AskUserQuestion), use reasonable defaults
- If the skill invokes external tools/CLIs, document what would be called but produce expected output directly
```
## Variable Substitution
| Variable | Source | Description |
|----------|--------|-------------|
| `${skillContent}` | Phase 02 reads all skill files | 完整 SKILL.md + phase 文件内容,用 markdown headers 分隔 |
| `${testScenario.description}` | iteration-state.json | 用户描述的测试场景 |
| `${testScenario.input_args}` | iteration-state.json | 模拟传给 skill 的参数 |
| `${testScenario.requirements}` | iteration-state.json | 质量要求列表 |
| `${testScenario.success_criteria}` | iteration-state.json | 成功标准定义 |
## Chain Mode Extension
When running in chain mode, the template is invoked once per skill in `chain_order`. Each invocation includes:
### Additional Variable
| Variable | Source | Description |
|----------|--------|-------------|
| `${previousChainOutput}` | Phase 02 chain loop | 前序 skill 的 artifacts 摘要 (chain 模式下非首个 skill) |
### Chain Prompt Modification
When `execution_mode === 'chain'`, the prompt includes:
```
PREVIOUS CHAIN OUTPUT (from upstream skill "${previousSkillName}"):
${previousChainOutput}
IMPORTANT: Use the above output as input context for this skill's execution.
```
This section is only added for skills at position 2+ in the chain. The first skill in the chain receives no upstream context.
## skillContent Construction
```javascript
// Read only executable skill files and format with consistent headers
const skillMd = Read(`${skill.path}/SKILL.md`);
const phaseFiles = Glob(`${skill.path}/phases/*.md`).map(f => ({
relativePath: f.replace(skill.path + '/', ''),
content: Read(f)
}));
const specFiles = Glob(`${skill.path}/specs/*.md`).map(f => ({
relativePath: f.replace(skill.path + '/', ''),
content: Read(f)
}));
const skillContent = `
### File: SKILL.md
${skillMd}
${phaseFiles.map(f => `### File: ${f.relativePath}\n${f.content}`).join('\n\n')}
${specFiles.length > 0 ? specFiles.map(f => `### File: ${f.relativePath}\n${f.content}`).join('\n\n') : ''}
`.trim();
```

View File

@@ -32,6 +32,18 @@ Universal team coordination skill: analyze task -> generate role-specs -> dispat
ccw cli --mode write - code generation and modification
```
## Shared Constants
| Constant | Value |
|----------|-------|
| Session prefix | `TC` |
| Session path | `.workflow/.team/TC-<slug>-<date>/` |
| Worker agent | `team-worker` |
| Message bus | `mcp__ccw-tools__team_msg(session_id=<session-id>, ...)` |
| CLI analysis | `ccw cli --mode analysis` |
| CLI write | `ccw cli --mode write` |
| Max roles | 5 |
## Role Router
This skill is **coordinator-only**. Workers do NOT invoke this skill -- they are spawned as `team-worker` agents directly.
@@ -85,6 +97,9 @@ User provides task description
|---------|--------|
| `check` / `status` | Output execution status graph, no advancement |
| `resume` / `continue` | Check worker states, advance next step |
| `revise <TASK-ID> [feedback]` | Revise specific task with optional feedback |
| `feedback <text>` | Inject feedback into active pipeline |
| `improve [dimension]` | Auto-improve weakest quality dimension |
---
@@ -150,6 +165,17 @@ AskUserQuestion({
---
## Specs Reference
| Spec | Purpose |
|------|---------|
| [specs/pipelines.md](specs/pipelines.md) | Dynamic pipeline model, task naming, dependency graph |
| [specs/role-spec-template.md](specs/role-spec-template.md) | Template for dynamic role-spec generation |
| [specs/quality-gates.md](specs/quality-gates.md) | Quality thresholds and scoring dimensions |
| [specs/knowledge-transfer.md](specs/knowledge-transfer.md) | Context transfer protocols between roles |
---
## Session Directory
```

View File

@@ -16,6 +16,20 @@ Parse user task description -> detect required capabilities -> build dependency
If task context requires codebase knowledge, set `needs_research: true`. Phase 2 will spawn researcher worker.
## When to Use
| Trigger | Condition |
|---------|-----------|
| New task | Coordinator Phase 1 receives task description |
| Re-analysis | User provides revised requirements |
| Adapt | handleAdapt extends analysis for new capability |
## Strategy
- **Delegation**: Inline execution (coordinator processes directly)
- **Mode**: Text-level analysis only (no codebase reading)
- **Output**: `<session>/task-analysis.json`
## Phase 2: Context Loading
| Input | Source | Required |

View File

@@ -4,6 +4,20 @@
Create task chains from dynamic dependency graphs. Builds pipelines from the task-analysis.json produced by Phase 1. Workers are spawned as team-worker agents with role-spec paths.
## When to Use
| Trigger | Condition |
|---------|-----------|
| After analysis | Phase 1 complete, task-analysis.json exists |
| After adapt | handleAdapt created new roles, needs new tasks |
| Re-dispatch | Pipeline restructuring (rare) |
## Strategy
- **Delegation**: Inline execution (coordinator processes directly)
- **Inputs**: task-analysis.json + team-session.json
- **Output**: TaskCreate calls with dependency chains
## Phase 2: Context Loading
| Input | Source | Required |

View File

@@ -4,6 +4,22 @@
Event-driven pipeline coordination with Spawn-and-Stop pattern. Role names are read from `team-session.json#roles`. Workers are spawned as `team-worker` agents with role-spec paths. Includes `handleComplete` for pipeline completion action and `handleAdapt` for mid-pipeline capability gap handling.
## When to Use
| Trigger | Condition |
|---------|-----------|
| Worker callback | Message contains [role-name] from session roles |
| User command | "check", "status", "resume", "continue" |
| Capability gap | Worker reports capability_gap |
| Pipeline spawn | After dispatch, initial spawn needed |
| Pipeline complete | All tasks done |
## Strategy
- **Delegation**: Inline execution with handler routing
- **Beat model**: ONE_STEP_PER_INVOCATION — one handler then STOP
- **Workers**: Spawned as team-worker via Agent() in background
## Constants
| Constant | Value | Description |

View File

@@ -1,3 +1,7 @@
---
role: coordinator
---
# Coordinator Role
Orchestrate the team-coordinate workflow: task analysis, dynamic role-spec generation, task dispatching, progress monitoring, session state, and completion action. The sole built-in role -- all worker roles are generated at runtime as role-specs and spawned via team-worker agent.
@@ -33,6 +37,30 @@ Orchestrate the team-coordinate workflow: task analysis, dynamic role-spec gener
---
## Message Types
| Type | Direction | Trigger |
|------|-----------|---------|
| state_update | outbound | Session init, pipeline progress |
| task_unblocked | outbound | Task ready for execution |
| fast_advance | inbound | Worker skipped coordinator |
| capability_gap | inbound | Worker needs new capability |
| error | inbound | Worker failure |
| impl_complete | inbound | Worker task done |
| consensus_blocked | inbound | Discussion verdict conflict |
## Message Bus Protocol
All coordinator state changes MUST be logged to team_msg BEFORE SendMessage:
1. `team_msg(operation="log", ...)` — log the event
2. `SendMessage(...)` — communicate to worker/user
3. `TaskUpdate(...)` — update task state
Read state before every handler: `team_msg(operation="get_state", session_id=<session-id>)`
---
## Command Execution Protocol
When coordinator needs to execute a command (analyze-task, dispatch, monitor):
@@ -52,6 +80,20 @@ Phase 1 needs task analysis
-> Continue to Phase 2
```
## Toolbox
| Tool | Type | Purpose |
|------|------|---------|
| commands/analyze-task.md | Command | Task analysis and role design |
| commands/dispatch.md | Command | Task chain creation |
| commands/monitor.md | Command | Pipeline monitoring and handlers |
| team-worker | Subagent | Worker spawning |
| TeamCreate / TeamDelete | System | Team lifecycle |
| TaskCreate / TaskList / TaskGet / TaskUpdate | System | Task lifecycle |
| team_msg | System | Message bus operations |
| SendMessage | System | Inter-agent communication |
| AskUserQuestion | System | User interaction |
---
## Entry Router

View File

@@ -0,0 +1,111 @@
# Knowledge Transfer Protocols
## 1. Transfer Channels
| Channel | Scope | Mechanism | When to Use |
|---------|-------|-----------|-------------|
| **Artifacts** | Producer -> Consumer | Write to `<session>/artifacts/<name>.md`, consumer reads in Phase 2 | Structured deliverables (reports, plans, specs) |
| **State Updates** | Cross-role | `team_msg(operation="log", type="state_update", data={...})` / `team_msg(operation="get_state", session_id=<session-id>)` | Key findings, decisions, metadata (small, structured data) |
| **Wisdom** | Cross-task | Append to `<session>/wisdom/{learnings,decisions,conventions,issues}.md` | Patterns, conventions, risks discovered during execution |
| **Context Accumulator** | Intra-role (inner loop) | In-memory array, passed to each subsequent task in same-prefix loop | Prior task summaries within same role's inner loop |
| **Exploration Cache** | Cross-role | `<session>/explorations/cache-index.json` + per-angle JSON | Codebase discovery results, prevents duplicate exploration |
## 2. Context Loading Protocol (Phase 2)
Every role MUST load context in this order before starting work.
| Step | Action | Required |
|------|--------|----------|
| 1 | Extract session path from task description | Yes |
| 2 | `team_msg(operation="get_state", session_id=<session-id>)` | Yes |
| 3 | Read artifact files from upstream state's `ref` paths | Yes |
| 4 | Read `<session>/wisdom/*.md` if exists | Yes |
| 5 | Check `<session>/explorations/cache-index.json` before new exploration | If exploring |
| 6 | For inner_loop roles: load context_accumulator from prior tasks | If inner_loop |
**Loading rules**:
- Never skip step 2 -- state contains key decisions and findings
- If `ref` path in state does not exist, log warning and continue
- Wisdom files are append-only -- read all entries, newest last
## 3. Context Publishing Protocol (Phase 4)
| Step | Action | Required |
|------|--------|----------|
| 1 | Write deliverable to `<session>/artifacts/<task-id>-<name>.md` | Yes |
| 2 | Send `team_msg(type="state_update")` with payload (see schema below) | Yes |
| 3 | Append wisdom entries for learnings, decisions, issues found | If applicable |
## 4. State Update Schema
Sent via `team_msg(type="state_update")` on task completion.
```json
{
"status": "task_complete",
"task_id": "<TASK-NNN>",
"ref": "<session>/artifacts/<filename>",
"key_findings": [
"Finding 1",
"Finding 2"
],
"decisions": [
"Decision with rationale"
],
"files_modified": [
"path/to/file.ts"
],
"verification": "self-validated | peer-reviewed | tested"
}
```
**Field rules**:
- `ref`: Always an artifact path, never inline content
- `key_findings`: Max 5 items, each under 100 chars
- `decisions`: Include rationale, not just the choice
- `files_modified`: Only for implementation tasks
- `verification`: One of `self-validated`, `peer-reviewed`, `tested`
**Write state** (namespaced by role):
```
team_msg(operation="log", session_id=<session-id>, from=<role>, type="state_update", data={
"<role_name>": { "key_findings": [...], "scope": "..." }
})
```
**Read state**:
```
team_msg(operation="get_state", session_id=<session-id>)
// Returns merged state from all state_update messages
```
## 5. Exploration Cache Protocol
Prevents redundant research across tasks and discussion rounds.
| Step | Action |
|------|--------|
| 1 | Read `<session>/explorations/cache-index.json` |
| 2 | If angle already explored, read cached result from `explore-<angle>.json` |
| 3 | If not cached, perform exploration |
| 4 | Write result to `<session>/explorations/explore-<angle>.json` |
| 5 | Update `cache-index.json` with new entry |
**cache-index.json format**:
```json
{
"entries": [
{
"angle": "competitor-analysis",
"file": "explore-competitor-analysis.json",
"created_by": "RESEARCH-001",
"timestamp": "2026-01-15T10:30:00Z"
}
]
}
```
**Rules**:
- Cache key is the exploration `angle` (normalized to kebab-case)
- Cache entries never expire within a session
- Any role can read cached explorations; only the creator updates them

View File

@@ -81,3 +81,17 @@ message_types:
## Specs Reference
- [role-spec-template.md](role-spec-template.md) — Template for generating dynamic role-specs
- [quality-gates.md](quality-gates.md) — Quality thresholds and scoring dimensions
- [knowledge-transfer.md](knowledge-transfer.md) — Context transfer protocols between roles
## Quality Gate Integration
Dynamic pipelines reference quality thresholds from [specs/quality-gates.md](quality-gates.md).
| Gate Point | Trigger | Criteria Source |
|------------|---------|----------------|
| After artifact production | Producer role Phase 4 | Behavioral Traits in role-spec |
| After validation tasks | Tester/analyst completion | quality-gates.md thresholds |
| Pipeline completion | All tasks done | Aggregate scoring |
Issue classification: Error (blocks) > Warning (proceed with justification) > Info (log for future).

View File

@@ -0,0 +1,112 @@
# Quality Gates
## 1. Quality Thresholds
| Result | Score | Action |
|--------|-------|--------|
| Pass | >= 80% | Report completed |
| Review | 60-79% | Report completed with warnings |
| Fail | < 60% | Retry Phase 3 (max 2 retries) |
## 2. Scoring Dimensions
| Dimension | Weight | Criteria |
|-----------|--------|----------|
| Completeness | 25% | All required outputs present with substantive content |
| Consistency | 25% | Terminology, formatting, cross-references are uniform |
| Accuracy | 25% | Outputs are factually correct and verifiable against sources |
| Depth | 25% | Sufficient detail for downstream consumers to act on deliverables |
**Score** = weighted average of all dimensions (0-100 per dimension).
## 3. Dynamic Role Quality Checks
Quality checks vary by `output_type` (from task-analysis.json role metadata).
### output_type: artifact
| Check | Pass Criteria |
|-------|---------------|
| Artifact exists | File written to `<session>/artifacts/` |
| Content non-empty | Substantive content, not just headers |
| Format correct | Expected format (MD, JSON) matches deliverable |
| Cross-references | All references to upstream artifacts resolve |
### output_type: codebase
| Check | Pass Criteria |
|-------|---------------|
| Files modified | Claimed files actually changed (Read to confirm) |
| Syntax valid | No syntax errors in modified files |
| No regressions | Existing functionality preserved |
| Summary artifact | Implementation summary written to artifacts/ |
### output_type: mixed
All checks from both `artifact` and `codebase` apply.
## 4. Verification Protocol
Derived from Behavioral Traits in [role-spec-template.md](role-spec-template.md).
| Step | Action | Required |
|------|--------|----------|
| 1 | Verify all claimed files exist via Read | Yes |
| 2 | Confirm artifact written to `<session>/artifacts/` | Yes |
| 3 | Check verification summary fields present | Yes |
| 4 | Score against quality dimensions | Yes |
| 5 | Apply threshold -> Pass/Review/Fail | Yes |
**On Fail**: Retry Phase 3 (max 2 retries). After 2 retries, report `partial_completion`.
**On Review**: Proceed with warnings logged to `<session>/wisdom/issues.md`.
## 5. Code Review Dimensions
For REVIEW-* or validation tasks during implementation pipelines.
### Quality
| Check | Severity |
|-------|----------|
| Empty catch blocks | Error |
| `as any` type casts | Warning |
| `@ts-ignore` / `@ts-expect-error` | Warning |
| `console.log` in production code | Warning |
| Unused imports/variables | Info |
### Security
| Check | Severity |
|-------|----------|
| Hardcoded secrets/credentials | Error |
| SQL injection vectors | Error |
| `eval()` or `Function()` usage | Error |
| `innerHTML` assignment | Warning |
| Missing input validation | Warning |
### Architecture
| Check | Severity |
|-------|----------|
| Circular dependencies | Error |
| Deep cross-boundary imports (3+ levels) | Warning |
| Files > 500 lines | Warning |
| Functions > 50 lines | Info |
### Requirements Coverage
| Check | Severity |
|-------|----------|
| Core functionality implemented | Error if missing |
| Acceptance criteria covered | Error if missing |
| Edge cases handled | Warning |
| Error states handled | Warning |
## 6. Issue Classification
| Class | Label | Action |
|-------|-------|--------|
| Error | Must fix | Blocks progression, must resolve before proceeding |
| Warning | Should fix | Should resolve, can proceed with justification |
| Info | Nice to have | Optional improvement, log for future |

View File

@@ -46,6 +46,7 @@ message_types:
| `prefix` | Yes | Task prefix to filter (e.g., RESEARCH, DRAFT, IMPL) |
| `inner_loop` | Yes | Whether team-worker loops through same-prefix tasks |
| `CLI tools` | No | Array of CLI tool types this role may call |
| `output_tag` | Yes | Output tag for all messages, e.g., `[researcher]` |
| `message_types` | Yes | Message type mapping for team_msg |
| `message_types.success` | Yes | Type string for successful completion |
| `message_types.error` | Yes | Type string for errors (usually "error") |
@@ -63,6 +64,29 @@ message_types:
| `<placeholder>` notation | Use angle brackets for variable substitution |
| Reference CLI tools by name | team-worker resolves invocation from its delegation templates |
## Generated Role-Spec Structure
Every generated role-spec MUST include these blocks:
### Identity Block (mandatory — first section of generated spec)
```
Tag: [<role_name>] | Prefix: <PREFIX>-*
Responsibility: <one-line from task analysis>
```
### Boundaries Block (mandatory — after Identity)
```
### MUST
- <3-5 rules derived from task analysis>
### MUST NOT
- Execute work outside assigned prefix
- Modify artifacts from other roles
- Skip Phase 4 verification
```
## Behavioral Traits
All dynamically generated role-specs MUST embed these traits into Phase 4. Coordinator copies this section verbatim into every generated role-spec as a Phase 4 appendix.
@@ -93,6 +117,11 @@ Phase 4 must produce a verification summary with these fields:
- Still fails → report `partial_completion` with details, NOT `completed`
- Update shared state via `team_msg(operation="log", type="state_update", data={...})` after verification passes
Quality thresholds from [specs/quality-gates.md](quality-gates.md):
- Pass >= 80%: report completed
- Review 60-79%: report completed with warnings
- Fail < 60%: retry Phase 3 (max 2)
### Error Protocol
- Primary approach fails → try alternative (different CLI tool / different tool)
@@ -139,48 +168,25 @@ Coordinator MAY reference these patterns when composing Phase 2-4 content for a
## Knowledge Transfer Protocol
How context flows between roles. Coordinator MUST reference this when composing Phase 2 of any role-spec.
Full protocol: [specs/knowledge-transfer.md](knowledge-transfer.md)
### Transfer Channels
Generated role-specs Phase 2 MUST declare which upstream sources to load.
Generated role-specs Phase 4 MUST include state update and artifact publishing.
| Channel | Scope | Mechanism | When to Use |
|---------|-------|-----------|-------------|
| **Artifacts** | Producer -> Consumer | Write to `<session>/artifacts/<name>.md`, consumer reads in Phase 2 | Structured deliverables (reports, plans, specs) |
| **State Updates** | Cross-role | `team_msg(operation="log", type="state_update", data={...})` / `team_msg(operation="get_state", session_id=<session-id>)` | Key findings, decisions, metadata (small, structured data) |
| **Wisdom** | Cross-task | Append to `<session>/wisdom/{learnings,decisions,conventions,issues}.md` | Patterns, conventions, risks discovered during execution |
| **context_accumulator** | Intra-role (inner loop) | In-memory array, passed to each subsequent task in same-prefix loop | Prior task summaries within same role's inner loop |
| **Exploration cache** | Cross-role | `<session>/explorations/cache-index.json` + per-angle JSON | Codebase discovery results, prevents duplicate exploration |
---
### Phase 2 Context Loading (role-spec must specify)
## Generated Role-Spec Validation
Every generated role-spec Phase 2 MUST declare which upstream sources to load:
Coordinator verifies before writing each role-spec:
```
1. Extract session path from task description
2. Read upstream artifacts: <list which artifacts from which upstream role>
3. Read cross-role state via `team_msg(operation="get_state", session_id=<session-id>)`
4. Load wisdom files for accumulated knowledge
5. For inner_loop roles: load context_accumulator from prior tasks
6. Check exploration cache before running new explorations
```
### State Update Convention
Cross-role state is managed via `team_msg` state updates instead of a separate file:
- **Write state**: `team_msg(operation="log", session_id=<session-id>, from=<role>, type="state_update", data={ "<role_name>": { ... } })`
- **Read state**: `team_msg(operation="get_state", session_id=<session-id>)`
- **Namespaced keys**: Each role writes under its own namespace key in `data`
- **Small data only**: Key findings, decision summaries, metadata. NOT full documents
- **State stored in**: `.msg/meta.json` (auto-managed by team_msg)
- **Example write**:
```
team_msg(operation="log", session_id="TC-auth-2026-03-03", from="researcher", type="state_update", data={
"researcher": { "key_findings": [...], "scope": "..." }
})
```
- **Example read**:
```
team_msg(operation="get_state", session_id="TC-auth-2026-03-03")
// Returns merged state from all state_update messages
```
| Check | Criteria |
|-------|----------|
| Frontmatter complete | All required fields present (role, prefix, inner_loop, output_tag, message_types, CLI tools) |
| Identity block | Tag, prefix, responsibility defined |
| Boundaries | MUST and MUST NOT rules present |
| Phase 2 | Context loading sources specified |
| Phase 3 | Execution goal clear, not prescriptive about tools |
| Phase 4 | Behavioral Traits copied verbatim |
| Error Handling | Table with 3+ scenarios |
| Line count | Target ~80 lines (max 120) |
| No built-in overlap | No Phase 1/5, no message bus code, no consensus handling |

View File

@@ -148,7 +148,7 @@ AskUserQuestion({
|----------|------------|
| Unknown --role value | Error with role registry list |
| Role file not found | Error with expected path (roles/{name}/role.md) |
| project-tech.json missing | Coordinator invokes /workflow:init |
| project-tech.json missing | Coordinator invokes /workflow:spec:setup |
| Phase verification fails with gaps | Coordinator triggers gap closure loop (max 3 iterations) |
| Max gap closure iterations (3) | Report to user, ask for guidance |
| Worker crash | Respawn worker, reassign task |

View File

@@ -284,7 +284,7 @@ Delegate to `commands/monitor.md`:
| Scenario | Resolution |
|----------|------------|
| project-tech.json missing | Invoke /workflow:init automatically |
| project-tech.json missing | Invoke /workflow:spec:setup automatically |
| User cancels roadmap discussion | Save session state, exit gracefully |
| Planner fails | Retry once, then ask user for guidance |
| Executor fails on plan | Mark plan as failed, continue with next |

View File

@@ -85,7 +85,7 @@
"init_prerequisite": {
"required_files": [".workflow/project-tech.json"],
"optional_files": [".workflow/specs/*.md"],
"init_command": "/workflow:init"
"init_command": "/workflow:spec:setup "
},
"_metadata": {
"created_at": "2026-02-24",

View File

@@ -502,40 +502,3 @@ if (plannerAgent) {
| `check` / `status` | Show progress: planned / executing / completed / failed counts |
| `resume` / `continue` | Re-enter loop from Phase 2 |
---
## Coordinator Role Constraints (Main Agent)
**CRITICAL**: The coordinator (main agent executing this skill) is responsible for **orchestration only**, NOT implementation.
15. **Coordinator Does NOT Execute Code**: The main agent MUST NOT write, modify, or implement any code directly. All implementation work is delegated to spawned team agents. The coordinator only:
- Spawns agents with task assignments
- Waits for agent callbacks
- Merges results and coordinates workflow
- Manages workflow transitions between phases
16. **Patient Waiting is Mandatory**: Agent execution takes significant time (typically 10-30 minutes per phase, sometimes longer). The coordinator MUST:
- Wait patiently for `wait()` calls to complete
- NOT skip workflow steps due to perceived delays
- NOT assume agents have failed just because they're taking time
- Trust the timeout mechanisms defined in the skill
17. **Use send_input for Clarification**: When agents need guidance or appear stuck, the coordinator MUST:
- Use `send_input()` to ask questions or provide clarification
- NOT skip the agent or move to next phase prematurely
- Give agents opportunity to respond before escalating
- Example: `send_input({ id: agent_id, message: "Please provide status update or clarify blockers" })`
18. **No Workflow Shortcuts**: The coordinator MUST NOT:
- Skip phases or stages defined in the workflow
- Bypass required approval or review steps
- Execute dependent tasks before prerequisites complete
- Assume task completion without explicit agent callback
- Make up or fabricate agent results
19. **Respect Long-Running Processes**: This is a complex multi-agent workflow that requires patience:
- Total execution time may range from 30-90 minutes or longer
- Each phase may take 10-30 minutes depending on complexity
- The coordinator must remain active and attentive throughout the entire process
- Do not terminate or skip steps due to time concerns

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

BIN
assets/wechat-group-qr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

View File

@@ -0,0 +1,154 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { fireEvent } from '@testing-library/react';
import { render, screen, waitFor } from '@/test/i18n';
import { AdvancedTab } from './AdvancedTab';
vi.mock('@/hooks', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/hooks')>();
return {
...actual,
useCodexLensEnv: vi.fn(),
useUpdateCodexLensEnv: vi.fn(),
useCodexLensIgnorePatterns: vi.fn(),
useUpdateIgnorePatterns: vi.fn(),
useNotifications: vi.fn(),
};
});
import {
useCodexLensEnv,
useUpdateCodexLensEnv,
useCodexLensIgnorePatterns,
useUpdateIgnorePatterns,
useNotifications,
} from '@/hooks';
const mockRefetchEnv = vi.fn().mockResolvedValue(undefined);
const mockRefetchPatterns = vi.fn().mockResolvedValue(undefined);
const mockUpdateEnv = vi.fn().mockResolvedValue({ success: true, message: 'Saved' });
const mockUpdatePatterns = vi.fn().mockResolvedValue({
success: true,
patterns: ['dist', 'frontend/dist'],
extensionFilters: ['*.min.js', 'frontend/skip.ts'],
defaults: {
patterns: ['dist', 'build'],
extensionFilters: ['*.min.js'],
},
});
const mockToastSuccess = vi.fn();
const mockToastError = vi.fn();
function setupDefaultMocks() {
vi.mocked(useCodexLensEnv).mockReturnValue({
data: { success: true, env: {}, settings: {}, raw: '', path: '~/.codexlens/.env' },
raw: '',
env: {},
settings: {},
isLoading: false,
error: null,
refetch: mockRefetchEnv,
});
vi.mocked(useUpdateCodexLensEnv).mockReturnValue({
updateEnv: mockUpdateEnv,
isUpdating: false,
error: null,
});
vi.mocked(useCodexLensIgnorePatterns).mockReturnValue({
data: {
success: true,
patterns: ['dist', 'coverage'],
extensionFilters: ['*.min.js', '*.map'],
defaults: {
patterns: ['dist', 'build', 'coverage'],
extensionFilters: ['*.min.js', '*.map'],
},
},
patterns: ['dist', 'coverage'],
extensionFilters: ['*.min.js', '*.map'],
defaults: {
patterns: ['dist', 'build', 'coverage'],
extensionFilters: ['*.min.js', '*.map'],
},
isLoading: false,
error: null,
refetch: mockRefetchPatterns,
});
vi.mocked(useUpdateIgnorePatterns).mockReturnValue({
updatePatterns: mockUpdatePatterns,
isUpdating: false,
error: null,
});
vi.mocked(useNotifications).mockReturnValue({
success: mockToastSuccess,
error: mockToastError,
} as unknown as ReturnType<typeof useNotifications>);
}
describe('AdvancedTab', () => {
beforeEach(() => {
vi.clearAllMocks();
setupDefaultMocks();
});
it('renders existing filter configuration', () => {
render(<AdvancedTab enabled={true} />);
expect(screen.getByLabelText(/Ignored directories \/ paths/i)).toHaveValue('dist\ncoverage');
expect(screen.getByLabelText(/Skipped files \/ globs/i)).toHaveValue('*.min.js\n*.map');
expect(screen.getByText(/Directory filters: 2/i)).toBeInTheDocument();
expect(screen.getByText(/File filters: 2/i)).toBeInTheDocument();
});
it('saves parsed filter configuration', async () => {
render(<AdvancedTab enabled={true} />);
const ignorePatternsInput = screen.getByLabelText(/Ignored directories \/ paths/i);
const extensionFiltersInput = screen.getByLabelText(/Skipped files \/ globs/i);
fireEvent.change(ignorePatternsInput, { target: { value: 'dist,\nfrontend/dist' } });
fireEvent.change(extensionFiltersInput, { target: { value: '*.min.js,\nfrontend/skip.ts' } });
fireEvent.click(screen.getByRole('button', { name: /Save filters/i }));
await waitFor(() => {
expect(mockUpdatePatterns).toHaveBeenCalledWith({
patterns: ['dist', 'frontend/dist'],
extensionFilters: ['*.min.js', 'frontend/skip.ts'],
});
});
expect(mockRefetchPatterns).toHaveBeenCalled();
});
it('restores default filter values before saving', async () => {
render(<AdvancedTab enabled={true} />);
fireEvent.click(screen.getByRole('button', { name: /Restore defaults/i }));
expect(screen.getByLabelText(/Ignored directories \/ paths/i)).toHaveValue('dist\nbuild\ncoverage');
expect(screen.getByLabelText(/Skipped files \/ globs/i)).toHaveValue('*.min.js\n*.map');
fireEvent.click(screen.getByRole('button', { name: /Save filters/i }));
await waitFor(() => {
expect(mockUpdatePatterns).toHaveBeenCalledWith({
patterns: ['dist', 'build', 'coverage'],
extensionFilters: ['*.min.js', '*.map'],
});
});
});
it('blocks invalid filter entries before saving', async () => {
render(<AdvancedTab enabled={true} />);
fireEvent.change(screen.getByLabelText(/Ignored directories \/ paths/i), {
target: { value: 'bad pattern!' },
});
fireEvent.click(screen.getByRole('button', { name: /Save filters/i }));
expect(mockUpdatePatterns).not.toHaveBeenCalled();
expect(screen.getByText(/Invalid ignore patterns/i)).toBeInTheDocument();
});
});

View File

@@ -5,13 +5,18 @@
import { useState, useEffect } from 'react';
import { useIntl } from 'react-intl';
import { Save, RefreshCw, AlertTriangle, FileCode, AlertCircle } from 'lucide-react';
import { Save, RefreshCw, AlertTriangle, FileCode, AlertCircle, Filter } from 'lucide-react';
import { Card } from '@/components/ui/Card';
import { Textarea } from '@/components/ui/Textarea';
import { Button } from '@/components/ui/Button';
import { Label } from '@/components/ui/Label';
import { Badge } from '@/components/ui/Badge';
import { useCodexLensEnv, useUpdateCodexLensEnv } from '@/hooks';
import {
useCodexLensEnv,
useCodexLensIgnorePatterns,
useUpdateCodexLensEnv,
useUpdateIgnorePatterns,
} from '@/hooks';
import { useNotifications } from '@/hooks';
import { cn } from '@/lib/utils';
import { CcwToolsCard } from './CcwToolsCard';
@@ -22,6 +27,28 @@ interface AdvancedTabProps {
interface FormErrors {
env?: string;
ignorePatterns?: string;
extensionFilters?: string;
}
const FILTER_ENTRY_PATTERN = /^[-\w.*\\/]+$/;
function parseListEntries(text: string): string[] {
return text
.split(/[\n,]/)
.map((entry) => entry.trim())
.filter(Boolean);
}
function normalizeListEntries(entries: string[]): string {
return entries
.map((entry) => entry.trim())
.filter(Boolean)
.join('\n');
}
function normalizeListText(text: string): string {
return normalizeListEntries(parseListEntries(text));
}
export function AdvancedTab({ enabled = true }: AdvancedTabProps) {
@@ -37,14 +64,32 @@ export function AdvancedTab({ enabled = true }: AdvancedTabProps) {
refetch,
} = useCodexLensEnv({ enabled });
const {
patterns,
extensionFilters,
defaults,
isLoading: isLoadingPatterns,
error: patternsError,
refetch: refetchPatterns,
} = useCodexLensIgnorePatterns({ enabled });
const { updateEnv, isUpdating } = useUpdateCodexLensEnv();
const { updatePatterns, isUpdating: isUpdatingPatterns } = useUpdateIgnorePatterns();
// Form state
const [envInput, setEnvInput] = useState('');
const [ignorePatternsInput, setIgnorePatternsInput] = useState('');
const [extensionFiltersInput, setExtensionFiltersInput] = useState('');
const [errors, setErrors] = useState<FormErrors>({});
const [hasChanges, setHasChanges] = useState(false);
const [hasFilterChanges, setHasFilterChanges] = useState(false);
const [showWarning, setShowWarning] = useState(false);
const currentIgnorePatterns = patterns ?? [];
const currentExtensionFilters = extensionFilters ?? [];
const defaultIgnorePatterns = defaults?.patterns ?? [];
const defaultExtensionFilters = defaults?.extensionFilters ?? [];
// Initialize form from env - handles both undefined (loading) and empty string (empty file)
// The hook returns raw directly, so we check if it's been set (not undefined means data loaded)
useEffect(() => {
@@ -58,6 +103,31 @@ export function AdvancedTab({ enabled = true }: AdvancedTabProps) {
}
}, [raw, isLoadingEnv]);
useEffect(() => {
if (!isLoadingPatterns) {
const nextIgnorePatterns = patterns ?? [];
const nextExtensionFilters = extensionFilters ?? [];
setIgnorePatternsInput(nextIgnorePatterns.join('\n'));
setExtensionFiltersInput(nextExtensionFilters.join('\n'));
setErrors((prev) => ({
...prev,
ignorePatterns: undefined,
extensionFilters: undefined,
}));
setHasFilterChanges(false);
}
}, [extensionFilters, isLoadingPatterns, patterns]);
const updateFilterChangeState = (nextIgnorePatternsInput: string, nextExtensionFiltersInput: string) => {
const normalizedCurrentIgnorePatterns = normalizeListEntries(currentIgnorePatterns);
const normalizedCurrentExtensionFilters = normalizeListEntries(currentExtensionFilters);
setHasFilterChanges(
normalizeListText(nextIgnorePatternsInput) !== normalizedCurrentIgnorePatterns
|| normalizeListText(nextExtensionFiltersInput) !== normalizedCurrentExtensionFilters
);
};
const handleEnvChange = (value: string) => {
setEnvInput(value);
// Check if there are changes - compare with raw value (handle undefined as empty)
@@ -69,6 +139,22 @@ export function AdvancedTab({ enabled = true }: AdvancedTabProps) {
}
};
const handleIgnorePatternsChange = (value: string) => {
setIgnorePatternsInput(value);
updateFilterChangeState(value, extensionFiltersInput);
if (errors.ignorePatterns) {
setErrors((prev) => ({ ...prev, ignorePatterns: undefined }));
}
};
const handleExtensionFiltersChange = (value: string) => {
setExtensionFiltersInput(value);
updateFilterChangeState(ignorePatternsInput, value);
if (errors.extensionFilters) {
setErrors((prev) => ({ ...prev, extensionFilters: undefined }));
}
};
const parseEnvVariables = (text: string): Record<string, string> => {
const envObj: Record<string, string> = {};
const lines = text.split('\n');
@@ -101,10 +187,50 @@ export function AdvancedTab({ enabled = true }: AdvancedTabProps) {
);
}
setErrors(newErrors);
setErrors((prev) => ({ ...prev, env: newErrors.env }));
return Object.keys(newErrors).length === 0;
};
const validateFilterForm = (): boolean => {
const nextErrors: Pick<FormErrors, 'ignorePatterns' | 'extensionFilters'> = {};
const parsedIgnorePatterns = parseListEntries(ignorePatternsInput);
const parsedExtensionFilters = parseListEntries(extensionFiltersInput);
const invalidIgnorePatterns = parsedIgnorePatterns.filter(
(entry) => !FILTER_ENTRY_PATTERN.test(entry)
);
if (invalidIgnorePatterns.length > 0) {
nextErrors.ignorePatterns = formatMessage(
{
id: 'codexlens.advanced.validation.invalidIgnorePatterns',
defaultMessage: 'Invalid ignore patterns: {values}',
},
{ values: invalidIgnorePatterns.join(', ') }
);
}
const invalidExtensionFilters = parsedExtensionFilters.filter(
(entry) => !FILTER_ENTRY_PATTERN.test(entry)
);
if (invalidExtensionFilters.length > 0) {
nextErrors.extensionFilters = formatMessage(
{
id: 'codexlens.advanced.validation.invalidExtensionFilters',
defaultMessage: 'Invalid file filters: {values}',
},
{ values: invalidExtensionFilters.join(', ') }
);
}
setErrors((prev) => ({
...prev,
ignorePatterns: nextErrors.ignorePatterns,
extensionFilters: nextErrors.extensionFilters,
}));
return Object.values(nextErrors).every((value) => !value);
};
const handleSave = async () => {
if (!validateForm()) {
return;
@@ -138,12 +264,68 @@ export function AdvancedTab({ enabled = true }: AdvancedTabProps) {
const handleReset = () => {
// Reset to current raw value (handle undefined as empty)
setEnvInput(raw ?? '');
setErrors({});
setErrors((prev) => ({ ...prev, env: undefined }));
setHasChanges(false);
setShowWarning(false);
};
const handleSaveFilters = async () => {
if (!validateFilterForm()) {
return;
}
const parsedIgnorePatterns = parseListEntries(ignorePatternsInput);
const parsedExtensionFilters = parseListEntries(extensionFiltersInput);
try {
const result = await updatePatterns({
patterns: parsedIgnorePatterns,
extensionFilters: parsedExtensionFilters,
});
if (result.success) {
setIgnorePatternsInput((result.patterns ?? parsedIgnorePatterns).join('\n'));
setExtensionFiltersInput((result.extensionFilters ?? parsedExtensionFilters).join('\n'));
setHasFilterChanges(false);
setErrors((prev) => ({
...prev,
ignorePatterns: undefined,
extensionFilters: undefined,
}));
await refetchPatterns();
}
} catch {
// Hook-level mutation already reports the error.
}
};
const handleResetFilters = () => {
setIgnorePatternsInput(currentIgnorePatterns.join('\n'));
setExtensionFiltersInput(currentExtensionFilters.join('\n'));
setErrors((prev) => ({
...prev,
ignorePatterns: undefined,
extensionFilters: undefined,
}));
setHasFilterChanges(false);
};
const handleRestoreDefaultFilters = () => {
const defaultIgnoreText = defaultIgnorePatterns.join('\n');
const defaultExtensionText = defaultExtensionFilters.join('\n');
setIgnorePatternsInput(defaultIgnoreText);
setExtensionFiltersInput(defaultExtensionText);
setErrors((prev) => ({
...prev,
ignorePatterns: undefined,
extensionFilters: undefined,
}));
updateFilterChangeState(defaultIgnoreText, defaultExtensionText);
};
const isLoading = isLoadingEnv;
const isLoadingFilters = isLoadingPatterns;
// Get current env variables as array for display
const currentEnvVars = env
@@ -242,6 +424,204 @@ export function AdvancedTab({ enabled = true }: AdvancedTabProps) {
{/* CCW Tools Card */}
<CcwToolsCard />
{/* Index Filters */}
<Card className="p-6">
<div className="flex flex-col gap-3 mb-4 lg:flex-row lg:items-center lg:justify-between">
<div className="flex items-center gap-2">
<Filter className="w-5 h-5 text-muted-foreground" />
<h3 className="text-lg font-semibold text-foreground">
{formatMessage({
id: 'codexlens.advanced.indexFilters',
defaultMessage: 'Index Filters',
})}
</h3>
</div>
<div className="flex flex-wrap items-center gap-2">
<Badge variant="outline" className="text-xs">
{formatMessage(
{
id: 'codexlens.advanced.ignorePatternCount',
defaultMessage: 'Directory filters: {count}',
},
{ count: currentIgnorePatterns.length }
)}
</Badge>
<Badge variant="outline" className="text-xs">
{formatMessage(
{
id: 'codexlens.advanced.extensionFilterCount',
defaultMessage: 'File filters: {count}',
},
{ count: currentExtensionFilters.length }
)}
</Badge>
</div>
</div>
<div className="space-y-4">
{patternsError && (
<div className="rounded-md border border-destructive/20 bg-destructive/5 p-3">
<div className="flex items-start gap-2">
<AlertCircle className="w-4 h-4 mt-0.5 text-destructive" />
<div>
<p className="text-sm font-medium text-destructive">
{formatMessage({
id: 'codexlens.advanced.filtersLoadError',
defaultMessage: 'Unable to load current filter settings',
})}
</p>
<p className="text-xs text-destructive/80 mt-1">{patternsError.message}</p>
</div>
</div>
</div>
)}
<div className="grid gap-4 xl:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="ignore-patterns-input">
{formatMessage({
id: 'codexlens.advanced.ignorePatterns',
defaultMessage: 'Ignored directories / paths',
})}
</Label>
<Textarea
id="ignore-patterns-input"
value={ignorePatternsInput}
onChange={(event) => handleIgnorePatternsChange(event.target.value)}
placeholder={formatMessage({
id: 'codexlens.advanced.ignorePatternsPlaceholder',
defaultMessage: 'dist\nfrontend/dist\ncoverage',
})}
className={cn(
'min-h-[220px] font-mono text-sm',
errors.ignorePatterns && 'border-destructive focus-visible:ring-destructive'
)}
disabled={isLoadingFilters || isUpdatingPatterns}
/>
{errors.ignorePatterns && (
<p className="text-sm text-destructive">{errors.ignorePatterns}</p>
)}
<p className="text-xs text-muted-foreground">
{formatMessage({
id: 'codexlens.advanced.ignorePatternsHint',
defaultMessage: 'One entry per line. Supports exact names, relative paths, and glob patterns.',
})}
</p>
</div>
<div className="space-y-2">
<Label htmlFor="extension-filters-input">
{formatMessage({
id: 'codexlens.advanced.extensionFilters',
defaultMessage: 'Skipped files / globs',
})}
</Label>
<Textarea
id="extension-filters-input"
value={extensionFiltersInput}
onChange={(event) => handleExtensionFiltersChange(event.target.value)}
placeholder={formatMessage({
id: 'codexlens.advanced.extensionFiltersPlaceholder',
defaultMessage: '*.min.js\n*.map\npackage-lock.json',
})}
className={cn(
'min-h-[220px] font-mono text-sm',
errors.extensionFilters && 'border-destructive focus-visible:ring-destructive'
)}
disabled={isLoadingFilters || isUpdatingPatterns}
/>
{errors.extensionFilters && (
<p className="text-sm text-destructive">{errors.extensionFilters}</p>
)}
<p className="text-xs text-muted-foreground">
{formatMessage({
id: 'codexlens.advanced.extensionFiltersHint',
defaultMessage: 'Use this for generated or low-value files that should stay out of the index.',
})}
</p>
</div>
</div>
<div className="grid gap-3 rounded-md border border-border/60 bg-muted/30 p-3 md:grid-cols-2">
<div>
<p className="text-xs font-medium text-foreground">
{formatMessage({
id: 'codexlens.advanced.defaultIgnorePatterns',
defaultMessage: 'Default directory filters',
})}
</p>
<div className="mt-2 flex flex-wrap gap-2">
{defaultIgnorePatterns.slice(0, 6).map((pattern) => (
<Badge key={pattern} variant="secondary" className="font-mono text-xs">
{pattern}
</Badge>
))}
{defaultIgnorePatterns.length > 6 && (
<Badge variant="outline" className="text-xs">
+{defaultIgnorePatterns.length - 6}
</Badge>
)}
</div>
</div>
<div>
<p className="text-xs font-medium text-foreground">
{formatMessage({
id: 'codexlens.advanced.defaultExtensionFilters',
defaultMessage: 'Default file filters',
})}
</p>
<div className="mt-2 flex flex-wrap gap-2">
{defaultExtensionFilters.slice(0, 6).map((pattern) => (
<Badge key={pattern} variant="secondary" className="font-mono text-xs">
{pattern}
</Badge>
))}
{defaultExtensionFilters.length > 6 && (
<Badge variant="outline" className="text-xs">
+{defaultExtensionFilters.length - 6}
</Badge>
)}
</div>
</div>
</div>
<div className="flex flex-wrap items-center gap-2">
<Button
onClick={handleSaveFilters}
disabled={isLoadingFilters || isUpdatingPatterns || !hasFilterChanges}
>
<Save className={cn('w-4 h-4 mr-2', isUpdatingPatterns && 'animate-spin')} />
{isUpdatingPatterns
? formatMessage({ id: 'codexlens.advanced.saving', defaultMessage: 'Saving...' })
: formatMessage({
id: 'codexlens.advanced.saveFilters',
defaultMessage: 'Save filters',
})
}
</Button>
<Button
variant="outline"
onClick={handleResetFilters}
disabled={isLoadingFilters || !hasFilterChanges}
>
<RefreshCw className="w-4 h-4 mr-2" />
{formatMessage({ id: 'codexlens.advanced.reset', defaultMessage: 'Reset' })}
</Button>
<Button
variant="outline"
onClick={handleRestoreDefaultFilters}
disabled={isLoadingFilters || isUpdatingPatterns}
>
{formatMessage({
id: 'codexlens.advanced.restoreDefaults',
defaultMessage: 'Restore defaults',
})}
</Button>
</div>
</div>
</Card>
{/* Environment Variables Editor */}
<Card className="p-6">
<div className="flex items-center justify-between mb-4">

View File

@@ -212,7 +212,7 @@ export function McpServerDialog({
const { formatMessage } = useIntl();
const queryClient = useQueryClient();
const projectPath = useWorkflowStore(selectProjectPath);
const { error: showError } = useNotifications();
const { error: showError, success: showSuccess } = useNotifications();
// Fetch templates from backend
const { templates, isLoading: templatesLoading } = useMcpTemplates();
@@ -241,6 +241,10 @@ export function McpServerDialog({
const [saveAsTemplate, setSaveAsTemplate] = useState(false);
const projectConfigType: McpProjectConfigType = configType === 'claude-json' ? 'claude' : 'mcp';
// JSON import mode state
const [inputMode, setInputMode] = useState<'form' | 'json'>('form');
const [jsonInput, setJsonInput] = useState('');
// Helper to detect transport type from server data
const detectTransportType = useCallback((serverData: McpServer | undefined): McpTransportType => {
if (!serverData) return 'stdio';
@@ -249,6 +253,73 @@ export function McpServerDialog({
return 'stdio';
}, []);
// Parse JSON config and populate form
const parseJsonConfig = useCallback(() => {
try {
const config = JSON.parse(jsonInput);
// Detect transport type based on config structure
if (config.url) {
// HTTP transport
setTransportType('http');
// Parse headers
const headers: HttpHeader[] = [];
if (config.headers && typeof config.headers === 'object') {
Object.entries(config.headers).forEach(([name, value], idx) => {
headers.push({
id: `header-${Date.now()}-${idx}`,
name,
value: String(value),
isEnvVar: false,
});
});
}
setFormData(prev => ({
...prev,
url: config.url || '',
headers,
bearerTokenEnvVar: config.bearer_token_env_var || config.bearerTokenEnvVar || '',
}));
} else {
// STDIO transport
setTransportType('stdio');
const args = Array.isArray(config.args) ? config.args : [];
const env = config.env && typeof config.env === 'object' ? config.env : {};
setFormData(prev => ({
...prev,
command: config.command || '',
args,
env,
}));
setArgsInput(args.join(', '));
setEnvInput(
Object.entries(env)
.map(([k, v]) => `${k}=${v}`)
.join('\n')
);
}
// Switch to form mode to show parsed data
setInputMode('form');
setErrors({});
showSuccess(
formatMessage({ id: 'mcp.dialog.json.parseSuccess' }),
formatMessage({ id: 'mcp.dialog.json.parseSuccessDesc' })
);
} catch (error) {
setErrors({
name: formatMessage({ id: 'mcp.dialog.json.parseError' }, {
error: error instanceof Error ? error.message : 'Invalid JSON'
})
});
}
}, [jsonInput, formatMessage, showSuccess]);
// Initialize form from server prop (edit mode)
useEffect(() => {
if (server && mode === 'edit') {
@@ -578,9 +649,96 @@ export function McpServerDialog({
</DialogTitle>
</DialogHeader>
{/* Input Mode Switcher - Only in add mode */}
{mode === 'add' && (
<div className="flex gap-2 border-b pb-3">
<Button
type="button"
variant={inputMode === 'form' ? 'default' : 'outline'}
size="sm"
onClick={() => setInputMode('form')}
className="flex-1"
>
{formatMessage({ id: 'mcp.dialog.mode.form' })}
</Button>
<Button
type="button"
variant={inputMode === 'json' ? 'default' : 'outline'}
size="sm"
onClick={() => setInputMode('json')}
className="flex-1"
>
{formatMessage({ id: 'mcp.dialog.mode.json' })}
</Button>
</div>
)}
<div className="space-y-4">
{/* Template Selector - Only for STDIO */}
{transportType === 'stdio' && (
{/* JSON Input Mode */}
{inputMode === 'json' ? (
<div className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">
{formatMessage({ id: 'mcp.dialog.json.label' })}
</label>
<textarea
value={jsonInput}
onChange={(e) => setJsonInput(e.target.value)}
placeholder={formatMessage({ id: 'mcp.dialog.json.placeholder' })}
className={cn(
'flex min-h-[300px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 font-mono',
errors.name && 'border-destructive focus-visible:ring-destructive'
)}
/>
{errors.name && (
<p className="text-sm text-destructive">{errors.name}</p>
)}
<p className="text-xs text-muted-foreground">
{formatMessage({ id: 'mcp.dialog.json.hint' })}
</p>
</div>
{/* Example JSON */}
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">
{formatMessage({ id: 'mcp.dialog.json.example' })}
</label>
<div className="bg-muted p-3 rounded-md">
<p className="text-xs font-medium mb-2">STDIO:</p>
<pre className="text-xs overflow-x-auto">
{`{
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path"],
"env": {
"API_KEY": "your-key"
}
}`}
</pre>
<p className="text-xs font-medium mt-3 mb-2">HTTP:</p>
<pre className="text-xs overflow-x-auto">
{`{
"url": "http://localhost:3000",
"headers": {
"Authorization": "Bearer token"
}
}`}
</pre>
</div>
</div>
<Button
type="button"
onClick={parseJsonConfig}
disabled={!jsonInput.trim()}
className="w-full"
>
{formatMessage({ id: 'mcp.dialog.json.parse' })}
</Button>
</div>
) : (
<>
{/* Template Selector - Only for STDIO */}
{transportType === 'stdio' && (
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">
{formatMessage({ id: 'mcp.dialog.form.template' })}
@@ -901,6 +1059,8 @@ export function McpServerDialog({
</label>
</div>
)}
</>
)}
</div>
<DialogFooter>

View File

@@ -6,13 +6,10 @@ import { VersionCheckModal } from './VersionCheckModal';
interface VersionData {
currentVersion: string;
latestVersion: string;
updateAvailable: boolean;
}
interface VersionCheckResponse {
success: boolean;
data?: VersionData;
error?: string;
hasUpdate: boolean;
packageName?: string;
updateCommand?: string;
checkedAt?: string;
}
/**
@@ -42,30 +39,26 @@ export function VersionCheck() {
const checkVersion = async (silent = false) => {
if (!silent) setChecking(true);
try {
const response = await fetch('/api/config/version');
const response = await fetch('/api/version-check');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data: VersionCheckResponse = await response.json();
const data: VersionData = await response.json();
// Validate response structure
if (!data || typeof data !== 'object') {
throw new Error('Invalid response format');
}
if (!data.success) {
throw new Error(data.error || 'Version check failed');
}
if (!data.data || typeof data.data !== 'object') {
if (!data.currentVersion) {
throw new Error('Invalid version data in response');
}
setVersionData(data.data);
setVersionData(data);
if (data.data.updateAvailable && !silent) {
toast.info('新版本可用: ' + data.data.latestVersion);
if (data.hasUpdate && !silent) {
toast.info('新版本可用: ' + data.latestVersion);
}
} catch (error) {
console.error('Version check failed:', error);
@@ -98,7 +91,7 @@ export function VersionCheck() {
localStorage.setItem('ccw.autoUpdate', JSON.stringify(enabled));
};
if (!versionData?.updateAvailable) {
if (!versionData?.hasUpdate) {
return null; // Don't show anything if no update
}

View File

@@ -1,4 +1,4 @@
import { describe, expect, it, vi } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
fetchMcpServers,
toggleMcpServer,
@@ -26,7 +26,29 @@ function getLastFetchCall(fetchMock: any) {
return calls[calls.length - 1] as [RequestInfo | URL, RequestInit | undefined];
}
const TEST_CSRF_TOKEN = 'test-csrf-token';
function mockFetchWithCsrf(
handler: (input: RequestInfo | URL, init?: RequestInit) => Response | Promise<Response>
) {
return vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
if (input === '/api/csrf-token') {
return jsonResponse({ csrfToken: TEST_CSRF_TOKEN });
}
return handler(input, init);
});
}
describe('MCP API (frontend ↔ backend contract)', () => {
beforeEach(() => {
vi.restoreAllMocks();
vi.spyOn(console, 'warn').mockImplementation(() => {});
});
afterEach(() => {
vi.restoreAllMocks();
});
it('fetchMcpServers derives lists from /api/mcp-config and computes enabled from disabledMcpServers', async () => {
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
jsonResponse({
@@ -58,7 +80,8 @@ describe('MCP API (frontend ↔ backend contract)', () => {
expect(fetchMock.mock.calls[0]?.[0]).toBe('/api/mcp-config');
expect(result.global.map((s) => s.name).sort()).toEqual(['global1', 'globalDup']);
expect(result.project.map((s) => s.name)).toEqual(['projOnly']);
expect(result.project.map((s) => s.name)).toEqual(['projOnly', 'globalDup', 'entDup']);
expect(result.conflicts.map((c) => c.name)).toEqual(['globalDup']);
const global1 = result.global.find((s) => s.name === 'global1');
expect(global1?.enabled).toBe(false);
@@ -76,9 +99,7 @@ describe('MCP API (frontend ↔ backend contract)', () => {
});
it('toggleMcpServer uses /api/mcp-toggle with { projectPath, serverName, enable }', async () => {
const fetchMock = vi
.spyOn(globalThis, 'fetch')
.mockImplementation(async (input, _init) => {
const fetchMock = mockFetchWithCsrf(async (input, _init) => {
if (input === '/api/mcp-toggle') {
return jsonResponse({ success: true, serverName: 'global1', enabled: false });
}
@@ -111,7 +132,7 @@ describe('MCP API (frontend ↔ backend contract)', () => {
});
it('deleteMcpServer calls the correct backend endpoint for project/global scopes', async () => {
const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
const fetchMock = mockFetchWithCsrf(async (input) => {
if (input === '/api/mcp-remove-global-server') {
return jsonResponse({ success: true });
}
@@ -129,9 +150,7 @@ describe('MCP API (frontend ↔ backend contract)', () => {
});
it('createMcpServer (project) uses /api/mcp-copy-server and includes serverName + serverConfig', async () => {
const fetchMock = vi
.spyOn(globalThis, 'fetch')
.mockImplementation(async (input) => {
const fetchMock = mockFetchWithCsrf(async (input) => {
if (input === '/api/mcp-copy-server') {
return jsonResponse({ success: true });
}
@@ -181,7 +200,7 @@ describe('MCP API (frontend ↔ backend contract)', () => {
});
it('updateMcpServer (global) upserts via /api/mcp-add-global-server', async () => {
const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
const fetchMock = mockFetchWithCsrf(async (input) => {
if (input === '/api/mcp-add-global-server') {
return jsonResponse({ success: true });
}
@@ -232,7 +251,7 @@ describe('MCP API (frontend ↔ backend contract)', () => {
});
it('crossCliCopy codex->claude copies via /api/mcp-copy-server per server', async () => {
const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
const fetchMock = mockFetchWithCsrf(async (input) => {
if (input === '/api/codex-mcp-config') {
return jsonResponse({ servers: { s1: { command: 'node' } }, configPath: 'x', exists: true });
}

View File

@@ -115,6 +115,20 @@
"addHeader": "Add Header"
}
},
"mode": {
"form": "Form Mode",
"json": "JSON Mode"
},
"json": {
"label": "JSON Configuration",
"placeholder": "Paste MCP server JSON configuration...",
"hint": "Paste complete MCP server configuration JSON, then click Parse button",
"example": "Example Format",
"parse": "Parse JSON",
"parseSuccess": "JSON Parsed Successfully",
"parseSuccessDesc": "Configuration has been populated to the form, please review and save",
"parseError": "JSON Parse Error: {error}"
},
"templates": {
"npx-stdio": "NPX STDIO",
"python-stdio": "Python STDIO",

View File

@@ -102,6 +102,6 @@
},
"empty": {
"title": "No Project Overview",
"message": "Run /workflow:init to initialize project analysis"
"message": "Run /workflow:spec:setup to initialize project analysis"
}
}

View File

@@ -104,6 +104,20 @@
"addHeader": "添加请求头"
}
},
"mode": {
"form": "表单模式",
"json": "JSON 模式"
},
"json": {
"label": "JSON 配置",
"placeholder": "粘贴 MCP 服务器 JSON 配置...",
"hint": "粘贴完整的 MCP 服务器配置 JSON然后点击解析按钮",
"example": "示例格式",
"parse": "解析 JSON",
"parseSuccess": "JSON 解析成功",
"parseSuccessDesc": "配置已填充到表单中,请检查并保存",
"parseError": "JSON 解析失败:{error}"
},
"templates": {
"npx-stdio": "NPX STDIO",
"python-stdio": "Python STDIO",

View File

@@ -102,6 +102,6 @@
},
"empty": {
"title": "暂无项目概览",
"message": "运行 /workflow:init 初始化项目分析"
"message": "运行 /workflow:spec:setup 初始化项目分析"
}
}

View File

@@ -306,15 +306,17 @@ export async function installCommand(options: InstallOptions): Promise<void> {
info(` Files in manifest: ${existingManifest.files?.length || 0}`);
info(` Installed: ${new Date(existingManifest.installation_date).toLocaleDateString()}`);
const { backup } = await inquirer.prompt([{
type: 'confirm',
name: 'backup',
message: 'Create backup before reinstalling?',
default: true
}]);
if (!options.force) {
const { backup } = await inquirer.prompt([{
type: 'confirm',
name: 'backup',
message: 'Create backup before reinstalling?',
default: true
}]);
if (backup) {
await createBackup(existingManifest);
if (backup) {
await createBackup(existingManifest);
}
}
// Clean based on manifest records
@@ -495,7 +497,7 @@ export async function installCommand(options: InstallOptions): Promise<void> {
});
// Install Git Bash fix on Windows
if (platform() === 'win32') {
if (platform() === 'win32' && !options.force) {
console.log('');
const { installFix } = await inquirer.prompt([{
type: 'confirm',

View File

@@ -35,29 +35,6 @@ import {
saveConversation
} from './cli-executor-state.js';
// Debug logging for history save investigation (Iteration 4)
const DEBUG_SESSION_ID = 'DBG-parallel-ccw-cli-test-2026-03-07';
const DEBUG_LOG_PATH = path.join(process.cwd(), '.workflow', '.debug', DEBUG_SESSION_ID, 'debug-save.log');
// Ensure debug log directory exists
try {
const debugDir = path.dirname(DEBUG_LOG_PATH);
if (!fs.existsSync(debugDir)) {
fs.mkdirSync(debugDir, { recursive: true });
}
} catch (err) {
// Ignore directory creation errors
}
function writeDebugLog(event: string, data: Record<string, any>): void {
try {
const logEntry = JSON.stringify({ event, ...data, timestamp: new Date().toISOString() }) + '\n';
fs.appendFileSync(DEBUG_LOG_PATH, logEntry, 'utf8');
} catch (err) {
// Silently ignore logging errors
}
}
// Track all running child processes for cleanup on interruption (multi-process support)
const runningChildProcesses = new Set<ChildProcess>();
@@ -1296,11 +1273,8 @@ async function executeCliTool(
};
// Try to save conversation to history
try {
writeDebugLog('BEFORE_SAVE_CONV', { conversationId: conversation.id, workingDir, tool });
saveConversation(workingDir, conversation);
writeDebugLog('AFTER_SAVE_CONV', { conversationId: conversation.id, workingDir, tool });
} catch (err) {
writeDebugLog('SAVE_CONV_OUTER_ERROR', { conversationId: conversation.id, workingDir, tool, error: (err as Error).message, stack: (err as Error).stack });
// Non-fatal: continue even if history save fails
console.error('[CLI Executor] Failed to save history:', (err as Error).message);
}

View File

@@ -9,29 +9,6 @@ import type { CliOutputUnit } from './cli-output-converter.js';
import * as fs from 'fs';
import * as path from 'path';
// Debug logging for history save investigation (Iteration 4)
const DEBUG_SESSION_ID = 'DBG-parallel-ccw-cli-test-2026-03-07';
const DEBUG_LOG_PATH = path.join(process.cwd(), '.workflow', '.debug', DEBUG_SESSION_ID, 'debug-save.log');
// Ensure debug log directory exists
try {
const debugDir = path.dirname(DEBUG_LOG_PATH);
if (!fs.existsSync(debugDir)) {
fs.mkdirSync(debugDir, { recursive: true });
}
} catch (err) {
// Ignore directory creation errors
}
function writeDebugLog(event: string, data: Record<string, any>): void {
try {
const logEntry = JSON.stringify({ event, ...data, timestamp: new Date().toISOString() }) + '\n';
fs.appendFileSync(DEBUG_LOG_PATH, logEntry, 'utf8');
} catch (err) {
// Silently ignore logging errors
}
}
// Lazy-loaded SQLite store module
let sqliteStoreModule: typeof import('./cli-history-store.js') | null = null;
@@ -39,10 +16,8 @@ let sqliteStoreModule: typeof import('./cli-history-store.js') | null = null;
* Get or initialize SQLite store (async)
*/
export async function getSqliteStore(baseDir: string) {
writeDebugLog('GET_STORE', { baseDir, baseDirType: typeof baseDir, moduleInitialized: sqliteStoreModule !== null });
if (!sqliteStoreModule) {
sqliteStoreModule = await import('./cli-history-store.js');
writeDebugLog('MODULE_LOADED', { baseDir });
}
return sqliteStoreModule.getHistoryStore(baseDir);
}
@@ -163,20 +138,15 @@ async function saveConversationAsync(baseDir: string, conversation: Conversation
* @param baseDir - Project base directory (NOT historyDir)
*/
export function saveConversation(baseDir: string, conversation: ConversationRecord): void {
writeDebugLog('SAVE_CONV_START', { baseDir, conversationId: conversation.id, moduleInitialized: sqliteStoreModule !== null });
try {
const store = getSqliteStoreSync(baseDir);
writeDebugLog('SAVE_CONV_SYNC', { baseDir, conversationId: conversation.id });
// Fire and forget - don't block on async save in sync context
store.saveConversation(conversation).catch(err => {
writeDebugLog('SAVE_CONV_ERROR', { baseDir, conversationId: conversation.id, error: err.message, stack: err.stack });
console.error('[CLI Executor] Failed to save conversation:', err.message);
});
} catch (err) {
writeDebugLog('SAVE_CONV_FALLBACK_ASYNC', { baseDir, conversationId: conversation.id, error: (err as Error).message });
// If sync not available, queue for async save
saveConversationAsync(baseDir, conversation).catch(err => {
writeDebugLog('SAVE_CONV_ASYNC_ERROR', { baseDir, conversationId: conversation.id, error: err.message, stack: err.stack });
console.error('[CLI Executor] Failed to save conversation:', err.message);
});
}

View File

@@ -11,29 +11,6 @@ import { getDiscoverer, getNativeSessions } from './native-session-discovery.js'
import { StoragePaths, ensureStorageDir, getProjectId, getCCWHome } from '../config/storage-paths.js';
import { createOutputParser, flattenOutputUnits, type CliOutputUnit } from './cli-output-converter.js';
// Debug logging for history save investigation (Iteration 4)
const DEBUG_SESSION_ID = 'DBG-parallel-ccw-cli-test-2026-03-07';
const DEBUG_LOG_PATH = join(process.cwd(), '.workflow', '.debug', DEBUG_SESSION_ID, 'debug-save.log');
// Ensure debug log directory exists
try {
const debugDir = dirname(DEBUG_LOG_PATH);
if (!existsSync(debugDir)) {
mkdirSync(debugDir, { recursive: true });
}
} catch (err) {
// Ignore directory creation errors
}
function writeDebugLog(event: string, data: Record<string, any>): void {
try {
const logEntry = JSON.stringify({ event, ...data, timestamp: new Date().toISOString() }) + '\n';
appendFileSync(DEBUG_LOG_PATH, logEntry, 'utf8');
} catch (err) {
// Silently ignore logging errors
}
}
function reconstructFinalOutputFromStdout(rawStdout: string, canTrustStdout: boolean): string | undefined {
if (!canTrustStdout || !rawStdout.trim()) {
return undefined;
@@ -154,29 +131,22 @@ export class CliHistoryStore {
private projectPath: string;
constructor(baseDir: string) {
writeDebugLog('STORE_CONSTRUCT_START', { baseDir });
this.projectPath = baseDir;
// Use centralized storage path
const paths = StoragePaths.project(baseDir);
const historyDir = paths.cliHistory;
writeDebugLog('STORAGE_PATHS', { baseDir, historyDir, historyDb: paths.historyDb });
ensureStorageDir(historyDir);
this.dbPath = paths.historyDb;
writeDebugLog('DB_INSTANCE_CREATE', { dbPath: this.dbPath });
this.db = new Database(this.dbPath);
writeDebugLog('DB_INSTANCE_CREATED', { dbPath: this.dbPath });
this.db.pragma('journal_mode = WAL');
this.db.pragma('synchronous = NORMAL');
this.db.pragma('busy_timeout = 10000'); // Wait up to 10 seconds for locks (increased for write-heavy scenarios)
this.db.pragma('wal_autocheckpoint = 1000'); // Optimize WAL checkpointing
writeDebugLog('INIT_SCHEMA_START', { dbPath: this.dbPath });
this.initSchema();
writeDebugLog('INIT_SCHEMA_COMPLETE', { dbPath: this.dbPath });
this.migrateFromJson(historyDir);
writeDebugLog('STORE_CONSTRUCT_COMPLETE', { baseDir, dbPath: this.dbPath });
}
/**

View File

@@ -0,0 +1,671 @@
/**
* Generate DDD Docs Tool
* Generate DDD documentation from doc-index.json with deterministic output paths.
* Supports 5 strategies: component (L3), feature (L2), index, overview, schema
*/
import { z } from 'zod';
import type { ToolSchema, ToolResult } from '../types/tool.js';
import { existsSync, readFileSync, mkdirSync, writeFileSync, unlinkSync } from 'fs';
import { join, resolve, dirname } from 'path';
import { execSync } from 'child_process';
import { tmpdir } from 'os';
import { getSecondaryModel } from './cli-config-manager.js';
// Default doc-index path relative to project root
const DEFAULT_DOC_INDEX_PATH = '.workflow/.doc-index/doc-index.json';
// Define Zod schema for validation
const ParamsSchema = z.object({
strategy: z.enum(['component', 'feature', 'index', 'overview', 'schema']),
entityId: z.string().optional(),
docIndexPath: z.string().default(DEFAULT_DOC_INDEX_PATH),
tool: z.enum(['gemini', 'qwen', 'codex']).default('gemini'),
model: z.string().optional(),
});
type Params = z.infer<typeof ParamsSchema>;
interface ToolOutput {
success: boolean;
strategy: string;
entity_id?: string;
output_path: string;
tool: string;
model?: string;
duration_seconds?: number;
message?: string;
error?: string;
}
// --- doc-index.json type definitions ---
interface CodeLocation {
path: string;
symbols?: string[];
lineRange?: [number, number];
}
interface TechnicalComponent {
id: string;
name: string;
type: string;
responsibility?: string;
adrId?: string | null;
docPath?: string;
codeLocations?: CodeLocation[];
dependsOn?: string[];
featureIds?: string[];
actionIds?: string[];
}
interface Feature {
id: string;
name: string;
epicId?: string | null;
status?: string;
docPath?: string;
requirementIds?: string[];
techComponentIds?: string[];
tags?: string[];
}
interface DocIndex {
version?: string;
schema_version?: string;
project?: string;
build_path?: string;
last_updated?: string;
features?: Feature[];
technicalComponents?: TechnicalComponent[];
requirements?: Array<{ id: string; title?: string; priority?: string }>;
architectureDecisions?: Array<{ id: string; title?: string; componentIds?: string[] }>;
actions?: Array<{ id: string; description?: string; type?: string; timestamp?: string; affectedComponents?: string[]; affectedFeatures?: string[] }>;
glossary?: Array<{ id: string; term: string; definition?: string }>;
[key: string]: unknown;
}
// --- Core functions ---
/**
* Load and parse doc-index.json
*/
function loadDocIndex(indexPath: string): DocIndex {
const absPath = resolve(process.cwd(), indexPath);
if (!existsSync(absPath)) {
throw new Error(`doc-index.json not found at: ${absPath}. Run /ddd:scan or /ddd:index-build first.`);
}
const raw = readFileSync(absPath, 'utf8');
return JSON.parse(raw) as DocIndex;
}
/**
* Calculate deterministic output path based on strategy and entityId.
* All paths are relative to the doc-index directory.
*/
function calculateDddOutputPath(
strategy: string,
entityId: string | undefined,
docIndexDir: string
): string {
switch (strategy) {
case 'component': {
if (!entityId) throw new Error('entityId is required for component strategy');
// tech-{slug} -> {slug}.md
const slug = entityId.replace(/^tech-/, '');
return join(docIndexDir, 'tech-registry', `${slug}.md`);
}
case 'feature': {
if (!entityId) throw new Error('entityId is required for feature strategy');
// feat-{slug} -> {slug}.md
const slug = entityId.replace(/^feat-/, '');
return join(docIndexDir, 'feature-maps', `${slug}.md`);
}
case 'index':
// Generate _index.md files - entityId determines which subdirectory
if (entityId) {
return join(docIndexDir, entityId, '_index.md');
}
// Default: generate all index files (return the doc-index dir itself)
return docIndexDir;
case 'overview':
if (entityId === 'architecture') {
return join(docIndexDir, 'ARCHITECTURE.md');
}
return join(docIndexDir, 'README.md');
case 'schema':
return join(docIndexDir, 'SCHEMA.md');
default:
throw new Error(`Unknown strategy: ${strategy}`);
}
}
/**
* Build YAML frontmatter string from entity metadata
*/
function buildFrontmatter(
strategy: string,
entity: TechnicalComponent | Feature | null,
docIndex: DocIndex
): string {
const now = new Date().toISOString();
switch (strategy) {
case 'component': {
const comp = entity as TechnicalComponent;
if (!comp) return '';
const featureIds = comp.featureIds || [];
const codeLocations = (comp.codeLocations || []).map(loc => {
const symbolsStr = loc.symbols && loc.symbols.length > 0
? `\n symbols: [${loc.symbols.join(', ')}]`
: '';
return ` - path: ${loc.path}${symbolsStr}`;
}).join('\n');
return [
'---',
'layer: 3',
`component_id: ${comp.id}`,
`name: ${comp.name}`,
`type: ${comp.type || 'unknown'}`,
`features: [${featureIds.join(', ')}]`,
codeLocations ? `code_locations:\n${codeLocations}` : 'code_locations: []',
`generated_at: ${now}`,
'---',
].join('\n');
}
case 'feature': {
const feat = entity as Feature;
if (!feat) return '';
const reqIds = feat.requirementIds || [];
const techIds = feat.techComponentIds || [];
const tags = feat.tags || [];
return [
'---',
'layer: 2',
`feature_id: ${feat.id}`,
`name: ${feat.name}`,
`epic_id: ${feat.epicId || 'null'}`,
`status: ${feat.status || 'planned'}`,
`requirements: [${reqIds.join(', ')}]`,
`components: [${techIds.join(', ')}]`,
`depends_on_layer3: [${techIds.join(', ')}]`,
`tags: [${tags.join(', ')}]`,
`generated_at: ${now}`,
'---',
].join('\n');
}
case 'index':
case 'overview': {
const featureIds = (docIndex.features || []).map(f => f.id);
return [
'---',
'layer: 1',
`depends_on_layer2: [${featureIds.join(', ')}]`,
`generated_at: ${now}`,
'---',
].join('\n');
}
case 'schema':
return [
'---',
`schema_version: ${docIndex.schema_version || docIndex.version || '1.0'}`,
`generated_at: ${now}`,
'---',
].join('\n');
default:
return '';
}
}
/**
* Build CLI prompt combining frontmatter, content instructions, and code context
*/
function buildDddPrompt(
strategy: string,
entity: TechnicalComponent | Feature | null,
frontmatter: string,
docIndex: DocIndex,
outputPath: string
): string {
const absOutputPath = resolve(process.cwd(), outputPath);
switch (strategy) {
case 'component': {
const comp = entity as TechnicalComponent;
const contextPaths = (comp.codeLocations || []).map(loc => `@${loc.path}`).join(' ');
// Build change history from actions
const compActions = (docIndex.actions || [])
.filter(a => (a.affectedComponents || []).includes(comp.id))
.map(a => `- ${a.timestamp?.split('T')[0] || 'unknown'} | ${a.type || 'change'} | ${a.description || a.id}`)
.join('\n');
const changeHistoryBlock = compActions
? `\n\nChange History (include as "## Change History" section):\n${compActions}`
: '';
return `PURPOSE: Generate component documentation for ${comp.name}
TASK:
- Document component purpose and responsibility
- List exported symbols (classes, functions, types)
- Document dependencies (internal and external)
- Include code examples for key APIs
- Document integration points with other components
- Include change history timeline
MODE: write
CONTEXT: ${contextPaths || '@**/*'}
EXPECTED: Markdown file with: Overview, API Reference, Dependencies, Usage Examples, Change History
CONSTRAINTS: Focus on public API | Include type signatures
OUTPUT FILE: ${absOutputPath}
The file MUST start with this exact frontmatter:
${frontmatter}
Sections to include after frontmatter:
- Responsibility
- Code Locations
- Related Requirements
- Architecture Decisions
- Dependencies (in/out)
- Change History${changeHistoryBlock}`;
}
case 'feature': {
const feat = entity as Feature;
const techIds = feat.techComponentIds || [];
const componentDocs = techIds
.map(id => {
const slug = id.replace(/^tech-/, '');
return `@.workflow/.doc-index/tech-registry/${slug}.md`;
})
.join(' ');
// Build change history from actions
const featActions = (docIndex.actions || [])
.filter(a => (a.affectedFeatures || []).includes(feat.id))
.map(a => `- ${a.timestamp?.split('T')[0] || 'unknown'} | ${a.type || 'change'} | ${a.description || a.id}`)
.join('\n');
const featChangeHistoryBlock = featActions
? `\n\nChange History (include as "## Change History" section):\n${featActions}`
: '';
return `PURPOSE: Generate feature documentation for ${feat.name}
TASK:
- Describe feature purpose and business value
- List requirements (from requirementIds)
- Document components involved (from techComponentIds)
- Include architecture decisions (from adrIds)
- Provide integration guide
- Include change history timeline
MODE: write
CONTEXT: ${componentDocs || '@.workflow/.doc-index/tech-registry/*.md'}
EXPECTED: Markdown file with: Overview, Requirements, Components, Architecture, Integration, Change History
CONSTRAINTS: Reference Layer 3 component docs | Business-focused language
OUTPUT FILE: ${absOutputPath}
The file MUST start with this exact frontmatter:
${frontmatter}
Sections to include after frontmatter:
- Overview
- Requirements (with mapping status)
- Technical Components
- Architecture Decisions
- Change History${featChangeHistoryBlock}`;
}
case 'index': {
const docIndexDir = dirname(resolve(process.cwd(), outputPath));
const parentDir = dirname(docIndexDir);
return `PURPOSE: Generate index document for ${docIndexDir}
TASK:
- List all entries in this directory with brief descriptions
- Create a navigable catalog with links to each document
- Include status/type columns where applicable
MODE: write
CONTEXT: @${parentDir}/doc-index.json
EXPECTED: Markdown index file with: table of entries, descriptions, links
CONSTRAINTS: Catalog format | Link to sibling documents
OUTPUT FILE: ${absOutputPath}
The file MUST start with this exact frontmatter:
${frontmatter}`;
}
case 'overview': {
const isArchitecture = outputPath.endsWith('ARCHITECTURE.md');
if (isArchitecture) {
return `PURPOSE: Generate architecture overview document
TASK:
- System design overview
- Component relationships and dependencies
- Key architecture decisions (from ADRs)
- Technology stack
MODE: write
CONTEXT: @.workflow/.doc-index/doc-index.json @.workflow/.doc-index/tech-registry/*.md
EXPECTED: ARCHITECTURE.md with: System Design, Component Diagram, ADRs, Tech Stack
CONSTRAINTS: Architecture-focused | Reference component docs for details
OUTPUT FILE: ${absOutputPath}
The file MUST start with this exact frontmatter:
${frontmatter}`;
}
return `PURPOSE: Generate project README with overview and navigation
TASK:
- Project summary and purpose
- Quick start guide
- Navigation to features, components, and architecture
- Link to doc-index.json
MODE: write
CONTEXT: @.workflow/.doc-index/doc-index.json @.workflow/.doc-index/feature-maps/_index.md
EXPECTED: README.md with: Overview, Quick Start, Navigation, Links
CONSTRAINTS: High-level only | Entry point for new developers
OUTPUT FILE: ${absOutputPath}
The file MUST start with this exact frontmatter:
${frontmatter}`;
}
case 'schema': {
return `PURPOSE: Document doc-index.json schema structure and versioning
TASK:
- Document current schema structure (all fields)
- Define versioning policy (semver: major.minor)
- Document migration protocol for version upgrades
- Provide examples for each schema section
MODE: write
CONTEXT: @.workflow/.doc-index/doc-index.json
EXPECTED: SCHEMA.md with: Schema Structure, Versioning Policy, Migration Protocol, Examples
CONSTRAINTS: Complete field documentation | Clear migration steps
OUTPUT FILE: ${absOutputPath}
The file MUST start with this exact frontmatter:
${frontmatter}`;
}
default:
throw new Error(`Unknown strategy: ${strategy}`);
}
}
/**
* Create temporary prompt file and return path
*/
function createPromptFile(prompt: string): string {
const timestamp = Date.now();
const randomSuffix = Math.random().toString(36).substring(2, 8);
const promptFile = join(tmpdir(), `ddd-docs-prompt-${timestamp}-${randomSuffix}.txt`);
writeFileSync(promptFile, prompt, 'utf8');
return promptFile;
}
/**
* Build CLI command using stdin piping
*/
function buildCliCommand(tool: string, promptFile: string, model: string): string {
const normalizedPath = promptFile.replace(/\\/g, '/');
const isWindows = process.platform === 'win32';
const catCmd = isWindows ? `Get-Content -Raw "${normalizedPath}" | ` : `cat "${normalizedPath}" | `;
const modelFlag = model ? ` -m "${model}"` : '';
switch (tool) {
case 'qwen':
return `${catCmd}qwen${modelFlag} --yolo`;
case 'codex':
if (isWindows) {
return `codex --full-auto exec (Get-Content -Raw "${normalizedPath}")${modelFlag} --skip-git-repo-check -s danger-full-access`;
}
return `codex --full-auto exec "$(cat "${normalizedPath}")"${modelFlag} --skip-git-repo-check -s danger-full-access`;
case 'gemini':
default:
return `${catCmd}gemini${modelFlag} --yolo`;
}
}
/**
* Resolve entity from doc-index based on strategy and entityId
*/
function resolveEntity(
strategy: string,
entityId: string | undefined,
docIndex: DocIndex
): TechnicalComponent | Feature | null {
if (strategy === 'component') {
if (!entityId) throw new Error('entityId is required for component strategy');
const comp = (docIndex.technicalComponents || []).find(c => c.id === entityId);
if (!comp) throw new Error(`Component not found in doc-index: ${entityId}`);
return comp;
}
if (strategy === 'feature') {
if (!entityId) throw new Error('entityId is required for feature strategy');
const feat = (docIndex.features || []).find(f => f.id === entityId);
if (!feat) throw new Error(`Feature not found in doc-index: ${entityId}`);
return feat;
}
// index, overview, schema do not require a specific entity
return null;
}
/**
* For the index strategy, generate _index.md for multiple directories
*/
function getIndexTargets(entityId: string | undefined): string[] {
if (entityId) {
return [entityId];
}
// Default: all standard subdirectories
return ['feature-maps', 'tech-registry', 'action-logs', 'planning'];
}
// Tool schema for MCP
export const schema: ToolSchema = {
name: 'generate_ddd_docs',
description: `Generate DDD documentation from doc-index.json with deterministic output paths.
Strategies:
- component: Layer 3 technical component doc (tech-registry/{slug}.md)
- feature: Layer 2 feature map doc (feature-maps/{slug}.md)
- index: Layer 1 _index.md catalog files for subdirectories
- overview: Layer 1 README.md or ARCHITECTURE.md
- schema: SCHEMA.md documenting doc-index.json structure
Requires doc-index.json from /ddd:scan or /ddd:index-build.
Output: .workflow/.doc-index/...`,
inputSchema: {
type: 'object',
properties: {
strategy: {
type: 'string',
enum: ['component', 'feature', 'index', 'overview', 'schema'],
description: 'Document generation strategy: component (L3), feature (L2), index, overview, schema (L1)'
},
entityId: {
type: 'string',
description: 'Entity ID from doc-index.json (required for component/feature, optional for index/overview). For overview: "architecture" to generate ARCHITECTURE.md, omit for README.md. For index: subdirectory name or omit for all.'
},
docIndexPath: {
type: 'string',
description: 'Path to doc-index.json (default: .workflow/.doc-index/doc-index.json)',
default: '.workflow/.doc-index/doc-index.json'
},
tool: {
type: 'string',
enum: ['gemini', 'qwen', 'codex'],
description: 'CLI tool to use (default: gemini)',
default: 'gemini'
},
model: {
type: 'string',
description: 'Model name (optional, uses tool defaults)'
}
},
required: ['strategy']
}
};
// Handler function
export async function handler(params: Record<string, unknown>): Promise<ToolResult<ToolOutput>> {
const parsed = ParamsSchema.safeParse(params);
if (!parsed.success) {
return { success: false, error: `Invalid params: ${parsed.error.message}` };
}
const { strategy, entityId, docIndexPath, tool, model } = parsed.data;
try {
// Load doc-index.json
const docIndex = loadDocIndex(docIndexPath);
const docIndexDir = dirname(resolve(process.cwd(), docIndexPath));
// Resolve model
let actualModel = model || '';
if (!actualModel) {
try {
actualModel = getSecondaryModel(process.cwd(), tool);
} catch {
actualModel = '';
}
}
// Handle index strategy separately (may generate multiple files)
if (strategy === 'index') {
const targets = getIndexTargets(entityId);
const results: string[] = [];
for (const target of targets) {
const outputPath = join(docIndexDir, target, '_index.md');
const absOutputDir = dirname(resolve(process.cwd(), outputPath));
// Ensure directory exists
mkdirSync(absOutputDir, { recursive: true });
const frontmatter = buildFrontmatter('index', null, docIndex);
const prompt = buildDddPrompt('index', null, frontmatter, docIndex, outputPath);
const promptFile = createPromptFile(prompt);
const command = buildCliCommand(tool, promptFile, actualModel);
console.log(`[DDD] Generating index: ${target}/_index.md`);
try {
const startTime = Date.now();
execSync(command, {
cwd: docIndexDir,
encoding: 'utf8',
stdio: 'inherit',
timeout: 600000,
shell: process.platform === 'win32' ? 'powershell.exe' : '/bin/bash'
});
const duration = Math.round((Date.now() - startTime) / 1000);
results.push(`${target}/_index.md (${duration}s)`);
} finally {
try { unlinkSync(promptFile); } catch { /* ignore */ }
}
}
return {
success: true,
result: {
success: true,
strategy,
entity_id: entityId,
output_path: docIndexDir,
tool,
model: actualModel,
message: `Generated index files: ${results.join(', ')}`
}
};
}
// Single-file strategies: component, feature, overview, schema
const entity = resolveEntity(strategy, entityId, docIndex);
const outputPath = calculateDddOutputPath(strategy, entityId, docIndexDir);
const absOutputDir = dirname(resolve(process.cwd(), outputPath));
// Ensure output directory exists
mkdirSync(absOutputDir, { recursive: true });
// Build frontmatter and prompt
const frontmatter = buildFrontmatter(strategy, entity, docIndex);
const prompt = buildDddPrompt(strategy, entity, frontmatter, docIndex, outputPath);
// Create temp prompt file
const promptFile = createPromptFile(prompt);
// Build CLI command
const command = buildCliCommand(tool, promptFile, actualModel);
console.log(`[DDD] Generating ${strategy}: ${outputPath}`);
console.log(`[DDD] Tool: ${tool} | Model: ${actualModel || 'default'}`);
try {
const startTime = Date.now();
execSync(command, {
cwd: docIndexDir,
encoding: 'utf8',
stdio: 'inherit',
timeout: 600000,
shell: process.platform === 'win32' ? 'powershell.exe' : '/bin/bash'
});
const duration = Math.round((Date.now() - startTime) / 1000);
// Cleanup
try { unlinkSync(promptFile); } catch { /* ignore */ }
console.log(`[DDD] Completed in ${duration}s: ${outputPath}`);
return {
success: true,
result: {
success: true,
strategy,
entity_id: entityId,
output_path: outputPath,
tool,
model: actualModel,
duration_seconds: duration,
message: `Documentation generated successfully in ${duration}s`
}
};
} catch (error) {
// Cleanup on error
try { unlinkSync(promptFile); } catch { /* ignore */ }
// Tool fallback: gemini -> qwen -> codex
const fallbackChain = ['gemini', 'qwen', 'codex'];
const currentIdx = fallbackChain.indexOf(tool);
if (currentIdx >= 0 && currentIdx < fallbackChain.length - 1) {
const nextTool = fallbackChain[currentIdx + 1];
console.log(`[DDD] ${tool} failed, falling back to ${nextTool}`);
return handler({ ...params, tool: nextTool });
}
return {
success: false,
error: `Documentation generation failed: ${(error as Error).message}`
};
}
} catch (error) {
return {
success: false,
error: `Tool execution failed: ${(error as Error).message}`
};
}
}

View File

@@ -14,6 +14,7 @@ import * as classifyFoldersMod from './classify-folders.js';
import * as detectChangedModulesMod from './detect-changed-modules.js';
import * as discoverDesignFilesMod from './discover-design-files.js';
import * as generateModuleDocsMod from './generate-module-docs.js';
import * as generateDddDocsMod from './generate-ddd-docs.js';
import * as convertTokensToCssMod from './convert-tokens-to-css.js';
import * as sessionManagerMod from './session-manager.js';
import * as cliExecutorMod from './cli-executor.js';
@@ -358,6 +359,7 @@ registerTool(toLegacyTool(classifyFoldersMod));
registerTool(toLegacyTool(detectChangedModulesMod));
registerTool(toLegacyTool(discoverDesignFilesMod));
registerTool(toLegacyTool(generateModuleDocsMod));
registerTool(toLegacyTool(generateDddDocsMod));
registerTool(toLegacyTool(convertTokensToCssMod));
registerTool(toLegacyTool(sessionManagerMod));
registerTool(toLegacyTool(cliExecutorMod));

View File

@@ -0,0 +1,144 @@
/**
* Integration tests for CodexLens ignore-pattern configuration routes.
*
* Notes:
* - Targets runtime implementation shipped in `ccw/dist`.
* - Calls route handler directly (no HTTP server required).
* - Uses temporary CODEXLENS_DATA_DIR to isolate ~/.codexlens writes.
*/
import { after, before, describe, it, mock } from 'node:test';
import assert from 'node:assert/strict';
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
const CODEXLENS_HOME = mkdtempSync(join(tmpdir(), 'codexlens-ignore-home-'));
const PROJECT_ROOT = mkdtempSync(join(tmpdir(), 'codexlens-ignore-project-'));
const configRoutesUrl = new URL('../../dist/core/routes/codexlens/config-handlers.js', import.meta.url);
configRoutesUrl.searchParams.set('t', String(Date.now()));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let mod: any;
const originalEnv = {
CODEXLENS_DATA_DIR: process.env.CODEXLENS_DATA_DIR,
};
async function callConfigRoute(
initialPath: string,
method: string,
path: string,
body?: unknown,
): Promise<{ handled: boolean; status: number; json: any }> {
const url = new URL(path, 'http://localhost');
let status = 0;
let text = '';
let postPromise: Promise<void> | null = null;
const res = {
writeHead(code: number) {
status = code;
},
end(chunk?: unknown) {
text = chunk === undefined ? '' : String(chunk);
},
};
const handlePostRequest = (_req: unknown, _res: unknown, handler: (parsed: any) => Promise<any>) => {
postPromise = (async () => {
const result = await handler(body ?? {});
const errorValue = result && typeof result === 'object' ? result.error : undefined;
const statusValue = result && typeof result === 'object' ? result.status : undefined;
if (typeof errorValue === 'string' && errorValue.length > 0) {
res.writeHead(typeof statusValue === 'number' ? statusValue : 500);
res.end(JSON.stringify({ error: errorValue }));
return;
}
res.writeHead(200);
res.end(JSON.stringify(result));
})();
};
const handled = await mod.handleCodexLensConfigRoutes({
pathname: url.pathname,
url,
req: { method },
res,
initialPath,
handlePostRequest,
broadcastToClients() {},
});
if (postPromise) {
await postPromise;
}
return { handled, status, json: text ? JSON.parse(text) : null };
}
describe('codexlens ignore-pattern routes integration', async () => {
before(async () => {
process.env.CODEXLENS_DATA_DIR = CODEXLENS_HOME;
mock.method(console, 'log', () => {});
mock.method(console, 'warn', () => {});
mock.method(console, 'error', () => {});
mod = await import(configRoutesUrl.href);
});
after(() => {
mock.restoreAll();
process.env.CODEXLENS_DATA_DIR = originalEnv.CODEXLENS_DATA_DIR;
rmSync(CODEXLENS_HOME, { recursive: true, force: true });
rmSync(PROJECT_ROOT, { recursive: true, force: true });
});
it('GET /api/codexlens/ignore-patterns returns defaults before config exists', async () => {
const res = await callConfigRoute(PROJECT_ROOT, 'GET', '/api/codexlens/ignore-patterns');
assert.equal(res.handled, true);
assert.equal(res.status, 200);
assert.equal(res.json.success, true);
assert.equal(Array.isArray(res.json.patterns), true);
assert.equal(Array.isArray(res.json.extensionFilters), true);
assert.ok(res.json.patterns.includes('dist'));
assert.ok(res.json.extensionFilters.includes('*.min.js'));
});
it('POST /api/codexlens/ignore-patterns persists custom patterns and filters', async () => {
const saveRes = await callConfigRoute(PROJECT_ROOT, 'POST', '/api/codexlens/ignore-patterns', {
patterns: ['dist', 'frontend/dist'],
extensionFilters: ['*.min.js', 'frontend/skip.ts'],
});
assert.equal(saveRes.handled, true);
assert.equal(saveRes.status, 200);
assert.equal(saveRes.json.success, true);
assert.deepEqual(saveRes.json.patterns, ['dist', 'frontend/dist']);
assert.deepEqual(saveRes.json.extensionFilters, ['*.min.js', 'frontend/skip.ts']);
const settingsPath = join(CODEXLENS_HOME, 'settings.json');
assert.equal(existsSync(settingsPath), true);
const savedSettings = JSON.parse(readFileSync(settingsPath, 'utf8'));
assert.deepEqual(savedSettings.ignore_patterns, ['dist', 'frontend/dist']);
assert.deepEqual(savedSettings.extension_filters, ['*.min.js', 'frontend/skip.ts']);
const getRes = await callConfigRoute(PROJECT_ROOT, 'GET', '/api/codexlens/ignore-patterns');
assert.equal(getRes.status, 200);
assert.deepEqual(getRes.json.patterns, ['dist', 'frontend/dist']);
assert.deepEqual(getRes.json.extensionFilters, ['*.min.js', 'frontend/skip.ts']);
});
it('POST /api/codexlens/ignore-patterns rejects invalid entries', async () => {
const res = await callConfigRoute(PROJECT_ROOT, 'POST', '/api/codexlens/ignore-patterns', {
patterns: ['bad pattern!'],
});
assert.equal(res.handled, true);
assert.equal(res.status, 400);
assert.match(String(res.json.error), /Invalid patterns:/);
});
});

View File

@@ -0,0 +1,10 @@
{
"ignore_patterns": [
"dist",
"frontend/dist"
],
"extension_filters": [
"*.min.js",
"frontend/skip.ts"
]
}

View File

@@ -0,0 +1 @@
{"ignore_patterns": ["frontend/dist"], "extension_filters": ["*.min.js"]}

View File

@@ -0,0 +1 @@
export const app = 1

View File

@@ -0,0 +1 @@
export const bundle = 1

View File

@@ -0,0 +1 @@
export const compiled = 1

View File

@@ -0,0 +1 @@
export const bundle = 1

View File

@@ -0,0 +1 @@
export const app = 1

View File

@@ -0,0 +1 @@
print('artifact')

View File

@@ -0,0 +1 @@
print('artifact')

View File

@@ -0,0 +1 @@
print('artifact')

View File

@@ -0,0 +1 @@
print('artifact')

View File

@@ -0,0 +1 @@
print('artifact')

View File

@@ -0,0 +1 @@
print('artifact')

View File

@@ -0,0 +1 @@
print('artifact')

View File

@@ -0,0 +1 @@
print('ok')

View File

@@ -0,0 +1 @@
print('artifact')

View File

@@ -0,0 +1 @@
export const app = 1

View File

@@ -0,0 +1 @@
export const bundle = 1

View File

@@ -0,0 +1 @@
export const skip = 1

View File

@@ -0,0 +1 @@
{"ignore_patterns": ["frontend/dist", "coverage"], "extension_filters": ["*.min.js", "*.map"]}

View File

@@ -0,0 +1 @@
print('compiled')

View File

@@ -123,11 +123,12 @@ class IndexTreeBuilder:
"""
self.registry = registry
self.mapper = mapper
self.config = config or Config()
self.config = config or Config.load()
self.parser_factory = ParserFactory(self.config)
self.logger = logging.getLogger(__name__)
self.incremental = incremental
self.ignore_patterns = self._resolve_ignore_patterns()
self.extension_filters = self._resolve_extension_filters()
def _resolve_ignore_patterns(self) -> Tuple[str, ...]:
configured_patterns = getattr(self.config, "ignore_patterns", None)
@@ -139,6 +140,18 @@ class IndexTreeBuilder:
cleaned.append(pattern)
return tuple(dict.fromkeys(cleaned))
def _resolve_extension_filters(self) -> Tuple[str, ...]:
configured_filters = getattr(self.config, "extension_filters", None)
if not configured_filters:
return tuple()
cleaned: List[str] = []
for item in configured_filters:
pattern = str(item).strip().replace('\\', '/').rstrip('/')
if pattern:
cleaned.append(pattern)
return tuple(dict.fromkeys(cleaned))
def _is_ignored_dir(self, dir_path: Path, source_root: Optional[Path] = None) -> bool:
name = dir_path.name
if name.startswith('.'):
@@ -159,6 +172,25 @@ class IndexTreeBuilder:
return False
def _is_filtered_file(self, file_path: Path, source_root: Optional[Path] = None) -> bool:
if not self.extension_filters:
return False
rel_path: Optional[str] = None
if source_root is not None:
try:
rel_path = file_path.relative_to(source_root).as_posix()
except ValueError:
rel_path = None
for pattern in self.extension_filters:
if pattern == file_path.name or fnmatch.fnmatch(file_path.name, pattern):
return True
if rel_path and (pattern == rel_path or fnmatch.fnmatch(rel_path, pattern)):
return True
return False
def build(
self,
source_root: Path,
@@ -259,6 +291,7 @@ class IndexTreeBuilder:
dirs,
languages,
workers,
source_root=source_root,
project_id=project_info.id,
global_index_db_path=global_index_db_path,
)
@@ -410,6 +443,7 @@ class IndexTreeBuilder:
return self._build_single_dir(
source_path,
languages=None,
source_root=project_root,
project_id=project_info.id,
global_index_db_path=global_index_db_path,
)
@@ -491,7 +525,7 @@ class IndexTreeBuilder:
return False
# Check for supported files in this directory
source_files = self._iter_source_files(dir_path, languages)
source_files = self._iter_source_files(dir_path, languages, source_root=source_root)
if len(source_files) > 0:
return True
@@ -519,7 +553,7 @@ class IndexTreeBuilder:
True if directory tree contains indexable files
"""
# Check for supported files in this directory
source_files = self._iter_source_files(dir_path, languages)
source_files = self._iter_source_files(dir_path, languages, source_root=source_root)
if len(source_files) > 0:
return True
@@ -543,6 +577,7 @@ class IndexTreeBuilder:
languages: List[str],
workers: int,
*,
source_root: Path,
project_id: int,
global_index_db_path: Path,
) -> List[DirBuildResult]:
@@ -570,6 +605,7 @@ class IndexTreeBuilder:
result = self._build_single_dir(
dirs[0],
languages,
source_root=source_root,
project_id=project_id,
global_index_db_path=global_index_db_path,
)
@@ -585,6 +621,7 @@ class IndexTreeBuilder:
"static_graph_relationship_types": self.config.static_graph_relationship_types,
"use_astgrep": getattr(self.config, "use_astgrep", False),
"ignore_patterns": list(getattr(self.config, "ignore_patterns", [])),
"extension_filters": list(getattr(self.config, "extension_filters", [])),
}
worker_args = [
@@ -595,6 +632,7 @@ class IndexTreeBuilder:
config_dict,
int(project_id),
str(global_index_db_path),
str(source_root),
)
for dir_path in dirs
]
@@ -631,6 +669,7 @@ class IndexTreeBuilder:
dir_path: Path,
languages: List[str] = None,
*,
source_root: Path,
project_id: int,
global_index_db_path: Path,
) -> DirBuildResult:
@@ -663,7 +702,7 @@ class IndexTreeBuilder:
store.initialize()
# Get source files in this directory only
source_files = self._iter_source_files(dir_path, languages)
source_files = self._iter_source_files(dir_path, languages, source_root=source_root)
files_count = 0
symbols_count = 0
@@ -731,7 +770,7 @@ class IndexTreeBuilder:
d.name
for d in dir_path.iterdir()
if d.is_dir()
and not self._is_ignored_dir(d)
and not self._is_ignored_dir(d, source_root=source_root)
]
store.update_merkle_root()
@@ -826,7 +865,7 @@ class IndexTreeBuilder:
)
def _iter_source_files(
self, dir_path: Path, languages: List[str] = None
self, dir_path: Path, languages: List[str] = None, source_root: Optional[Path] = None
) -> List[Path]:
"""Iterate source files in directory (non-recursive).
@@ -852,6 +891,9 @@ class IndexTreeBuilder:
if item.name.startswith("."):
continue
if self._is_filtered_file(item, source_root=source_root):
continue
# Check language support
language_id = self.config.language_for_path(item)
if not language_id:
@@ -1027,19 +1069,37 @@ def _compute_graph_neighbors(
# === Worker Function for ProcessPoolExecutor ===
def _matches_ignore_patterns(path: Path, patterns: List[str]) -> bool:
name = path.name
if name.startswith('.'):
return True
def _matches_path_patterns(path: Path, patterns: List[str], source_root: Optional[Path] = None) -> bool:
rel_path: Optional[str] = None
if source_root is not None:
try:
rel_path = path.relative_to(source_root).as_posix()
except ValueError:
rel_path = None
for pattern in patterns:
normalized = str(pattern).strip().replace('\\', '/').rstrip('/')
if not normalized:
continue
if normalized == name or fnmatch.fnmatch(name, normalized):
if normalized == path.name or fnmatch.fnmatch(path.name, normalized):
return True
if rel_path and (normalized == rel_path or fnmatch.fnmatch(rel_path, normalized)):
return True
return False
def _matches_ignore_patterns(path: Path, patterns: List[str], source_root: Optional[Path] = None) -> bool:
if path.name.startswith('.'):
return True
return _matches_path_patterns(path, patterns, source_root)
def _matches_extension_filters(path: Path, patterns: List[str], source_root: Optional[Path] = None) -> bool:
if not patterns:
return False
return _matches_path_patterns(path, patterns, source_root)
def _build_dir_worker(args: tuple) -> DirBuildResult:
"""Worker function for parallel directory building.
@@ -1047,12 +1107,12 @@ def _build_dir_worker(args: tuple) -> DirBuildResult:
Reconstructs necessary objects from serializable arguments.
Args:
args: Tuple of (dir_path, index_db_path, languages, config_dict, project_id, global_index_db_path)
args: Tuple of (dir_path, index_db_path, languages, config_dict, project_id, global_index_db_path, source_root)
Returns:
DirBuildResult for the directory
"""
dir_path, index_db_path, languages, config_dict, project_id, global_index_db_path = args
dir_path, index_db_path, languages, config_dict, project_id, global_index_db_path, source_root = args
# Reconstruct config
config = Config(
@@ -1064,9 +1124,11 @@ def _build_dir_worker(args: tuple) -> DirBuildResult:
static_graph_relationship_types=list(config_dict.get("static_graph_relationship_types", ["imports", "inherits"])),
use_astgrep=bool(config_dict.get("use_astgrep", False)),
ignore_patterns=list(config_dict.get("ignore_patterns", [])),
extension_filters=list(config_dict.get("extension_filters", [])),
)
parser_factory = ParserFactory(config)
source_root_path = Path(source_root) if source_root else None
global_index: GlobalSymbolIndex | None = None
try:
@@ -1092,6 +1154,9 @@ def _build_dir_worker(args: tuple) -> DirBuildResult:
if item.name.startswith("."):
continue
if _matches_extension_filters(item, config.extension_filters, source_root_path):
continue
language_id = config.language_for_path(item)
if not language_id:
continue
@@ -1146,7 +1211,7 @@ def _build_dir_worker(args: tuple) -> DirBuildResult:
subdirs = [
d.name
for d in dir_path.iterdir()
if d.is_dir() and not _matches_ignore_patterns(d, ignore_patterns)
if d.is_dir() and not _matches_ignore_patterns(d, ignore_patterns, source_root_path)
]
store.update_merkle_root()

View File

@@ -19,6 +19,7 @@ def test_load_settings_reads_ignore_patterns_and_extension_filters(tmp_path: Pat
)
config = Config(data_dir=tmp_path)
config.load_settings()
assert config.ignore_patterns == ["frontend/dist", "coverage"]
assert config.extension_filters == ["*.min.js", "*.map"]

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
import json
from pathlib import Path
from unittest.mock import MagicMock
@@ -84,3 +85,63 @@ def test_collect_dirs_by_depth_respects_relative_ignore_patterns_from_config(tmp
assert "frontend/src" in discovered_dirs
assert "frontend/dist" not in discovered_dirs
def test_iter_source_files_respects_extension_filters_and_relative_patterns(tmp_path: Path) -> None:
frontend_dir = tmp_path / "frontend"
frontend_dir.mkdir()
(frontend_dir / "app.ts").write_text("export const app = 1\n", encoding="utf-8")
(frontend_dir / "bundle.min.js").write_text("export const bundle = 1\n", encoding="utf-8")
(frontend_dir / "skip.ts").write_text("export const skip = 1\n", encoding="utf-8")
builder = IndexTreeBuilder(
registry=MagicMock(),
mapper=MagicMock(),
config=Config(
data_dir=tmp_path / "data",
extension_filters=["*.min.js", "frontend/skip.ts"],
),
incremental=False,
)
source_files = builder._iter_source_files(frontend_dir, source_root=tmp_path)
assert [path.name for path in source_files] == ["app.ts"]
assert builder._should_index_dir(frontend_dir, source_root=tmp_path) is True
def test_builder_loads_saved_ignore_and_extension_filters_by_default(tmp_path: Path, monkeypatch) -> None:
codexlens_home = tmp_path / "codexlens-home"
codexlens_home.mkdir()
(codexlens_home / "settings.json").write_text(
json.dumps(
{
"ignore_patterns": ["frontend/dist"],
"extension_filters": ["*.min.js"],
}
),
encoding="utf-8",
)
monkeypatch.setenv("CODEXLENS_DATA_DIR", str(codexlens_home))
frontend_dir = tmp_path / "frontend"
frontend_dir.mkdir()
dist_dir = frontend_dir / "dist"
dist_dir.mkdir()
(frontend_dir / "app.ts").write_text("export const app = 1\n", encoding="utf-8")
(frontend_dir / "bundle.min.js").write_text("export const bundle = 1\n", encoding="utf-8")
(dist_dir / "compiled.ts").write_text("export const compiled = 1\n", encoding="utf-8")
builder = IndexTreeBuilder(
registry=MagicMock(),
mapper=MagicMock(),
config=None,
incremental=False,
)
source_files = builder._iter_source_files(frontend_dir, source_root=tmp_path)
dirs_by_depth = builder._collect_dirs_by_depth(tmp_path)
discovered_dirs = _relative_dirs(tmp_path, dirs_by_depth)
assert [path.name for path in source_files] == ["app.ts"]
assert "frontend/dist" not in discovered_dirs

View File

@@ -37,7 +37,7 @@
| [`/workflow-multi-cli-plan`](./workflow.md#multi-cli-plan) | 多 CLI 协作规划 | Intermediate |
| [`/workflow:review`](./workflow.md#review) | 实现后审查 | Intermediate |
| [`/workflow:clean`](./workflow.md#clean) | 智能代码清理 | Intermediate |
| [`/workflow:init`](./workflow.md#init) | 初始化项目状态 | Intermediate |
| [`/workflow:spec:setup `](./workflow.md#init) | 初始化项目状态 | Intermediate |
| [`/workflow:brainstorm-with-file`](./workflow.md#brainstorm-with-file) | 交互式头脑风暴 | Intermediate |
| [`/workflow:analyze-with-file`](./workflow.md#analyze-with-file) | 交互式协作分析 | Beginner |
| [`/workflow:debug-with-file`](./workflow.md#debug-with-file) | 交互式假设驱动调试 | Intermediate |

View File

@@ -0,0 +1,415 @@
# Maestro 品牌命名系统
> **文档版本**: 1.0.0
> **最后更新**: 2026-03-09
> **状态**: 已确定
## 概述
本文档定义了 Maestro 项目的完整品牌命名系统包括总品牌、子品牌工作流、包名、CLI 命令、域名等命名规范。
### 品牌理念
**Maestro**(指挥家/编排大师)是一个智能编排平台,协调多个 AI CLI 工具,为开发者提供统一的工作流体验。
---
## 品牌架构
```
Maestro (总平台/编排系统)
├── Maestro Claude (基于 Claude Code 的工作流)
├── Maestro Codex (基于 Codex 的工作流)
├── Maestro Gemini (基于 Gemini 的工作流)
└── Maestro Qwen (基于 Qwen 的工作流)
```
### 设计原则
1. **清晰直观** - 工作流名称直接表明使用的 CLI 工具
2. **品牌统一** - 所有工作流都在 Maestro 品牌下
3. **易于扩展** - 未来添加新 CLI 时,命名规则保持一致
4. **技术透明** - 开发者清楚底层技术栈
---
## 完整命名规范
| 工作流 | 品牌名 | NPM 包 | CLI 命令 | GitHub Repo | 域名 |
|--------|--------|---------|----------|-------------|------|
| Claude Code 工作流 | **Maestro Claude** | `@maestro/claude` | `maestro claude` | `maestro-claude` | `claude.maestro.dev` |
| Codex 工作流 | **Maestro Codex** | `@maestro/codex` | `maestro codex` | `maestro-codex` | `codex.maestro.dev` |
| Gemini 工作流 | **Maestro Gemini** | `@maestro/gemini` | `maestro gemini` | `maestro-gemini` | `gemini.maestro.dev` |
| Qwen 工作流 | **Maestro Qwen** | `@maestro/qwen` | `maestro qwen` | `maestro-qwen` | `qwen.maestro.dev` |
### 命名规则
- **品牌名**: `Maestro <CLI名称>`
- **NPM 包**: `@maestro/<cli-name>`(小写,使用 scope
- **CLI 命令**: `maestro <cli-name>`(小写)
- **GitHub 仓库**: `maestro-<cli-name>`(小写,连字符)
- **域名**: `<cli-name>.maestro.dev`(小写,子域名)
---
## 目录结构
### 推荐的项目结构
```
maestro/
├── packages/
│ ├── core/ # Maestro 核心引擎
│ │ ├── src/
│ │ └── package.json
│ ├── claude/ # Maestro Claude 工作流
│ │ ├── src/
│ │ └── package.json
│ ├── codex/ # Maestro Codex 工作流
│ │ ├── src/
│ │ └── package.json
│ ├── gemini/ # Maestro Gemini 工作流
│ │ ├── src/
│ │ └── package.json
│ ├── qwen/ # Maestro Qwen 工作流
│ │ ├── src/
│ │ └── package.json
│ └── podium/ # Maestro UI (原 CCW)
│ ├── frontend/
│ ├── backend/
│ └── package.json
├── docs/
│ ├── branding/ # 品牌文档
│ ├── guides/ # 使用指南
│ └── api/ # API 文档
├── .codex/ # Codex 配置和技能
├── .workflow/ # 工作流配置
├── package.json # Monorepo 根配置
└── README.md
```
---
## CLI 使用示例
### 基本调用
```bash
# 使用 Claude Code 工作流
maestro claude --prompt "implement user authentication"
# 使用 Codex 工作流
maestro codex --analyze "src/**/*.ts"
# 使用 Gemini 工作流
maestro gemini --task "summarize this document"
# 使用 Qwen 工作流
maestro qwen --prompt "explain this code"
```
### 带参数调用
```bash
# Claude 工作流 - 代码生成
maestro claude generate --file "components/Button.tsx" --prompt "add loading state"
# Codex 工作流 - 代码分析
maestro codex search --pattern "useEffect" --path "src/"
# Gemini 工作流 - 多模态任务
maestro gemini analyze --image "screenshot.png" --prompt "describe this UI"
# Qwen 工作流 - 快速任务
maestro qwen translate --from "en" --to "zh" --text "Hello World"
```
### 工作流选择
```bash
# 查看可用工作流
maestro list
# 查看特定工作流信息
maestro info claude
# 设置默认工作流
maestro config set default-workflow claude
```
---
## 配置文件
### maestro.config.json
```json
{
"version": "1.0.0",
"workflows": {
"claude": {
"name": "Maestro Claude",
"description": "Claude Code workflow for code generation and refactoring",
"cli": "claude-code",
"enabled": true,
"defaultModel": "claude-sonnet-4",
"capabilities": ["generate", "refactor", "explain", "chat"]
},
"codex": {
"name": "Maestro Codex",
"description": "Codex workflow for code analysis and understanding",
"cli": "codex",
"enabled": true,
"defaultModel": "gpt-5.2",
"capabilities": ["analyze", "search", "visualize", "index"]
},
"gemini": {
"name": "Maestro Gemini",
"description": "Gemini workflow for general-purpose AI tasks",
"cli": "gemini",
"enabled": true,
"defaultModel": "gemini-2.5-pro",
"capabilities": ["multimodal", "general", "experimental"]
},
"qwen": {
"name": "Maestro Qwen",
"description": "Qwen workflow for fast response and experimental tasks",
"cli": "qwen",
"enabled": true,
"defaultModel": "coder-model",
"capabilities": ["fast", "experimental", "assistant"]
}
},
"branding": {
"name": "Maestro",
"tagline": "Orchestrate Your Development Workflow",
"website": "https://maestro.dev",
"repository": "https://github.com/maestro-suite/maestro"
}
}
```
---
## 包名规范
### NPM 包
#### @maestro/claude
```json
{
"name": "@maestro/claude",
"version": "1.0.0",
"description": "Maestro Claude - Claude Code workflow orchestration",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"bin": {
"maestro-claude": "./bin/cli.js"
},
"keywords": [
"maestro",
"claude",
"claude-code",
"workflow",
"ai",
"code-generation"
],
"repository": {
"type": "git",
"url": "https://github.com/maestro-suite/maestro-claude"
}
}
```
#### @maestro/codex
```json
{
"name": "@maestro/codex",
"version": "1.0.0",
"description": "Maestro Codex - Codex workflow orchestration",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"bin": {
"maestro-codex": "./bin/cli.js"
},
"keywords": [
"maestro",
"codex",
"workflow",
"ai",
"code-analysis"
],
"repository": {
"type": "git",
"url": "https://github.com/maestro-suite/maestro-codex"
}
}
```
### Python 包(如果需要)
#### maestro-claude
```toml
[project]
name = "maestro-claude"
version = "1.0.0"
description = "Maestro Claude - Claude Code workflow orchestration"
readme = "README.md"
requires-python = ">=3.8"
keywords = ["maestro", "claude", "workflow", "ai"]
[project.urls]
Homepage = "https://claude.maestro.dev"
Repository = "https://github.com/maestro-suite/maestro-claude"
```
---
## 品牌视觉
### Logo 设计
每个工作流使用不同的颜色来区分,但保持统一的设计语言:
| 工作流 | 主色 | 辅助色 | 图标元素 |
|--------|------|--------|----------|
| **Maestro Claude** | 橙色 `#FF6B35` | 深橙 `#D94F1C` | Claude 的 C 字母 + 指挥棒 |
| **Maestro Codex** | 绿色 `#00D084` | 深绿 `#00A86B` | Codex 的代码符号 + 指挥棒 |
| **Maestro Gemini** | 蓝色 `#4285F4` | 深蓝 `#1967D2` | Gemini 的双子星 + 指挥棒 |
| **Maestro Qwen** | 紫色 `#9C27B0` | 深紫 `#7B1FA2` | Qwen 的 Q 字母 + 指挥棒 |
### 总品牌色彩
- **主色**: 深蓝/午夜蓝 `#192A56` - 专业、稳定
- **强调色**: 活力青/薄荷绿 `#48D1CC` - 智能、创新
- **中性色**: 浅灰 `#F5F5F5`, 深灰 `#333333`
### 设计元素
- **指挥棒**: 所有 Logo 的核心元素,象征编排和指挥
- **声波/轨迹**: 动态的线条,表示工作流的流动
- **几何化**: 现代、简洁的几何图形
---
## 方案优点
### 1. 清晰直观
- 用户一眼就知道使用的是哪个 CLI 工具
- 不需要学习额外的术语映射
### 2. 易于理解
- 命名规则简单一致
- 新用户快速上手
### 3. 灵活扩展
- 未来添加新 CLI 时,命名规则保持一致
- 例如:添加 `Maestro GPT``Maestro Llama`
### 4. 品牌统一
- 所有工作流都在 Maestro 品牌下
- 强化 Maestro 作为编排平台的定位
### 5. 技术透明
- 开发者清楚底层使用的技术栈
- 便于调试和问题排查
---
## 注意事项
### 1. 商标问题
使用 "Maestro Claude"、"Maestro Codex" 等名称时,需要注意:
- ⚠️ 确保不侵犯原 CLI 的商标权
- ✅ 在文档中明确说明这些是"基于 XXX 的工作流",而不是官方产品
- ✅ 添加免责声明:
```
Maestro Claude 是基于 Claude Code 的工作流编排系统。
Claude 和 Claude Code 是 Anthropic 的商标。
本项目与 Anthropic 无关联。
```
### 2. 命名冲突
在发布前需要检查:
- [ ] npm 包名 `@maestro/claude`、`@maestro/codex` 等是否可用
- [ ] PyPI 包名 `maestro-claude`、`maestro-codex` 等是否可用
- [ ] GitHub 组织名 `maestro-suite` 是否可用
- [ ] 域名 `maestro.dev`、`claude.maestro.dev` 等是否可用
### 3. 用户认知
需要在文档中清楚说明:
- **Maestro** 是编排平台(总品牌)
- **Maestro Claude/Codex/Gemini/Qwen** 是工作流系统(子品牌)
- 底层使用的是对应的 CLI 工具(技术实现)
示例说明:
```
Maestro 是一个 AI 工作流编排平台。
Maestro Claude 是基于 Claude Code 的工作流系统,
它调用 Claude Code CLI 来执行代码生成和重构任务。
```
---
## 下一步行动
### 阶段 1: 资源可用性检查
- [ ] 检查域名可用性
- [ ] `maestro.dev`
- [ ] `claude.maestro.dev`
- [ ] `codex.maestro.dev`
- [ ] `gemini.maestro.dev`
- [ ] `qwen.maestro.dev`
- [ ] 检查 npm 包名可用性
- [ ] `@maestro/core`
- [ ] `@maestro/claude`
- [ ] `@maestro/codex`
- [ ] `@maestro/gemini`
- [ ] `@maestro/qwen`
- [ ] 检查 GitHub 可用性
- [ ] 组织名 `maestro-suite`
- [ ] 仓库名 `maestro`, `maestro-claude`, `maestro-codex` 等
### 阶段 2: 迁移计划
- [ ] 创建迁移文档(详见 `docs/migration/renaming-plan.md`
- [ ] 重命名根目录:`Claude_dms3` → `maestro`
- [ ] 重组包结构:创建 `packages/` 目录
- [ ] 更新所有配置文件
- [ ] 更新代码中的引用
### 阶段 3: 实施和发布
- [ ] 执行迁移
- [ ] 更新文档和 README
- [ ] 创建 Logo 和品牌资产
- [ ] 发布到 npm/PyPI
- [ ] 配置域名和网站
---
## 参考资料
- [品牌架构设计](./brand-architecture.md)
- [迁移计划](../migration/renaming-plan.md)
- [视觉设计指南](./visual-identity.md)
---
## 变更历史
| 版本 | 日期 | 变更内容 | 作者 |
|------|------|----------|------|
| 1.0.0 | 2026-03-09 | 初始版本,确定品牌命名系统 | - |

View File

@@ -37,7 +37,7 @@
| [`/workflow-multi-cli-plan`](./workflow.md#multi-cli-plan) | Multi-CLI collaborative planning | Intermediate |
| [`/workflow:review`](./workflow.md#review) | Post-implementation review | Intermediate |
| [`/workflow:clean`](./workflow.md#clean) | Smart code cleanup | Intermediate |
| [`/workflow:init`](./workflow.md#init) | Initialize project state | Intermediate |
| [`/workflow:spec:setup `](./workflow.md#init) | Initialize project state | Intermediate |
| [`/workflow:brainstorm-with-file`](./workflow.md#brainstorm-with-file) | Interactive brainstorming | Intermediate |
| [`/workflow:analyze-with-file`](./workflow.md#analyze-with-file) | Interactive collaborative analysis | Beginner |
| [`/workflow:debug-with-file`](./workflow.md#debug-with-file) | Interactive hypothesis-driven debugging | Intermediate |

View File

@@ -59,7 +59,7 @@
| Command | Function | Syntax |
|---------|----------|--------|
| [`clean`](#clean) | Smart code cleanup | `/workflow:clean [-y] [--dry-run] ["focus area"]` |
| [`init`](#init) | Initialize project state | `/workflow:init [--regenerate]` |
| [`init`](#init) | Initialize project state | `/workflow:spec:setup [--regenerate]` |
| [`plan-verify`](#plan-verify) | Verify planning consistency | `/workflow-plan-verify [--session session-id]` |
## Command Details

View File

@@ -81,7 +81,7 @@ This creates a new workflow session. All subsequent operations will be performed
### 2.2.2 Initialize Project Specs
```
/workflow:init
/workflow:spec:setup
```
This creates the `project-tech.json` file, recording your project's technology stack information.
@@ -89,7 +89,7 @@ This creates the `project-tech.json` file, recording your project's technology s
### 2.2.3 Populate Project Specs
```
/workflow:init-guidelines
/workflow:spec:setup -guidelines
```
Interactively populate project specifications, including coding style, architectural decisions, and other information.
@@ -282,7 +282,7 @@ npm install
/workflow:session:start
# 5. Initialize project
/workflow:init
/workflow:spec:setup
```
### Common Commands

View File

@@ -50,7 +50,7 @@ Create a simple workflow in under 5 minutes:
# Prompt: "Fix the login timeout issue" # Execute without confirmation prompts
# Or use specific workflow commands
/workflow:init # Initialize project state
/workflow:spec:setup # Initialize project state
/workflow-plan
# Prompt: "Add OAuth2 authentication" # Create implementation plan
/workflow-execute # Execute planned tasks

View File

@@ -74,7 +74,7 @@ To check available CCW commands, you can list them:
Available CCW Commands:
- /ccw - Main workflow orchestrator
- /ccw-coordinator - External CLI orchestration
- /workflow:init - Initialize project configuration
- /workflow:spec:setup - Initialize project configuration
- /workflow:status - Generate project views
- /issue:discover - Discover and plan issues
- /brainstorm - Multi-perspective brainstorming
@@ -145,7 +145,7 @@ Create `CLAUDE.md` in your project root to define project-specific instructions:
Initialize CCW in your project:
```
/workflow:init
/workflow:spec:setup
```
This creates `.workflow/project-tech.json` with your project's technology stack.
@@ -343,7 +343,7 @@ After installation, try these commands to verify everything works:
```
# 1. Initialize in your project
/workflow:init
/workflow:spec:setup
# 2. Try a simple analysis using the CLI tool
Bash: ccw cli -p "Analyze the project structure" --tool gemini --mode analysis
@@ -369,7 +369,7 @@ What would you like to accomplish? Please describe your task.
**Using workflow init:**
```
You: /workflow:init
You: /workflow:spec:setup
✔ Created .workflow/project-tech.json
✔ Project configuration complete
```

View File

@@ -37,7 +37,7 @@
| [`/workflow-multi-cli-plan`](./workflow.md#multi-cli-plan) | 多 CLI 协作规划 | Intermediate |
| [`/workflow:review`](./workflow.md#review) | 实现后审查 | Intermediate |
| [`/workflow:clean`](./workflow.md#clean) | 智能代码清理 | Intermediate |
| [`/workflow:init`](./workflow.md#init) | 初始化项目状态 | Intermediate |
| [`/workflow:spec:setup `](./workflow.md#init) | 初始化项目状态 | Intermediate |
| [`/workflow:brainstorm-with-file`](./workflow.md#brainstorm-with-file) | 交互式头脑风暴 | Intermediate |
| [`/workflow:analyze-with-file`](./workflow.md#analyze-with-file) | 交互式协作分析 | Beginner |
| [`/workflow:debug-with-file`](./workflow.md#debug-with-file) | 交互式假设驱动调试 | Intermediate |

View File

@@ -59,7 +59,7 @@
| 命令 | 功能 | 语法 |
| --- | --- | --- |
| [`clean`](#clean) | 智能代码清理 | `/workflow:clean [-y] [--dry-run] ["焦点区域"]` |
| [`init`](#init) | 初始化项目状态 | `/workflow:init [--regenerate]` |
| [`init`](#init) | 初始化项目状态 | `/workflow:spec:setup [--regenerate]` |
| [`plan-verify`](#plan-verify) | 验证规划一致性 | `/workflow-plan-verify [--session session-id]` |
## 命令详解

View File

@@ -67,7 +67,7 @@ API Keys 也可以在项目级别配置 `.claude/settings.json`,项目级配
### 2.2.2 初始化项目规范
```
/workflow:init
/workflow:spec:setup
```
这会创建 `project-tech.json` 文件,记录项目的技术栈信息。
@@ -75,7 +75,7 @@ API Keys 也可以在项目级别配置 `.claude/settings.json`,项目级配
### 2.2.3 填充项目规范
```
/workflow:init-guidelines
/workflow:spec:setup -guidelines
```
交互式填充项目规范,包括编码风格、架构决策等信息。
@@ -272,7 +272,7 @@ npm install
/workflow:session:start
# 5. 初始化项目
/workflow:init
/workflow:spec:setup
```
### 常用命令

View File

@@ -1,6 +1,6 @@
{
"name": "claude-code-workflow",
"version": "7.2.4",
"version": "7.2.6",
"description": "JSON-driven multi-agent development framework with intelligent CLI orchestration (Gemini/Qwen/Codex), context-first architecture, and automated workflow execution",
"type": "module",
"main": "ccw/dist/index.js",