From 3007d9a78df830acc3af027a754037f1dd36f5eb Mon Sep 17 00:00:00 2001 From: Sopaco Date: Sat, 4 Jul 2026 15:14:24 +0800 Subject: [PATCH 01/29] add terrain assets --- .agents/skills/ai-context-generator/README.md | 252 - .agents/skills/ai-context-generator/SKILL.md | 216 - .../skills/ai-context-generator/_meta.json | 6 - .../references/WRITING-GUIDE.md | 357 - .../ai-context-generator/scripts/generate.ts | 785 - .agents/skills/codegraph-skill/SKILL.md | 99 + .agents/skills/repomix-context-skill/SKILL.md | 84 + .agents/skills/rtk-skill/SKILL.md | 213 + .../skills/terrain-knowledge-skill/SKILL.md | 69 + .terrain/.meta/freshness.json | 40 + .terrain/.meta/sync.json | 9 + .terrain/agent/context.md | 88 + .terrain/agent/meta-inputs.md | 173 + .terrain/agent/meta.json | 95 + .terrain/agent/repomix.md | 43172 ++++++++++++++++ .terrain/index.md | 55 + .terrain/knowledge/00-glossary.md | 58 + .terrain/knowledge/10-internal-framework.md | 108 + .terrain/knowledge/20-api-usage.md | 222 + .terrain/knowledge/30-scaffolding.md | 389 + Cargo.lock | 2305 +- adk-rust-learning.md | 1802 - 22 files changed, 45401 insertions(+), 5196 deletions(-) delete mode 100644 .agents/skills/ai-context-generator/README.md delete mode 100644 .agents/skills/ai-context-generator/SKILL.md delete mode 100644 .agents/skills/ai-context-generator/_meta.json delete mode 100644 .agents/skills/ai-context-generator/references/WRITING-GUIDE.md delete mode 100644 .agents/skills/ai-context-generator/scripts/generate.ts create mode 100644 .agents/skills/codegraph-skill/SKILL.md create mode 100644 .agents/skills/repomix-context-skill/SKILL.md create mode 100644 .agents/skills/rtk-skill/SKILL.md create mode 100644 .agents/skills/terrain-knowledge-skill/SKILL.md create mode 100644 .terrain/.meta/freshness.json create mode 100644 .terrain/.meta/sync.json create mode 100644 .terrain/agent/context.md create mode 100644 .terrain/agent/meta-inputs.md create mode 100644 .terrain/agent/meta.json create mode 100644 .terrain/agent/repomix.md create mode 100644 .terrain/index.md create mode 100644 .terrain/knowledge/00-glossary.md create mode 100644 .terrain/knowledge/10-internal-framework.md create mode 100644 .terrain/knowledge/20-api-usage.md create mode 100644 .terrain/knowledge/30-scaffolding.md delete mode 100644 adk-rust-learning.md diff --git a/.agents/skills/ai-context-generator/README.md b/.agents/skills/ai-context-generator/README.md deleted file mode 100644 index 7246ae6..0000000 --- a/.agents/skills/ai-context-generator/README.md +++ /dev/null @@ -1,252 +0,0 @@ -# AI Context Generator - -> A reusable Agent Skill for creating project knowledge bases optimized for coding agents. - ---- - -## What Is This? - -This skill helps you generate a `.ai-context/` directory structure that provides coding agents (like Claude Code, Cursor, etc.) with pre-generated project knowledge. The knowledge is organized by stability tiers, making it easy for agents to understand: - -- **What** the project is (PROJECT-ESSENCE.md) -- **How** components fit together (ARCHITECTURE.md) -- **Why** design decisions were made (DECISIONS.md) -- **What issues** are currently active (DYNAMICS.md) - ---- - -## Why Use This? - -**Problem:** Coding agents spend valuable tokens and time exploring code to understand project context. This exploration is repeated in every session. - -**Solution:** Pre-generated knowledge that: -- Reduces onboarding time from ~10 minutes to ~30 seconds -- Preserves institutional knowledge (decisions, constraints) -- Uses ~4000 tokens vs ~50,000+ for full code exploration -- Guides agents to relevant code locations faster - ---- - -## Quick Start - -### Option 1: Use with an AI Agent - -Simply tell your coding agent: - -``` -Use the ai-context-generator skill to setup .ai-context for this project -``` - -The agent will: -1. Read project files (AGENTS.md, README.md, package.json, etc.) -2. Generate the `.ai-context/` structure -3. Ask clarifying questions if needed -4. Create all necessary files - -### Option 2: Manual Generation - -1. Copy the `templates/` directory contents -2. Replace `{{PLACEHOLDERS}}` with your project details -3. Create the `.ai-context/` directory structure - ---- - -## Generated Structure - -``` -.ai-context/ -├── SKILL.md # Entry point with activation rules -├── DYNAMICS.md # Active issues & constraints -├── references/ -│ ├── PROJECT-ESSENCE.md # What & why (stable) -│ ├── ARCHITECTURE.md # Component relationships -│ └── DECISIONS.md # Design decisions & rationale -└── meta/ - ├── MAINTENANCE.md # How to maintain this knowledge - ├── templates/ # (Optional) Custom templates - └── scripts/ # (Optional) Maintenance scripts -``` - ---- - -## Stability Tiers - -| Tier | File | Stability | Tokens | Update Frequency | -|------|------|-----------|--------|------------------| -| 0 | PROJECT-ESSENCE.md | High | ~500 | Quarterly | -| 1 | ARCHITECTURE.md | Medium | ~1000 | Monthly | -| 2 | DECISIONS.md | Low | ~800 | Per decision | -| 3 | DYNAMICS.md | Dynamic | ~600 | As needed | - -Total budget: ~4000 tokens (within typical context limits) - ---- - -## Files Overview - -### SKILL.md -The entry point that tells agents: -- When to activate this knowledge -- Which file to read for specific needs -- How to keep knowledge updated - -### references/PROJECT-ESSENCE.md -One-page summary answering: -- What is this project? -- Why does it exist? -- Who is it for? -- What does it provide? - -### references/ARCHITECTURE.md -Component-level overview with: -- System diagram (ASCII or Mermaid) -- Component responsibilities -- Data flow descriptions -- Key dependencies - -### references/DECISIONS.md -Architecture Decision Records (ADRs) format: -- Context (the problem) -- Decision (the choice) -- Rationale (why) -- Trade-offs -- Implications - -### DYNAMICS.md -Living document tracking: -- Active issues (blockers) -- Known constraints -- Workarounds -- Recently resolved issues - -### meta/MAINTENANCE.md -Guide for keeping knowledge accurate: -- What triggers updates -- How to make updates -- Quality standards -- Anti-patterns to avoid - ---- - -## Integration with AGENTS.md - -This skill complements (not replaces) `AGENTS.md`: - -| File | Purpose | Author | -|------|---------|--------| -| `AGENTS.md` | How to work (commands, style, rules) | Project maintainers | -| `.ai-context/` | What the project is (architecture, decisions) | Generated | - -**Both should be read at session start for optimal agent performance.** - ---- - -## Customization - -### Custom Templates - -Copy templates to your project and modify: - -``` -your-project/ -├── .ai-context/ -│ └── meta/ -│ └── templates/ # Override defaults here -``` - -The skill will use local templates if they exist. - -### Custom Scripts - -Add automation scripts to `meta/scripts/`: - -| Script | Purpose | -|--------|---------| -| `check-drift.ts` | Detect documentation drift from code | -| `audit-dynamics.ts` | Flag stale issues | -| `generate-from-code.ts` | Auto-generate from code analysis | - ---- - -## Best Practices - -### Do: -- ✅ Generate once, maintain regularly -- ✅ Keep each file under 150 lines -- ✅ Use diagrams over paragraphs -- ✅ Update "Last updated" dates -- ✅ Remove resolved issues from DYNAMICS.md - -### Don't: -- ❌ Copy-paste code snippets (link to files instead) -- ❌ Document every file/function -- ❌ Include details that change frequently -- ❌ Let knowledge go stale - ---- - -## Example Usage - -### Scenario 1: New Project Setup - -``` -User: Setup ai-context for my new Express.js API project - -Agent: -1. Reads package.json, identifies Express.js + TypeScript -2. Scans src/ directory structure -3. Generates PROJECT-ESSENCE.md describing the API -4. Creates ARCHITECTURE.md with component diagram -5. Initializes empty DECISIONS.md and DYNAMICS.md -6. Asks about any non-obvious design choices -``` - -### Scenario 2: Existing Project - -``` -User: My project has 50k lines of code, help agents understand it faster - -Agent: -1. Reads AGENTS.md, README.md, existing docs/ -2. Analyzes directory structure for components -3. Extracts key architecture patterns -4. Generates concise knowledge base -5. Highlights areas needing clarification -``` - ---- - -## Compatibility - -- **Agent Skills Spec**: Fully compliant with [agentskills.io](https://agentskills.io/specification) -- **Claude Code**: Works with Claude Code's skill system -- **Cursor**: Compatible with Cursor's context system -- **Other Agents**: Portable to any agent supporting Agent Skills - ---- - -## Contributing - -To improve this skill: - -1. Fork and modify templates -2. Test with your projects -3. Submit improvements via PR - ---- - -## License - -MIT License — Use freely in any project. - ---- - -## References - -- [Agent Skills Specification](https://agentskills.io/specification) -- [Architecture Decision Records](https://adr.github.io/) -- [Project README Template](https://github.com/LinusBorg/project-readme-template) - ---- - -*Generate better project knowledge. Help agents work smarter.* \ No newline at end of file diff --git a/.agents/skills/ai-context-generator/SKILL.md b/.agents/skills/ai-context-generator/SKILL.md deleted file mode 100644 index 32f2f70..0000000 --- a/.agents/skills/ai-context-generator/SKILL.md +++ /dev/null @@ -1,216 +0,0 @@ ---- -name: ai-context-generator -description: | - Generates .ai-context knowledge base for coding agents. Activate when: (1) setting up a new project for AI-assisted development, (2) user asks to "create project knowledge" or "setup ai-context", (3) existing .ai-context needs regeneration. Creates tiered documentation structure optimized for agent comprehension and token efficiency. ---- - -# AI Context Generator - -> A reusable skill for creating project knowledge bases that help coding agents work faster and smarter. - ---- - -## 🎯 When to Use This Skill - -**Activate when:** -- Setting up a new project for AI-assisted development -- User requests: "create ai-context", "setup project knowledge", "generate .ai-context" -- Existing `.ai-context` is outdated and needs regeneration -- After major project restructuring - -**Do NOT activate when:** -- Project already has fresh `.ai-context` (check `SKILL.md` date) -- User asks for unrelated documentation -- Simple code tasks with clear existing context - ---- - -## 📋 What This Skill Generates - -Creates a `.ai-context/` directory with: - -``` -.ai-context/ -├── SKILL.md # Entry point with activation rules -├── DYNAMICS.md # Active issues & constraints (Dynamic) -├── references/ -│ ├── PROJECT-ESSENCE.md # What & why (High stability) -│ ├── ARCHITECTURE.md # Component relationships (Medium stability) -│ └── DECISIONS.md # Design decisions (Update on change) -└── meta/ - ├── MAINTENANCE.md # How to maintain this knowledge - ├── templates/ # (Optional) Custom templates - └── scripts/ # (Optional) Maintenance scripts -``` - -### Stability Tiers - -| Tier | File | Update Frequency | Token Budget | -|------|------|-------------------|--------------| -| 0 | PROJECT-ESSENCE.md | Quarterly / Major version | ~500 tokens | -| 1 | ARCHITECTURE.md | Monthly / Sprint | ~1000 tokens | -| 2 | DECISIONS.md | Per decision change | ~800 tokens | -| 3 | DYNAMICS.md | As needed (issues) | ~600 tokens | - ---- - -## 🔧 Generation Process - -### Step 1: Gather Project Intelligence - -Before generating, collect: - -``` -□ Read AGENTS.md (if exists) — operational rules -□ Read README.md — user-facing description -□ Read package.json — dependencies, scripts, entry points -□ Scan directory structure — identify components -□ Read docs/ or litho.docs/ — existing documentation -□ Identify key source files — main entry points -□ Note technology stack — frameworks, languages, platforms -``` - -### Step 2: Extract Knowledge - -**For PROJECT-ESSENCE.md:** -- What is this project? (one sentence) -- Why does it exist? (problem/solution) -- Who is it for? (target users) -- What does it provide? (key features) -- Core constraints? (security, compatibility) - -**For ARCHITECTURE.md:** -- System diagram (ASCII or Mermaid) -- Component responsibilities -- Data flow between components -- Key dependencies -- Important patterns - -**For DECISIONS.md:** -- Non-obvious design choices -- Trade-offs made -- Constraints accepted -- Decisions that might be revisited - -**For DYNAMICS.md:** -- Current blockers -- Known workarounds -- Temporary constraints -- Recently resolved issues (brief) - -### Step 3: Generate Files - -Use templates from `templates/` directory: - -1. Start with `SKILL.md` — entry point with activation rules -2. Generate `references/PROJECT-ESSENCE.md` — core identity -3. Generate `references/ARCHITECTURE.md` — component map -4. Generate `references/DECISIONS.md` — design rationale -5. Generate `DYNAMICS.md` — active issues -6. Generate `meta/MAINTENANCE.md` — upkeep guide - -### Step 4: Validate Quality - -``` -□ SKILL.md has clear activation triggers -□ PROJECT-ESSENCE.md readable in 2 minutes -□ ARCHITECTURE.md shows big picture (no code) -□ DECISIONS.md justified with rationale -□ DYNAMICS.md only contains current issues -□ All files dated at top -□ Total token budget < 4000 tokens -``` - ---- - -## 📝 Writing Principles - -### Do: -- ✅ Write for someone who knows nothing about the project -- ✅ Use diagrams over paragraphs -- ✅ Focus on "why" not "how" -- ✅ Keep files under 150 lines each -- ✅ Link between related sections -- ✅ Include "Last updated" dates - -### Don't: -- ❌ Copy-paste code snippets (link to files instead) -- ❌ Document every file/function -- ❌ Include details that change frequently -- ❌ Duplicate content across files -- ❌ Use jargon without context - ---- - -## 🔄 Integration with AGENTS.md - -``` -AGENTS.md = "How to work" (commands, style, rules) -.ai-context = "What the project is" (architecture, decisions, issues) -``` - -Both should be read at session start. They serve different purposes and should not overlap. - ---- - -## 📚 Template Reference - -Templates are provided in `templates/`: - -| Template | Purpose | -|----------|---------| -| `skill.md.tmpl` | SKILL.md with placeholder prompts | -| `essence.md.tmpl` | PROJECT-ESSENCE.md structure | -| `architecture.md.tmpl` | ARCHITECTURE.md with diagram prompts | -| `decisions.md.tmpl` | DECISIONS.md with ADR format | -| `dynamics.md.tmpl` | DYNAMICS.md with status tracking | -| `maintenance.md.tmpl` | MAINTENANCE.md guide | - ---- - -## 🛠️ Automation Scripts - -Scripts in `scripts/` can help with: - -| Script | Purpose | -|--------|---------| -| `generate.ts` | Interactive generation from templates | -| `check-drift.ts` | Compare documented vs actual structure | -| `audit-dynamics.ts` | Flag stale issues (>30 days) | - ---- - -## 💡 Example Usage - -**User:** "Setup ai-context for my project" - -**Agent:** -1. Activate this skill -2. Read AGENTS.md, README.md, package.json -3. Scan directory structure -4. Generate each file using templates -5. Ask clarifying questions if needed: - - "What's the main problem this project solves?" - - "Any non-obvious design decisions I should know about?" - - "Current blockers or workarounds?" - ---- - -## ⚠️ Important Notes - -- Generated knowledge is a **starting point**, not final truth -- Agent should verify against actual code during first session -- User should review generated content for accuracy -- Schedule regular audits (monthly recommended) - ---- - -## 📖 References - -- [Agent Skills Specification](https://agentskills.io/specification) -- [Architecture Decision Records](https://adr.github.io/) -- [Writing Readable Docs](references/WRITING-GUIDE.md) - ---- - -*This skill creates knowledge bases optimized for AI agents. For questions or improvements, see MAINTENANCE.md.* \ No newline at end of file diff --git a/.agents/skills/ai-context-generator/_meta.json b/.agents/skills/ai-context-generator/_meta.json deleted file mode 100644 index 78d056b..0000000 --- a/.agents/skills/ai-context-generator/_meta.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "ownerId": "kn7a2vy7ys7aj3gzm2vvw7zc158311b2", - "slug": "ai-context-generator", - "version": "1.0.0", - "publishedAt": 1776327011803 -} \ No newline at end of file diff --git a/.agents/skills/ai-context-generator/references/WRITING-GUIDE.md b/.agents/skills/ai-context-generator/references/WRITING-GUIDE.md deleted file mode 100644 index 5dc6718..0000000 --- a/.agents/skills/ai-context-generator/references/WRITING-GUIDE.md +++ /dev/null @@ -1,357 +0,0 @@ -# Writing Guide for AI Context Knowledge Base - -> Practical guidance for creating documentation that coding agents can effectively use. Last updated: 2025-01-11. - ---- - -## Core Philosophy - -**Goal:** Create knowledge that helps agents work faster, not knowledge that documents everything. - -**Principle:** Agents have two sources of truth: -1. **Static Knowledge** (`.ai-context/`) — Mental model, context, rationale -2. **Dynamic Exploration** (code reading) — Current state, implementation details - -Write static knowledge to complement dynamic exploration, not replace it. - ---- - -## Writing for Agents vs Humans - -| Aspect | Human Documentation | Agent Knowledge | -|--------|---------------------|-----------------| -| Detail level | Comprehensive | Minimal, strategic | -| Code examples | Plentiful | Link to files instead | -| Update frequency | Per feature | Per structural change | -| Audience | Varies | Technical, literal | -| Format flexibility | Prose-heavy | Structured, scannable | - ---- - -## The Token Budget - -Each file has an implicit token budget. Respect it. - -| File | Budget | Why | -|------|--------|-----| -| PROJECT-ESSENCE.md | ~500 tokens | Must read every session | -| ARCHITECTURE.md | ~1000 tokens | Read when working across components | -| DECISIONS.md | ~800 tokens | Read when changing patterns | -| DYNAMICS.md | ~600 tokens | Read when debugging | -| SKILL.md | ~400 tokens | Loaded for discovery | - -**Total: ~3300 tokens** — Less than one typical function's worth of context. - ---- - -## Writing Techniques - -### 1. Prefer Diagrams Over Paragraphs - -❌ **Bad:** -``` -The system has a gateway that loads plugins. The gateway talks to cortex-mem-service -via HTTP. The service then connects to Qdrant for vector storage and the filesystem -for markdown storage. -``` - -✅ **Good:** -``` -┌─────────────┐ -│ Gateway │ -└──────┬──────┘ - │ HTTP - ▼ -┌─────────────────┐ -│ cortex-mem-svc │ -└────┬───────┬────┘ - │ │ - ▼ ▼ - Qdrant Files -``` - -### 2. Link, Don't Copy - -❌ **Bad:** -```markdown -The config format is: -```toml -[server] -port = 8085 -host = "localhost" -``` - -✅ **Good:** -```markdown -Configuration format: see `config.example.toml` or `src/config.ts` for schema. -``` - -**Why:** Code copies become stale. Links stay valid. - -### 3. State the Non-Obvious - -❌ **Bad:** -```markdown -We use TypeScript for type safety. -``` - -✅ **Good:** -```markdown -We use TypeScript strict mode. Avoid `any` — we prefer runtime validation via Zod -instead of type assertions. -``` - -**Why:** The first is obvious to any TypeScript user. The second captures project-specific practices. - -### 4. Use Tables for Comparisons - -❌ **Bad:** -```markdown -The memory plugin is for explicit calls while the context engine handles automatic -lifecycle hooks. They can both be installed together. -``` - -✅ **Good:** -```markdown -| Aspect | Memory Plugin | Context Engine | -|--------|---------------|----------------| -| Trigger | Explicit tool call | Automatic lifecycle | -| Control | Full | None | -| Co-install | Yes | Yes | -``` - -### 5. Date Everything - -Every file should have a "Last updated" or "Last reviewed" date at the top. - -**Why:** Agents need to know if knowledge might be stale. - ---- - -## Anti-Patterns - -### Anti-Pattern 1: The Encyclopedia - -```markdown -## File Structure - -/src/ - /components/ - /Button/ - Button.tsx # Button component - Button.test.tsx # Tests - styles.css # Styles - /Input/ - Input.tsx - ... -``` - -**Problem:** Becomes wrong immediately. Use `find_path` instead. - -**Fix:** -```markdown -Components live in `src/components/`. Each is self-contained with its own directory. -``` - ---- - -### Anti-Pattern 2: The Tutorial - -```markdown -## How to Add a New Tool - -1. Create a new file in `src/tools/` -2. Import the tool interface -3. Implement the execute method -4. Register in tool-registry.ts -5. Add tests -... -``` - -**Problem:** This is a procedure, not knowledge. Procedures belong in AGENTS.md. - -**Fix:** -```markdown -Tools are registered in `tool-registry.ts`. Each tool implements `ToolInterface`. -``` - ---- - -### Anti-Pattern 3: The Decision Dump - -```markdown -## ADR-042: Use tabs for indentation - -We decided to use tabs because... - -## ADR-043: Use semicolons - -We decided to use semicolons because... -``` - -**Problem:** Not all decisions matter equally. - -**Fix:** Only document decisions that: -- Have significant trade-offs -- Affect multiple components -- Might be questioned later -- Have non-obvious rationale - ---- - -### Anti-Pattern 4: The Issue Graveyard - -```markdown -## Fixed in 2024-01 - -The config parser was broken... - -## Fixed in 2024-02 - -The API endpoint was wrong... -``` - -**Problem:** Resolved issues hide active ones. - -**Fix:** DYNAMICS.md should only contain: -- Active blockers -- Known constraints -- Items under consideration -- Recently resolved (last 2 weeks, brief) - ---- - -## What to Put Where - -### PROJECT-ESSENCE.md — "What and Why" - -- What is this? (one sentence) -- Why does it exist? -- Who uses it? -- Key value proposition -- Core constraints -- Success metrics - -**Not:** Technical details, architecture, decisions - ---- - -### ARCHITECTURE.md — "How Things Connect" - -- System diagram -- Component responsibilities -- Data flow -- Key patterns -- Dependencies -- Configuration layers - -**Not:** Implementation details, every file, API specs - ---- - -### DECISIONS.md — "Why We Chose This" - -- Non-obvious choices -- Trade-offs accepted -- Constraints embraced -- Things we might revisit - -**Not:** Naming conventions, style choices, one-off decisions - ---- - -### DYNAMICS.md — "What's Happening Now" - -- Active blockers -- Workarounds in use -- Temporary constraints -- Items under review - -**Not:** History, resolved issues, wishlists - ---- - -## Review Checklist - -Before finalizing any file: - -``` -□ Is this knowledge that code exploration can't easily reveal? -□ Would this still be accurate in 3 months? -□ Is the token budget respected? -□ Is there a "Last updated" date? -□ Did I link instead of copy? -□ Did I state the non-obvious? -□ Is this scannable (tables, lists, diagrams)? -``` - ---- - -## Example Transformations - -### Before (Human-Style): - -```markdown -# Architecture - -This document describes the architecture of our system. The system is built using -TypeScript and runs on Node.js. We use Express for the HTTP server and PostgreSQL -for the database. The frontend is built with React. - -The main components are: -- API Layer: Handles HTTP requests -- Business Logic: Contains the core functionality -- Data Layer: Manages database access - -We chose PostgreSQL because it's reliable and has good JSON support. -``` - -### After (Agent-Optimized): - -```markdown -# Architecture - -> Last updated: 2025-01-11 - -## System Diagram - -``` -┌─────────────┐ ┌─────────────┐ ┌───────────┐ -│ Express │────▶│ Business │────▶│ PostgreSQL│ -│ :3000 │ │ Logic │ │ :5432 │ -└─────────────┘ └─────────────┘ └───────────┘ - │ - ▼ -┌─────────────┐ -│ React │ -│ Client │ -└─────────────┘ -``` - -## Components - -| Component | Entry Point | Responsibility | -|-----------|-------------|----------------| -| API Layer | `src/api/` | HTTP routing, auth | -| Business Logic | `src/core/` | Domain operations | -| Data Layer | `src/db/` | Queries, migrations | - -## Key Decisions - -See [DECISIONS.md](DECISIONS.md) for rationale on PostgreSQL, Express, etc. -``` - ---- - -## Summary - -1. **Minimize tokens** — Every token costs attention -2. **Link, don't copy** — Code changes, links stay valid -3. **State the non-obvious** — Obvious things don't need documentation -4. **Structure for scanning** — Tables, lists, diagrams -5. **Date everything** — Agents need to assess staleness -6. **Separate concerns** — Each file has a purpose - ---- - -_Writing for agents is writing for a literal, token-constrained, but technically competent reader who prefers structure over prose._ \ No newline at end of file diff --git a/.agents/skills/ai-context-generator/scripts/generate.ts b/.agents/skills/ai-context-generator/scripts/generate.ts deleted file mode 100644 index 9d96ab9..0000000 --- a/.agents/skills/ai-context-generator/scripts/generate.ts +++ /dev/null @@ -1,785 +0,0 @@ -#!/usr/bin/env bun -/** - * AI Context Generator - * - * Generates .ai-context knowledge base structure for coding agents. - * Run with: bun run generate.ts [project-path] - */ - -import * as fs from 'fs' -import * as path from 'path' - -// ============================================================================= -// Types & Interfaces -// ============================================================================= - -interface ProjectInfo { - name: string - description: string - problem: string - solution: string - targetUsers: Array<{ user: string; useCase: string }> - components: Array<{ name: string; purpose: string; entry?: string }> - decisions: Array<{ title: string; context: string; decision: string; rationale: string }> - issues: Array<{ title: string; status: 'active' | 'known'; impact: string; workaround: string }> - constraints: string[] -} - -interface TemplateData { - PROJECT_NAME: string - DATE: string - [key: string]: string | string[] | object[] -} - -// ============================================================================= -// Template Engine -// ============================================================================= - -function renderTemplate(template: string, data: TemplateData): string { - let result = template - - // Replace simple {{KEY}} placeholders - for (const [key, value] of Object.entries(data)) { - if (typeof value === 'string') { - result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), value) - } - } - - return result -} - -// ============================================================================= -// File Templates -// ============================================================================= - -const SKILL_TEMPLATE = `--- -name: ai-context -description: | - Project knowledge base for coding agents. Activate when: (1) starting a new session in this project, (2) encountering unfamiliar code patterns or architecture decisions, (3) user asks about project design or rationale, (4) before making significant structural changes. Contains tiered knowledge from stable design principles to dynamic issues. ---- - -# AI Context — {{PROJECT_NAME}} - -> This skill provides pre-generated project knowledge to help you understand the project faster and work more effectively. - ---- - -## 🎯 When to Activate This Skill - -**Activate immediately when:** -- Starting a new coding session in this project -- You need to understand "why something is designed this way" -- User asks about project architecture, design decisions, or constraints - -**Refer to specific sections when:** -- Encountering unexpected behavior or errors → \`DYNAMICS.md\` -- Planning structural changes → \`references/DECISIONS.md\` -- Need high-level overview → \`references/PROJECT-ESSENCE.md\` -- Need component relationships → \`references/ARCHITECTURE.md\` - -**Do NOT activate when:** -- Simple code edits with clear context -- User requests are purely mechanical (rename, format, etc.) -- You already have sufficient context from recent conversation - ---- - -## 📁 Knowledge Tiers - -| Tier | File | Stability | Update Frequency | -|------|------|-----------|------------------| -| **Tier 0** | \`PROJECT-ESSENCE.md\` | High | Quarterly / Major version | -| **Tier 1** | \`ARCHITECTURE.md\` | Medium | Monthly / Sprint | -| **Tier 2** | \`DECISIONS.md\` | Low | Per decision change | -| **Tier 3** | \`DYNAMICS.md\` | Dynamic | As needed | - -### Reading Order (Recommended) - -\`\`\` -1. PROJECT-ESSENCE.md ← Start here (1-2 min read) -2. ARCHITECTURE.md ← If working across components -3. DECISIONS.md ← If changing established patterns -4. DYNAMICS.md ← If something feels wrong -\`\`\` - ---- - -## 🔧 How to Use This Knowledge - -### 1. Session Start Protocol -\`\`\` -□ Read PROJECT-ESSENCE.md (always) -□ Scan DYNAMICS.md for active issues -□ Read ARCHITECTURE.md if working across subprojects -□ Proceed with dynamic code exploration -\`\`\` - -### 2. Dynamic Code Exploration -- Use \`grep\` and \`find_path\` to locate actual implementations -- Verify knowledge against current code state -- Update knowledge if you find drift - -### 3. Decision Validation -Before changing established patterns: -\`\`\` -□ Check DECISIONS.md for existing decisions -□ If decision exists: follow it or explicitly propose change -□ If new decision needed: document after implementation -\`\`\` - ---- - -## 🔄 When to Update - -### Update PROJECT-ESSENCE.md when: -- Project purpose or scope fundamentally changes -- New major capability is added - -### Update ARCHITECTURE.md when: -- New component/subproject added -- Component responsibilities shift -- Data flow changes significantly - -### Update DECISIONS.md when: -- A new design decision is made -- An existing decision is revisited/changed - -### Update DYNAMICS.md when: -- New issue discovered -- Issue resolved -- Workaround found - ---- - -## 📚 File Reference - -- [Project Essence](references/PROJECT-ESSENCE.md) -- [Architecture](references/ARCHITECTURE.md) -- [Decisions](references/DECISIONS.md) -- [Dynamics](DYNAMICS.md) -- [Maintenance Guide](meta/MAINTENANCE.md) - ---- - -*Generated by ai-context-generator on {{DATE}}* -` - -const ESSENCE_TEMPLATE = `# Project Essence — {{PROJECT_NAME}} - -> **Stability: HIGH** | Update: Quarterly or major version changes -> -> Last reviewed: {{DATE}} - ---- - -## What Is This Project? - -{{PROJECT_DESCRIPTION}} - ---- - -## Why Does It Exist? - -**Problem:** {{PROJECT_PROBLEM}} - -**Solution:** -{{PROJECT_SOLUTION}} - ---- - -## Who Is This For? - -| User | Use Case | -|------|----------| -{{#TARGET_USERS}} -| {{user}} | {{useCase}} | -{{/TARGET_USERS}} - ---- - -## Key Constraints - -{{PROJECT_CONSTRAINTS}} - ---- - -*This file captures the stable essence of the project. For architecture details, see [ARCHITECTURE.md](ARCHITECTURE.md).* -` - -const ARCHITECTURE_TEMPLATE = `# Architecture — {{PROJECT_NAME}} - -> How components fit together. Last updated: {{DATE}}. -> -> **Update this when:** New component added, responsibilities shift, data flow changes. - ---- - -## System Overview - -\`\`\` -[DIAGRAM_PLACEHOLDER - Replace with your system diagram] - -Example: -┌─────────────────────────────────────────────────────────────────┐ -│ [Main System/Platform] │ -│ │ -│ ┌───────────────────┐ ┌────────────────────────┐ │ -│ │ [Component A] │ │ [Component B] │ │ -│ │ │ │ │ │ -│ │ • [responsibility]│ │ • [responsibility] │ │ -│ └────────┬──────────┘ └───────────┬────────────┘ │ -│ │ │ │ -│ └────────────┬───────────────┘ │ -│ ▼ │ -│ ┌────────────────────────┐ │ -│ │ [Backend Service] │ ← Port XXXX │ -│ └────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ -\`\`\` - ---- - -## Component Responsibilities - -{{#COMPONENTS}} -### {{name}} -- **Purpose:** {{purpose}} -{{#entry}} -- **Entry:** \`{{entry}}\` -{{/entry}} - -{{/COMPONENTS}} - ---- - -## Data Flow - -[TBD: Describe key data flows] - ---- - -## Dependencies - -| Package | Purpose | Version | -|---------|---------|---------| -| [package] | [purpose] | [version] | - ---- - -*This file describes component relationships. For implementation details, explore the source code.* -` - -const DECISIONS_TEMPLATE = `# Design Decisions — {{PROJECT_NAME}} - -> Key architectural and design decisions. Update when decisions are made or revisited. -> -> Last reviewed: {{DATE}} - ---- - -## Decision Index - -| ID | Decision | Status | Date | -|----|----------|--------|------| -{{#DECISIONS}} -| ADR-XXX | {{title}} | Active | {{DATE}} | -{{/DECISIONS}} - ---- - -{{#DECISIONS}} -## ADR-XXX: {{title}} - -**Context**: {{context}} - -**Decision**: {{decision}} - -**Rationale**: {{rationale}} - -**Trade-offs**: -- (+) [Benefit] -- (-) [Cost] - ---- - -{{/DECISIONS}} - -## Template for New Decisions - -\`\`\`markdown -## ADR-XXX: [Short Title] - -**Context**: [What is the issue?] - -**Decision**: [What did we decide?] - -**Rationale**: [Why this choice?] - -**Trade-offs**: -- (+) [Benefit] -- (-) [Cost] -\`\`\` - ---- - -*This file captures decisions that aren't obvious from code.* -` - -const DYNAMICS_TEMPLATE = `# Dynamics — Active Issues & Constraints - -> **Last updated:** {{DATE}} -> **Stability:** Dynamic — Update as issues arise/resolve - ---- - -## ⚡ Quick Scan - -| Status | Issue | Impact | Workaround | -|--------|-------|--------|------------| -{{#ISSUES}} -| {{statusIcon}} | {{title}} | {{impact}} | {{workaround}} | -{{/ISSUES}} - ---- - -## 🔴 Active Issues - -{{#ISSUES}} -{{#isActive}} -### {{title}} - -**What:** [Description] - -**Impact:** {{impact}} - -**Workaround:** {{workaround}} - ---- - -{{/isActive}} -{{/ISSUES}} - -## 🟡 Known Constraints - -[TBD: Add known constraints that affect development] - ---- - -## 🟢 Recently Resolved - -| Issue | Resolution | Date | -|-------|------------|------| -| [issue] | [resolution] | [date] | - ---- - -## 📋 Under Consideration - -[TBD: Items being evaluated] - ---- - -*Remember: This file changes frequently. Verify against current code state.* -` - -const MAINTENANCE_TEMPLATE = `# AI Context Maintenance Guide - -> How to keep this knowledge base accurate. Last updated: {{DATE}}. - ---- - -## 🎯 Purpose - -This guide tells you (the coding agent) how to maintain \`.ai-context\`. - ---- - -## 📋 Maintenance Triggers - -| Observation | Action | File | -|-------------|--------|------| -| Code contradicts docs | Fix docs | Relevant file | -| New component | Add entry | ARCHITECTURE.md | -| Major decision | Document | DECISIONS.md | -| Blocking issue | Add entry | DYNAMICS.md | -| Issue resolved | Remove | DYNAMICS.md | - ---- - -## ✍️ Writing Guidelines - -### PROJECT-ESSENCE.md -- Under 100 lines -- "What" and "why", not "how" -- No code snippets -- Update: Quarterly - -### ARCHITECTURE.md -- Diagrams over paragraphs -- Component-level, not file-level -- Show data flow -- Update: Monthly - -### DECISIONS.md -- Non-obvious choices only -- Include rationale -- Update: As decisions made - -### DYNAMICS.md -- Current issues only -- Remove when resolved -- Update: As needed - ---- - -## 🔄 Update Workflow - -1. Identify file needing update -2. Read current content -3. Make minimal changes -4. Update "Last updated" date -5. Continue your task - ---- - -## ❌ Anti-Patterns - -- Don't copy code snippets -- Don't document every file -- Don't keep resolved issues -- Don't duplicate across files - ---- - -## ✅ Quality Checklist - -- [ ] PROJECT-ESSENCE.md readable in 2 min -- [ ] ARCHITECTURE.md shows big picture -- [ ] DECISIONS.md has rationale -- [ ] DYNAMICS.md only current issues -- [ ] All files dated - ---- - -*Update this guide when you discover better maintenance patterns.* -` - -// ============================================================================= -// Project Analysis -// ============================================================================= - -async function analyzeProject(projectPath: string): Promise { - const packageJsonPath = path.join(projectPath, 'package.json') - const readmePath = path.join(projectPath, 'README.md') - const agentsPath = path.join(projectPath, 'AGENTS.md') - - let projectName = path.basename(projectPath) - let description = '' - - // Read package.json if exists - if (fs.existsSync(packageJsonPath)) { - const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')) - projectName = pkg.name || projectName - description = pkg.description || '' - } - - // Read README if exists - let readme = '' - if (fs.existsSync(readmePath)) { - readme = fs.readFileSync(readmePath, 'utf-8') - if (!description) { - // Extract first paragraph as description - const match = readme.match(/^#\s.*\n+([^#\n].*?)(?:\n\n|$)/m) - if (match) { - description = match[1].trim() - } - } - } - - // Read AGENTS.md if exists - let agents = '' - if (fs.existsSync(agentsPath)) { - agents = fs.readFileSync(agentsPath, 'utf-8') - } - - // Scan directory structure - const components = scanComponents(projectPath) - - return { - name: projectName, - description: description || 'A software project', - problem: '[TBD: What problem does this project solve?]', - solution: '[TBD: How does this project solve it?]', - targetUsers: [{ user: 'Developer', useCase: 'Building software' }], - components, - decisions: [], - issues: [], - constraints: [] - } -} - -function scanComponents(projectPath: string): ProjectInfo['components'] { - const components: ProjectInfo['components'] = [] - - const entries = fs.readdirSync(projectPath, { withFileTypes: true }) - - for (const entry of entries) { - if (entry.isDirectory() && !entry.name.startsWith('.') && !entry.name.startsWith('node_modules')) { - const subPath = path.join(projectPath, entry.name) - - // Check if it's a subproject (has package.json) - const subPackageJson = path.join(subPath, 'package.json') - if (fs.existsSync(subPackageJson)) { - const pkg = JSON.parse(fs.readFileSync(subPackageJson, 'utf-8')) - components.push({ - name: entry.name, - purpose: pkg.description || 'Subproject', - entry: path.join(entry.name, 'index.ts') - }) - } else { - // Regular directory - check for src - const srcPath = path.join(subPath, 'src') - if (fs.existsSync(srcPath)) { - components.push({ - name: entry.name, - purpose: `${entry.name} module` - }) - } - } - } - } - - return components -} - -// ============================================================================= -// Generator Functions -// ============================================================================= - -function generateSkillMd(info: ProjectInfo): string { - const date = new Date().toISOString().split('T')[0] - return renderTemplate(SKILL_TEMPLATE, { - PROJECT_NAME: info.name, - DATE: date - }) -} - -function generateProjectEssence(info: ProjectInfo): string { - const date = new Date().toISOString().split('T')[0] - - let targetUsersStr = '' - for (const tu of info.targetUsers) { - targetUsersStr += `| ${tu.user} | ${tu.useCase} |\n` - } - - let constraintsStr = '' - for (let i = 0; i < info.constraints.length; i++) { - constraintsStr += `${i + 1}. **${info.constraints[i]}**\n` - } - if (!constraintsStr) { - constraintsStr = '1. **[TBD: Add key constraints]**\n' - } - - let template = ESSENCE_TEMPLATE - template = template.replace('{{PROJECT_NAME}}', info.name) - template = template.replace('{{DATE}}', date) - template = template.replace('{{PROJECT_DESCRIPTION}}', info.description) - template = template.replace('{{PROJECT_PROBLEM}}', info.problem) - template = template.replace('{{PROJECT_SOLUTION}}', info.solution) - template = template.replace('{{#TARGET_USERS}}\n| {{user}} | {{useCase}} |\n{{/TARGET_USERS}}', targetUsersStr) - template = template.replace('{{PROJECT_CONSTRAINTS}}', constraintsStr) - - return template -} - -function generateArchitecture(info: ProjectInfo): string { - const date = new Date().toISOString().split('T')[0] - - let componentsStr = '' - for (const comp of info.components) { - componentsStr += `### ${comp.name}\n` - componentsStr += `- **Purpose:** ${comp.purpose}\n` - if (comp.entry) { - componentsStr += `- **Entry:** \`${comp.entry}\`\n` - } - componentsStr += '\n' - } - - let template = ARCHITECTURE_TEMPLATE - template = template.replace('{{PROJECT_NAME}}', info.name) - template = template.replace('{{DATE}}', date) - - // Handle component block - const compBlockMatch = template.match(/{{#COMPONENTS}}[\s\S]*?{{\/COMPONENTS}}/) - if (compBlockMatch) { - template = template.replace(compBlockMatch[0], componentsStr) - } - - return template -} - -function generateDecisions(info: ProjectInfo): string { - const date = new Date().toISOString().split('T')[0] - - let decisionsStr = '' - let decisionBlocks = '' - - for (const dec of info.decisions) { - decisionsStr += `| ADR-XXX | ${dec.title} | Active | ${date} |\n` - decisionBlocks += `## ADR-XXX: ${dec.title}\n\n` - decisionBlocks += `**Context**: ${dec.context}\n\n` - decisionBlocks += `**Decision**: ${dec.decision}\n\n` - decisionBlocks += `**Rationale**: ${dec.rationale}\n\n` - decisionBlocks += '**Trade-offs**:\n- (+) [Benefit]\n- (-) [Cost]\n\n---\n\n' - } - - if (!decisionsStr) { - decisionsStr = '| ADR-001 | [First decision title] | Active | YYYY-MM |\n' - decisionBlocks = `## ADR-001: [Decision Title] - -**Context**: [What is the issue?] - -**Decision**: [What did we decide?] - -**Rationale**: [Why this choice?] - -**Trade-offs**: -- (+) [Benefit] -- (-) [Cost] - ---- - -` - } - - let template = DECISIONS_TEMPLATE - template = template.replace('{{PROJECT_NAME}}', info.name) - template = template.replace('{{DATE}}', date) - - // Handle index block - const indexBlockMatch = template.match(/{{#DECISIONS}}[\s\S]*?{{\/DECISIONS}}/) - if (indexBlockMatch) { - // For index, just use the title - template = template.replace(indexBlockMatch[0], decisionsStr) - } - - // Handle decision blocks - need to re-match after first replacement - const blockMatch = template.match(/{{#DECISIONS}}[\s\S]*?{{\/DECISIONS}}/) - if (blockMatch) { - template = template.replace(blockMatch[0], decisionBlocks) - } - - return template -} - -function generateDynamics(info: ProjectInfo): string { - const date = new Date().toISOString().split('T')[0] - - let issuesStr = '' - let activeStr = '' - - for (const issue of info.issues) { - const icon = issue.status === 'active' ? '🔴' : '🟡' - issuesStr += `| ${icon} | ${issue.title} | ${issue.impact} | ${issue.workaround} |\n` - - if (issue.status === 'active') { - activeStr += `### ${issue.title}\n\n` - activeStr += `**What:** [Description]\n\n` - activeStr += `**Impact:** ${issue.impact}\n\n` - activeStr += `**Workaround:** ${issue.workaround}\n\n---\n\n` - } - } - - if (!issuesStr) { - issuesStr = '| 🔴 | [No active issues documented] | — | — |\n' - } - - let template = DYNAMICS_TEMPLATE - template = template.replace('{{DATE}}', date) - - // Handle quick scan block - const scanBlockMatch = template.match(/{{#ISSUES}}[\s\S]*?{{\/ISSUES}}/) - if (scanBlockMatch) { - template = template.replace(scanBlockMatch[0], issuesStr) - } - - return template -} - -function generateMaintenance(info: ProjectInfo): string { - const date = new Date().toISOString().split('T')[0] - return renderTemplate(MAINTENANCE_TEMPLATE, { - DATE: date - }) -} - -// ============================================================================= -// Main Generator -// ============================================================================= - -async function generate(projectPath: string): Promise { - console.log(`\n🚀 Generating .ai-context for: ${projectPath}\n`) - - // Analyze project - console.log('📊 Analyzing project...') - const info = await analyzeProject(projectPath) - console.log(` Found: ${info.name}`) - console.log(` Components: ${info.components.length}`) - - // Create directory structure - const aiContextPath = path.join(projectPath, '.ai-context') - const referencesPath = path.join(aiContextPath, 'references') - const metaPath = path.join(aiContextPath, 'meta') - - console.log('\n📁 Creating directory structure...') - fs.mkdirSync(aiContextPath, { recursive: true }) - fs.mkdirSync(referencesPath, { recursive: true }) - fs.mkdirSync(metaPath, { recursive: true }) - - // Generate files - console.log('\n📝 Generating files...') - - // SKILL.md - const skillContent = generateSkillMd(info) - fs.writeFileSync(path.join(aiContextPath, 'SKILL.md'), skillContent) - console.log(' ✓ SKILL.md') - - // PROJECT-ESSENCE.md - const essenceContent = generateProjectEssence(info) - fs.writeFileSync(path.join(referencesPath, 'PROJECT-ESSENCE.md'), essenceContent) - console.log(' ✓ references/PROJECT-ESSENCE.md') - - // ARCHITECTURE.md - const archContent = generateArchitecture(info) - fs.writeFileSync(path.join(referencesPath, 'ARCHITECTURE.md'), archContent) - console.log(' ✓ references/ARCHITECTURE.md') - - // DECISIONS.md - const decisionsContent = generateDecisions(info) - fs.writeFileSync(path.join(referencesPath, 'DECISIONS.md'), decisionsContent) - console.log(' ✓ references/DECISIONS.md') - - // DYNAMICS.md - const dynamicsContent = generateDynamics(info) - fs.writeFileSync(path.join(aiContextPath, 'DYNAMICS.md'), dynamicsContent) - console.log(' ✓ DYNAMICS.md') - - // MAINTENANCE.md - const maintenanceContent = generateMaintenance(info) - fs.writeFileSync(path.join(metaPath, 'MAINTENANCE.md'), maintenanceContent) - console.log(' ✓ meta/MAINTENANCE.md') - - console.log('\n✅ .ai-context generated successfully!') - console.log('\n📌 Next steps:') - console.log(' 1. Review generated files and fill in [TBD] placeholders') - console.log(' 2. Add your project-specific architecture details') - console.log(' 3. Document any non-obvious design decisions') - console.log(' 4. Add current issues to DYNAMICS.md if any\n') -} - -// ============================================================================= -// CLI Entry Point -// ============================================================================= - -const projectPath = process.argv[2] || process.cwd() -generate(projectPath).catch(console.error) diff --git a/.agents/skills/codegraph-skill/SKILL.md b/.agents/skills/codegraph-skill/SKILL.md new file mode 100644 index 0000000..c02c7ff --- /dev/null +++ b/.agents/skills/codegraph-skill/SKILL.md @@ -0,0 +1,99 @@ +--- +name: codegraph-skill +description: Use when a coding agent needs symbol relationships, callers, callees, or change impact. Guides CodeGraph CLI usage (not MCP). +version: 1.2.0 +--- + +# CodeGraph Skill + +[CodeGraph](https://colbymchenry.github.io/codegraph/) provides a pre-indexed **AST code graph** for this project. + +Terrain uses **CLI only** — do not run `codegraph install` (that configures MCP/agent rules separately). + +## Resolve the CodeGraph command (read first) + +Use **conventional paths only** — never machine-specific absolute paths like `/Users/...` or `C:\Users\...`. + +On Windows, the wrapper may be `codegraph.cmd` or `codegraph.exe` under `~/.terrain/bin/`. + +| Priority | Command | When | +|----------|---------|------| +| 1 | `~/.terrain/bin/codegraph` | Terrain env integration (see existence check below) | +| 2 | `bunx codegraph` | No Terrain install; needs network once | +| 3 | `npx codegraph` | Same as bunx if Bun unavailable | + +Optional manifest (local, gitignored): `.terrain/env/agent-tools.json` — same `~/.terrain/bin/…` conventions. + +**Existence check (cross-platform):** + +| Shell | Check | +|-------|-------| +| bash / zsh / Git Bash | `[ -x ~/.terrain/bin/codegraph ] \|\| [ -f ~/.terrain/bin/codegraph.exe ] \|\| [ -f ~/.terrain/bin/codegraph.cmd ]` | +| PowerShell | `Test-Path "$HOME\.terrain\bin\codegraph*"` | + +In examples below, `` means your resolved prefix (`~/.terrain/bin/codegraph` or `bunx codegraph`). + +## Prerequisites + +The **index** is per-repo under `.codegraph/`. If missing, initialize from the repo root: + +```bash +# bash / Git Bash +if [ -x ~/.terrain/bin/codegraph ] || [ -f ~/.terrain/bin/codegraph.exe ] || [ -f ~/.terrain/bin/codegraph.cmd ]; then + ~/.terrain/bin/codegraph init -i +else + bunx codegraph init -i +fi +``` + +Refresh after edits: ` sync` + +Health check: + +```bash + status +``` + +## CLI commands + +| Intent | Command | +|--------|---------| +| Find symbol by name | ` query ` | +| Who calls X | ` callers ` | +| What X calls | ` callees ` | +| Change blast radius | ` impact ` | +| Tests affected by file changes | ` affected ` | +| Project file tree | ` files` | +| Index health | ` status` | +| Refresh after edits | ` sync` | + +## Recommended workflow + +1. Read `.terrain/agent/context.md` (`terrain-knowledge-skill`) +2. ` query ` to locate definition +3. `callers` / `callees` / `impact` for relationship questions +4. `repomix-context-skill` for full source text of a specific file +5. Use **`rtk-skill`** for any follow-up shell commands (tests, git) + +## When to use vs other skills + +| Use CodeGraph | Use instead | +|---------------|-------------| +| Symbol lookup, call chains | Architecture → `context.md` | +| Impact before refactor | Business rules → `knowledge/` | +| File/symbol graph | Raw source slice → repomix | +| Verbose test/git output | ` cargo test`, ` git diff` | + +## Do not + +- Run `codegraph install` (Terrain manages AGENTS.md) +- Blind `grep` the whole repo to re-verify CodeGraph AST results +- Chain `query` + manual file reads when `impact` answers the question + +## Staleness + +If ` status` shows pending files after your edits: + +```bash + sync +``` diff --git a/.agents/skills/repomix-context-skill/SKILL.md b/.agents/skills/repomix-context-skill/SKILL.md new file mode 100644 index 0000000..03f4138 --- /dev/null +++ b/.agents/skills/repomix-context-skill/SKILL.md @@ -0,0 +1,84 @@ +--- +name: repomix-context-skill +description: Use when an agent needs source code from the local repomix index under .terrain/agent/repomix.md (not committed; regenerate via Terrain scan). +version: 1.3.0 +--- + +# Repomix Context Skill + +Terrain stores a **local repomix index** at `.terrain/agent/repomix.md` (gitignored, fast to regenerate). + +Read **architecture first** via `terrain-knowledge-skill` → `.terrain/agent/context.md`. + +## When to use + +- Implementation details after reading `context.md` +- Cross-file symbol search within the indexed snapshot +- Locating handlers, types, routes + +## Query strategy (mandatory) + +1. **Read meta** — `.terrain/agent/meta.json` (`total_tokens`, `synced_at`, `top_files_by_tokens`) +2. **Search the pack** — never load the entire file: + ```bash + # = ~/.terrain/bin/rtk or bunx @terrain-ai/rtk (see rtk-skill) + grep "struct ProjectOverview" .terrain/agent/repomix.md + grep "### src/lib/api.ts" .terrain/agent/repomix.md + ``` + Or agent Grep limited to that path with tight patterns. +3. **Read slices** — extract matching `### path/to/file` sections only: + ```bash + read .terrain/agent/repomix.md -l aggressive + ``` + Then read the specific `### file` block (line range), ≤150 lines per read. +4. **Refresh** — if `meta.json.synced_at` is stale, ask user to run Terrain **重建源码索引** / scan + +## Repomix section format + +Sections look like: + +```markdown +### src/lib/foo.ts + +\`\`\`typescript +... file content ... +\`\`\` +``` + +Grep for `### relative/path` to jump to a file. + +## Paths + +| File | Purpose | +|------|---------| +| `.terrain/agent/repomix.md` | Full indexed snapshot (local only) | +| `.terrain/agent/meta.json` | Pack metrics | +| `.terrain/agent/context.md` | Architecture (read first) | + +## Do not + +- Commit or assume `repomix.md` exists in git +- `cat` / Read the entire `repomix.md` (can be 100k+ tokens) +- Read the live repository tree when the pack covers the question +- Skip `context.md` and grep source for architecture questions + +## Regenerate index + +If `repomix.md` is missing: + +```bash +# bash / Git Bash — = ~/.terrain/bin/terrain or bunx @terrain-ai/cli +if [ -x ~/.terrain/bin/terrain ] || [ -x ~/.terrain/bin/terrain.exe ]; then + ~/.terrain/bin/terrain assets pack-agent . +else + bunx @terrain-ai/cli assets pack-agent . +fi +# or from repo root in Terrain UI: 重建源码索引 +``` + +If neither `~/.terrain/bin/terrain` nor `bunx @terrain-ai/cli` works, ask the user to install Terrain or run **重建源码索引** from the desktop app. + +## Related skills + +- **codegraph-skill** — symbol relationships (prefer before wide repomix grep) +- **rtk-skill** — resolve `` prefix for grep/read on the pack file diff --git a/.agents/skills/rtk-skill/SKILL.md b/.agents/skills/rtk-skill/SKILL.md new file mode 100644 index 0000000..c1d0ef9 --- /dev/null +++ b/.agents/skills/rtk-skill/SKILL.md @@ -0,0 +1,213 @@ +--- +name: rtk-skill +description: Use when running shell commands that produce verbose output (git, test, build, lint, package managers, docker). Prefix with rtk to save 60-90% tokens. Terrain projects use explicit rtk prefix (no global hook). +version: 1.2.0 +--- + +# RTK Skill (Rust Token Killer) + +[RTK](https://github.com/rtk-ai/rtk) is a CLI proxy that **filters and compresses command output** before it reaches the LLM (typically **60–90% token savings**). + +## Resolve the RTK command (read first) + +Use **conventional paths only** — never machine-specific absolute paths like `/Users/...` or `C:\Users\...`. + +On Windows, tools deploy to `%USERPROFILE%\.terrain\bin\` (also written as `~/.terrain/bin/` in Git Bash / PowerShell 7+). Binaries use `.exe` extensions; PATHEXT resolves `rtk` → `rtk.exe`. + +| Priority | Command prefix | When | +|----------|----------------|------| +| 1 | `~/.terrain/bin/rtk` | Terrain env integration or desktop app (see existence check below) | +| 2 | `bunx @terrain-ai/rtk` | No Terrain install; needs network once | +| 3 | `npx @terrain-ai/rtk` | Same as bunx if Bun unavailable | + +Optional manifest (local, gitignored): `.terrain/env/agent-tools.json` — same `~/.terrain/bin/…` conventions. + +**Existence check (cross-platform):** + +| Shell | Check | +|-------|-------| +| bash / zsh / Git Bash | `[ -x ~/.terrain/bin/rtk ] \|\| [ -x ~/.terrain/bin/rtk.exe ]` | +| PowerShell | `Test-Path "$HOME\.terrain\bin\rtk.exe"` | +| cmd | `if exist "%USERPROFILE%\.terrain\bin\rtk.exe"` | + +**Shell rules:** + +- Invoke as `~/.terrain/bin/rtk ` — tilde expands at word start in bash/zsh/Git Bash/PowerShell 7+. +- Do **not** `export RTK="$(jq -r .rtk …)"` then `"$RTK"` — quoted variables do not expand `~`. +- Do **not** assume bare `rtk` is on PATH. + +Example (pick one prefix per session after the existence check): + +```bash +# bash / Git Bash +if [ -x ~/.terrain/bin/rtk ] || [ -x ~/.terrain/bin/rtk.exe ]; then + PREFIX=~/.terrain/bin/rtk +else + PREFIX="bunx @terrain-ai/rtk" +fi +$PREFIX git status +``` + +```powershell +# PowerShell +$PREFIX = if (Test-Path "$HOME\.terrain\bin\rtk.exe") { "$HOME\.terrain\bin\rtk.exe" } else { "bunx @terrain-ai/rtk" } +& $PREFIX git status +``` + +Verify: + +```bash +~/.terrain/bin/rtk gain || bunx @terrain-ai/rtk gain +``` + +We do **not** run `rtk init` / global hooks. + +## Golden rule + +> For **any shell command** that prints more than a few lines, run it as **` `** instead of bare ``. + +Applies to: git, tests, builds, linters, package managers, docker/kubectl, `ls`/`grep`/`find`, `gh`, etc. + +In examples below, `` means your resolved prefix (`~/.terrain/bin/rtk` or `bunx @terrain-ai/rtk`). + +## Critical: built-in Read / Grep / Glob + +Claude Code, Cursor, and similar agents often have **native Read/Grep tools that bypass Bash hooks**. + +Those tools **do not** auto-rewrite to RTK. For token-efficient file/search workflows, use: + +| Instead of native tool | Use | +|------------------------|-----| +| Read large source file | ` read path/to/file.rs` | +| Read signatures only | ` read path/to/file.rs -l aggressive` | +| Grep / search repo | ` grep "pattern" .` or ` rg "pattern"` | +| Find files | ` find "*.ts" .` | +| List directory | ` ls .` | + +**Exception:** `.terrain/agent/context.md` and short `knowledge/*.md` — read directly (already dense). + +## Command reference (by category) + +### Git (high savings) + +```bash + git status + git log -n 20 --oneline + git diff + git diff --staged + add -A && git commit -m "msg" + git push + git pull +``` + +### Tests (failures-focused, ~90% savings) + +```bash + cargo test + test cargo test + bun test + vitest + jest + pytest + go test +``` + +### Build & lint + +```bash + cargo build + cargo clippy + tsc + eslint . + ruff check + next build +``` + +### Package managers + +```bash + pnpm list + bun install + pip list +``` + +### Files & search + +```bash + ls src/ + read src/lib/foo.rs + read src/lib/foo.rs -l aggressive + grep "fn handle_" . + find "*.svelte" . + diff file1 file2 +``` + +### Containers / cloud (when used) + +```bash + docker ps + docker logs + kubectl get pods + gh pr list +``` + +### Errors only + +```bash + err npm run build +``` + +## Global flags + +```bash + -u git status + -v cargo test +``` + +## When command fails + +RTK may collapse output but preserves **exit codes**. On failure, look for a tee path in output: + +``` +FAILED: 2/15 tests +[full output: ~/.local/share/rtk/tee/....log] +``` + +Read that log if you need the full unfiltered output — do not blindly re-run the same verbose command. + +## Bypass RTK (rare) + +```bash +RTK_DISABLED=1 git status +``` + +## When NOT to use RTK + +| Use RTK | Use other Terrain skills instead | +|---------|----------------------------------| +| Shell command output | Architecture → `terrain-knowledge-skill` / `context.md` | +| git test build lint | Symbol relations → `codegraph-skill` | +| ` read` for code slices | Structured repomix index → `repomix-context-skill` | + +**Workflow order:** Terrain knowledge → codegraph → repomix slices → **rtk** for remaining shell work. + +## Analytics (optional) + +```bash + gain + gain --history + discover +``` + +## Do not + +- Run `rtk init` or `rtk init -g` — Terrain owns agent guidance via AGENTS.md +- Assume hooks rewrite commands — **you** must type the rtk prefix +- Re-run identical verbose commands after RTK already summarized them +- Use wrong rtk package (verify with ` gain`) + +## Complements + +- **terrain-knowledge-skill** — what project knowledge to read first +- **codegraph-skill** — AST/call-graph queries +- **repomix-context-skill** — grep repomix pack for source sections diff --git a/.agents/skills/terrain-knowledge-skill/SKILL.md b/.agents/skills/terrain-knowledge-skill/SKILL.md new file mode 100644 index 0000000..92846a0 --- /dev/null +++ b/.agents/skills/terrain-knowledge-skill/SKILL.md @@ -0,0 +1,69 @@ +--- +name: terrain-knowledge-skill +description: Use when a coding agent needs project knowledge from Terrain .terrain/ assets. Guides layered reading of context, private knowledge, and repomix index. +version: 1.1.0 +--- + +# Terrain Knowledge Skill + +Terrain stores **AI knowledge assets** under **`.terrain/`** in this repository (not a global `~/.terrain/knowledge` directory). The desktop app registry at `~/.terrain/registry.json` only maps slugs → repo paths. + +Load **`rtk-skill`** when you need to run shell commands during investigation (git, grep repomix file, tests). Resolve tools via **conventional paths** (`~/.terrain/bin/…`) with `bunx`/`npx` fallback — see `rtk-skill` / `codegraph-skill`. + +## Knowledge layers (mandatory order) + +1. **Architecture** — `.terrain/agent/context.md` + - Module map, core flows, system boundaries, tech stack + - Check `.terrain/agent/context-meta.json` or `meta.json` for freshness + - Check `.terrain/.meta/freshness.json` for drift score before trusting architecture context + - Read directly (short); no RTK needed + +2. **Private domain** — `.terrain/knowledge/**/*.md` + - Business glossary, internal frameworks, APIs, scaffolding guides + - Team-maintained markdown; read in filename sort order when surveying + +3. **Structured meta** — `.terrain/agent/meta-inputs.md` + - Compiled from `terrain-meta.json` and `knowledge/` scans + +4. **Source index** — see `repomix-context-skill` + - Local `.terrain/agent/repomix.md` (gitignored; regenerate via Terrain scan) + +## Query workflow + +``` +Task received + → Read context.md (or relevant section) + → If business/internal terms → read knowledge/*.md + → If symbol / call graph → codegraph-skill + → If implementation / source → repomix-context-skill + → If shell/git/test needed → rtk-skill (prefix with rtk) +``` + +## Rules + +- **Do not** invent module names that contradict `context.md` or `meta-inputs.md` +- **Do not** read the entire live repository tree when indexed assets exist +- **Do not** load full `repomix.md` into context — grep slices only (`rtk grep` on the file is OK) +- Prefer `.terrain/` over guessing project structure + +## Private knowledge directory + +`.terrain/knowledge/` — developers add markdown here; Terrain scans on context generation. + +Example files: +- `00-glossary.md` — domain terms +- `10-internal-framework.md` — internal libs +- `20-api-usage.md` — internal APIs +- `30-scaffolding.md` — project generators + +## Human docs (optional) + +`.terrain/human/` — Litho-generated docs for humans; useful for onboarding context but denser than `context.md`. + +## Related skills + +| Skill | When | +|-------|------| +| `repomix-context-skill` | Source code from repomix index | +| `codegraph-skill` | Callers, callees, impact | +| `rtk-skill` | All verbose shell commands | diff --git a/.terrain/.meta/freshness.json b/.terrain/.meta/freshness.json new file mode 100644 index 0000000..306cabe --- /dev/null +++ b/.terrain/.meta/freshness.json @@ -0,0 +1,40 @@ +{ + "context.md": { + "created_at": "2026-07-04T02:35:00.000000000+00:00", + "updated_at": "2026-07-04T02:35:00.000000000+00:00", + "drift_score": 0.0, + "version": 1 + }, + "meta-inputs.md": { + "created_at": "2026-07-04T02:35:00.000000000+00:00", + "updated_at": "2026-07-04T02:35:00.000000000+00:00", + "drift_score": 0.0, + "version": 1 + }, + "knowledge": { + "00-glossary.md": { + "created_at": "2026-07-04T02:35:00.000000000+00:00", + "updated_at": "2026-07-04T02:35:00.000000000+00:00", + "drift_score": 0.0, + "version": 1 + }, + "10-internal-framework.md": { + "created_at": "2026-07-04T02:35:00.000000000+00:00", + "updated_at": "2026-07-04T02:35:00.000000000+00:00", + "drift_score": 0.0, + "version": 1 + }, + "20-api-usage.md": { + "created_at": "2026-07-04T02:35:00.000000000+00:00", + "updated_at": "2026-07-04T02:35:00.000000000+00:00", + "drift_score": 0.0, + "version": 1 + }, + "30-scaffolding.md": { + "created_at": "2026-07-04T02:35:00.000000000+00:00", + "updated_at": "2026-07-04T02:35:00.000000000+00:00", + "drift_score": 0.0, + "version": 1 + } + } +} \ No newline at end of file diff --git a/.terrain/.meta/sync.json b/.terrain/.meta/sync.json new file mode 100644 index 0000000..9b76131 --- /dev/null +++ b/.terrain/.meta/sync.json @@ -0,0 +1,9 @@ +{ + "project": "cowork-forge", + "repo_path": ".", + "synced_at": "2026-07-04T02:30:40.995935300+00:00", + "collectors": [ + "git", + "repomix" + ] +} \ No newline at end of file diff --git a/.terrain/agent/context.md b/.terrain/agent/context.md new file mode 100644 index 0000000..d90d96a --- /dev/null +++ b/.terrain/agent/context.md @@ -0,0 +1,88 @@ +# Cowork Forge - Architecture Context + +## Project Overview + +**Cowork Forge** is an AI-native multi-agent software development platform. It orchestrates specialized AI agents through a 7-stage pipeline to transform ideas into production-ready software. + +| Aspect | Detail | +|--------|--------| +| Language | Rust (edition 2024) | +| Agent Framework | adk-rust 0.5.0 | +| GUI | Tauri + React 18 + Ant Design | +| Architecture | Hexagonal + DDD | +| License | MIT | + +## Module Map + +``` +crates/ +├── cowork-core/ # Domain logic, pipeline, tools, agents (MAIN crate) +│ └── src/ +│ ├── pipeline/ # 7-stage orchestration & stage executor +│ ├── domain/ # Project, Iteration, Memory aggregates +│ ├── tools/ # 40+ ADK tools + MCP integration +│ ├── agents/ # Agent wrappers (iterative, PM, legacy analyzer) +│ ├── interaction/ # InteractiveBackend trait (CLI/GUI abstraction) +│ ├── acp/ # Agent Client Protocol for external agents +│ ├── config_definition/ # Data-driven config (agents, stages, flows) +│ ├── instructions/ # Agent prompt library +│ ├── skills/ # agentskills.io standard skill system +│ ├── integration/ # Hook manager for external integrations +│ └── persistence/ # JSON-based storage +├── cowork-cli/ # CLI adapter (clap + dialoguer) +└── cowork-gui/ # Tauri + React GUI + ├── src-tauri/ # Rust backend (Tauri commands + events) + └── src/ # React frontend (TypeScript + Ant Design) +``` + +## Core Flows + +### 7-Stage Pipeline + +1. **Idea** - Transform raw ideas into structured concepts +2. **PRD** - Generate Product Requirements Documents +3. **Design** - Create system architecture and design +4. **Plan** - Develop implementation plans +5. **Coding** - Generate and refine code +6. **Check** - Validate and test implementations +7. **Delivery** - Package and deploy solutions + +### Agent Orchestration + +- Each stage uses specialized agents (Actor-Critic pattern) +- Agents self-iterate until quality thresholds are met +- Human-in-the-loop (HITL) for critical decisions + +## System Boundaries + +- **Workspace**: All file operations validated against workspace boundaries +- **LLM Integration**: Rate-limited (30 req/min) with global semaphore +- **External Agents**: ACP protocol for external agent integration +- **Persistence**: JSON-based storage for projects and iterations + +## Key Patterns + +| Pattern | Where | Purpose | +|---------|-------|---------| +| Actor-Critic | PRD, Design, Plan, Coding stages | Iterative self-refinement | +| Strategy | Stage trait implementations | Pluggable stage behavior | +| Template Method | Pipeline execution flow | Fixed stage sequence with hooks | +| Repository | Persistence stores | Abstract data access | +| Decorator | LLM rate limiting | Transparent cross-cutting concern | + +## Tech Stack + +- **Backend**: Rust with Tokio async runtime +- **Frontend**: React 18 + TypeScript + Ant Design +- **Desktop**: Tauri framework +- **Agent Framework**: adk-rust 0.5.0 +- **Serialization**: serde with derive macros +- **Error Handling**: anyhow::Result (no unwrap in production) + +## Security Model + +- Path validation for all file operations +- Command sanitization (dangerous commands blocked) +- LLM rate limiting with concurrency control +- Workspace containment for file operations +- No secrets in code (API keys from config/env) \ No newline at end of file diff --git a/.terrain/agent/meta-inputs.md b/.terrain/agent/meta-inputs.md new file mode 100644 index 0000000..4e376c2 --- /dev/null +++ b/.terrain/agent/meta-inputs.md @@ -0,0 +1,173 @@ +# Cowork Forge - Meta Inputs + +## Project Metadata + +### Basic Information +- **Project Name**: Cowork Forge +- **Repository Path**: . +- **Language**: Rust (edition 2024) +- **Framework**: adk-rust 0.5.0 +- **GUI**: Tauri + React 18 + Ant Design +- **Architecture**: Hexagonal + DDD +- **License**: MIT + +### Repository Statistics +- **Total Files**: 262 +- **Total Tokens**: 401,973 +- **Total Characters**: 1,503,951 +- **Last Synced**: 2026-07-04T02:30:40.995935300+00:00 +- **Baseline Git Head**: 0063d857ce13b366ae440fa1073a0b61cf14ebd0 + +## Module Structure + +### Core Modules +1. **cowork-core** - Domain logic, pipeline, tools, agents +2. **cowork-cli** - CLI adapter (clap + dialoguer) +3. **cowork-gui** - Tauri + React GUI + +### Cowork Core Submodules +- **pipeline/** - 7-stage orchestration & stage executor +- **domain/** - Project, Iteration, Memory aggregates +- **tools/** - 40+ ADK tools + MCP integration +- **agents/** - Agent wrappers (iterative, PM, legacy analyzer) +- **interaction/** - InteractiveBackend trait (CLI/GUI abstraction) +- **acp/** - Agent Client Protocol for external agents +- **config_definition/** - Data-driven config (agents, stages, flows) +- **instructions/** - Agent prompt library +- **skills/** - agentskills.io standard skill system +- **integration/** - Hook manager for external integrations +- **persistence/** - JSON-based storage + +## Key Files by Token Count + +### Documentation +1. `litho.docs/zh/2、架构概览.md` - 15,541 tokens +2. `litho.docs/en/2.Architecture.md` - 13,666 tokens +3. `litho.docs/zh/4、深入探索/4.8、Cowork GUI前端.md` - 8,452 tokens +4. `litho.docs/en/4.Deep-Exploration/GUI Frontend Domain.md` - 7,266 tokens +5. `litho.docs/zh/3、工作流程.md` - 7,061 tokens + +### Source Code +1. `crates/cowork-core/src/tools/data_tools.rs` - 8,207 tokens +2. `crates/cowork-core/src/tools/file_tools.rs` - 6,550 tokens +3. `crates/cowork-core/src/runtime_analyzer.rs` - 6,108 tokens +4. `crates/cowork-gui/src/components/config/AgentConfigForm.tsx` - 6,072 tokens +5. `crates/cowork-core/src/config_definition/registry.rs` - 5,960 tokens + +## Configuration Schema + +### Agent Configuration +- **id**: Unique identifier +- **name**: Human-readable name +- **description**: Agent purpose +- **model**: LLM model to use +- **temperature**: Generation temperature (0.0-1.0) +- **max_tokens**: Maximum tokens per generation +- **system_prompt**: System instructions +- **tools**: Available tool identifiers + +### Stage Configuration +- **id**: Unique identifier +- **name**: Human-readable name +- **description**: Stage purpose +- **agent**: Agent to use for this stage +- **max_iterations**: Maximum actor-critic cycles +- **quality_threshold**: Minimum quality score (0.0-1.0) +- **inputs**: Required input artifacts +- **outputs**: Generated output artifacts +- **next_stage**: Next stage in pipeline + +### Flow Configuration +- **id**: Unique identifier +- **name**: Human-readable name +- **description**: Flow purpose +- **stages**: Ordered list of stage IDs +- **transitions**: Stage transition rules + +## Domain Entities + +### Project +- **id**: UUID +- **name**: Project name +- **description**: Project description +- **created_at**: Creation timestamp +- **updated_at**: Last update timestamp +- **iterations**: List of iterations +- **memory**: Persistent knowledge + +### Iteration +- **id**: UUID +- **project_id**: Parent project ID +- **version**: Version number +- **created_at**: Creation timestamp +- **artifacts**: Generated artifacts +- **status**: Current status + +### Memory +- **id**: UUID +- **project_id**: Parent project ID +- **knowledge**: Persistent knowledge base +- **created_at**: Creation timestamp +- **updated_at**: Last update timestamp + +## Integration Points + +### External Systems +- **LLM Providers**: OpenAI-compatible endpoints +- **File System**: Workspace-contained operations +- **Git**: Version control integration +- **MCP**: Model Context Protocol for external tools + +### Protocols +- **ACP**: Agent Client Protocol for external agents +- **InteractiveBackend**: CLI/GUI abstraction +- **Tauri Events**: Real-time GUI communication + +## Security Model + +### Path Validation +- All file operations validated against workspace boundaries +- No access to paths outside project workspace +- Path traversal prevention + +### Command Sanitization +- Dangerous commands blocked (rm -rf, sudo, etc.) +- Command whitelist for allowed operations +- Input validation for all commands + +### LLM Rate Limiting +- Global semaphore (concurrency=1) +- 2-second delay between requests +- 30 requests per minute maximum + +## Development Patterns + +### Actor-Critic Pattern +- **Actor**: Generates initial output +- **Critic**: Reviews and suggests improvements +- **Iteration**: Continues until quality threshold met +- **Used In**: PRD, Design, Plan, Coding stages + +### Strategy Pattern +- Stage implementations are pluggable +- Each stage defines its own behavior +- Common interface via trait implementations + +### Template Method Pattern +- Pipeline execution flow is fixed +- Stage sequence with hooks for customization +- Consistent lifecycle across all stages + +## Error Handling + +### Pattern +- Always use `anyhow::Result` +- No `unwrap()` in production code +- Proper error propagation with `?` operator +- Context added with `.context()` method + +### Error Categories +- **Domain Errors**: Business logic violations +- **Infrastructure Errors**: I/O and external system failures +- **Configuration Errors**: Invalid settings or parameters +- **Security Errors**: Path validation or command sanitization failures \ No newline at end of file diff --git a/.terrain/agent/meta.json b/.terrain/agent/meta.json new file mode 100644 index 0000000..79e864b --- /dev/null +++ b/.terrain/agent/meta.json @@ -0,0 +1,95 @@ +{ + "project": "cowork-forge", + "repo_path": ".", + "generator": "repomix-core", + "pack_strategy": "architecture-context", + "output_file": "repomix.md", + "total_files": 262, + "total_tokens": 401973, + "total_characters": 1503951, + "top_files_by_tokens": [ + { + "path": "litho.docs/zh/2、架构概览.md", + "tokens": 15541 + }, + { + "path": "litho.docs/en/2.Architecture.md", + "tokens": 13666 + }, + { + "path": "litho.docs/zh/4、深入探索/4.8、Cowork GUI前端.md", + "tokens": 8452 + }, + { + "path": "crates/cowork-core/src/tools/data_tools.rs", + "tokens": 8207 + }, + { + "path": "litho.docs/zh/4、深入探索/4.7、Cowork CLI.md", + "tokens": 7396 + }, + { + "path": "litho.docs/en/4.Deep-Exploration/GUI Frontend Domain.md", + "tokens": 7266 + }, + { + "path": "litho.docs/zh/3、工作流程.md", + "tokens": 7061 + }, + { + "path": "litho.docs/en/4.Deep-Exploration/CLI Domain.md", + "tokens": 6985 + }, + { + "path": "crates/cowork-core/src/tools/file_tools.rs", + "tokens": 6550 + }, + { + "path": "litho.docs/zh/4、深入探索/4.2、流程调度.md", + "tokens": 6384 + }, + { + "path": "litho.docs/en/3.Workflow.md", + "tokens": 6287 + }, + { + "path": "crates/cowork-core/src/runtime_analyzer.rs", + "tokens": 6108 + }, + { + "path": "litho.docs/zh/4、深入探索/4.4、Agent工具系统.md", + "tokens": 6089 + }, + { + "path": "crates/cowork-gui/src/components/config/AgentConfigForm.tsx", + "tokens": 6072 + }, + { + "path": "litho.docs/en/4.Deep-Exploration/Tools Domain.md", + "tokens": 5965 + }, + { + "path": "crates/cowork-core/src/config_definition/registry.rs", + "tokens": 5960 + }, + { + "path": "litho.docs/en/4.Deep-Exploration/Pipeline Domain.md", + "tokens": 5935 + }, + { + "path": "litho.docs/zh/4、深入探索/4.9、Cowork GUI后端.md", + "tokens": 5696 + }, + { + "path": "litho.docs/zh/4、深入探索/4.1、领域实体.md", + "tokens": 5567 + }, + { + "path": "litho.docs/zh/4、深入探索/4.6、自迭代记忆系统.md", + "tokens": 5562 + } + ], + "directory_structure": "crates/\n cowork-cli/\n src/\n commands/\n config.rs\n continue_cmd.rs\n delete.rs\n import.rs\n init.rs\n iter.rs\n knowledge.rs\n list.rs\n mod.rs\n show.rs\n status.rs\n main.rs\n utils.rs\n Cargo.toml\n cowork-core/\n src/\n acp/\n client.rs\n mod.rs\n agents/\n external_coding_agent.rs\n iterative_assistant.rs\n legacy_project_analyzer.rs\n mod.rs\n config_definition/\n default_configs/\n agents/\n built-in/\n check_agent.json\n coding_actor.json\n coding_critic.json\n delivery_agent.json\n design_actor.json\n design_critic.json\n idea_agent.json\n knowledge_gen_agent.json\n plan_actor.json\n plan_critic.json\n pm_agent.json\n prd_actor.json\n prd_critic.json\n summary_agent.json\n flows/\n default.json\n stages/\n check.json\n coding.json\n delivery.json\n design.json\n idea.json\n plan.json\n prd.json\n agent_definition.rs\n builtin.rs\n flow_definition.rs\n integration_definition.rs\n mod.rs\n registry.rs\n stage_definition.rs\n validator.rs\n data/\n mod.rs\n models.rs\n domain/\n iteration.rs\n memory.rs\n mod.rs\n project.rs\n importer/\n artifact_generator.rs\n import_config.rs\n mod.rs\n project_analyzer.rs\n instructions/\n check.rs\n coding.rs\n delivery.rs\n design.rs\n idea.rs\n knowledge_gen.rs\n legacy_project_analyzer.rs\n mod.rs\n plan.rs\n prd.rs\n project_manager.rs\n summary.rs\n integration/\n USAGE_EXAMPLE.md\n adapters.rs\n hooks.rs\n mod.rs\n interaction/\n cli.rs\n mod.rs\n tauri.rs\n llm/\n mod.rs\n rate_limiter.rs\n persistence/\n iteration_data.rs\n iteration_store.rs\n memory_store.rs\n mod.rs\n project_store.rs\n pipeline/\n executor/\n interaction_ext.rs\n knowledge.rs\n mod.rs\n workspace.rs\n stages/\n check.rs\n coding.rs\n delivery.rs\n design.rs\n idea.rs\n mod.rs\n plan.rs\n prd.rs\n mod.rs\n stage_executor.rs\n skills/\n manager.rs\n mod.rs\n tools/\n artifact_tools.rs\n control_tools.rs\n data_tools.rs\n deployment_tools.rs\n file_tools.rs\n goto_stage_tool.rs\n hitl_content_tools.rs\n hitl_tools.rs\n knowledge_tools.rs\n legacy_project_analyzer_tools.rs\n load_artifacts.rs\n memory_tools.rs\n mod.rs\n pm_tools.rs\n test_lint_tools.rs\n validation_tools.rs\n config.rs\n lib.rs\n project_runtime.rs\n runtime_analyzer.rs\n runtime_security.rs\n tech_stack.rs\n Cargo.toml\n cowork-gui/\n src/\n components/\n chat/\n ChatPanel.tsx\n InputArea.tsx\n MessageList.tsx\n index.ts\n common/\n LoadingScreen.tsx\n MarkdownMessage.tsx\n StatusBadge.tsx\n index.ts\n config/\n AgentConfigForm.tsx\n AgentsSetupPanel.tsx\n FlowConfigPanel.tsx\n IntegrationConfig.tsx\n SkillManager.tsx\n index.ts\n iterations/\n CreateIterationModal.tsx\n InitProjectModal.tsx\n IterationDetailsModal.tsx\n index.ts\n onboarding/\n index.ts\n projects/\n CreateProjectModal.tsx\n EditProjectModal.tsx\n ImportProjectModal.tsx\n index.ts\n ArtifactsViewer.tsx\n CodeEditor.tsx\n CommandPalette.tsx\n IterationsPanel.tsx\n KnowledgePanel.tsx\n MemoryPanel.tsx\n PreviewPanel.tsx\n ProjectsPanel.tsx\n RunnerPanel.tsx\n constants/\n events.ts\n index.ts\n stages.ts\n status.ts\n hooks/\n index.ts\n useAppEvents.ts\n useAutoScroll.ts\n useChatInput.ts\n useIterationActions.ts\n useIterationEvents.ts\n useIterationsData.ts\n useLoading.ts\n useModal.ts\n usePMAgent.ts\n useProjectEvents.ts\n useProjectsData.ts\n useRefreshTrigger.ts\n useTauriEvent.ts\n stores/\n agentStore.ts\n configStore.ts\n index.ts\n projectStore.ts\n uiStore.ts\n styles/\n antd-overrides.css\n chat.css\n components.css\n global.css\n layout.css\n markdown.css\n theme.css\n types/\n agent.ts\n artifacts.ts\n chat.ts\n config.ts\n index.ts\n iteration.ts\n knowledge.ts\n project.ts\n registry.ts\n utils/\n errorHandler.ts\n index.ts\n App.tsx\n assets.d.ts\n main.tsx\n styles.css\n src-tauri/\n capabilities/\n default.json\n src/\n commands/\n file.rs\n import_cmd.rs\n memory.rs\n mod.rs\n path_utils.rs\n pm.rs\n preview.rs\n runner.rs\n system.rs\n template.rs\n gui_types.rs\n iteration_commands.rs\n lib.rs\n main.rs\n project_manager.rs\n project_runner.rs\n static_server.rs\n Cargo.toml\n build.rs\n tauri.conf.json\n README.md\n index.html\n package.json\n tsconfig.json\n tsconfig.node.json\n vite.config.js\nlitho.docs/\n context-aware/\n adr-guide.md\n maintenance.md\n methodology.md\n templates.md\n en/\n 4.Deep-Exploration/\n CLI Domain.md\n Domain Logic.md\n GUI Backend Domain.md\n GUI Frontend Domain.md\n Interaction Domain.md\n LLM Integration Domain.md\n Memory Domain.md\n Persistence Domain.md\n Pipeline Domain.md\n Tools Domain.md\n 1.Overview.md\n 2.Architecture.md\n 3.Workflow.md\n zh/\n 4、深入探索/\n 4.10 、LLM集成.md\n 4.1、领域实体.md\n 4.2、流程调度.md\n 4.3、HITL人机协同.md\n 4.4、Agent工具系统.md\n 4.5、Artifacts存储.md\n 4.6、自迭代记忆系统.md\n 4.7、Cowork CLI.md\n 4.8、Cowork GUI前端.md\n 4.9、Cowork GUI后端.md\n 1、项目概述.md\n 2、架构概览.md\n 3、工作流程.md\nAGENTS.md\nCargo.toml\nLICENSE", + "synced_at": "2026-07-04T02:30:40.992643800+00:00", + "baseline_git_head": "0063d857ce13b366ae440fa1073a0b61cf14ebd0" +} \ No newline at end of file diff --git a/.terrain/agent/repomix.md b/.terrain/agent/repomix.md new file mode 100644 index 0000000..c56c1f9 --- /dev/null +++ b/.terrain/agent/repomix.md @@ -0,0 +1,43172 @@ +# Repository Packed for AI Analysis + +This file contains the packed representation of the repository. + +## Purpose + +This file contains the packed representation of the repository. + +## File Format + +The content is organized as follows: +1. This header section contains metadata about the packing process. +2. This directory structure section shows the repository structure. +3. Multiple file entries, each consisting of: + - File path as a heading + - Full contents of the file in a code block + +## Custom Instructions + +Terrain Agent Source Pack (repomix-core / architecture-context) +Purpose: Indexed snapshot of project source code for Ask-mode retrieval. +Use grep_agent_pack and read_agent_pack_file on demand — never load this entire file into LLM context. +Auto-packed on first Ask when missing; use 重建源码索引 in the Terrain UI to refresh after large codebase changes. + + +## Directory Structure + +``` +crates/ + cowork-cli/ + src/ + commands/ + config.rs + continue_cmd.rs + delete.rs + import.rs + init.rs + iter.rs + knowledge.rs + list.rs + mod.rs + show.rs + status.rs + main.rs + utils.rs + Cargo.toml + cowork-core/ + src/ + acp/ + client.rs + mod.rs + agents/ + external_coding_agent.rs + iterative_assistant.rs + legacy_project_analyzer.rs + mod.rs + config_definition/ + default_configs/ + agents/ + built-in/ + check_agent.json + coding_actor.json + coding_critic.json + delivery_agent.json + design_actor.json + design_critic.json + idea_agent.json + knowledge_gen_agent.json + plan_actor.json + plan_critic.json + pm_agent.json + prd_actor.json + prd_critic.json + summary_agent.json + flows/ + default.json + stages/ + check.json + coding.json + delivery.json + design.json + idea.json + plan.json + prd.json + agent_definition.rs + builtin.rs + flow_definition.rs + integration_definition.rs + mod.rs + registry.rs + stage_definition.rs + validator.rs + data/ + mod.rs + models.rs + domain/ + iteration.rs + memory.rs + mod.rs + project.rs + importer/ + artifact_generator.rs + import_config.rs + mod.rs + project_analyzer.rs + instructions/ + check.rs + coding.rs + delivery.rs + design.rs + idea.rs + knowledge_gen.rs + legacy_project_analyzer.rs + mod.rs + plan.rs + prd.rs + project_manager.rs + summary.rs + integration/ + USAGE_EXAMPLE.md + adapters.rs + hooks.rs + mod.rs + interaction/ + cli.rs + mod.rs + tauri.rs + llm/ + mod.rs + rate_limiter.rs + persistence/ + iteration_data.rs + iteration_store.rs + memory_store.rs + mod.rs + project_store.rs + pipeline/ + executor/ + interaction_ext.rs + knowledge.rs + mod.rs + workspace.rs + stages/ + check.rs + coding.rs + delivery.rs + design.rs + idea.rs + mod.rs + plan.rs + prd.rs + mod.rs + stage_executor.rs + skills/ + manager.rs + mod.rs + tools/ + artifact_tools.rs + control_tools.rs + data_tools.rs + deployment_tools.rs + file_tools.rs + goto_stage_tool.rs + hitl_content_tools.rs + hitl_tools.rs + knowledge_tools.rs + legacy_project_analyzer_tools.rs + load_artifacts.rs + memory_tools.rs + mod.rs + pm_tools.rs + test_lint_tools.rs + validation_tools.rs + config.rs + lib.rs + project_runtime.rs + runtime_analyzer.rs + runtime_security.rs + tech_stack.rs + Cargo.toml + cowork-gui/ + src/ + components/ + chat/ + ChatPanel.tsx + InputArea.tsx + MessageList.tsx + index.ts + common/ + LoadingScreen.tsx + MarkdownMessage.tsx + StatusBadge.tsx + index.ts + config/ + AgentConfigForm.tsx + AgentsSetupPanel.tsx + FlowConfigPanel.tsx + IntegrationConfig.tsx + SkillManager.tsx + index.ts + iterations/ + CreateIterationModal.tsx + InitProjectModal.tsx + IterationDetailsModal.tsx + index.ts + onboarding/ + index.ts + projects/ + CreateProjectModal.tsx + EditProjectModal.tsx + ImportProjectModal.tsx + index.ts + ArtifactsViewer.tsx + CodeEditor.tsx + CommandPalette.tsx + IterationsPanel.tsx + KnowledgePanel.tsx + MemoryPanel.tsx + PreviewPanel.tsx + ProjectsPanel.tsx + RunnerPanel.tsx + constants/ + events.ts + index.ts + stages.ts + status.ts + hooks/ + index.ts + useAppEvents.ts + useAutoScroll.ts + useChatInput.ts + useIterationActions.ts + useIterationEvents.ts + useIterationsData.ts + useLoading.ts + useModal.ts + usePMAgent.ts + useProjectEvents.ts + useProjectsData.ts + useRefreshTrigger.ts + useTauriEvent.ts + stores/ + agentStore.ts + configStore.ts + index.ts + projectStore.ts + uiStore.ts + styles/ + antd-overrides.css + chat.css + components.css + global.css + layout.css + markdown.css + theme.css + types/ + agent.ts + artifacts.ts + chat.ts + config.ts + index.ts + iteration.ts + knowledge.ts + project.ts + registry.ts + utils/ + errorHandler.ts + index.ts + App.tsx + assets.d.ts + main.tsx + styles.css + src-tauri/ + capabilities/ + default.json + src/ + commands/ + file.rs + import_cmd.rs + memory.rs + mod.rs + path_utils.rs + pm.rs + preview.rs + runner.rs + system.rs + template.rs + gui_types.rs + iteration_commands.rs + lib.rs + main.rs + project_manager.rs + project_runner.rs + static_server.rs + Cargo.toml + build.rs + tauri.conf.json + README.md + index.html + package.json + tsconfig.json + tsconfig.node.json + vite.config.js +litho.docs/ + context-aware/ + adr-guide.md + maintenance.md + methodology.md + templates.md + en/ + 4.Deep-Exploration/ + CLI Domain.md + Domain Logic.md + GUI Backend Domain.md + GUI Frontend Domain.md + Interaction Domain.md + LLM Integration Domain.md + Memory Domain.md + Persistence Domain.md + Pipeline Domain.md + Tools Domain.md + 1.Overview.md + 2.Architecture.md + 3.Workflow.md + zh/ + 4、深入探索/ + 4.10 、LLM集成.md + 4.1、领域实体.md + 4.2、流程调度.md + 4.3、HITL人机协同.md + 4.4、Agent工具系统.md + 4.5、Artifacts存储.md + 4.6、自迭代记忆系统.md + 4.7、Cowork CLI.md + 4.8、Cowork GUI前端.md + 4.9、Cowork GUI后端.md + 1、项目概述.md + 2、架构概览.md + 3、工作流程.md +AGENTS.md +Cargo.toml +LICENSE +``` + +## Files + +### crates/cowork-gui/src-tauri/src/lib.rs (285 lines) + +``` +1: TauriBackend +2: ⋮---- +3: { +4: app_handle: tauri::AppHandle, +5: pending_requests: Arc>>>, +6: } +7: ⋮---- +8: TauriBackend +9: ⋮---- +10: { +11: pub fn new( +12: app_handle: tauri::AppHandle, +13: pending_requests: Arc>>>, +14: ) -> Self { +15: Self { +16: app_handle, +17: pending_requests, +18: } +19: } +20: } +21: ⋮---- +22: TauriBackend +23: ⋮---- +24: { +25: async fn show_message(&self, level: cowork_core::interaction::MessageLevel, content: String) { +26: +27: let _ = self.app_handle.emit("agent_event", serde_json::json!({ +28: "content": content, +29: "agent_name": "System", +30: "message_type": "normal", +31: "level": format!("{:?}", level) +32: })); +33: +34: +35: let _ = self.app_handle.emit("message", (format!("{:?}", level), content)); +36: } +37: +38: async fn show_message_with_context(&self, level: cowork_core::interaction::MessageLevel, content: String, context: MessageContext) { +39: +40: let message_type_str = match &context.message_type { +41: MessageType::Normal => "normal", +42: MessageType::Thinking => "thinking", +43: MessageType::ToolCall { .. } => "tool_call", +44: MessageType::ToolResult { .. } => "tool_result", +45: MessageType::Streaming { .. } => "streaming", +46: }; +47: +48: +49: let _ = self.app_handle.emit("agent_event", serde_json::json!({ +50: "content": content, +51: "agent_name": context.agent_name, +52: "message_type": message_type_str, +53: "stage_name": context.stage_name, +54: "level": format!("{:?}", level), +55: "details": &context.message_type +56: })); +57: } +58: +59: async fn send_streaming(&self, content: String, agent_name: &str, is_thinking: bool) { +60: +61: let _ = self.app_handle.emit("agent_streaming", serde_json::json!({ +62: "content": content, +63: "agent_name": agent_name, +64: "is_thinking": is_thinking +65: })); +66: } +67: +68: async fn send_tool_call(&self, tool_name: &str, arguments: &Value, agent_name: &str) { +69: +70: let _ = self.app_handle.emit("tool_call", serde_json::json!({ +71: "tool_name": tool_name, +72: "arguments": arguments, +73: "agent_name": agent_name +74: })); +75: } +76: +77: async fn send_tool_result(&self, tool_name: &str, result: &str, success: bool, agent_name: &str) { +78: +79: let _ = self.app_handle.emit("tool_result", serde_json::json!({ +80: "tool_name": tool_name, +81: "result": result, +82: "success": success, +83: "agent_name": agent_name +84: })); +85: } +86: +87: async fn request_input(&self, prompt: &str, options: Vec, _initial_content: Option) -> anyhow::Result { +88: use std::time::Duration; +89: +90: +91: let request_id = format!("req-{}", chrono::Utc::now().timestamp_millis()); +92: +93: println!("[HITL] Requesting input: {} (ID: {})", prompt, request_id); +94: println!("[HITL] Options: {:?}", options.iter().map(|o| &o.id).collect::>()); +95: +96: +97: let (tx, rx) = oneshot::channel(); +98: +99: +100: { +101: let mut pending = self.pending_requests.lock().map_err(|e| anyhow::anyhow!("Lock error: {}", e))?; +102: pending.insert(request_id.clone(), tx); +103: } +104: +105: +106: let _ = self.app_handle.emit("input_request", (request_id.clone(), prompt, options)); +107: println!("[HITL] Request sent to frontend"); +108: +109: +110: tokio::select! { +111: result = rx => { +112: match result { +113: Ok(response) => { +114: println!("[HITL] Received response: {:?}", response); +115: Ok(response) +116: }, +117: Err(_) => { +118: println!("[HITL] Request canceled"); +119: anyhow::bail!("Request canceled") +120: }, +121: } +122: } +123: _ = tokio::time::sleep(Duration::from_secs(3000)) => { +124: println!("[HITL] Request timeout after 3000 seconds"); +125: anyhow::bail!("Request timeout") +126: } +127: } +128: } +129: +130: async fn show_progress(&self, task_id: String, progress: cowork_core::interaction::ProgressInfo) { +131: let _ = self.app_handle.emit("progress", (task_id, progress)); +132: } +133: +134: async fn submit_response(&self, _request_id: String, _response: String) -> anyhow::Result<()> { +135: +136: Ok(()) +137: } +138: +139: +140: } +141: ⋮---- +142: AppState +143: ⋮---- +144: { +145: pub pending_requests: Arc>>>, +146: pub project_registry_manager: Arc>, +147: pub workspace_path: Arc>>, +148: +149: pub config_ready: Arc, +150: } +151: ⋮---- +152: AppState +153: ⋮---- +154: { +155: pub fn new() -> Result { +156: let project_registry_manager = ProjectRegistryManager::new() +157: .context("Failed to initialize project registry manager")?; +158: +159: Ok(Self { +160: pending_requests: Arc::new(Mutex::new(HashMap::new())), +161: project_registry_manager: Arc::new(Mutex::new(project_registry_manager)), +162: workspace_path: Arc::new(Mutex::new(None)), +163: config_ready: Arc::new(std::sync::atomic::AtomicBool::new(false)), +164: }) +165: } +166: } +167: ⋮---- +168: register_project +169: ⋮---- +170: ( +171: workspace_path: String, +172: name: String, +173: description: Option, +174: state: State<'_, AppState>, +175: ) +176: ⋮---- +177: get_all_projects +178: ⋮---- +179: ( +180: status: Option, +181: search: Option, +182: limit: Option, +183: state: State<'_, AppState>, +184: ) +185: ⋮---- +186: delete_project +187: ⋮---- +188: ( +189: project_id: String, +190: state: State<'_, AppState>, +191: ) +192: ⋮---- +193: update_project +194: ⋮---- +195: ( +196: project_id: String, +197: name: Option, +198: description: Option, +199: status: Option, +200: state: State<'_, AppState>, +201: ) +202: ⋮---- +203: open_project +204: ⋮---- +205: ( +206: project_id: String, +207: state: State<'_, AppState>, +208: ) +209: ⋮---- +210: auto_register_current_project +211: ⋮---- +212: ( +213: state: State<'_, AppState>, +214: ) +215: ⋮---- +216: set_workspace +217: ⋮---- +218: ( +219: workspace_path: String, +220: state: State<'_, AppState>, +221: window: Window, +222: ) +223: ⋮---- +224: reset_running_iterations +225: ⋮---- +226: () +227: ⋮---- +228: has_open_project +229: ⋮---- +230: ( +231: state: State<'_, AppState>, +232: ) +233: ⋮---- +234: is_config_ready +235: ⋮---- +236: ( +237: state: State<'_, AppState>, +238: ) +239: ⋮---- +240: open_project_in_current_window +241: ⋮---- +242: ( +243: project_id: String, +244: state: State<'_, AppState>, +245: window: Window, +246: ) +247: ⋮---- +248: get_workspace +249: ⋮---- +250: ( +251: state: State<'_, AppState>, +252: ) +253: ⋮---- +254: path_exists +255: ⋮---- +256: (path: String) +257: ⋮---- +258: CreateProjectResult +259: ⋮---- +260: { +261: project_id: String, +262: created_dir: bool, +263: } +264: ⋮---- +265: create_project_at_path +266: ⋮---- +267: ( +268: path: String, +269: name: String, +270: description: Option, +271: state: State<'_, AppState>, +272: ) +273: ⋮---- +274: submit_input_response +275: ⋮---- +276: ( +277: request_id: String, +278: response: String, +279: response_type: String, +280: state: State<'_, AppState>, +281: ) +282: ⋮---- +283: run +284: ⋮---- +285: () +``` + +### crates/cowork-core/src/pipeline/stage_executor.rs (271 lines) + +``` +1: get_save_tool_name +2: ⋮---- +3: (stage_name: &str) +4: ⋮---- +5: get_artifact_filename +6: ⋮---- +7: (stage_name: &str) +8: ⋮---- +9: get_display_name +10: ⋮---- +11: (agent_name: &str) +12: ⋮---- +13: execute_stage_with_instruction +14: ⋮---- +15: ( +16: ctx: &PipelineContext, +17: interaction: Arc, +18: stage_name: &str, +19: _instruction: &str, +20: feedback: Option<&str>, +21: ) +22: ⋮---- +23: get_truncated_message +24: ⋮---- +25: () +26: ⋮---- +27: truncate_content +28: ⋮---- +29: (content: &str, max_chars: usize) +30: ⋮---- +31: load_artifact_content +32: ⋮---- +33: (ctx: &PipelineContext, artifact_name: &str) +34: ⋮---- +35: build_prompt +36: ⋮---- +37: (ctx: &PipelineContext, stage_name: &str, feedback: Option<&str>) +38: ⋮---- +39: SimpleInvocationContext +40: ⋮---- +41: { +42: invocation_id: String, +43: agent_name: String, +44: user_id: String, +45: app_name: String, +46: session_id: String, +47: banch: String, +48: user_content: Content, +49: agent: Arc, +50: memory: Option>, +51: session: Box, +52: run_config: adk_core::RunConfig, +53: ended: std::sync::atomic::AtomicBool, +54: artifacts: Option>, +55: } +56: ⋮---- +57: SimpleInvocationContext +58: ⋮---- +59: { +60: pub fn new(ctx: &PipelineContext, content: &Content, agent: Arc) -> Self { +61: Self { +62: invocation_id: uuid::Uuid::new_v4().to_string(), +63: agent_name: agent.name().to_string(), +64: user_id: "default_user".to_string(), +65: app_name: "cowork_forge".to_string(), +66: session_id: ctx.iteration.id.clone(), +67: banch: "main".to_string(), +68: user_content: content.clone(), +69: agent, +70: +71: +72: +73: memory: None, +74: session: Box::new(SimpleSession::new(&ctx.iteration.id, content.clone())), +75: run_config: adk_core::RunConfig { +76: streaming_mode: adk_core::StreamingMode::SSE, +77: ..adk_core::RunConfig::default() +78: }, +79: ended: std::sync::atomic::AtomicBool::new(false), +80: artifacts: None, +81: } +82: } +83: } +84: ⋮---- +85: SimpleInvocationContext +86: ⋮---- +87: { +88: fn clone(&self) -> Self { +89: Self { +90: invocation_id: self.invocation_id.clone(), +91: agent_name: self.agent_name.clone(), +92: user_id: self.user_id.clone(), +93: app_name: self.app_name.clone(), +94: session_id: self.session_id.clone(), +95: banch: self.banch.clone(), +96: user_content: self.user_content.clone(), +97: agent: self.agent.clone(), +98: memory: self.memory.clone(), +99: +100: session: Box::new(SimpleSession::new( +101: &self.session_id, +102: self.user_content.clone(), +103: )), +104: run_config: self.run_config.clone(), +105: ended: std::sync::atomic::AtomicBool::new( +106: self.ended.load(std::sync::atomic::Ordering::SeqCst), +107: ), +108: artifacts: self.artifacts.clone(), +109: } +110: } +111: } +112: ⋮---- +113: SimpleInvocationContext +114: ⋮---- +115: { +116: fn agent(&self) -> Arc { +117: self.agent.clone() +118: } +119: +120: fn memory(&self) -> Option> { +121: self.memory.clone() +122: } +123: +124: fn session(&self) -> &dyn adk_core::Session { +125: self.session.as_ref() +126: } +127: +128: fn run_config(&self) -> &adk_core::RunConfig { +129: &self.run_config +130: } +131: +132: fn end_invocation(&self) { +133: self.ended.store(true, std::sync::atomic::Ordering::SeqCst); +134: } +135: +136: fn ended(&self) -> bool { +137: self.ended.load(std::sync::atomic::Ordering::SeqCst) +138: } +139: } +140: ⋮---- +141: SimpleInvocationContext +142: ⋮---- +143: { +144: fn artifacts(&self) -> Option> { +145: self.artifacts.clone() +146: } +147: } +148: ⋮---- +149: SimpleInvocationContext +150: ⋮---- +151: { +152: fn invocation_id(&self) -> &str { +153: &self.invocation_id +154: } +155: +156: fn agent_name(&self) -> &str { +157: &self.agent_name +158: } +159: +160: fn user_id(&self) -> &str { +161: &self.user_id +162: } +163: +164: fn app_name(&self) -> &str { +165: &self.app_name +166: } +167: +168: fn session_id(&self) -> &str { +169: &self.session_id +170: } +171: +172: fn banch(&self) -> &str { +173: &self.banch +174: } +175: +176: fn user_content(&self) -> &Content { +177: &self.user_content +178: } +179: } +180: ⋮---- +181: SimpleSession +182: ⋮---- +183: { +184: session_id: String, +185: app_name: String, +186: user_id: String, +187: simple_state: SimpleState, +188: messages: Vec, +189: } +190: ⋮---- +191: SimpleSession +192: ⋮---- +193: { +194: fn new(session_id: &str, initial_message: Content) -> Self { +195: Self { +196: session_id: session_id.to_string(), +197: app_name: "cowork_forge".to_string(), +198: user_id: "default_user".to_string(), +199: simple_state: SimpleState::new(), +200: messages: vec![initial_message], +201: } +202: } +203: } +204: ⋮---- +205: SimpleSession +206: ⋮---- +207: { +208: fn id(&self) -> &str { +209: &self.session_id +210: } +211: +212: fn app_name(&self) -> &str { +213: &self.app_name +214: } +215: +216: fn user_id(&self) -> &str { +217: &self.user_id +218: } +219: +220: fn state(&self) -> &dyn adk_core::State { +221: &self.simple_state +222: } +223: +224: fn conversation_history(&self) -> Vec { +225: self.messages.clone() +226: } +227: +228: fn append_to_history(&self, _content: Content) { +229: +230: } +231: } +232: ⋮---- +233: SimpleState +234: ⋮---- +235: { +236: data: std::collections::HashMap, +237: } +238: ⋮---- +239: SimpleState +240: ⋮---- +241: { +242: fn new() -> Self { +243: Self { +244: data: std::collections::HashMap::new(), +245: } +246: } +247: } +248: ⋮---- +249: SimpleState +250: ⋮---- +251: { +252: fn get(&self, key: &str) -> Option { +253: self.data.get(key).cloned() +254: } +255: +256: fn set(&mut self, key: String, value: serde_json::Value) { +257: self.data.insert(key, value); +258: } +259: +260: fn all(&self) -> std::collections::HashMap { +261: self.data.clone() +262: } +263: } +264: ⋮---- +265: extract_text_from_content +266: ⋮---- +267: (content: &Content) +268: ⋮---- +269: extract_text_from_event +270: ⋮---- +271: (event: &Event) +``` + +### crates/cowork-core/src/lib.rs (109 lines) + +``` +1: pub mod config; +2: +3: +4: pub mod config_definition; +5: +6: +7: pub mod acp; +8: +9: +10: pub mod domain; +11: pub mod persistence; +12: +13: +14: pub mod data; +15: +16: +17: pub mod tech_stack; +18: +19: +20: pub mod project_runtime; +21: pub mod runtime_security; +22: pub mod runtime_analyzer; +23: +24: +25: pub mod llm; +26: pub mod tools; +27: pub mod agents; +28: pub mod pipeline; +29: pub mod instructions; +30: pub mod interaction; +31: +32: +33: pub mod skills; +34: +35: +36: pub mod integration; +37: +38: +39: pub mod importer; +40: +41: +42: pub use domain::*; +43: pub use persistence::*; +44: pub use data::*; +45: pub use llm::*; +46: pub use agents::{create_project_manager_agent, execute_pm_agent_message, execute_pm_agent_message_streaming, PMAgentResult, PMAgentAction, PMAgentStreamCallback, create_legacy_project_analyzer, create_legacy_project_analyzer_with_context}; +47: pub use tech_stack::*; +48: +49: +50: pub use project_runtime::{ +51: ProjectRuntimeConfig, RuntimeType, FrontendFramework, FrontendRuntime, +52: BackendFramework, BackendRuntime, FullstackRuntime, DependencyConfig, +53: ProxyConfig, PackageManager as RuntimePackageManager, SecurityCheckResult, +54: get_preset_config, +55: }; +56: pub use runtime_security::RuntimeSecurityChecker; +57: pub use runtime_analyzer::{ +58: RuntimeAnalyzer, ProjectInfo, save_runtime_config, load_runtime_config, has_runtime_config, +59: }; +60: +61: +62: pub use config::{get_system_locale, set_system_locale, get_language_instruction}; +63: +64: +65: pub use acp::{AcpClient, AcpTaskResult}; +66: +67: +68: pub use config_definition::{ +69: AgentDefinition, AgentType, ModelConfig, ToolReference, IncludeContentsMode, +70: StageDefinition, StageType, HookConfig, HookPoint, ArtifactConfig, StageRetryConfig, +71: FlowDefinition, StageReference, FlowConfig, MemoryScope, InheritanceConfig, InheritanceMode, +72: IntegrationDefinition, IntegrationType, ConnectionConfig, AuthConfig, IntegrationEvent, +73: ConfigRegistry, global_registry, LoadReport, ConfigValidator, ValidationResult, +74: create_agent_for_stage, create_agent_from_config, initialize_config_registry, +75: initialize_mcp_toolsets, is_mcp_initialized, +76: }; +77: +78: +79: pub use skills::{ +80: SkillManager, SkillManagerConfig, +81: +82: SkillDocument, SkillIndex, SkillSummary, SkillMatch, +83: +84: SelectionPolicy, +85: +86: SkillInjector, SkillInjectorConfig, +87: apply_skill_injection, select_skill_prompt_block, +88: +89: load_skill_index, parse_skill_markdown, parse_instruction_markdown, +90: +91: discover_skill_files, discover_instruction_files, +92: +93: SkillError, SkillResult, +94: }; +95: +96: +97: pub use integration::{ +98: HookManager, HookExecutionContext, HookExecutionResult, +99: IntegrationAdapter, AdapterError, RestAdapter, +100: }; +101: +102: +103: pub use importer::{ +104: ImportConfig, ImportResult, ImportPreview, ArtifactOptions, +105: ProjectAnalysis, ProjectStructure, DetectedTechnology, +106: }; +107: +108: +109: pub const VERSION: &str = env!("CARGO_PKG_VERSION"); +``` + +### crates/cowork-gui/src/components/chat/MessageList.tsx (34 lines) + +``` +1: ToolCallMessage +2: ⋮---- +3: { +4: toolName: string; +5: arguments: Record; +6: agentName: string; +7: } +8: ⋮---- +9: ToolResultMessage +10: ⋮---- +11: { +12: toolName: string; +13: result: string; +14: success: boolean; +15: agentName: string; +16: } +17: ⋮---- +18: PMAgentMessage +19: ⋮---- +20: { +21: actions?: PMAction[]; +22: } +23: ⋮---- +24: MessageListProps +25: ⋮---- +26: { +27: messages: ChatMessage[]; +28: pmMessages?: (ChatMessage & { type: 'user' | 'pm_agent' })[]; +29: mode: 'pipeline' | 'pm_agent'; +30: isProcessing: boolean; +31: currentAgent: string | null; +32: onToggleThinking: (index: number) => void; +33: onActionClick?: (action: PMAction) => void; +34: } +``` + +### litho.docs/en/1.Overview.md (296 lines) + +```` +1: **Generation Time:** 2026-02-14 05:07:50 (UTC) +2: **Timestamp:** 1771045670 +3: +4: --- +5: +6: # Project Overview: Cowork Forge System Context +7: +8: ## 1. Executive Summary +9: +10: **Cowork Forge** is an AI-native iterative software development platform that orchestrates autonomous multi-agent workflows through a structured seven-stage development pipeline. The system transforms natural language ideas into production-ready software by leveraging Large Language Models (LLMs) while maintaining project continuity through a sophisticated memory management system. +11: +12: Operating as a self-contained desktop application built on a Rust-based architecture, Cowork Forge provides dual interaction modes: a command-line interface for automation-focused workflows and a Tauri-based graphical interface for interactive development. The platform implements Domain-Driven Design (DDD) principles with a Hexagonal Architecture pattern, ensuring clear separation between core business logic, infrastructure concerns, and presentation layers. +13: +14: ## 2. System Overview +15: +16: ### 2.1 Core Objectives +17: +18: Cowork Forge addresses the challenge of maintaining architectural consistency and contextual continuity in AI-assisted software development. The system's primary objectives include: +19: +20: - **Autonomous Development Orchestration**: Automating the complete software development lifecycle from conceptualization to delivery through a deterministic 7-stage pipeline (Idea → PRD → Design → Plan → Coding → Check → Delivery) +21: - **Contextual Memory Preservation**: Maintaining institutional knowledge across development iterations through persistent memory aggregation, enabling evolutionary development with three inheritance modes (Full, Partial, None) +22: - **Human-in-the-Loop Governance**: Implementing validation gates at critical stages where human oversight ensures quality control and architectural alignment +23: - **Dual-Interface Accessibility**: Supporting both automated CLI workflows and interactive GUI experiences through a shared core domain +24: +25: ### 2.2 Business Value +26: +27: The platform delivers measurable value through: +28: +29: | Value Proposition | Description | +30: |-------------------|-------------| +31: | **Development Acceleration** | Reduces boilerplate generation time by automating requirements documentation, architectural design, and initial code scaffolding | +32: | **Architectural Consistency** | Enforces standardized workflows through the Actor-Critic pattern, where critic agents validate outputs against quality standards | +33: | **Knowledge Retention** | Preserves architectural decisions, design patterns, and technical insights across iterations, preventing context loss in long-running projects | +34: | **Standardized Methodology** | Provides teams with consistent development practices through structured stage gates and validation protocols | +35: | **Local Execution** | Operates entirely on local infrastructure without cloud dependencies, ensuring data privacy and reducing operational costs | +36: | **External Capability Extension** | Integrates external AI services via Model Context Protocol (MCP), expanding agent capabilities with Tavily search, DeepWiki documentation queries, and other third-party MCP servers | +37: +38: ### 2.3 Technical Characteristics +39: +40: **Architecture Pattern**: Hexagonal/Ports and Adapters with Domain-Driven Design +41: **Core Technology Stack**: +42: - **Backend**: Rust (Tokio async runtime) with adk-rust agent framework +43: - **Frontend**: React 18 with Ant Design (GUI), clap (CLI) +44: - **Desktop Shell**: Tauri runtime for cross-platform native capabilities +45: - **Persistence**: JSON-based file storage with workspace containment +46: +47: **Key Technical Features**: +48: - **External Integration**: Model Context Protocol (MCP) HTTP client via adk-tool +49: - **Rate-Limited LLM Integration**: Decorator-pattern implementation enforcing 30 requests/minute with single concurrency control +50: - **Event-Driven Communication**: Real-time bidirectional IPC between Tauri backend and React frontend via event emission +51: - **Security-First Operations**: Path validation and workspace containment for all file system operations +52: - **Memory-Centric Design**: Project continuity through persistent memory aggregation (decisions, patterns, knowledge snapshots) +53: +54: ## 3. Target Users and Stakeholders +55: +56: ### 3.1 Individual Developers +57: +58: **Profile**: Software developers and technical leads seeking productivity gains in prototype development and boilerplate reduction. +59: +60: **Usage Scenarios**: +61: - Rapid prototyping from natural language descriptions +62: - Automated generation of project requirements and technical documentation +63: - Iterative refinement of existing codebases through evolution iterations +64: - Local development environment management with integrated preview capabilities +65: +66: **Key Needs**: +67: - Dual interface flexibility (CLI for scripting, GUI for exploration) +68: - Minimal configuration overhead with technology auto-detection +69: - Ability to pause and intervene during automated execution +70: - Local artifact storage with full transparency of generated outputs +71: +72: ### 3.2 Development Teams +73: +74: **Profile**: Small to medium-sized development teams requiring standardized workflows and knowledge preservation across multiple projects. +75: +76: **Usage Scenarios**: +77: - Onboarding new team members with standardized project initialization +78: - Maintaining architectural consistency across multiple microservices or modules +79: - Preserving design decisions and patterns for organizational knowledge bases +80: - Collaborative review of AI-generated artifacts through shared project memory +81: +82: **Key Needs**: +83: - Multi-project management with technology stack detection +84: - Standardized development methodology enforcement +85: - Knowledge retention between team members and iterations +86: - Human-in-the-loop validation for critical architectural decisions +87: +88: ### 3.3 AI-Augmented Developers +89: +90: **Profile**: Technical early adopters exploring AI-assisted development methodologies who require visibility into AI decision-making processes. +91: +92: **Usage Scenarios**: +93: - Experimenting with AI-generated architecture designs with full audit trails +94: - Providing feedback to AI agents for iterative refinement of outputs +95: - Studying AI reasoning patterns through stage-by-stage execution monitoring +96: - Validating AI-generated code against security and performance standards +97: +98: **Key Needs**: +99: - Transparent agent workflows with real-time progress monitoring +100: - Comprehensive artifact visibility (requirements, designs, plans, code) +101: - Feedback loops for Actor-Critic pattern interaction +102: - Control mechanisms for proceeding or regenerating at each pipeline stage +103: +104: ## 4. System Scope and Boundaries +105: +106: ### 4.1 System Scope Definition +107: +108: Cowork Forge operates as a **self-contained AI-assisted development environment** that generates project artifacts, writes code, validates implementations, and manages project evolution. The system functions entirely on the local machine, orchestrating AI agents through external LLM APIs while maintaining all project state, iteration history, and generated artifacts in local storage. +109: +110: ### 4.2 Included Components +111: +112: The system boundary encompasses the following core architectural components: +113: +114: | Component Category | Specific Components | +115: |-------------------|---------------------| +116: | **Core Domain Logic** | Project and Iteration aggregates, lifecycle management, inheritance mode logic | +117: | **AI Pipeline Engine** | 7-stage workflow controller, Stage Executor with ADK integration, Actor-Critic pattern implementation, modular Iteration Executor | +118: | **Agent Instruction System** | ~2000 lines of prompt engineering (Actor/Critic instructions per stage, knowledge generation, Legacy Project Analyzer) | +119: | **Configuration System** | Config Registry, Agent/Stage/Flow/Skill/Integration definitions, user config management, validation | +120: | **Skills Module** | adk-skill integration (agentskills.io standard), skill discovery, selection, and injection | +121: | **Tool Ecosystem** | 40+ ADK Tools (File Tools, Data Tools, Validation Tools, HITL Tools, Memory Tools, Deployment Tools, Legacy Project Analysis Tools), with MCP Remote Tool Integration (Tavily, DeepWiki, etc.) | +122: | **Memory Management** | ProjectMemory and IterationKnowledge domains, query indexing, knowledge promotion workflows | +123: | **Specialized Agents** | PM Agent for post-delivery interaction, Legacy Project Analyzer for existing project import and reverse engineering, Knowledge Generation Agent for insight extraction | +124: | **Importer Module** | Project analyzer, artifact generator, import configuration, technology stack detection | +125: | **Persistence Layer** | ProjectStore, IterationStore, MemoryStore with JSON-based storage and workspace directory structure (`.cowork-v2`) | +126: | **User Interfaces** | CLI interface (clap-based), Tauri-based GUI (React frontend with 9-panel interface) | +127: | **Infrastructure Services** | Rate-limited LLM client factory, InteractiveBackend trait implementations (CLI and Tauri), ProcessRunner for development servers | +128: +129: ### 4.3 Excluded Components +130: +131: The following components operate outside the system boundary and are treated as external dependencies: +132: +133: - **Version Control Systems**: Git operations and repository management +134: - **Cloud Infrastructure**: Hosted services, cloud deployment platforms, remote compute resources +135: - **CI/CD Pipelines**: Continuous integration and delivery automation +136: - **Package Registries**: npm, crates.io, PyPI, and other external dependency repositories +137: - **LLM Training Infrastructure**: Model training, fine-tuning, or hosting infrastructure +138: +139: ## 5. External System Interactions +140: +141: ### 5.1 External Systems Landscape +142: +143: ```mermaid +144: C4Context +145: title System Context Diagram - Cowork Forge +146: +147: Person(individual, "Individual Developer", "Uses CLI or GUI for AI-assisted development") +148: Person(team, "Development Team", "Collaborates on standardized AI workflows") +149: +150: System_Boundary(cf, "Cowork Forge") { +151: System(core, "Core Engine", "Domain logic, pipeline execution, memory management") +152: System(gui, "Desktop GUI", "Tauri + React interactive interface") +153: System(cli, "CLI Interface", "Command-line automation tool") +154: } +155: +156: System_Ext(llm, "LLM Provider APIs", "OpenAI-compatible APIs for agent orchestration") +157: System_Ext(fs, "Local File System", "Workspace storage and artifact persistence") +158: System_Ext(shell, "Shell/Command Executor", "Build tools, dev servers, system commands") +159: System_Ext(editor, "External Editor", "System default editor for HITL content review") +160: System_Ext(devserver, "Development Server", "Vite/similar for live application preview") +161: System_Ext(tauri, "Tauri Runtime", "Desktop GUI framework and native APIs") +162: System_Ext(mcp_servers, "MCP Servers", "External AI services providing tools via Model Context Protocol (Tavily search, DeepWiki docs)") +163: +164: Rel(individual, gui, "Interacts with via desktop application") +165: Rel(individual, cli, "Executes commands via terminal") +166: Rel(team, gui, "Collaborates through shared project memory") +167: +168: Rel(core, llm, "Sends API requests\n(Rate-limited: 30 req/min)", "HTTPS/JSON") +169: Rel(core, mcp_servers, "Queries for external tools and services", "MCP/HTTP") +170: Rel(core, fs, "Reads/Writes project files\n(Workspace-contained)", "File I/O") +171: Rel(core, shell, "Executes validation commands\nand build processes", "Process Execution") +172: Rel(core, editor, "Invokes for content\nreview and editing", "System Call") +173: +174: Rel(gui, tauri, "Renders UI and\nmanages window state", "Runtime API") +175: Rel(gui, core, "Orchestrates via\nTauri commands", "Internal IPC") +176: Rel(cli, core, "Invokes domain\noperations", "Library Call") +177: +178: Rel(gui, devserver, "Manages lifecycle\nand log streaming", "Process Management") +179: Rel(devserver, fs, "Serves generated\napplication files", "File Access") +180: +181: UpdateLayoutConfig($c4ShapeInRow="3", $c4BoundaryInRow="1") +182: ``` +183: +184: ### 5.2 Interaction Details +185: +186: #### 5.2.1 LLM Provider APIs +187: - **Interaction Type**: Outbound API Calls (HTTPS/JSON) +188: - **Purpose**: Agent-based code generation, document creation, architectural decisions, and critic validation +189: - **Constraints**: Rate-limited to 30 requests/minute with concurrency control (single semaphore) to ensure API quota compliance and cost control +190: - **Data Exchange**: Prompts containing project context and instructions; streaming responses containing generated artifacts +191: +192: #### 5.2.2 Local File System +193: - **Interaction Type**: Local File I/O +194: - **Purpose**: Workspace management, artifact storage, project file operations, and memory persistence +195: - **Security Model**: Path validation and workspace containment ensuring all operations remain within designated project directories (`.cowork-v2` convention) +196: - **Data Exchange**: JSON metadata files, markdown documentation, source code files, and knowledge snapshots +197: +198: #### 5.2.3 Shell/Command Executor +199: - **Interaction Type**: Process Execution +200: - **Purpose**: Project validation (compilation, testing), dependency installation, build processes, and development server management +201: - **Execution Context**: Cross-platform command execution with working directory isolation within project workspaces +202: - **Data Exchange**: Command stdin/stdout/stderr streams, exit codes for success/failure determination +203: +204: #### 5.2.4 External Editor +205: - **Interaction Type**: External Process Invocation +206: - **Purpose**: Human-in-the-Loop content review workflows where users edit generated content in preferred editors +207: - **Integration**: System default editor detection and invocation for temporary files containing stage outputs +208: - **Data Exchange**: File paths to temporary content files; user modifications saved back to system +209: +210: #### 5.2.5 Development Server +211: - **Interaction Type**: Process Management and Monitoring +212: - **Purpose**: Live preview of generated frontend applications (e.g., Vite, Webpack dev servers) +213: - **Lifecycle Management**: Process spawning, log streaming via Tauri events, graceful shutdown coordination +214: - **Data Exchange**: Real-time stdout/stderr log streaming, HTTP serving of application assets +215: +216: #### 5.2.6 Tauri Runtime +217: - **Interaction Type**: Runtime Framework Dependency +218: - **Purpose**: Desktop GUI capabilities, window management, cross-platform native APIs, and secure IPC between Rust backend and JavaScript frontend +219: - **Architecture Role**: Provides the presentation layer container for the React frontend, enabling system tray integration, native menus, and secure context isolation +220: - **Data Exchange**: Command invocations (invoke), event emissions (agent events, streaming responses), and binary asset management +221: +222: +223: #### 5.2.7 MCP Servers +224: - **Interaction Type**: HTTP-based Protocol Integration +225: - **Purpose**: Extend agent capabilities with external AI services through Model Context Protocol (MCP). Currently supports Tavily for web search and AI-powered research, and DeepWiki for code documentation queries. +226: - **Configuration**: Managed via `config.toml` under `[mcp]` section with `tavily_api_key` and `deepwiki_enabled` flags. Automatic initialization at application startup connects to configured MCP servers and injects their toolsets into all Agents. +227: - **Data Exchange**: MCP HTTP requests/responses; remote tools appear as native ADK tools to agents after injection. +228: +229: ## 6. Key Architectural Decisions +230: +231: ### 6.1 Multi-Stage Pipeline with Actor-Critic Pattern +232: The system implements a deterministic 7-stage workflow (Idea → PRD → Design → Plan → Coding → Check → Delivery) where each stage employs an Actor-Critic pattern. This ensures that AI-generated outputs undergo validation before proceeding, with feedback loops enabling regeneration when quality criteria are not met. +233: +234: ### 6.2 Dual Interface Strategy with Shared Core +235: The architecture supports both CLI and GUI interfaces through the `InteractiveBackend` trait abstraction, ensuring that business logic remains interface-agnostic. This enables automation-focused users to leverage scripting capabilities while interactive users benefit from real-time visualization and control. +236: +237: ### 6.3 Memory-Centric Evolutionary Development +238: Rather than treating each development session as isolated, the system maintains persistent memory aggregates (ProjectMemory, IterationKnowledge) that capture decisions, patterns, and insights. This supports three inheritance modes (Full, Partial, None) for evolution iterations, enabling sophisticated refactoring and incremental development workflows. +239: +240: ### 6.4 Workspace Containment and Security +241: All file operations are constrained to designated workspace directories with path validation mechanisms. This security-first approach prevents AI agents from accessing or modifying files outside the intended project scope, addressing safety concerns in autonomous code generation. +242: +243: ### 6.5 Event-Driven GUI Architecture +244: The Tauri-based GUI employs an event-driven model for real-time execution monitoring. Backend events (agent messages, tool calls, streaming responses, progress updates) are emitted to the React frontend, enabling live visualization of AI agent activities without polling overhead. +245: +246: ### 6.6 Data-Driven Configuration System +247: Introduces a configuration-driven approach where Agents, Stages, Flows, Skills, and Integrations are defined as JSON configurations rather than hardcoded implementations. This enables users to customize development workflows, create specialized agents, and extend system capabilities without modifying source code. The configuration registry manages built-in and user-defined configurations with validation and hot-reload support. +248: +249: ### 6.7 Legacy Project Import and Reverse Engineering +250: +251: The system supports importing any existing project (even those not created with Cowork Forge) through the Legacy Project Analyzer Agent. This agent performs reverse engineering by scanning project structure, detecting technology stacks, reading existing documentation and key source files, then using LLM to generate comprehensive documentation (idea.md, prd.md, design.md, plan.md). +252: +253: **Import Workflow:** +254: 1. **Project Analysis**: Scan project structure, detect tech stack, read README and key configuration files +255: 2. **Artifact Generation**: Generate document artifacts via LLM Agent or template mode +256: 3. **Project Initialization**: Create Cowork Forge project structure and initial iteration record +257: +258: **Technical Implementation:** +259: - `importer` module: Project analyzer, artifact generator, import configuration +260: - Legacy Project Analyzer Agent: Specialized agent equipped with project scanning tools +261: - CLI `import` command: Supports both LLM generation and template generation modes +262: +263: **CLI Usage Examples:** +264: ```bash +265: # Import project with all documentation +266: cowork import /path/to/project --idea --prd --design --plan +267: +268: # Template-only generation (no LLM config required) +269: cowork import /path/to/project --idea --template-only +270: ``` +271: +272: This enables users to quickly bring existing codebases into Cowork Forge's iterative development workflow. +273: +274: ### 6.8 Post-Delivery PM Agent Integration +275: After iteration completion, the Project Manager Agent provides an interactive interface for continued project engagement. PM Agent implements intent recognition to classify user requests (bug fixes, requirement changes, new features, consultations) and executes appropriate operations (stage navigation, new iteration creation, Q&A responses). This bridges the gap between automated pipeline execution and ongoing project maintenance. +276: +277: ### 6.9 Skills Extension with agentskills.io Standard +278: The skills module implements the agentskills.io standard for domain-specific capability injection. Skills are discovered from `.skills/` directories, semantically matched against user queries, and injected into agent contexts as additional instructions and tools. This enables extensible, community-driven development of specialized capabilities without core system modifications. +279: +280: ## 7. Technology Stack Summary +281: +282: | Layer | Technology | Purpose | +283: |-------|-----------|---------| +284: | **Core Domain** | Rust, Tokio, adk-rust | Async agent orchestration, domain logic | +285: | **Persistence** | JSON, File System | Entity storage, memory indexing | +286: | **LLM Integration** | OpenAI-compatible APIs, Custom Rate Limiter | Agent capabilities with quota management | +287: | **CLI** | clap, dialoguer, colored | Command-line interface with interactive prompts | +288: | **GUI Backend** | Tauri, Rust | Desktop runtime, system integration | +289: | **GUI Frontend** | React 18, TypeScript, Ant Design, Monaco Editor, Zustand | Interactive user interface, code editing | +290: | **Process Management** | Tokio process, cross-platform shells | Development server control, build execution | +291: +292: --- +293: +294: **Document Version**: 1.0 +295: **Classification**: Architecture Documentation (C4 System Context Level) +296: **Next Steps**: Refer to Container and Component Level diagrams for detailed internal architecture documentation. +```` + +### AGENTS.md (262 lines) + +```` +1: # AGENTS.md — Cowork Forge +2: +3: > This file provides AI coding agents with the context needed to work effectively on this project. +4: > For project knowledge (architecture, decisions, issues), see [`.ai-context/SKILL.md`](.ai-context/SKILL.md). +5: +6: --- +7: +8: ## Project Overview +9: +10: **Cowork Forge** is an AI-native multi-agent software development platform. It orchestrates specialized AI agents (Product Manager, Architect, Project Manager, Engineer) through a 7-stage pipeline to transform ideas into production-ready software. +11: +12: | Aspect | Detail | +13: |--------|--------| +14: | Language | Rust (edition 2024) | +15: | Agent Framework | adk-rust 0.5.0 | +16: | GUI | Tauri + React 18 + Ant Design | +17: | Architecture | Hexagonal + DDD | +18: | License | MIT | +19: +20: ### Workspace Structure +21: +22: ``` +23: crates/ +24: ├── cowork-core/ # Domain logic, pipeline, tools, agents (MAIN crate) +25: │ └── src/ +26: │ ├── pipeline/ # 7-stage orchestration & stage executor +27: │ ├── domain/ # Project, Iteration, Memory aggregates +28: │ ├── tools/ # 40+ ADK tools + MCP integration +29: │ ├── agents/ # Agent wrappers (iterative, PM, legacy analyzer) +30: │ ├── interaction/ # InteractiveBackend trait (CLI/GUI abstraction) +31: │ ├── acp/ # Agent Client Protocol for external agents +32: │ ├── config_definition/ # Data-driven config (agents, stages, flows) +33: │ ├── instructions/ # Agent prompt library +34: │ ├── skills/ # agentskills.io standard skill system +35: │ ├── integration/ # Hook manager for external integrations +36: │ └── persistence/ # JSON-based storage +37: ├── cowork-cli/ # CLI adapter (clap + dialoguer) +38: └── cowork-gui/ # Tauri + React GUI +39: ├── src-tauri/ # Rust backend (Tauri commands + events) +40: └── src/ # React frontend (TypeScript + Ant Design) +41: ``` +42: +43: --- +44: +45: ## Dev Environment Setup +46: +47: ### Prerequisites +48: +49: - **Rust** (edition 2024, stable toolchain) +50: - **Node.js** (for GUI frontend build) +51: - **LLM API Key** (OpenAI-compatible endpoint) +52: +53: ### Build +54: +55: ```bash +56: # Build entire workspace +57: cargo build +58: +59: # Release build +60: cargo build --release +61: +62: # Build GUI only (installs frontend deps automatically) +63: cd crates/cowork-gui && cargo tauri dev +64: ``` +65: +66: ### Run +67: +68: ```bash +69: # CLI +70: cargo run --package cowork-cli -- +71: +72: # GUI (development mode) +73: cd crates/cowork-gui && cargo tauri dev +74: ``` +75: +76: ### Configuration +77: +78: Config file location: +79: +80: | Platform | Path | +81: |----------|------| +82: | Windows | `%APPDATA%\CoworkCreative\config.toml` | +83: | macOS | `~/Library/Application Support/CoworkCreative/config.toml` | +84: | Linux | `~/.config/CoworkCreative/config.toml` | +85: +86: User-facing config directory: +87: +88: | Platform | Path | +89: |----------|------| +90: | Windows | `%APPDATA%\com.cowork-forge.app\config\` | +91: | macOS | `~/Library/Application Support/com.cowork-forge.app/config/` | +92: | Linux | `~/.config/com.cowork-forge.app/config/` | +93: +94: --- +95: +96: ## Build and Test Commands +97: +98: ```bash +99: # Run all tests +100: cargo test +101: +102: # Test a specific crate +103: cargo test -p cowork-core +104: +105: # Test a specific module +106: cargo test -p cowork-core pipeline +107: +108: # Run with all features +109: cargo test --all-features +110: +111: # Check compilation without building +112: cargo check +113: +114: # Lint (if clippy configured) +115: cargo clippy +116: ``` +117: +118: ### GUI Frontend +119: +120: ```bash +121: cd crates/cowork-gui +122: +123: # Install dependencies +124: npm install # or: bun install +125: +126: # Build frontend only +127: npm run build +128: +129: # Development server +130: npm run dev +131: ``` +132: +133: --- +134: +135: ## Code Style and Conventions +136: +137: ### Rust +138: +139: - **Error handling**: Always use `anyhow::Result`. Never use `unwrap()` in production code. +140: - **Async traits**: Use `async_trait` for async trait methods. +141: - **Naming**: `snake_case` for functions/variables, `PascalCase` for types/traits. +142: - **Architecture**: Follow hexagonal architecture — domain logic has zero external dependencies. Infrastructure adapters implement domain ports. +143: - **Trait-based abstraction**: `InteractiveBackend` is the key port for CLI/GUI abstraction. All user interaction flows through this trait. +144: - **Serialization**: `serde` with derive macros for all domain entities. +145: - **Async runtime**: Tokio with `features = ["full"]`. +146: +147: ### TypeScript / React (GUI) +148: +149: - Component-based architecture with Ant Design. +150: - Tauri commands for request-response, events for streaming. +151: - State management via React hooks. +152: +153: ### Key Patterns +154: +155: | Pattern | Where | Purpose | +156: |---------|-------|---------| +157: | Actor-Critic | PRD, Design, Plan, Coding stages | Iterative self-refinement | +158: | Strategy | Stage trait implementations | Pluggable stage behavior | +159: | Template Method | Pipeline execution flow | Fixed stage sequence with hooks | +160: | Repository | Persistence stores | Abstract data access | +161: | Decorator | LLM rate limiting | Transparent cross-cutting concern | +162: +163: --- +164: +165: ## Key Files +166: +167: When working on specific areas, start from these files: +168: +169: | Area | Primary File | Related | +170: |------|-------------|---------| +171: | Pipeline execution | `crates/cowork-core/src/pipeline/executor/mod.rs` | `stage_executor.rs`, `knowledge.rs` | +172: | Stage implementations | `crates/cowork-core/src/pipeline/stages/*.rs` | 7 stage files: idea, prd, design, plan, coding, check, delivery | +173: | Tool implementations | `crates/cowork-core/src/tools/mod.rs` | `file_tools.rs`, `data_tools.rs`, `hitl_tools.rs`, `pm_tools.rs`, etc. | +174: | Domain entities | `crates/cowork-core/src/domain/mod.rs` | `project.rs`, `iteration.rs`, `memory.rs` | +175: | HITL interface | `crates/cowork-core/src/interaction/mod.rs` | `cli.rs`, `tauri.rs` | +176: | Agent configs | `crates/cowork-core/src/config_definition/` | `default_configs/*.json` | +177: | Agent prompts | `crates/cowork-core/src/instructions/*.rs` | 12 instruction modules | +178: | External agent | `crates/cowork-core/src/acp/client.rs` | ACP protocol client | +179: | Skill system | `crates/cowork-core/src/skills/` | agentskills.io standard | +180: +181: --- +182: +183: ## Security Considerations +184: +185: - **Path validation**: All file operations are validated against workspace boundaries. Never bypass `validate_path()` checks. +186: - **Command sanitization**: Dangerous commands (`rm -rf`, `sudo`, etc.) are blocked. Do not circumvent the command whitelist. +187: - **LLM rate limiting**: Global semaphore (concurrency=1) + 2s delay = 30 req/min. Do not bypass rate limiting. +188: - **Workspace containment**: File tools must not access paths outside the project workspace. +189: - **No secrets in code**: API keys are loaded from `config.toml` or environment variables, never hardcoded. +190: - **Watchdog monitoring**: Agent behavior is monitored for objective deviation. +191: +192: --- +193: +194: ## Project Knowledge (.ai-context) +195: +196: This project uses a tiered knowledge base in `.ai-context/` for architectural context that code alone cannot convey. **AGENTS.md tells you *how to work*; `.ai-context/` tells you *what the project is*.** +197: +198: ### When to Read `.ai-context` +199: +200: | Situation | What to Read | +201: |-----------|-------------| +202: | Starting a new session | `.ai-context/references/PROJECT-ESSENCE.md` | +203: | Working across components | `.ai-context/references/ARCHITECTURE.md` | +204: | Changing established patterns | `.ai-context/references/DECISIONS.md` | +205: | Debugging unexpected behavior | `.ai-context/DYNAMICS.md` | +206: | Unsure *why* something is designed a way | `.ai-context/references/DECISIONS.md` | +207: +208: ### Session Start Protocol +209: +210: ``` +211: 1. Read this file (AGENTS.md) +212: 2. Read .ai-context/references/PROJECT-ESSENCE.md +213: 3. Scan .ai-context/DYNAMICS.md for active issues +214: 4. Proceed with code exploration +215: ``` +216: +217: ### Knowledge Tiers +218: +219: | Tier | File | Update Frequency | +220: |------|------|------------------| +221: | 0 | `references/PROJECT-ESSENCE.md` | Quarterly / Major version | +222: | 1 | `references/ARCHITECTURE.md` | Monthly / Sprint | +223: | 2 | `references/DECISIONS.md` | Per decision change | +224: | 3 | `DYNAMICS.md` | As needed | +225: +226: Full entry point: [`.ai-context/SKILL.md`](.ai-context/SKILL.md) +227: +228: ### Updating `.ai-context` +229: +230: When making significant changes, update the corresponding knowledge file: +231: +232: | What Changed | Update | +233: |-------------|--------| +234: | New crate or major component | `.ai-context/references/ARCHITECTURE.md` | +235: | Architecture decision | `.ai-context/references/DECISIONS.md` | +236: | New active issue / constraint | `.ai-context/DYNAMICS.md` | +237: | Project scope change | `.ai-context/references/PROJECT-ESSENCE.md` | +238: +239: Before updating, read `.ai-context/meta/MAINTENANCE.md` for writing guidelines. +240: +241: **No update needed for**: struct fields, function signatures, refactoring, bug fixes. +242: +243: --- +244: +245: ## PR Guidelines +246: +247: - Run `cargo test` and `cargo clippy` before committing. +248: - Ensure no `unwrap()` in production code paths. +249: - If you added a new tool or stage, update `.ai-context/references/ARCHITECTURE.md`. +250: - If you made a non-obvious design choice, add an ADR to `.ai-context/references/DECISIONS.md`. +251: - Commit messages: use conventional commits format (`feat:`, `fix:`, `refactor:`, etc.). +252: +253: --- +254: +255: ## Common Pitfalls +256: +257: - **Don't bypass `InteractiveBackend`**: Never call CLI-specific functions (e.g., `dialoguer`) from `cowork-core`. All user interaction must go through the `InteractiveBackend` trait. +258: - **Don't ignore rate limiting**: LLM calls are serialized for a reason. Don't try to parallelize them. +259: - **Don't access files outside workspace**: The security layer validates all paths. If you need to access a new path, update the validation logic, don't bypass it. +260: - **Don't hardcode stage IDs**: Use `create_stage_by_id()` or flow configuration instead of string matching. +261: - **Don't use `unwrap()`**: Use `anyhow::Result` with proper error propagation (`?` operator or `.context()`). +262: - **Don't duplicate knowledge**: If information exists in `.ai-context/`, link to it rather than repeating it here. +```` + +### Cargo.toml (53 lines) + +``` +1: [workspace] +2: resolver = "2" +3: members = [ +4: "crates/cowork-core", +5: "crates/cowork-cli", +6: "crates/cowork-gui/src-tauri", +7: ] +8: +9: [workspace.package] +10: version = "2.5.1" +11: edition = "2024" +12: authors = ["Sopaco"] +13: license = "MIT" +14: repository = "https://github.com/sopaco/cowork-forge" +15: +16: [workspace.dependencies] +17: adk-rust = "0.5.0" +18: adk-core = "0.5.0" +19: adk-agent = "0.5.0" +20: adk-model = { version = "0.5.0", features = ["openai"] } +21: adk-tool = "0.5.0" +22: adk-runner = "0.5.0" +23: adk-session = "0.5.0" +24: adk-skill = "0.5.0" +25: +26: tokio = { version = "1", features = ["full"] } +27: tokio-util = { version = "0.7", features = ["compat"] } +28: anyhow = "1" +29: thiserror = "2" +30: serde = { version = "1", features = ["derive"] } +31: serde_json = "1" +32: +33: toml = "1.0" +34: +35: clap = { version = "4", features = ["derive"] } +36: dialoguer = "0.12" +37: console = "0.16" +38: +39: tracing = "0.1" +40: tracing-subscriber = { version = "0.3", features = ["env-filter"] } +41: +42: chrono = { version = "0.4", features = ["serde"] } +43: uuid = { version = "1", features = ["v4", "serde"] } +44: +45: dirs = "6" +46: walkdir = "2" +47: ignore = "0.4" +48: +49: futures = "0.3" +50: +51: tempfile = "3" +52: +53: agent-client-protocol = "0.9" +``` + +### crates/cowork-gui/src/App.tsx (413 lines) + +``` +1: import React, { useEffect, useRef, useState, useMemo, useCallback, Suspense, lazy } from 'react'; +2: import { Layout, Menu, Button, Empty, App as AntApp, Tag, Spin } from 'antd'; +3: import { +4: FolderOutlined, +5: FileTextOutlined, +6: CodeOutlined, +7: EyeOutlined, +8: PlayCircleOutlined, +9: ReloadOutlined, +10: MessageOutlined, +11: AppstoreOutlined, +12: DatabaseOutlined, +13: BranchesOutlined, +14: CheckCircleOutlined, +15: RocketOutlined, +16: BookOutlined, +17: SettingOutlined, +18: ControlOutlined +19: } from '@ant-design/icons'; +20: +21: import { useProjectStore, useAgentStore, useUIStore } from './stores'; +22: import { LoadingScreen, StatusBadge } from './components/common'; +23: import { useAppEvents, usePMAgent, useIterationActions, useChatInput } from './hooks'; +24: +25: import type { ChatMode, PMAction, PMAgentMessage, ChatMessage } from './stores'; +26: +27: +28: import ProjectsPanel from './components/ProjectsPanel'; +29: +30: +31: const ArtifactsViewer = lazy(() => import('./components/ArtifactsViewer')); +32: const CodeEditor = lazy(() => import('./components/CodeEditor')); +33: const RunnerPanel = lazy(() => import('./components/RunnerPanel')); +34: const MemoryPanel = lazy(() => import('./components/MemoryPanel')); +35: const KnowledgePanel = lazy(() => import('./components/KnowledgePanel')); +36: const CommandPalette = lazy(() => import('./components/CommandPalette')); +37: const IterationsPanel = lazy(() => import('./components/IterationsPanel')); +38: const SettingsPanel = lazy(() => import('./components/SettingsPanel')); +39: +40: const ChatPanel = lazy(() => import('./components/chat').then(m => ({ default: m.ChatPanel }))); +41: +42: const AgentsSetupPanel = lazy(() => import('./components/config').then(m => ({ default: m.AgentsSetupPanel }))); +43: +44: const { Sider, Content, Header, Footer } = Layout; +45: +46: function App() { +47: +48: const [userInput, setUserInput] = useState(''); +49: const messagesContainerRef = useRef(null); +50: const pmMessagesContainerRef = useRef(null); +51: +52: +53: const project = useProjectStore(state => state.project); +54: const iterations = useProjectStore(state => state.iterations); +55: const currentIteration = useProjectStore(state => state.currentIteration); +56: const loading = useProjectStore(state => state.loading); +57: const loadProject = useProjectStore(state => state.loadProject); +58: const setCurrentIteration = useProjectStore(state => state.setCurrentIteration); +59: const updateCurrentIterationStatus = useProjectStore(state => state.updateCurrentIterationStatus); +60: +61: +62: const messages = useAgentStore(state => state.messages); +63: const pmMessages = useAgentStore(state => state.pmMessages); +64: const isProcessing = useAgentStore(state => state.isProcessing); +65: const currentAgent = useAgentStore(state => state.currentAgent); +66: const currentStage = useAgentStore(state => state.currentStage); +67: const inputRequest = useAgentStore(state => state.inputRequest); +68: const pmProcessing = useAgentStore(state => state.pmProcessing); +69: const setInputRequest = useAgentStore(state => state.setInputRequest); +70: const loadPMWelcomeMessage = useAgentStore(state => state.loadPMWelcomeMessage); +71: +72: +73: const activeView = useUIStore(state => state.activeView); +74: const commandPaletteVisible = useUIStore(state => state.commandPaletteVisible); +75: const activeArtifactTab = useUIStore(state => state.activeArtifactTab); +76: const artifactsRefreshTrigger = useUIStore(state => state.artifactsRefreshTrigger); +77: const codeRefreshTrigger = useUIStore(state => state.codeRefreshTrigger); +78: const memoryRefreshTrigger = useUIStore(state => state.memoryRefreshTrigger); +79: const knowledgeRefreshTrigger = useUIStore(state => state.knowledgeRefreshTrigger); +80: const setActiveView = useUIStore(state => state.setActiveView); +81: const setCommandPaletteVisible = useUIStore(state => state.setCommandPaletteVisible); +82: const setActiveArtifactTab = useUIStore(state => state.setActiveArtifactTab); +83: +84: +85: useAppEvents(userInput, setUserInput); +86: const { handlePMSendMessage, handlePMAction } = usePMAgent(); +87: const { handleSelectIteration, handleExecuteIteration, handleOpenProjectFolder, handleOpenIterationFolder, handleCommandSelect } = useIterationActions(); +88: const { +89: inputRequest: chatInputRequest, +90: handleSendUserMessage, +91: handleSelectOption, +92: handleSubmitFeedback, +93: handleToggleThinking, +94: handleCancelFeedback +95: } = useChatInput(); +96: +97: +98: const chatMode = useMemo(() => { +99: if (!currentIteration) return 'disabled'; +100: if (currentIteration.status === 'Completed') return 'pm_agent'; +101: if (isProcessing || currentIteration.status === 'Running') return 'pipeline'; +102: return 'pipeline'; +103: }, [currentIteration, isProcessing]); +104: +105: +106: useEffect(() => { +107: if (chatMode === 'pm_agent' && currentIteration) { +108: const pmMessages = useAgentStore.getState().pmMessages; +109: if (pmMessages.length === 0) { +110: loadPMWelcomeMessage(currentIteration.id); +111: } +112: } +113: }, [chatMode, currentIteration?.id, loadPMWelcomeMessage]); +114: +115: +116: useEffect(() => { +117: if (messagesContainerRef.current) { +118: messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight; +119: } +120: }, [messages]); +121: +122: useEffect(() => { +123: if (pmMessagesContainerRef.current && pmMessages.length > 0) { +124: pmMessagesContainerRef.current.scrollTop = pmMessagesContainerRef.current.scrollHeight; +125: } +126: }, [pmMessages]); +127: +128: +129: const handleSend = useCallback(() => { +130: if (chatMode === 'pm_agent') { +131: handlePMSendMessage(userInput, setUserInput); +132: } else { +133: handleSendUserMessage(userInput, setUserInput); +134: } +135: }, [chatMode, userInput, handlePMSendMessage, handleSendUserMessage]); +136: +137: const handleSelectOptionWrapper = useCallback((option: Parameters[0]) => { +138: handleSelectOption(option, userInput, setUserInput); +139: }, [handleSelectOption, userInput]); +140: +141: const handleSubmitFeedbackWrapper = useCallback(() => { +142: handleSubmitFeedback(userInput, setUserInput, updateCurrentIterationStatus); +143: }, [handleSubmitFeedback, userInput, updateCurrentIterationStatus]); +144: +145: const handlePMActionWrapper = useCallback((action: PMAction) => { +146: handlePMAction(action, pmMessages as (ChatMessage & { type: 'user' | 'pm_agent' })[]); +147: }, [handlePMAction, pmMessages]); +148: +149: +150: const loadingFallback = ( +151:
+152: +153:
+154: ); +155: +156: +157: const renderContent = () => ( +158:
+159:
+160: +161: +166: +167:
+168: +169:
+170: +171:
+172: +173:
+174: {currentIteration ? ( +175: +176: +183: +184: ) : ( +185: +186: )} +187:
+188: +189:
+190: {currentIteration ? ( +191: +192: +197: +198: ) : ( +199: +200: )} +201:
+202: +203:
+204: {currentIteration ? ( +205: +206: +207: +208: ) : ( +209: +210: )} +211:
+212: +213:
+214: +215: +220: +221:
+222: +223:
+224: +225: +231: +232:
+233: +234:
+235: +236: +237: +238:
+239: +240:
+241: +242: +243: +244:
+245: +246:
+247: {currentIteration ? ( +248: +249: } +262: pmMessagesContainerRef={pmMessagesContainerRef as React.RefObject} +263: onUserInputChange={setUserInput} +264: onSend={handleSend} +265: onSelectOption={handleSelectOptionWrapper} +266: onSubmitFeedback={handleSubmitFeedbackWrapper} +267: onCancelFeedback={handleCancelFeedback} +268: onToggleThinking={handleToggleThinking} +269: onActionClick={handlePMActionWrapper} +270: /> +271: +272: ) : ( +273: +274: )} +275:
+276:
+277: ); +278: +279: if (loading) { +280: return ; +281: } +282: +283: return ( +284: +285:
+295:
+296:

+297: +298: Cowork Forge +299:

+300: {project && ( +301: +302: {project.name} +303: +304: )} +305:
+306: +307:
+308: {currentIteration && ( +309: <> +310: +311: {(currentIteration.status === 'Draft' || currentIteration.status === 'Paused') && ( +312: +326: )} +327: +328: )} +329:
+330:
+331: +332: +333: +334: setActiveView(key as typeof activeView)} +338: style={{ height: '100%', borderRight: 0 }} +339: items={[ +340: { key: 'projects', icon: , label: 'Projects' }, +341: { key: 'iterations', icon: , label: 'Iterations' }, +342: { key: 'chat', icon: , label: 'Collaborate' }, +343: { key: 'artifacts', icon: , label: 'Artifacts' }, +344: { key: 'code', icon: , label: 'Code' }, +345: { key: 'run', icon: , label: 'Run' }, +346: { key: 'execution-memory', icon: , label: 'Memory' }, +347: { key: 'project-knowledge', icon: , label: 'Knowledge' }, +348: { type: 'divider' }, +349: { key: 'config', icon: , label: 'Agents Setup' }, +350: { key: 'settings', icon: , label: 'Settings' } +351: ]} +352: /> +353: +354: +355: +356: {renderContent()} +357: +358: +359: +360:
+370:
+371: {project ? ( +372: <> +373: +374: Project: {project.name} +375: +376: currentIteration && handleOpenIterationFolder(currentIteration.id)} +379: title={currentIteration ? `Click to open iteration folder: ${currentIteration.id}` : undefined} +380: > +381: Iterations: {iterations.length} +382: {currentIteration && (#{currentIteration.number})} +383: +384: +385: ) : ( +386: 'No project loaded' +387: )} +388:
+389:
+390: {isProcessing ? ( +391: +392: +393: {currentAgent ? `${currentAgent} is working...` : 'Processing...'} +394: +395: ) : ( +396: +397: +398: Ready +399: +400: )} +401:
+402:
+403: +404: setCommandPaletteVisible(false)} +407: onCommandSelect={handleCommandSelect} +408: /> +409: +410: ); +411: } +412: +413: export default App; +``` + +### crates/cowork-gui/src/components/chat/ChatPanel.tsx (24 lines) + +``` +1: ChatPanelProps +2: ⋮---- +3: { +4: messages: ChatMessage[]; +5: pmMessages: (ChatMessage & { type: 'user' | 'pm_agent' })[]; +6: mode: 'pipeline' | 'pm_agent' | 'disabled'; +7: isProcessing: boolean; +8: pmProcessing: boolean; +9: currentAgent: string | null; +10: iterationTitle: string; +11: iterationDescription?: string; +12: currentStage?: string | null; +13: inputRequest?: InputRequest | null; +14: userInput: string; +15: messagesContainerRef: React.RefObject; +16: pmMessagesContainerRef: React.RefObject; +17: onUserInputChange: (value: string) => void; +18: onSend: () => void; +19: onSelectOption: (option: InputOption) => void; +20: onSubmitFeedback: () => void; +21: onCancelFeedback: () => void; +22: onToggleThinking: (index: number) => void; +23: onActionClick?: (action: PMAction) => void; +24: } +``` + +### crates/cowork-gui/src/components/chat/InputArea.tsx (14 lines) + +``` +1: InputAreaProps +2: ⋮---- +3: { +4: userInput: string; +5: onUserInputChange: (value: string) => void; +6: onSend: () => void; +7: onDumpChat: () => void; +8: inputRequest?: InputRequest | null; +9: onSelectOption: (option: InputOption) => void; +10: onSubmitFeedback: () => void; +11: onCancelFeedback: () => void; +12: disabled?: boolean; +13: mode: 'pipeline' | 'pm_agent'; +14: } +``` + +### crates/cowork-core/src/acp/client.rs (206 lines) + +``` +1: AgentMessage +2: ⋮---- +3: { +4: +5: Thinking(String), +6: +7: Output(String), +8: +9: Status(String), +10: +11: Error(String), +12: +13: Completed, +14: } +15: ⋮---- +16: CoworkClient +17: ⋮---- +18: { +19: output: Arc>, +20: message_tx: mpsc::UnboundedSender, +21: } +22: ⋮---- +23: CoworkClient +24: ⋮---- +25: { +26: async fn request_permission( +27: &self, +28: _args: acp::RequestPermissionRequest, +29: ) -> acp::Result { +30: Err(acp::Error::method_not_found()) +31: } +32: +33: async fn write_text_file( +34: &self, +35: _args: acp::WriteTextFileRequest, +36: ) -> acp::Result { +37: Err(acp::Error::method_not_found()) +38: } +39: +40: async fn read_text_file( +41: &self, +42: _args: acp::ReadTextFileRequest, +43: ) -> acp::Result { +44: Err(acp::Error::method_not_found()) +45: } +46: +47: async fn create_terminal( +48: &self, +49: _args: acp::CreateTerminalRequest, +50: ) -> Result { +51: Err(acp::Error::method_not_found()) +52: } +53: +54: async fn terminal_output( +55: &self, +56: _args: acp::TerminalOutputRequest, +57: ) -> acp::Result { +58: Err(acp::Error::method_not_found()) +59: } +60: +61: async fn release_terminal( +62: &self, +63: _args: acp::ReleaseTerminalRequest, +64: ) -> acp::Result { +65: Err(acp::Error::method_not_found()) +66: } +67: +68: async fn wait_for_terminal_exit( +69: &self, +70: _args: acp::WaitForTerminalExitRequest, +71: ) -> acp::Result { +72: Err(acp::Error::method_not_found()) +73: } +74: +75: async fn kill_terminal_command( +76: &self, +77: _args: acp::KillTerminalCommandRequest, +78: ) -> acp::Result { +79: Err(acp::Error::method_not_found()) +80: } +81: +82: async fn session_notification( +83: &self, +84: args: acp::SessionNotification, +85: ) -> acp::Result<(), acp::Error> { +86: match args.update { +87: acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk { content, .. }) => { +88: if let acp::ContentBlock::Text(text_content) = content { +89: let text = text_content.text.clone(); +90: eprintln!("AGENT: {}", text); +91: +92: let _ = self.message_tx.send(AgentMessage::Output(text)); +93: +94: if let Ok(mut out) = self.output.lock() { +95: out.push_str(&text_content.text); +96: } +97: } +98: } +99: acp::SessionUpdate::AgentThoughtChunk(acp::ContentChunk { content, .. }) => { +100: if let acp::ContentBlock::Text(text_content) = content { +101: let text = text_content.text.clone(); +102: eprintln!("AGENT THINKING: {}", text); +103: +104: let _ = self.message_tx.send(AgentMessage::Thinking(text)); +105: } +106: } +107: +108: _ => {} +109: } +110: Ok(()) +111: } +112: +113: async fn ext_method(&self, _args: acp::ExtRequest) -> acp::Result { +114: Err(acp::Error::method_not_found()) +115: } +116: +117: async fn ext_notification(&self, _args: acp::ExtNotification) -> acp::Result<()> { +118: Err(acp::Error::method_not_found()) +119: } +120: } +121: ⋮---- +122: AcpTaskResult +123: ⋮---- +124: { +125: +126: pub content: String, +127: +128: pub completed: bool, +129: +130: pub error: Option, +131: } +132: ⋮---- +133: AcpTaskResult +134: ⋮---- +135: { +136: pub fn new(content: String, completed: bool) -> Self { +137: Self { +138: content, +139: completed, +140: error: None, +141: } +142: } +143: +144: pub fn error(msg: String) -> Self { +145: Self { +146: content: String::new(), +147: completed: false, +148: error: Some(msg), +149: } +150: } +151: } +152: ⋮---- +153: execute_with_external_agent +154: ⋮---- +155: ( +156: config: CodingAgentConfig, +157: workspace: PathBuf, +158: task: String, +159: ) +160: ⋮---- +161: run_acp_in_thread +162: ⋮---- +163: ( +164: config: CodingAgentConfig, +165: workspace: PathBuf, +166: task: String, +167: message_tx: mpsc::UnboundedSender, +168: ) +169: ⋮---- +170: AcpClient +171: ⋮---- +172: { +173: config: CodingAgentConfig, +174: workspace: PathBuf, +175: } +176: ⋮---- +177: AcpClient +178: ⋮---- +179: { +180: +181: pub async fn from_config(config: &CodingAgentConfig, workspace: &PathBuf) -> Result { +182: Ok(Self { +183: config: config.clone(), +184: workspace: workspace.clone(), +185: }) +186: } +187: +188: +189: pub fn execute_task_stream( +190: self, +191: task: String, +192: ) -> (mpsc::UnboundedReceiver, impl std::future::Future>>) { +193: execute_with_external_agent(self.config, self.workspace, task) +194: } +195: +196: +197: pub async fn execute_task(&mut self, task: &str) -> Result { +198: let (_, result) = execute_with_external_agent( +199: self.config.clone(), +200: self.workspace.clone(), +201: task.to_string(), +202: ); +203: +204: result.await? +205: } +206: } +``` + +### crates/cowork-core/src/config_definition/builtin.rs (19 lines) + +``` +1: load_builtin_configs +2: ⋮---- +3: (registry: &ConfigRegistry) +4: ⋮---- +5: load_agent_from_embedded +6: ⋮---- +7: (contents: &[u8]) +8: ⋮---- +9: load_stage_from_embedded +10: ⋮---- +11: (contents: &[u8]) +12: ⋮---- +13: load_flow_from_embedded +14: ⋮---- +15: (contents: &[u8]) +16: ⋮---- +17: test_load_builtin_configs +18: ⋮---- +19: () +``` + +### crates/cowork-core/src/config_definition/flow_definition.rs (269 lines) + +``` +1: FlowDefinition +2: ⋮---- +3: { +4: +5: pub id: String, +6: +7: pub name: String, +8: +9: pub description: Option, +10: +11: pub version: Option, +12: +13: +14: pub stages: Vec, +15: +16: +17: #[serde(default)] +18: pub start_stage: Option, +19: +20: +21: #[serde(default)] +22: pub global_hooks: Vec, +23: +24: +25: #[serde(default)] +26: pub config: FlowConfig, +27: +28: +29: #[serde(default)] +30: pub tags: Vec, +31: +32: +33: #[serde(default)] +34: pub metadata: HashMap, +35: +36: +37: #[serde(default)] +38: #[serde(skip_serializing_if = "is_false")] +39: pub is_builtin: bool, +40: } +41: ⋮---- +42: is_false +43: ⋮---- +44: (value: &bool) +45: ⋮---- +46: StageReference +47: ⋮---- +48: { +49: +50: pub stage_id: String, +51: +52: pub alias: Option, +53: +54: #[serde(default)] +55: pub overrides: StageOverrides, +56: +57: #[serde(default)] +58: pub condition: Option, +59: +60: pub on_success: Option, +61: +62: pub on_failure: Option, +63: } +64: ⋮---- +65: StageOverrides +66: ⋮---- +67: { +68: +69: pub needs_confirmation: Option, +70: +71: #[serde(default)] +72: pub hooks: Vec, +73: +74: pub timeout_secs: Option, +75: +76: #[serde(default)] +77: pub skip: bool, +78: } +79: ⋮---- +80: GlobalHookConfig +81: ⋮---- +82: { +83: +84: pub integration_id: String, +85: +86: pub points: Vec, +87: +88: #[serde(default)] +89: pub blocking: bool, +90: +91: #[serde(default = "default_global_timeout")] +92: pub timeout_secs: u32, +93: } +94: ⋮---- +95: default_global_timeout +96: ⋮---- +97: () +98: ⋮---- +99: FlowConfig +100: ⋮---- +101: { +102: +103: #[serde(default = "default_stop_on_failure")] +104: pub stop_on_failure: bool, +105: +106: +107: pub max_total_time_secs: Option, +108: +109: +110: #[serde(default = "default_save_state")] +111: pub save_state_on_interrupt: bool, +112: +113: +114: #[serde(default)] +115: pub memory_scope: MemoryScope, +116: +117: +118: #[serde(default)] +119: pub inheritance: InheritanceConfig, +120: } +121: ⋮---- +122: FlowConfig +123: ⋮---- +124: { +125: fn default() -> Self { +126: Self { +127: stop_on_failure: true, +128: max_total_time_secs: None, +129: save_state_on_interrupt: true, +130: memory_scope: MemoryScope::default(), +131: inheritance: InheritanceConfig::default(), +132: } +133: } +134: } +135: ⋮---- +136: default_stop_on_failure +137: ⋮---- +138: () +139: ⋮---- +140: default_save_state +141: ⋮---- +142: () +143: ⋮---- +144: MemoryScope +145: ⋮---- +146: { +147: +148: Project, +149: +150: Iteration, +151: +152: #[default] +153: Merged, +154: } +155: ⋮---- +156: InheritanceConfig +157: ⋮---- +158: { +159: +160: #[serde(default)] +161: pub default_mode: InheritanceMode, +162: +163: #[serde(default)] +164: pub stage_mapping: HashMap, +165: } +166: ⋮---- +167: InheritanceConfig +168: ⋮---- +169: { +170: fn default() -> Self { +171: let mut stage_mapping = HashMap::new(); +172: stage_mapping.insert("none".to_string(), "idea".to_string()); +173: stage_mapping.insert("partial".to_string(), "idea".to_string()); +174: stage_mapping.insert("full".to_string(), "idea".to_string()); +175: +176: Self { +177: default_mode: InheritanceMode::Partial, +178: stage_mapping, +179: } +180: } +181: } +182: ⋮---- +183: InheritanceMode +184: ⋮---- +185: { +186: +187: None, +188: +189: #[default] +190: Partial, +191: +192: Full, +193: } +194: ⋮---- +195: FlowDefinition +196: ⋮---- +197: { +198: +199: pub fn new(id: impl Into, name: impl Into) -> Self { +200: Self { +201: id: id.into(), +202: name: name.into(), +203: description: None, +204: version: None, +205: stages: Vec::new(), +206: start_stage: None, +207: global_hooks: Vec::new(), +208: config: FlowConfig::default(), +209: tags: Vec::new(), +210: metadata: HashMap::new(), +211: is_builtin: false, +212: } +213: } +214: +215: +216: pub fn as_builtin(mut self) -> Self { +217: self.is_builtin = true; +218: self +219: } +220: +221: +222: pub fn with_stage(mut self, stage_id: impl Into) -> Self { +223: self.stages.push(StageReference { +224: stage_id: stage_id.into(), +225: alias: None, +226: overrides: StageOverrides::default(), +227: condition: None, +228: on_success: None, +229: on_failure: None, +230: }); +231: self +232: } +233: +234: +235: pub fn with_stage_alias(mut self, stage_id: impl Into, alias: impl Into) -> Self { +236: self.stages.push(StageReference { +237: stage_id: stage_id.into(), +238: alias: Some(alias.into()), +239: overrides: StageOverrides::default(), +240: condition: None, +241: on_success: None, +242: on_failure: None, +243: }); +244: self +245: } +246: +247: +248: pub fn start_at(mut self, stage: impl Into) -> Self { +249: self.start_stage = Some(stage.into()); +250: self +251: } +252: +253: +254: pub fn default_v3() -> Self { +255: Self::new("default", "Default Development Flow") +256: .with_stage("idea") +257: .with_stage("prd") +258: .with_stage("design") +259: .with_stage("plan") +260: .with_stage("coding") +261: .with_stage("check") +262: .with_stage("delivery") +263: .start_at("idea") +264: } +265: } +266: ⋮---- +267: test_flow_definition +268: ⋮---- +269: () +``` + +### crates/cowork-core/src/config_definition/registry.rs (653 lines) + +``` +1: get_user_config_dir +2: ⋮---- +3: () +4: ⋮---- +5: ensure_user_config_dir +6: ⋮---- +7: () +8: ⋮---- +9: ConfigRegistry +10: ⋮---- +11: { +12: +13: agents: RwLock>, +14: +15: stages: RwLock>, +16: +17: flows: RwLock>, +18: +19: integrations: RwLock>, +20: +21: default_flow: RwLock>, +22: } +23: ⋮---- +24: ConfigRegistry +25: ⋮---- +26: { +27: fn default() -> Self { +28: Self::new() +29: } +30: } +31: ⋮---- +32: ConfigRegistry +33: ⋮---- +34: { +35: +36: pub fn new() -> Self { +37: Self { +38: agents: RwLock::new(HashMap::new()), +39: stages: RwLock::new(HashMap::new()), +40: flows: RwLock::new(HashMap::new()), +41: integrations: RwLock::new(HashMap::new()), +42: default_flow: RwLock::new(None), +43: } +44: } +45: +46: +47: +48: +49: +50: +51: pub fn register_agent(&self, definition: AgentDefinition) -> Result<()> { +52: let id = definition.id.clone(); +53: let mut agents = self.agents.write().map_err(|e| anyhow::anyhow!("Lock error: {}", e))?; +54: agents.insert(id.clone(), definition); +55: tracing::debug!("Registered agent: {}", id); +56: Ok(()) +57: } +58: +59: +60: pub fn get_agent(&self, id: &str) -> Option { +61: let agents = self.agents.read().ok()?; +62: agents.get(id).cloned() +63: } +64: +65: +66: pub fn list_agents(&self) -> Vec { +67: let agents = self.agents.read().unwrap_or_else(|e| { +68: tracing::error!("Lock error: {}", e); +69: panic!("Lock error") +70: }); +71: agents.keys().cloned().collect() +72: } +73: +74: +75: pub fn unregister_agent(&self, id: &str) -> bool { +76: let mut agents = self.agents.write().unwrap_or_else(|e| { +77: tracing::error!("Lock error: {}", e); +78: panic!("Lock error") +79: }); +80: agents.remove(id).is_some() +81: } +82: +83: +84: +85: +86: +87: +88: pub fn register_stage(&self, definition: StageDefinition) -> Result<()> { +89: let id = definition.id.clone(); +90: let mut stages = self.stages.write().map_err(|e| anyhow::anyhow!("Lock error: {}", e))?; +91: stages.insert(id.clone(), definition); +92: tracing::debug!("Registered stage: {}", id); +93: Ok(()) +94: } +95: +96: +97: pub fn get_stage(&self, id: &str) -> Option { +98: let stages = self.stages.read().ok()?; +99: stages.get(id).cloned() +100: } +101: +102: +103: pub fn list_stages(&self) -> Vec { +104: let stages = self.stages.read().unwrap_or_else(|e| { +105: tracing::error!("Lock error: {}", e); +106: panic!("Lock error") +107: }); +108: stages.keys().cloned().collect() +109: } +110: +111: +112: +113: +114: +115: +116: pub fn register_flow(&self, definition: FlowDefinition) -> Result<()> { +117: let id = definition.id.clone(); +118: let mut flows = self.flows.write().map_err(|e| anyhow::anyhow!("Lock error: {}", e))?; +119: flows.insert(id.clone(), definition); +120: tracing::debug!("Registered flow: {}", id); +121: Ok(()) +122: } +123: +124: +125: pub fn get_flow(&self, id: &str) -> Option { +126: let flows = self.flows.read().ok()?; +127: flows.get(id).cloned() +128: } +129: +130: +131: pub fn list_flows(&self) -> Vec { +132: let flows = self.flows.read().unwrap_or_else(|e| { +133: tracing::error!("Lock error: {}", e); +134: panic!("Lock error") +135: }); +136: flows.keys().cloned().collect() +137: } +138: +139: +140: pub fn unregister_flow(&self, id: &str) -> bool { +141: let mut flows = self.flows.write().unwrap_or_else(|e| { +142: tracing::error!("Lock error: {}", e); +143: panic!("Lock error") +144: }); +145: flows.remove(id).is_some() +146: } +147: +148: +149: pub fn set_default_flow(&self, id: Option) -> Result<()> { +150: { +151: let mut default_flow = self.default_flow.write().map_err(|e| anyhow::anyhow!("Lock error: {}", e))?; +152: *default_flow = id.clone(); +153: } +154: +155: self.save_settings()?; +156: Ok(()) +157: } +158: +159: +160: +161: pub fn set_default_flow_without_save(&self, id: Option) { +162: if let Ok(mut default_flow) = self.default_flow.write() { +163: *default_flow = id; +164: } +165: } +166: +167: +168: pub fn get_default_flow_id(&self) -> Option { +169: let default_flow = self.default_flow.read().ok()?; +170: default_flow.clone() +171: } +172: +173: +174: pub fn get_default_flow(&self) -> Option { +175: let default_flow = self.default_flow.read().ok()?; +176: let flow_id = default_flow.as_ref()?; +177: self.get_flow(flow_id) +178: } +179: +180: +181: +182: +183: +184: +185: fn get_settings_file_path() -> Option { +186: get_user_config_dir().map(|dir| dir.join("settings.json")) +187: } +188: +189: +190: pub fn save_settings(&self) -> Result<()> { +191: let settings = Settings { +192: default_flow_id: self.get_default_flow_id(), +193: }; +194: +195: let config_dir = ensure_user_config_dir()?; +196: let file_path = config_dir.join("settings.json"); +197: let content = serde_json::to_string_pretty(&settings) +198: .with_context(|| "Failed to serialize settings")?; +199: +200: fs::write(&file_path, content) +201: .with_context(|| format!("Failed to write settings file: {:?}", file_path))?; +202: +203: tracing::debug!("Saved settings to {:?}", file_path); +204: Ok(()) +205: } +206: +207: +208: pub fn load_settings(&self) -> Result<()> { +209: if let Some(file_path) = Self::get_settings_file_path() { +210: if file_path.exists() { +211: match fs::read_to_string(&file_path) +212: .and_then(|content| serde_json::from_str::(&content) +213: .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))) +214: { +215: Ok(settings) => { +216: if let Some(flow_id) = settings.default_flow_id { +217: +218: if self.get_flow(&flow_id).is_some() { +219: let mut default_flow = self.default_flow.write() +220: .map_err(|e| anyhow::anyhow!("Lock error: {}", e))?; +221: *default_flow = Some(flow_id.clone()); +222: tracing::info!("Loaded default flow setting: {}", flow_id); +223: } else { +224: tracing::warn!("Default flow '{}' not found, ignoring setting", flow_id); +225: } +226: } +227: } +228: Err(e) => { +229: tracing::warn!("Failed to load settings: {}", e); +230: } +231: } +232: } +233: } +234: Ok(()) +235: } +236: +237: +238: +239: +240: +241: +242: pub fn register_integration(&self, definition: IntegrationDefinition) -> Result<()> { +243: let id = definition.id.clone(); +244: let mut integrations = self.integrations.write().map_err(|e| anyhow::anyhow!("Lock error: {}", e))?; +245: integrations.insert(id.clone(), definition); +246: tracing::debug!("Registered integration: {}", id); +247: Ok(()) +248: } +249: +250: +251: pub fn get_integration(&self, id: &str) -> Option { +252: let integrations = self.integrations.read().ok()?; +253: integrations.get(id).cloned() +254: } +255: +256: +257: pub fn list_integrations(&self) -> Vec { +258: let integrations = self.integrations.read().unwrap_or_else(|e| { +259: tracing::error!("Lock error: {}", e); +260: panic!("Lock error") +261: }); +262: integrations.keys().cloned().collect() +263: } +264: +265: +266: pub fn get_enabled_integrations(&self) -> Vec { +267: let integrations = self.integrations.read().unwrap_or_else(|e| { +268: tracing::error!("Lock error: {}", e); +269: panic!("Lock error") +270: }); +271: integrations.values().filter(|i| i.enabled).cloned().collect() +272: } +273: +274: +275: +276: +277: +278: +279: pub fn clear(&self) -> Result<()> { +280: { +281: let mut agents = self.agents.write().map_err(|e| anyhow::anyhow!("Lock error: {}", e))?; +282: agents.clear(); +283: } +284: { +285: let mut stages = self.stages.write().map_err(|e| anyhow::anyhow!("Lock error: {}", e))?; +286: stages.clear(); +287: } +288: { +289: let mut flows = self.flows.write().map_err(|e| anyhow::anyhow!("Lock error: {}", e))?; +290: flows.clear(); +291: } +292: { +293: let mut integrations = self.integrations.write().map_err(|e| anyhow::anyhow!("Lock error: {}", e))?; +294: integrations.clear(); +295: } +296: Ok(()) +297: } +298: +299: +300: pub fn stats(&self) -> RegistryStats { +301: RegistryStats { +302: agents: self.agents.read().map(|g| g.len()).unwrap_or(0), +303: stages: self.stages.read().map(|g| g.len()).unwrap_or(0), +304: flows: self.flows.read().map(|g| g.len()).unwrap_or(0), +305: integrations: self.integrations.read().map(|g| g.len()).unwrap_or(0), +306: } +307: } +308: +309: +310: +311: +312: +313: +314: pub fn save_agent_to_file(&self, agent: &AgentDefinition) -> Result<()> { +315: let config_dir = ensure_user_config_dir()?; +316: let agents_dir = config_dir.join("agents"); +317: fs::create_dir_all(&agents_dir) +318: .with_context(|| format!("Failed to create agents directory: {:?}", agents_dir))?; +319: +320: let file_path = agents_dir.join(format!("{}.json", agent.id)); +321: let content = serde_json::to_string_pretty(agent) +322: .with_context(|| format!("Failed to serialize agent: {}", agent.id))?; +323: +324: fs::write(&file_path, content) +325: .with_context(|| format!("Failed to write agent file: {:?}", file_path))?; +326: +327: tracing::info!("Saved agent '{}' to {:?}", agent.id, file_path); +328: Ok(()) +329: } +330: +331: +332: pub fn delete_agent_file(&self, id: &str) -> Result<()> { +333: if let Some(config_dir) = get_user_config_dir() { +334: let file_path = config_dir.join("agents").join(format!("{}.json", id)); +335: if file_path.exists() { +336: fs::remove_file(&file_path) +337: .with_context(|| format!("Failed to delete agent file: {:?}", file_path))?; +338: tracing::info!("Deleted agent file: {:?}", file_path); +339: } +340: } +341: Ok(()) +342: } +343: +344: +345: pub fn save_stage_to_file(&self, stage: &StageDefinition) -> Result<()> { +346: let config_dir = ensure_user_config_dir()?; +347: let stages_dir = config_dir.join("stages"); +348: fs::create_dir_all(&stages_dir) +349: .with_context(|| format!("Failed to create stages directory: {:?}", stages_dir))?; +350: +351: let file_path = stages_dir.join(format!("{}.json", stage.id)); +352: let content = serde_json::to_string_pretty(stage) +353: .with_context(|| format!("Failed to serialize stage: {}", stage.id))?; +354: +355: fs::write(&file_path, content) +356: .with_context(|| format!("Failed to write stage file: {:?}", file_path))?; +357: +358: tracing::info!("Saved stage '{}' to {:?}", stage.id, file_path); +359: Ok(()) +360: } +361: +362: +363: pub fn delete_stage_file(&self, id: &str) -> Result<()> { +364: if let Some(config_dir) = get_user_config_dir() { +365: let file_path = config_dir.join("stages").join(format!("{}.json", id)); +366: if file_path.exists() { +367: fs::remove_file(&file_path) +368: .with_context(|| format!("Failed to delete stage file: {:?}", file_path))?; +369: tracing::info!("Deleted stage file: {:?}", file_path); +370: } +371: } +372: Ok(()) +373: } +374: +375: +376: pub fn save_flow_to_file(&self, flow: &FlowDefinition) -> Result<()> { +377: let config_dir = ensure_user_config_dir()?; +378: let flows_dir = config_dir.join("flows"); +379: fs::create_dir_all(&flows_dir) +380: .with_context(|| format!("Failed to create flows directory: {:?}", flows_dir))?; +381: +382: let file_path = flows_dir.join(format!("{}.json", flow.id)); +383: let content = serde_json::to_string_pretty(flow) +384: .with_context(|| format!("Failed to serialize flow: {}", flow.id))?; +385: +386: fs::write(&file_path, content) +387: .with_context(|| format!("Failed to write flow file: {:?}", file_path))?; +388: +389: tracing::info!("Saved flow '{}' to {:?}", flow.id, file_path); +390: Ok(()) +391: } +392: +393: +394: pub fn delete_flow_file(&self, id: &str) -> Result<()> { +395: if let Some(config_dir) = get_user_config_dir() { +396: let file_path = config_dir.join("flows").join(format!("{}.json", id)); +397: if file_path.exists() { +398: fs::remove_file(&file_path) +399: .with_context(|| format!("Failed to delete flow file: {:?}", file_path))?; +400: tracing::info!("Deleted flow file: {:?}", file_path); +401: } +402: } +403: Ok(()) +404: } +405: +406: +407: pub fn save_integration_to_file(&self, integration: &IntegrationDefinition) -> Result<()> { +408: let config_dir = ensure_user_config_dir()?; +409: let integrations_dir = config_dir.join("integrations"); +410: fs::create_dir_all(&integrations_dir) +411: .with_context(|| format!("Failed to create integrations directory: {:?}", integrations_dir))?; +412: +413: let file_path = integrations_dir.join(format!("{}.json", integration.id)); +414: let content = serde_json::to_string_pretty(integration) +415: .with_context(|| format!("Failed to serialize integration: {}", integration.id))?; +416: +417: fs::write(&file_path, content) +418: .with_context(|| format!("Failed to write integration file: {:?}", file_path))?; +419: +420: tracing::info!("Saved integration '{}' to {:?}", integration.id, file_path); +421: Ok(()) +422: } +423: +424: +425: pub fn delete_integration_file(&self, id: &str) -> Result<()> { +426: if let Some(config_dir) = get_user_config_dir() { +427: let file_path = config_dir.join("integrations").join(format!("{}.json", id)); +428: if file_path.exists() { +429: fs::remove_file(&file_path) +430: .with_context(|| format!("Failed to delete integration file: {:?}", file_path))?; +431: tracing::info!("Deleted integration file: {:?}", file_path); +432: } +433: } +434: Ok(()) +435: } +436: +437: +438: pub fn load_user_configs(&self) -> Result { +439: let mut report = LoadUserReport::default(); +440: +441: if let Some(config_dir) = get_user_config_dir() { +442: if config_dir.exists() { +443: +444: let agents_dir = config_dir.join("agents"); +445: if agents_dir.exists() { +446: for entry in fs::read_dir(&agents_dir) +447: .with_context(|| format!("Failed to read agents directory: {:?}", agents_dir))? +448: .filter_map(|e| e.ok()) +449: .filter(|e| e.path().extension().map(|ext| ext == "json").unwrap_or(false)) +450: { +451: let path = entry.path(); +452: match fs::read_to_string(&path) +453: .and_then(|content| serde_json::from_str::(&content) +454: .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))) +455: { +456: Ok(agent) => { +457: let id = agent.id.clone(); +458: self.register_agent(agent)?; +459: report.agents_loaded += 1; +460: tracing::debug!("Loaded user agent: {} from {:?}", id, path); +461: } +462: Err(e) => { +463: report.errors.push(format!("Failed to load agent from {:?}: {}", path, e)); +464: } +465: } +466: } +467: } +468: +469: +470: let stages_dir = config_dir.join("stages"); +471: if stages_dir.exists() { +472: for entry in fs::read_dir(&stages_dir) +473: .with_context(|| format!("Failed to read stages directory: {:?}", stages_dir))? +474: .filter_map(|e| e.ok()) +475: .filter(|e| e.path().extension().map(|ext| ext == "json").unwrap_or(false)) +476: { +477: let path = entry.path(); +478: match fs::read_to_string(&path) +479: .and_then(|content| serde_json::from_str::(&content) +480: .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))) +481: { +482: Ok(stage) => { +483: let id = stage.id.clone(); +484: self.register_stage(stage)?; +485: report.stages_loaded += 1; +486: tracing::debug!("Loaded user stage: {} from {:?}", id, path); +487: } +488: Err(e) => { +489: report.errors.push(format!("Failed to load stage from {:?}: {}", path, e)); +490: } +491: } +492: } +493: } +494: +495: +496: let flows_dir = config_dir.join("flows"); +497: if flows_dir.exists() { +498: for entry in fs::read_dir(&flows_dir) +499: .with_context(|| format!("Failed to read flows directory: {:?}", flows_dir))? +500: .filter_map(|e| e.ok()) +501: .filter(|e| e.path().extension().map(|ext| ext == "json").unwrap_or(false)) +502: { +503: let path = entry.path(); +504: match fs::read_to_string(&path) +505: .and_then(|content| serde_json::from_str::(&content) +506: .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))) +507: { +508: Ok(flow) => { +509: let id = flow.id.clone(); +510: +511: if let Some(existing) = self.get_flow(&id) { +512: if existing.is_builtin { +513: tracing::debug!( +514: "Skipping user flow '{}' - builtin preset with same ID exists", +515: id +516: ); +517: continue; +518: } +519: } +520: self.register_flow(flow)?; +521: report.flows_loaded += 1; +522: tracing::debug!("Loaded user flow: {} from {:?}", id, path); +523: } +524: Err(e) => { +525: report.errors.push(format!("Failed to load flow from {:?}: {}", path, e)); +526: } +527: } +528: } +529: } +530: +531: +532: let integrations_dir = config_dir.join("integrations"); +533: if integrations_dir.exists() { +534: for entry in fs::read_dir(&integrations_dir) +535: .with_context(|| format!("Failed to read integrations directory: {:?}", integrations_dir))? +536: .filter_map(|e| e.ok()) +537: .filter(|e| e.path().extension().map(|ext| ext == "json").unwrap_or(false)) +538: { +539: let path = entry.path(); +540: match fs::read_to_string(&path) +541: .and_then(|content| serde_json::from_str::(&content) +542: .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))) +543: { +544: Ok(integration) => { +545: let id = integration.id.clone(); +546: self.register_integration(integration)?; +547: report.integrations_loaded += 1; +548: tracing::debug!("Loaded user integration: {} from {:?}", id, path); +549: } +550: Err(e) => { +551: report.errors.push(format!("Failed to load integration from {:?}: {}", path, e)); +552: } +553: } +554: } +555: } +556: +557: +558: if let Err(e) = self.load_settings() { +559: tracing::warn!("Failed to load settings: {}", e); +560: } +561: +562: +563: +564: if self.get_default_flow_id().is_none() { +565: if self.get_flow("default").is_some() { +566: if let Err(e) = self.set_default_flow(Some("default".to_string())) { +567: tracing::warn!("Failed to set default flow: {}", e); +568: } else { +569: tracing::info!("Set 'default' as the default flow (first time initialization)"); +570: } +571: } +572: } +573: } +574: } else { +575: +576: +577: if let Some(flow_id) = self.get_default_flow_id() { +578: if let Err(e) = self.set_default_flow(Some(flow_id)) { +579: tracing::warn!("Failed to save initial default flow setting: {}", e); +580: } +581: } +582: } +583: +584: Ok(report) +585: } +586: } +587: ⋮---- +588: RegistryStats +589: ⋮---- +590: { +591: pub agents: usize, +592: pub stages: usize, +593: pub flows: usize, +594: pub integrations: usize, +595: } +596: ⋮---- +597: LoadUserReport +598: ⋮---- +599: { +600: pub agents_loaded: usize, +601: pub stages_loaded: usize, +602: pub flows_loaded: usize, +603: pub integrations_loaded: usize, +604: pub errors: Vec, +605: } +606: ⋮---- +607: LoadReport +608: ⋮---- +609: { +610: pub agents_loaded: usize, +611: pub stages_loaded: usize, +612: pub flows_loaded: usize, +613: pub integrations_loaded: usize, +614: pub default_flow_set: bool, +615: pub errors: Vec, +616: } +617: ⋮---- +618: LoadReport +619: ⋮---- +620: { +621: pub fn total_loaded(&self) -> usize { +622: self.agents_loaded + self.stages_loaded + self.flows_loaded + self.integrations_loaded +623: } +624: +625: pub fn has_errors(&self) -> bool { +626: !self.errors.is_empty() +627: } +628: } +629: ⋮---- +630: Settings +631: ⋮---- +632: { +633: +634: pub default_flow_id: Option, +635: } +636: ⋮---- +637: Settings +638: ⋮---- +639: { +640: fn default() -> Self { +641: Self { +642: default_flow_id: None, +643: } +644: } +645: } +646: ⋮---- +647: global_registry +648: ⋮---- +649: () +650: ⋮---- +651: test_registry_operations +652: ⋮---- +653: () +``` + +### crates/cowork-core/src/domain/iteration.rs (319 lines) + +``` +1: Iteration +2: ⋮---- +3: { +4: pub id: String, +5: pub number: u32, +6: pub title: String, +7: pub description: String, +8: +9: +10: pub base_iteration_id: Option, +11: pub inheritance: InheritanceMode, +12: +13: +14: pub status: IterationStatus, +15: pub started_at: DateTime, +16: pub completed_at: Option>, +17: pub current_stage: Option, +18: pub completed_stages: Vec, +19: +20: +21: pub artifacts: Artifacts, +22: } +23: ⋮---- +24: Iteration +25: ⋮---- +26: { +27: pub fn create_genesis(project: &Project, title: String, description: String) -> Self { +28: let now = Utc::now(); +29: Self { +30: id: format!("iter-{}-{}", project.next_iteration_number(), now.timestamp()), +31: number: project.next_iteration_number(), +32: title, +33: description, +34: base_iteration_id: None, +35: inheritance: InheritanceMode::None, +36: status: IterationStatus::Draft, +37: started_at: now, +38: completed_at: None, +39: current_stage: None, +40: completed_stages: Vec::new(), +41: artifacts: Artifacts::default(), +42: } +43: } +44: +45: pub fn create_evolution( +46: project: &Project, +47: title: String, +48: description: String, +49: base_iteration_id: String, +50: inheritance: InheritanceMode, +51: ) -> Self { +52: let now = Utc::now(); +53: Self { +54: id: format!("iter-{}-{}", project.next_iteration_number(), now.timestamp()), +55: number: project.next_iteration_number(), +56: title, +57: description, +58: base_iteration_id: Some(base_iteration_id), +59: inheritance, +60: status: IterationStatus::Draft, +61: started_at: now, +62: completed_at: None, +63: current_stage: None, +64: completed_stages: Vec::new(), +65: artifacts: Artifacts::default(), +66: } +67: } +68: +69: pub fn start(&mut self) { +70: self.status = IterationStatus::Running; +71: self.started_at = Utc::now(); +72: } +73: +74: pub fn pause(&mut self) { +75: self.status = IterationStatus::Paused; +76: } +77: +78: pub fn resume(&mut self) { +79: self.status = IterationStatus::Running; +80: } +81: +82: pub fn complete(&mut self) { +83: self.status = IterationStatus::Completed; +84: self.completed_at = Some(Utc::now()); +85: self.current_stage = None; +86: +87: +88: +89: +90: self.finalize_completed_stages(); +91: } +92: +93: +94: +95: fn finalize_completed_stages(&mut self) { +96: +97: let flow_stages = self.get_flow_stages(); +98: +99: +100: let artifact_stages = [ +101: ("idea", &self.artifacts.idea), +102: ("prd", &self.artifacts.prd), +103: ("design", &self.artifacts.design), +104: ("plan", &self.artifacts.plan), +105: ("coding", &self.artifacts.coding), +106: ("delivery", &self.artifacts.delivery), +107: ]; +108: +109: for (stage_name, artifact) in artifact_stages { +110: if artifact.is_some() && !self.completed_stages.contains(&stage_name.to_string()) { +111: self.completed_stages.push(stage_name.to_string()); +112: } +113: } +114: +115: +116: +117: if self.completed_stages.is_empty() && !flow_stages.is_empty() { +118: self.completed_stages = flow_stages; +119: } +120: } +121: +122: +123: fn get_flow_stages(&self) -> Vec { +124: use crate::config_definition::registry::global_registry; +125: +126: if let Some(flow) = global_registry().get_default_flow() { +127: flow.stages.iter().map(|s| s.stage_id.clone()).collect() +128: } else { +129: +130: vec![ +131: "idea".to_string(), +132: "prd".to_string(), +133: "design".to_string(), +134: "plan".to_string(), +135: "coding".to_string(), +136: "check".to_string(), +137: "delivery".to_string(), +138: ] +139: } +140: } +141: +142: pub fn fail(&mut self) { +143: self.status = IterationStatus::Failed; +144: +145: +146: } +147: +148: pub fn set_stage(&mut self, stage: impl Into) { +149: self.current_stage = Some(stage.into()); +150: } +151: +152: pub fn complete_stage(&mut self, stage: impl Into, artifact_path: Option) { +153: let stage_name = stage.into(); +154: self.completed_stages.push(stage_name.clone()); +155: +156: +157: let path = artifact_path.unwrap_or_default(); +158: match stage_name.as_str() { +159: "idea" => self.artifacts.idea = Some(path), +160: "prd" => self.artifacts.prd = Some(path), +161: "design" => self.artifacts.design = Some(path), +162: "plan" => self.artifacts.plan = Some(path), +163: "coding" => self.artifacts.coding = Some(path), +164: "delivery" => self.artifacts.delivery = Some(path), +165: _ => {} +166: } +167: } +168: +169: +170: +171: pub fn determine_start_stage(&self) -> String { +172: let stage_mapping = self.get_stage_mapping_from_flow(); +173: +174: let mode_key = match self.inheritance { +175: InheritanceMode::None => "none", +176: InheritanceMode::Full => "full", +177: InheritanceMode::Partial => "partial", +178: }; +179: +180: stage_mapping +181: .get(mode_key) +182: .cloned() +183: .unwrap_or_else(|| "idea".to_string()) +184: } +185: +186: +187: fn get_stage_mapping_from_flow(&self) -> std::collections::HashMap { +188: use crate::config_definition::registry::global_registry; +189: +190: if let Some(flow) = global_registry().get_default_flow() { +191: flow.config.inheritance.stage_mapping +192: } else { +193: +194: let mut mapping = std::collections::HashMap::new(); +195: mapping.insert("none".to_string(), "idea".to_string()); +196: mapping.insert("partial".to_string(), "idea".to_string()); +197: mapping.insert("full".to_string(), "idea".to_string()); +198: mapping +199: } +200: } +201: +202: pub fn to_summary(&self) -> super::IterationSummary { +203: super::IterationSummary { +204: id: self.id.clone(), +205: number: self.number, +206: title: self.title.clone(), +207: status: self.status, +208: completed_stages: self.completed_stages.clone(), +209: created_at: self.started_at, +210: } +211: } +212: } +213: ⋮---- +214: InheritanceMode +215: ⋮---- +216: { +217: None, +218: Full, +219: Partial, +220: } +221: ⋮---- +222: InheritanceMode +223: ⋮---- +224: { +225: fn default() -> Self { +226: InheritanceMode::Full +227: } +228: } +229: ⋮---- +230: Artifacts +231: ⋮---- +232: { +233: pub idea: Option, +234: pub prd: Option, +235: pub design: Option, +236: pub plan: Option, +237: pub coding: Option, +238: pub delivery: Option, +239: } +240: ⋮---- +241: Artifacts +242: ⋮---- +243: { +244: pub fn get(&self, stage: &str) -> Option<&String> { +245: match stage { +246: "idea" => self.idea.as_ref(), +247: "prd" => self.prd.as_ref(), +248: "design" => self.design.as_ref(), +249: "plan" => self.plan.as_ref(), +250: "coding" => self.coding.as_ref(), +251: "delivery" => self.delivery.as_ref(), +252: _ => None, +253: } +254: } +255: +256: pub fn set(&mut self, stage: &str, path: String) { +257: match stage { +258: "idea" => self.idea = Some(path), +259: "prd" => self.prd = Some(path), +260: "design" => self.design = Some(path), +261: "plan" => self.plan = Some(path), +262: "coding" => self.coding = Some(path), +263: "delivery" => self.delivery = Some(path), +264: _ => {} +265: } +266: } +267: } +268: ⋮---- +269: create_test_project +270: ⋮---- +271: () +272: ⋮---- +273: test_create_genesis_iteration +274: ⋮---- +275: () +276: ⋮---- +277: test_create_evolution_iteration +278: ⋮---- +279: () +280: ⋮---- +281: test_iteration_status_transitions +282: ⋮---- +283: () +284: ⋮---- +285: test_iteration_fail +286: ⋮---- +287: () +288: ⋮---- +289: test_set_and_complete_stage +290: ⋮---- +291: () +292: ⋮---- +293: test_determine_start_stage_none_mode +294: ⋮---- +295: () +296: ⋮---- +297: test_determine_start_stage_partial_mode +298: ⋮---- +299: () +300: ⋮---- +301: test_determine_start_stage_full_mode +302: ⋮---- +303: () +304: ⋮---- +305: test_artifacts_get_set +306: ⋮---- +307: () +308: ⋮---- +309: test_to_summary +310: ⋮---- +311: () +312: ⋮---- +313: test_inheritance_mode_default +314: ⋮---- +315: () +316: ⋮---- +317: test_inheritance_mode_serde +318: ⋮---- +319: () +``` + +### crates/cowork-gui/src/hooks/useAppEvents.ts (432 lines) + +``` +1: function useAppEvents(userInput: string, setUserInput: (input: string) => void) { +2: const { message } = AntApp.useApp(); +3: const listenersRegistered = useRef(false); +4: +5: +6: const { +7: loadProject, +8: loadIterations, +9: setCurrentIteration, +10: updateCurrentIterationStatus, +11: setIsExecuting +12: } = useProjectStore(); +13: +14: +15: const { +16: setMessages, +17: clearMessages, +18: setPMMessages, +19: clearPMMessages, +20: setProcessing, +21: setCurrentAgent, +22: setCurrentStage, +23: setInputRequest, +24: setPmProcessing, +25: submitInput, +26: loadPMWelcomeMessage +27: } = useAgentStore(); +28: +29: +30: const { +31: commandPaletteVisible, +32: setActiveView, +33: setCommandPaletteVisible, +34: setActiveArtifactTab, +35: triggerArtifactsRefresh, +36: triggerCodeRefresh, +37: triggerMemoryRefresh, +38: triggerKnowledgeRefresh +39: } = useUIStore(); +40: +41: useEffect(() => { +42: const setupListeners = async () => { +43: if (listenersRegistered.current) return; +44: listenersRegistered.current = true; +45: +46: +47: const listenerPromises = [ +48: +49: listen('iteration_created', () => { +50: loadProject(); +51: message.success('Iteration created'); +52: }), +53: +54: listen('iteration_started', (event) => { +55: const iterationId = event.payload as string; +56: setProcessing(true); +57: setIsExecuting(true); +58: updateCurrentIterationStatus('Running'); +59: setActiveView('chat'); +60: message.info('Iteration started'); +61: }), +62: +63: listen('iteration_continued', (event) => { +64: const iterationId = event.payload as string; +65: setProcessing(true); +66: setIsExecuting(true); +67: updateCurrentIterationStatus('Running'); +68: setActiveView('chat'); +69: message.info('Iteration continued'); +70: }), +71: +72: listen('iteration_retrying', (event) => { +73: const iterationId = event.payload as string; +74: setProcessing(true); +75: setIsExecuting(true); +76: updateCurrentIterationStatus('Running'); +77: setActiveView('chat'); +78: message.info('Retrying iteration...'); +79: }), +80: +81: listen('iteration_completed', (event) => { +82: const iterationId = event.payload as string; +83: setProcessing(false); +84: setIsExecuting(false); +85: setCurrentAgent(null); +86: setCurrentStage(null); +87: setInputRequest(null); +88: updateCurrentIterationStatus('Completed'); +89: loadProject(); +90: triggerMemoryRefresh(); +91: triggerKnowledgeRefresh(); +92: clearPMMessages(); +93: setActiveView('chat'); +94: loadPMWelcomeMessage(iterationId); +95: message.success('Iteration completed'); +96: }), +97: +98: listen('iteration_failed', (event) => { +99: const [, error] = event.payload as [string, string]; +100: setProcessing(false); +101: setIsExecuting(false); +102: setCurrentAgent(null); +103: setCurrentStage(null); +104: setInputRequest(null); +105: updateCurrentIterationStatus('Failed'); +106: loadProject(); +107: message.error('Iteration failed: ' + error); +108: }), +109: +110: +111: listen('agent_event', (event) => { +112: const { content, agent_name, message_type, stage_name, level } = event.payload as { +113: content?: string; +114: agent_name?: string; +115: message_type?: string; +116: stage_name?: string; +117: level?: string; +118: }; +119: +120: if (agent_name) setCurrentAgent(agent_name); +121: if (stage_name) setCurrentStage(stage_name); +122: if (!content) return; +123: +124: setMessages((prev) => { +125: const lastMsg = prev[prev.length - 1]; +126: const isThinking = message_type === 'thinking'; +127: +128: if (isThinking) { +129: if ( +130: lastMsg?.type === 'thinking' && +131: (lastMsg as ThinkingMessage).isStreaming && +132: (lastMsg as ThinkingMessage).agentName === agent_name +133: ) { +134: return [ +135: ...prev.slice(0, -1), +136: { +137: ...lastMsg, +138: content: (lastMsg as ThinkingMessage).content + content +139: } as ChatMessage +140: ]; +141: } +142: return [ +143: ...prev, +144: { +145: type: 'thinking', +146: content, +147: agentName: agent_name || 'AI Agent', +148: stageName: stage_name, +149: isStreaming: true, +150: isExpanded: false, +151: timestamp: new Date().toISOString() +152: } as ThinkingMessage +153: ] as ChatMessage[]; +154: } else { +155: if ( +156: lastMsg?.type === 'agent' && +157: (lastMsg as { isStreaming?: boolean }).isStreaming && +158: (lastMsg as { agentName?: string }).agentName === agent_name +159: ) { +160: return [ +161: ...prev.slice(0, -1), +162: { +163: ...lastMsg, +164: content: (lastMsg as { content: string }).content + content +165: } as ChatMessage +166: ]; +167: } +168: return [ +169: ...prev, +170: { +171: type: 'agent', +172: content, +173: agentName: agent_name || 'AI Agent', +174: stageName: stage_name, +175: level, +176: isStreaming: true, +177: timestamp: new Date().toISOString() +178: } as ChatMessage +179: ]; +180: } +181: }); +182: }), +183: +184: +185: listen('agent_streaming', (event) => { +186: const { content, agent_name, is_thinking, is_first, is_last } = event.payload as { +187: content?: string; +188: agent_name?: string; +189: is_thinking?: boolean; +190: is_first?: boolean; +191: is_last?: boolean; +192: }; +193: +194: +195: if (agent_name === 'PM Agent') { +196: if (is_last && !content) { +197: setPMMessages((prev) => { +198: const lastMsg = prev[prev.length - 1]; +199: if (lastMsg?.type === 'pm_agent') { +200: return [...prev.slice(0, -1), { ...lastMsg } as PMAgentMessage]; +201: } +202: return prev; +203: }); +204: setPmProcessing(false); +205: return; +206: } +207: +208: if (!content) return; +209: +210: setPMMessages((prev) => { +211: const lastMsg = prev[prev.length - 1]; +212: if ( +213: is_first || +214: !lastMsg || +215: lastMsg.type !== 'pm_agent' || +216: !(lastMsg as PMAgentMessage & { isStreaming?: boolean }).isStreaming +217: ) { +218: return [ +219: ...prev, +220: { +221: type: 'pm_agent' as const, +222: content, +223: isStreaming: !is_last, +224: timestamp: new Date().toISOString() +225: } as PMAgentMessage & { isStreaming?: boolean } +226: ]; +227: } +228: return [ +229: ...prev.slice(0, -1), +230: { +231: ...lastMsg, +232: content: (lastMsg as PMAgentMessage).content + content, +233: isStreaming: !is_last +234: } as PMAgentMessage & { isStreaming?: boolean } +235: ]; +236: }); +237: return; +238: } +239: +240: +241: if (!content) return; +242: const msgType = is_thinking ? 'thinking' : 'agent'; +243: +244: setMessages((prev) => { +245: const lastMsg = prev[prev.length - 1]; +246: if ( +247: lastMsg?.type === msgType && +248: (lastMsg as { isStreaming?: boolean }).isStreaming && +249: (lastMsg as { agentName?: string }).agentName === agent_name +250: ) { +251: return [ +252: ...prev.slice(0, -1), +253: { +254: ...lastMsg, +255: content: (lastMsg as { content: string }).content + content, +256: isStreaming: !is_last +257: } as ChatMessage +258: ]; +259: } +260: return [ +261: ...prev, +262: { +263: type: msgType, +264: content, +265: agentName: agent_name || 'AI Agent', +266: isStreaming: !is_last, +267: isExpanded: false, +268: timestamp: new Date().toISOString() +269: } as ChatMessage +270: ]; +271: }); +272: }), +273: +274: +275: listen('tool_call', (event) => { +276: const { tool_name, arguments: args, agent_name } = event.payload as { +277: tool_name: string; +278: arguments: Record; +279: agent_name?: string; +280: }; +281: setMessages((prev) => [ +282: ...prev, +283: { +284: type: 'tool_call', +285: toolName: tool_name, +286: arguments: args, +287: agentName: agent_name || 'AI Agent', +288: timestamp: new Date().toISOString() +289: } as ChatMessage +290: ]); +291: }), +292: +293: listen('tool_result', (event) => { +294: const { tool_name, result, success, agent_name } = event.payload as { +295: tool_name: string; +296: result: string; +297: success: boolean; +298: agent_name?: string; +299: }; +300: setMessages((prev) => [ +301: ...prev, +302: { +303: type: 'tool_result', +304: toolName: tool_name, +305: result, +306: success, +307: agentName: agent_name || 'AI Agent', +308: timestamp: new Date().toISOString() +309: } as ChatMessage +310: ]); +311: }), +312: +313: +314: listen('pm_actions', (event) => { +315: const { actions } = event.payload as { actions: PMAction[] }; +316: setPMMessages((prev) => { +317: const lastMsg = prev[prev.length - 1]; +318: if (lastMsg?.type === 'pm_agent') { +319: return [ +320: ...prev.slice(0, -1), +321: { +322: ...lastMsg, +323: actions: [...((lastMsg as PMAgentMessage).actions || []), ...actions] +324: } as PMAgentMessage +325: ]; +326: } +327: return prev; +328: }); +329: }), +330: +331: +332: listen('input_request', async (event) => { +333: const [requestId, prompt, options] = event.payload as [string, string, InputOption[]]; +334: updateCurrentIterationStatus('Paused'); +335: +336: const artifactMatch = prompt.match(/\[ARTIFACT_TYPE:(\w+)\]$/); +337: if (artifactMatch) { +338: const artifactType = artifactMatch[1]; +339: const cleanPrompt = prompt.replace(/\[ARTIFACT_TYPE:\w+\]$/, '').trim(); +340: +341: await loadIterations(); +342: const latestIterations = useProjectStore.getState().iterations; +343: if (latestIterations && latestIterations.length > 0) { +344: const latestIteration = latestIterations[latestIterations.length - 1]; +345: const fullIteration = await API.iteration.get(latestIteration.id); +346: setCurrentIteration(fullIteration); +347: } +348: +349: setInputRequest({ +350: requestId, +351: prompt: cleanPrompt, +352: options, +353: isArtifactConfirmation: true, +354: artifactType +355: }); +356: } else { +357: setInputRequest({ requestId, prompt, options }); +358: } +359: setUserInput(''); +360: }), +361: +362: +363: listen('project_loaded', async () => { +364: setProcessing(false); +365: setCurrentAgent(null); +366: setInputRequest(null); +367: clearMessages(); +368: setCurrentIteration(null); +369: await loadProject(); +370: setActiveView('iterations'); +371: message.success('Project loaded'); +372: }), +373: +374: listen('project_initialized', async () => { +375: setProcessing(false); +376: setCurrentAgent(null); +377: setInputRequest(null); +378: clearMessages(); +379: setCurrentIteration(null); +380: await loadProject(); +381: setActiveView('iterations'); +382: message.success('Project initialized'); +383: }), +384: +385: +386: listen('knowledge_regeneration_completed', () => { +387: triggerKnowledgeRefresh(); +388: message.success('Knowledge updated'); +389: }), +390: +391: listen<[string, string]>('knowledge_regeneration_failed', (event) => { +392: const [iterationId, error] = event.payload; +393: console.error('[App] Knowledge regeneration failed:', iterationId, error); +394: message.error('Knowledge generation failed: ' + error); +395: }), +396: ]; +397: +398: +399: await Promise.all(listenerPromises); +400: +401: +402: +403: try { +404: const hasOpenProject = await API.workspace.hasOpen(); +405: if (hasOpenProject) { +406: console.log('[App] Detected open project on startup, loading project...'); +407: await loadProject(); +408: setActiveView('iterations'); +409: } +410: } catch (error) { +411: console.error('[App] Failed to check for open project:', error); +412: } +413: }; +414: +415: setupListeners(); +416: +417: +418: const handleKeyDown = (e: KeyboardEvent) => { +419: if ((e.ctrlKey || e.metaKey) && e.key === 'k') { +420: e.preventDefault(); +421: setCommandPaletteVisible(!commandPaletteVisible); +422: } +423: }; +424: +425: window.addEventListener('keydown', handleKeyDown); +426: return () => window.removeEventListener('keydown', handleKeyDown); +427: }, []); +428: +429: return { +430: +431: }; +432: } +``` + +### crates/cowork-gui/src/types/config.ts (291 lines) + +``` +1: AgentType +2: ⋮---- +3: "simple" | { loop: { max_iterations?: number } } +4: ⋮---- +5: ModelConfig +6: ⋮---- +7: { +8: model_id?: string; +9: temperature?: number; +10: max_tokens?: number; +11: top_p?: number; +12: } +13: ⋮---- +14: ToolReference +15: ⋮---- +16: { +17: tool_id: string; +18: config?: Record; +19: } +20: ⋮---- +21: IncludeContentsMode +22: ⋮---- +23: "none" | "all" | { selected: string[] } +24: ⋮---- +25: AgentDefinition +26: ⋮---- +27: { +28: id: string; +29: name: string; +30: description?: string; +31: version?: string; +32: agent_type: AgentType; +33: instruction: string; +34: tools: ToolReference[]; +35: skills: string[]; +36: model: ModelConfig; +37: include_contents: IncludeContentsMode; +38: tags: string[]; +39: metadata: Record; +40: } +41: ⋮---- +42: StageType +43: ⋮---- +44: | "idea" +45: | "prd" +46: | "design" +47: | "plan" +48: | "coding" +49: | "check" +50: | "delivery" +51: ⋮---- +52: HookPoint +53: ⋮---- +54: | "pre_execute" +55: | "post_execute" +56: | "pre_confirmation" +57: | "post_confirmation" +58: | "on_failure" +59: ⋮---- +60: HookConfig +61: ⋮---- +62: { +63: integration_id: string; +64: point: HookPoint; +65: action: string; +66: params?: Record; +67: blocking?: boolean; +68: timeout_secs?: number; +69: on_failure?: "ignore" | "warn" | "abort"; +70: } +71: ⋮---- +72: ArtifactConfig +73: ⋮---- +74: { +75: save_path: string; +76: format: string; +77: include_metadata?: boolean; +78: } +79: ⋮---- +80: StageRetryConfig +81: ⋮---- +82: { +83: max_retries: number; +84: backoff_ms?: number; +85: retry_on?: string[]; +86: } +87: ⋮---- +88: StageDefinition +89: ⋮---- +90: { +91: id: string; +92: name: string; +93: description?: string; +94: stage_type: StageType; +95: agent_id: string; +96: needs_confirmation?: boolean; +97: confirmation_prompt?: string; +98: hooks: HookConfig[]; +99: artifacts?: Record; +100: retry?: StageRetryConfig; +101: timeout_secs?: number; +102: tags: string[]; +103: } +104: ⋮---- +105: MemoryScope +106: ⋮---- +107: "project" | "iteration" | "merged" +108: ⋮---- +109: InheritanceMode +110: ⋮---- +111: "none" | "partial" | "full" +112: ⋮---- +113: InheritanceConfig +114: ⋮---- +115: { +116: default_mode: InheritanceMode; +117: stage_mapping: Record; +118: } +119: ⋮---- +120: FlowConfig +121: ⋮---- +122: { +123: stop_on_failure: boolean; +124: max_total_time_secs?: number; +125: save_state_on_interrupt: boolean; +126: memory_scope: MemoryScope; +127: inheritance: InheritanceConfig; +128: } +129: ⋮---- +130: StageOverrides +131: ⋮---- +132: { +133: needs_confirmation?: boolean; +134: hooks: HookConfig[]; +135: timeout_secs?: number; +136: skip: boolean; +137: } +138: ⋮---- +139: StageReference +140: ⋮---- +141: { +142: stage_id: string; +143: alias?: string; +144: overrides: StageOverrides; +145: condition?: string; +146: on_success?: string; +147: on_failure?: string; +148: } +149: ⋮---- +150: GlobalHookConfig +151: ⋮---- +152: { +153: integration_id: string; +154: points: HookPoint[]; +155: blocking: boolean; +156: timeout_secs: number; +157: } +158: ⋮---- +159: FlowDefinition +160: ⋮---- +161: { +162: id: string; +163: name: string; +164: description?: string; +165: version?: string; +166: stages: StageReference[]; +167: start_stage?: string; +168: global_hooks: GlobalHookConfig[]; +169: config: FlowConfig; +170: tags: string[]; +171: metadata: Record; +172: +173: is_builtin?: boolean; +174: } +175: ⋮---- +176: SkillInfo +177: ⋮---- +178: { +179: id: string; +180: name: string; +181: description: string; +182: tags: string[]; +183: body: string; +184: } +185: ⋮---- +186: IntegrationType +187: ⋮---- +188: | "rest_api" +189: | "webhook" +190: | "message_queue" +191: | "database" +192: ⋮---- +193: AuthType +194: ⋮---- +195: | "none" +196: | "api_key" +197: | "bearer_token" +198: | "basic_auth" +199: | "oauth2" +200: ⋮---- +201: CredentialSource +202: ⋮---- +203: "env" | "config" | "prompt" +204: ⋮---- +205: AuthConfig +206: ⋮---- +207: { +208: auth_type: AuthType; +209: credential_source: CredentialSource; +210: credential_key?: string; +211: additional_headers?: Record; +212: } +213: ⋮---- +214: ConnectionConfig +215: ⋮---- +216: { +217: base_url?: string; +218: timeout_secs?: number; +219: retry_count?: number; +220: retry_delay_ms?: number; +221: } +222: ⋮---- +223: IntegrationEvent +224: ⋮---- +225: | "on_stage_start" +226: | "on_stage_complete" +227: | "on_flow_start" +228: | "on_flow_complete" +229: | "on_error" +230: ⋮---- +231: IntegrationDefinition +232: ⋮---- +233: { +234: id: string; +235: name: string; +236: description?: string; +237: integration_type: IntegrationType; +238: connection: ConnectionConfig; +239: auth: AuthConfig; +240: events: IntegrationEvent[]; +241: enabled: boolean; +242: metadata: Record; +243: } +244: ⋮---- +245: ValidationIssue +246: ⋮---- +247: { +248: path: string; +249: message: string; +250: severity: "error" | "warning"; +251: } +252: ⋮---- +253: ValidationResult +254: ⋮---- +255: { +256: valid: boolean; +257: issues: ValidationIssue[]; +258: } +259: ⋮---- +260: ConfigRegistryState +261: ⋮---- +262: { +263: agents: Record; +264: stages: Record; +265: flows: Record; +266: skills: SkillInfo[]; +267: integrations: Record; +268: default_flow_id?: string; +269: } +270: ⋮---- +271: BuiltinInstruction +272: ⋮---- +273: { +274: id: string; +275: name: string; +276: description: string; +277: content: string; +278: } +279: ⋮---- +280: InstructionType +281: ⋮---- +282: "builtin" | "file" | "inline" +283: ⋮---- +284: ToolInfo +285: ⋮---- +286: { +287: id: string; +288: name: string; +289: category: string; +290: description: string; +291: } +``` + +### crates/cowork-gui/src-tauri/Cargo.toml (50 lines) + +``` +1: [package] +2: name = "cowork-gui" +3: version.workspace = true +4: edition.workspace = true +5: authors.workspace = true +6: license.workspace = true +7: description = "Cowork Forge GUI" +8: +9: [lib] +10: name = "cowork_gui_lib" +11: crate-type = ["staticlib", "cdylib", "rlib"] +12: +13: [build-dependencies] +14: tauri-build = { version = "2.5.5", features = [] } +15: +16: [dependencies] +17: tauri = { version = "2.10.2", features = [] } +18: tauri-plugin-opener = "2.5.3" +19: tauri-plugin-dialog = "2.6.0" +20: serde = { version = "1", features = ["derive"] } +21: serde_json = "1" +22: tokio = { version = "1", features = ["sync", "full"] } +23: tokio-stream = "0.1" +24: async-trait = "0.1" +25: chrono = "0.4" +26: uuid = { workspace = true } +27: anyhow = "1" +28: thiserror = "2" +29: futures = "0.3" +30: +31: tiny_http = "0.12" +32: attohttpc = "0.30" +33: urlencoding = "2.1" +34: +35: lazy_static = "1.5" +36: +37: tracing = "0.1" +38: +39: dirs = { workspace = true } +40: +41: sys-locale = "0.3" +42: +43: cowork-core = { path = "../../cowork-core" } +44: +45: adk-core = { workspace = true } +46: adk-runner = { workspace = true } +47: adk-session = { workspace = true } +48: adk-skill = { workspace = true } +49: adk-tool = { workspace = true, features = ["http-transport"] } +50: which = "8.0.0" +``` + +### litho.docs/en/2.Architecture.md (1224 lines) + +```` +1: # System Architecture Documentation: Cowork Forge +2: +3: **Document Version**: 1.0 +4: **Generation Time**: 2026-02-14 05:10:16 (UTC) +5: **Classification**: Architecture Overview +6: **Target Audience**: Software Architects, Senior Developers, DevOps Engineers +7: +8: --- +9: +10: ## 1. Architecture Overview +11: +12: ### 1.1 Design Philosophy +13: +14: Cowork Forge embodies a **Hybrid AI-Human Collaborative Architecture** designed to orchestrate autonomous software development while maintaining human oversight through structured intervention points. The architecture is built upon three foundational principles: +15: +16: 1. **Cognitive Augmentation**: The system acts as an extension of human developer cognition, preserving institutional knowledge across iterations while automating mechanical development tasks through AI agents. +17: +18: 2. **Structured Autonomy**: Rather than unconstrained AI generation, the system enforces a rigorous 7-stage pipeline (Idea→PRD→Design→Plan→Coding→Check→Delivery) with validation gates, ensuring quality assurance through the Actor-Critic pattern. +19: +20: 3. **Interface Agnosticism**: The core domain logic remains pure and independent of interface concerns, enabling simultaneous support for automation-focused CLI workflows and interactive GUI experiences through the Ports and Adapters pattern. +21: +22: ### 1.2 Core Architecture Patterns +23: +24: #### Hexagonal Architecture (Ports and Adapters) +25: The system implements a strict **Hexagonal Architecture** with the `cowork-core` crate at the center containing pure domain logic. All external concerns (LLM APIs, file systems, user interfaces) connect through well-defined ports: +26: +27: - **Inbound Ports**: `InteractiveBackend` trait enabling CLI and GUI implementations +28: - **Outbound Ports**: Repository abstractions for persistence, LLM client interfaces for AI integration +29: - **Adapters**: Concrete implementations in `cowork-cli`, `cowork-gui`, and infrastructure modules +30: +31: #### Domain-Driven Design (DDD) +32: The architecture follows DDD tactical patterns: +33: - **Aggregates**: `Project` (root), `Iteration`, `ProjectMemory` enforcing consistency boundaries +34: - **Value Objects**: `Artifacts`, `StageResult`, `InheritanceMode` (Full/Partial/None) +35: - **Domain Services**: Pipeline orchestration, inheritance analysis, change scope detection +36: - **Repositories**: `ProjectStore`, `IterationStore`, `MemoryStore` abstracting persistence +37: +38: #### Event-Driven Architecture (GUI Layer) +39: The Tauri-based desktop application implements an event-driven architecture: +40: - **Asymmetric Communication**: Commands (invoke) for requests, Events (emit) for streaming responses +41: - **Real-time Streaming**: LLM token streams, process logs, and agent activities flow through Tauri's event system +42: - **State Synchronization**: React frontend maintains local state while backend emits state change events +43: +44: #### Actor-Critic Pattern +45: Each pipeline stage implements the Actor-Critic pattern: +46: - **Actor**: Generates artifacts (code, documents, plans) based on instructions +47: - **Critic**: Validates quality, checks constraints, suggests improvements +48: - **Feedback Loop**: Human input regenerates Actor outputs with critique context +49: +50: ### 1.3 Technology Stack Overview +51: +52: | Layer | Technology | Purpose | Architectural Role | +53: |-------|-----------|---------|-------------------| +54: | **Core Domain** | Rust + Tokio | Async runtime for pipeline execution | Domain logic isolation | +55: | **AI Orchestration** | adk-rust | Agent framework and tool ecosystem | AI agent lifecycle management | +56: | **LLM Integration** | OpenAI-compatible APIs | Code generation and reasoning | Infrastructure adapter | +57: | **Rate Limiting** | Custom Semaphore + Delay | 30 req/min compliance | Cross-cutting concern | +58: | **CLI Interface** | clap + dialoguer | Command parsing and terminal UI | Primary adapter | +59: | **GUI Backend** | Tauri | Desktop runtime and system integration | Secondary adapter | +60: | **GUI Frontend** | React 18 + Ant Design | Component-based interactive UI | Presentation layer | +61: | **Persistence** | JSON + serde | Schema evolution and storage | Repository implementation | +62: | **Security** | Path validation + Sandboxing | Workspace containment | Security boundary | +63: +64: --- +65: +66: ## 2. System Context (C4 Level 1) +67: +68: ### 2.1 System Positioning and Value +69: +70: Cowork Forge operates as a **Local-First AI Development Environment** positioned between traditional IDEs and cloud-based AI coding assistants. Unlike cloud solutions, it maintains complete data locality while providing structured AI orchestration that simple code completion tools cannot achieve. +71: +72: **Core Value Propositions**: +73: - **Continuity**: Memory system preserves architectural decisions across development sessions +74: - **Consistency**: Enforced 7-stage pipeline ensures systematic development methodology +75: - **Control**: Human-in-the-Loop gates at critical stages prevent AI hallucinations from propagating +76: - **Flexibility**: Dual interface support accommodates both automation scripts and exploratory development +77: +78: ### 2.2 User Roles and Scenarios +79: +80: ```mermaid +81: flowchart TB +82: subgraph Users["User Ecosystem"] +83: ID["Individual Developers
Automation-focused"] +84: DT["Development Teams
Standardization-focused"] +85: AD["AI-Augmented Developers
Exploration-focused"] +86: end +87: +88: subgraph Value["Value Delivery"] +89: RP["Rapid Prototyping
Idea → Code in minutes"] +90: KM["Knowledge Management
Cross-iteration memory"] +91: QC["Quality Control
Actor-Critic validation"] +92: end +93: +94: ID -->|CLI Automation| RP +95: DT -->|Standardized Workflows| KM +96: AD -->|Interactive GUI| QC +97: ``` +98: +99: **Primary User Archetypes**: +100: +101: 1. **Individual Developers**: Utilize CLI for rapid prototyping, leveraging automation to generate boilerplate and scaffolding from natural language descriptions. +102: +103: 2. **Development Teams**: Employ the memory system to maintain architectural standards across multiple projects, using inheritance modes to evolve existing codebases systematically. +104: +105: 3. **AI-Augmented Developers**: Prefer GUI interface for visibility into AI decision-making, utilizing real-time streaming and HITL validation to guide the development process interactively. +106: +107: ### 2.3 External System Interactions +108: +109: ```mermaid +110: flowchart TB +111: subgraph CoworkForge["Cowork Forge System Boundary"] +112: Core["Core Domain Engine
(Rust)"] +113: CLI["CLI Interface"] +114: GUI["Desktop GUI
(Tauri + React)"] +115: end +116: +117: subgraph External["External Systems"] +118: LLM["LLM Provider APIs
(OpenAI-compatible)"] +119: FS["Local File System"] +120: Shell["Shell/Command Executor"] +121: Editor["External Editor
(System Default)"] +122: DevServer["Development Server
(Vite/etc.)"] +123: MCP["MCP Servers
(Tavily/DeepWiki)"] +124: end +125: +126: User["Developer/User"] -->|Commands| CLI +127: User -->|Interacts| GUI +128: GUI <-->|Events/Commands| Core +129: CLI -->|Invokes| Core +130: +131: Core -->|API Calls
Rate: 30 req/min| LLM +132: Core -->|Validated I/O| FS +133: Core -->|Process Spawning| Shell +134: Core -->|Edit Invocation| Editor +135: Core -->|Process Management| DevServer +136: Core -->|Remote Tool Queries| MCP +137: ``` +138: +139: **External Dependencies**: +140: +141: - **LLM Provider APIs**: OpenAI-compatible endpoints for agent reasoning. Interactions are rate-limited (30 requests/minute) with concurrency control (single semaphore) to manage API quotas and costs. +142: +143: - **Local File System**: Primary persistence mechanism for projects, iterations, and memory. Access is constrained through workspace validation to prevent path traversal attacks. +144: +145: - **Shell/Command Executor**: Used for project validation (dependency installation, builds, tests) and development server management. Commands are sanitized and executed within project workspace boundaries. +146: +147: - **External Editor**: System default editor invoked during HITL flows for content review and modification (e.g., vim, VS Code, nano). +148: +149: - **Development Server**: User-provided server processes (Vite, Webpack dev server) managed through ProcessRunner for live preview capabilities. +150: +151: - **MCP Servers**: Provide external AI capabilities via Model Context Protocol (e.g., Tavily web search, DeepWiki code documentation queries). Configured through `config.toml` under `[mcp]` section, with automatic initialization and tool injection into all Agents at startup. +152: +153: ### 2.4 System Boundary Definition +154: +155: **In-Scope Components**: +156: - Core domain logic (iterations, projects, memory aggregates) +157: - 7-stage AI agent pipeline with stage executors +158: - Agent instruction library (~2000 lines of prompt engineering) +159: - Tool ecosystem (40+ ADK tools: file, data, validation, HITL, memory, deployment, legacy analysis) with MCP Remote Tool Integration (Tavily, DeepWiki, etc.) +160: - Persistence layer with JSON-based project/iteration stores +161: - CLI command interface with argument parsing +162: - Tauri-based GUI with React frontend +163: - Real-time process runner for development servers +164: - Rate-limited LLM client factory +165: - Cross-platform desktop application shell +166: +167: **Out-of-Scope Components**: +168: - Third-party LLM training infrastructure +169: - Version control system integration (Git operations) +170: - External package registry management (npm, crates.io) +171: - Cloud deployment platforms and CI/CD pipelines +172: - Remote collaboration features (real-time multi-user editing) +173: +174: --- +175: +176: ## 3. Container View (C4 Level 2) +177: +178: ### 3.1 Domain Module Division +179: +180: Cowork Forge is structured as a **Multi-Crate Rust Workspace** with clear domain boundaries following DDD strategic design: +181: +182: ```mermaid +183: flowchart TB +184: subgraph Workspace["Cowork Forge Workspace"] +185: subgraph Presentation["Presentation Layer"] +186: CLI["cowork-cli
Command Router"] +187: GUI["cowork-gui
Tauri + React"] +188: end +189: +190: subgraph Application["Application Layer"] +191: CLIBackend["CLI Backend
InteractiveBackend Impl"] +192: GUIBackend["GUI Backend
Tauri Commands"] +193: ProjectRunner["Project Runner
Process Management"] +194: end +195: +196: subgraph Domain["Domain Layer (cowork-core)"] +197: subgraph CoreDomains["Core Business Domains"] +198: ProjectDomain["Project Domain
Aggregate Root"] +199: IterationDomain["Iteration Domain
Lifecycle Management"] +200: PipelineDomain["Pipeline Domain
7-Stage Orchestration"] +201: MemoryDomain["Memory Domain
Knowledge Management"] +202: end +203: +204: subgraph SupportingDomains["Supporting Domains"] +205: ToolsDomain["Tools Domain
40+ ADK Tools + MCP Remote Integration"] +206: InteractionDomain["Interaction Domain
Backend Abstraction"] +207: end +208: +209: subgraph Infrastructure["Infrastructure Layer"] +210: Persistence["Persistence
JSON Stores"] +211: LLMIntegration["LLM Integration
Rate-Limited Client"] +212: Security["Security
Path Validation"] +213: end +214: end +215: end +216: +217: CLI -->|implements| CLIBackend +218: GUI -->|invokes| GUIBackend +219: CLIBackend -->|uses| InteractionDomain +220: GUIBackend -->|uses| InteractionDomain +221: +222: InteractionDomain -->|drives| PipelineDomain +223: PipelineDomain -->|manages| IterationDomain +224: PipelineDomain -->|uses| ToolsDomain +225: IterationDomain -->|belongs to| ProjectDomain +226: +227: ToolsDomain -->|access| Persistence +228: ToolsDomain -->|query| MemoryDomain +229: PipelineDomain -->|calls| LLMIntegration +230: ToolsDomain -->|validated by| Security +231: ``` +232: +233: ### 3.2 Container Architecture +234: +235: #### Core Domain Container (`cowork-core`) +236: The heart of the system containing pure business logic with no external dependencies: +237: +238: - **Domain Layer**: Entities (`Project`, `Iteration`, `Memory`), Value Objects (`InheritanceMode`), and Domain Services (Pipeline orchestration) +239: - **Pipeline Layer**: Stage trait implementations, Stage Executor, and instruction library +240: - **Tool Layer**: 30+ ADK tools organized by function (File, Data, HITL, Memory, Validation) +241: - **Tool Layer**: 40+ ADK tools (File, Data, HITL, Memory, Validation, Deployment, Legacy analysis) with MCP Remote Tool Integration (Tavily, DeepWiki, etc.) +242: +243: #### CLI Container (`cowork-cli`) +244: Thin adapter implementing terminal-based interaction: +245: - **Clap Parser**: Command-line argument parsing and routing +246: - **InteractiveBackend Impl**: Terminal-based HITL with dialoguer prompts and colored output +247: - **Command Handlers**: Thin wrappers delegating to core domain +248: +249: #### GUI Container (`cowork-gui`) +250: Tauri-based desktop application with React frontend: +251: - **Tauri Backend**: Rust commands exposing core functionality via IPC +252: - **React Frontend**: 8-panel interface (Projects, Iterations, Editor, Runner, Memory, Knowledge) +253: - **Event System**: Real-time bidirectional communication for streaming and HITL +254: +255: ### 3.3 Storage Design +256: +257: The system implements **JSON-First Persistence** for portability and version control compatibility: +258: +259: ```mermaid +260: flowchart LR +261: subgraph Storage["Local Storage (.cowork-v2/)"] +262: ProjectStore[(Project Store
project.json)] +263: IterationStore[(Iteration Store
iterations/*/)] +264: MemoryStore[(Memory Store
memory.json)] +265: Workspace[(Workspace Directories
artifacts/)] +266: end +267: +268: subgraph Domain["Domain Layer"] +269: Project["Project Aggregate"] +270: Iteration["Iteration Entity"] +271: Memory["ProjectMemory Aggregate"] +272: end +273: +274: Persistence["Persistence Layer
(Stores)"] -->|manages| ProjectStore +275: Persistence -->|manages| IterationStore +276: Persistence -->|manages| MemoryStore +277: +278: Project -->|persists to| ProjectStore +279: Iteration -->|persists to| IterationStore +280: Memory -->|persists to| MemoryStore +281: +282: Iteration -->|generates artifacts| Workspace +283: ``` +284: +285: **Storage Characteristics**: +286: - **Project Store**: Metadata, tech stack detection, iteration summaries +287: - **Iteration Store**: Stage artifacts (idea.md, prd.md, design.md, plan.md, code files), execution state +288: - **Memory Store**: Architectural decisions, patterns, issues, learnings across iterations +289: - **Workspace**: File system artifacts generated by AI agents during pipeline execution +290: +291: ### 3.4 Inter-Domain Communication +292: +293: **Synchronous Communication**: +294: - **Command Pattern**: CLI/GUI invoke domain operations through command handlers +295: - **Repository Pattern**: Domain aggregates persist through store abstractions +296: - **Trait Abstraction**: `InteractiveBackend` trait enables polymorphic user interaction +297: +298: **Asynchronous Communication** (GUI only): +299: - **Event Streaming**: Tauri backend emits events (`agent_event`, `tool_call`, `input_request`) for real-time UI updates +300: - **Process Streaming**: Development server logs stream via `project_log` events +301: - **Backpressure Handling**: Tokio channels manage streaming LLM responses without blocking +302: +303: --- +304: +305: ## 4. Component View (C4 Level 3) +306: +307: ### 4.1 Core Functional Components +308: +309: #### Pipeline Orchestration Component +310: +311: ```mermaid +312: flowchart TB +313: subgraph Pipeline["Pipeline Domain"] +314: Controller["Pipeline Controller
(mod.rs)"] +315: Executor["Stage Executor
(stage_executor.rs)"] +316: Context["Pipeline Context
(Execution State)"] +317: +318: subgraph Stages["Stage Implementations"] +319: Idea["Idea Stage
(Actor + Critic)"] +320: PRD["PRD Stage
(Actor + Critic)"] +321: Design["Design Stage"] +322: Plan["Plan Stage"] +323: Coding["Coding Stage"] +324: Check["Check Stage"] +325: Delivery["Delivery Stage"] +326: end +327: +328: Controller -->|initializes| Context +329: Controller -->|executes| Executor +330: Executor -->|runs| Stages +331: Stages -->|update| Context +332: end +333: +334: subgraph Agents["Agent System"] +335: Assistant["Iterative Assistant
(adk-rust)"] +336: Instructions["Instruction Library
(~2000 lines)"] +337: end +338: +339: subgraph Tools["Tool Ecosystem"] +340: FileTools["File Tools
(6 tools)"] +341: DataTools["Data Tools
(12 tools)"] +342: HITL["HITL Tools
(Content/File Review)"] +343: Memory["Memory Tools
(6 tools)"] +344: end +345: +346: Executor -->|creates| Assistant +347: Assistant -->|uses| Instructions +348: Assistant -->|calls| Tools +349: Assistant -->|streams| LLM["LLM API"] +350: ``` +351: +352: **Component Responsibilities**: +353: - **Pipeline Controller**: Manages iteration lifecycle, stage sequencing, and error handling +354: - **Stage Executor**: Bridges domain logic with adk-rust framework, manages agent lifecycle +355: - **Stage Implementations**: Seven concrete strategies following the Strategy pattern, each with specific instructions and artifact generation logic +356: - **Iterative Assistant**: AI agent wrapper handling streaming, tool calls, and human interaction +357: +358: #### Memory Management Component +359: +360: ```mermaid +361: flowchart TB +362: subgraph MemoryDomain["Memory Domain"] +363: ProjectMemory["ProjectMemory
(Aggregate Root)"] +364: IterationKnowledge["IterationKnowledge
(Entity)"] +365: +366: subgraph Inheritance["Inheritance System"] +367: Full["Full Mode
(Artifacts + Code)"] +368: Partial["Partial Mode
(Code Only)"] +369: None["None Mode
(Fresh Start)"] +370: end +371: +372: QueryEngine["Query Engine
(Fuzzy Search)"] +373: end +374: +375: subgraph Tools["Memory Tools"] +376: Query["QueryMemoryTool"] +377: Save["Save*Tool
(Insight/Issue/Learning)"] +378: Promote["Promote*Tool
(To Decision/Pattern)"] +379: Knowledge["Knowledge Generation"] +380: end +381: +382: ProjectMemory -->|contains| IterationKnowledge +383: IterationKnowledge -->|uses| Inheritance +384: ProjectMemory -->|queried by| QueryEngine +385: +386: Tools -->|operate on| MemoryDomain +387: Knowledge -->|generates| IterationKnowledge +388: ``` +389: +390: **Memory Inheritance Modes**: +391: - **Full**: Complete artifact and code transfer for major refactoring (continues from any stage) +392: - **Partial**: Code-only inheritance for incremental feature development (typically starts at Coding stage) +393: - **None**: Fresh iteration without historical baggage (starts at Idea stage) +394: +395: ### 4.2 Technical Support Components +396: +397: #### LLM Integration with Rate Limiting +398: +399: ```mermaid +400: flowchart LR +401: subgraph LLM["LLM Integration Domain"] +402: Config["LlmConfig
(TOML/Env)"] +403: Factory["Client Factory"] +404: +405: subgraph RateLimiting["Rate Limiting Decorator"] +406: Semaphore["Global Semaphore
(Concurrency=1)"] +407: Delay["2s Delay
(30 req/min)"] +408: end +409: +410: Client["OpenAI Client
(adk-rust)"] +411: end +412: +413: Config -->|creates| Factory +414: Factory -->|wraps with| RateLimiting +415: RateLimiting -->|uses| Client +416: Client -->|calls| API["LLM Provider API"] +417: +418: Pipeline -->|requests| Factory +419: ``` +420: +421: **Rate Limiting Strategy**: +422: - **Token Bucket Alternative**: Uses semaphore (concurrency=1) combined with fixed delay (2 seconds) to enforce 30 requests/minute +423: - **Global Scope**: Rate limiter is shared across all pipeline stages to prevent API quota exhaustion +424: - **Backpressure**: Requests block until capacity available, ensuring compliance without dropping requests +425: +426: #### Security and Validation Layer +427: +428: ```mermaid +429: flowchart TB +430: subgraph Security["Security Components"] +431: PathValidation["Path Validation
(UNC Normalization)"] +432: WorkspaceContainment["Workspace Containment
(Project Boundary)"] +433: CommandSanitization["Command Sanitization"] +434: end +435: +436: subgraph Runtime["Runtime Security"] +437: Analyzer["Runtime Analyzer
(Behavior Monitoring)"] +438: SecurityCheck["Security Checks
(runtime_security.rs)"] +439: end +440: +441: FileTools["File Tools"] -->|validated by| PathValidation +442: FileTools -->|constrained by| WorkspaceContainment +443: ProcessRunner["Process Runner"] -->|sanitized by| CommandSanitization +444: +445: Pipeline -->|monitored by| Analyzer +446: Tools -->|enforced by| SecurityCheck +447: ``` +448: +449: ### 4.3 Component Interaction Relationships +450: +451: **Critical Path Dependencies**: +452: 1. **Pipeline → Tools**: Pipeline executor injects tool set into AI agents; tools access file system and memory +453: 2. **Tools → Persistence**: All data modifications flow through repository pattern to JSON stores +454: 3. **Pipeline → Interaction**: HITL gates suspend execution until InteractiveBackend returns user input +455: 4. **GUI ↔ Backend**: Tauri commands trigger domain operations; events stream progress back to React +456: +457: **Decoupling Mechanisms**: +458: - **Trait-Based Backend**: `InteractiveBackend` trait decouples pipeline from specific UI implementations +459: - **Dependency Injection**: Stage executor receives tool dependencies rather than constructing them +460: - **Event-Driven Updates**: GUI components react to events rather than polling, reducing coupling +461: +462: --- +463: +464: ## 5. Key Processes +465: +466: ### 5.1 Genesis Iteration Creation Flow +467: +468: The primary workflow transforming natural language ideas into complete software projects: +469: +470: ```mermaid +471: flowchart TD +472: Start([User Provides Idea]) --> Entry{Entry Point} +473: Entry -->|CLI: cowork iter| CLI[CLI Parser] +474: Entry -->|GUI: Create Button| GUI[React Frontend] +475: +476: CLI --> Init[Initialize Pipeline Context] +477: GUI --> Init +478: +479: Init --> Stage1[Idea Stage
Capture Requirements
Output: idea.md] +480: Stage1 -->|HITL Gate| Confirm1{User Confirmation} +481: Confirm1 -->|Edit/Feedback| Stage1 +482: Confirm1 -->|Approve| Stage2 +483: +484: Stage2[PRD Stage
Actor/Critic Pattern
Output: prd.md] -->|HITL Gate| Confirm2{User Confirmation} +485: Confirm2 -->|Edit/Feedback| Stage2 +486: Confirm2 -->|Approve| Stage3 +487: +488: Stage3[Design Stage
System Architecture
Output: design.md] -->|HITL Gate| Confirm3{User Confirmation} +489: Confirm3 -->|Edit/Feedback| Stage3 +490: Confirm3 -->|Approve| Stage4 +491: +492: Stage4[Plan Stage
Task Generation
Output: plan.md] -->|HITL Gate| Confirm4{User Confirmation} +493: Confirm4 -->|Edit/Feedback| Stage4 +494: Confirm4 -->|Approve| Stage5 +495: +496: Stage5[Coding Stage
Code Implementation
Output: Source Files] -->|HITL Gate| Confirm5{User Confirmation} +497: Confirm5 -->|Edit/Feedback| Stage5 +498: Confirm5 -->|Approve| Stage6 +499: +500: Stage6[Check Stage
Quality Validation
Output: check_report.md] -->|HITL Gate| Confirm6{User Confirmation} +501: Confirm6 -->|Edit/Feedback| Stage6 +502: Confirm6 -->|Approve| Stage7 +503: +504: Stage7[Delivery Stage
Final Report
Output: delivery_report.md] --> Deploy[Deploy to Project Root] +505: +506: Deploy --> Knowledge[Generate Knowledge Snapshot
Decisions, Patterns, Tech Stack] +507: Knowledge --> Persist[Persist to Memory Store] +508: Persist --> Complete([Iteration Complete]) +509: +510: style Start fill:#e1f5fe +511: style Complete fill:#c8e6c9 +512: style Stage1 fill:#fff3e0 +513: style Stage2 fill:#fff3e0 +514: style Stage3 fill:#fff3e0 +515: style Stage4 fill:#fff3e0 +516: style Stage5 fill:#fff3e0 +517: style Stage6 fill:#fff3e0 +518: style Stage7 fill:#fff3e0 +519: ``` +520: +521: **Process Characteristics**: +522: - **Stage Gates**: Each stage includes optional HITL confirmation; user can pass, edit, or request regeneration with feedback +523: - **Artifact Accumulation**: Each stage generates persistent artifacts (markdown documents, code files) visible in workspace +524: - **Actor-Critic Validation**: PRD and Design stages employ dual-agent validation where Critic agents review Actor outputs +525: - **Knowledge Capture**: Upon completion, system extracts architectural decisions, patterns, and tech stack for future iterations +526: +527: ### 5.2 Evolution Iteration Flow +528: +529: Enables incremental development by building upon previous iterations with intelligent change scope analysis: +530: +531: ```mermaid +532: flowchart TD +533: Start([Request Evolution]) --> Analyze[Analyze Change Description
Keyword-Based NLP] +534: +535: Analyze --> Scope{Determine Scope} +536: Scope -->|Architectural| Full[Full Inheritance
Artifacts + Code] +537: Scope -->|Feature| Partial[Partial Inheritance
Code Only] +538: Scope -->|Minor| None[No Inheritance
Fresh Start] +539: +540: Full --> StageSelect{Select Start Stage} +541: Partial --> StageSelect +542: None --> StageSelect +543: +544: StageSelect -->|Redesign| Idea[Idea Stage] +545: StageSelect -->|Requirements| PRD[PRD Stage] +546: StageSelect -->|Architecture| Design[Design Stage] +547: StageSelect -->|Implementation| Plan[Plan Stage] +548: +549: Idea --> Load[Load Base Knowledge
From Previous Iteration] +550: PRD --> Load +551: Design --> Load +552: Plan --> Load +553: +554: Load --> Resume[Resume Pipeline
From Selected Stage] +555: Resume --> Continue[Execute Remaining Stages] +556: Continue --> Complete([Evolution Complete]) +557: +558: style Start fill:#e1f5fe +559: style Complete fill:#c8e6c9 +560: ``` +561: +562: **Inheritance Strategy**: +563: - **Change Scope Analysis**: NLP keyword matching determines optimal starting stage ("redesign" → Idea, "add feature" → Coding, "fix bug" → Check) +564: - **Knowledge Transfer**: Base knowledge (decisions, patterns, issues) from parent iteration loads into agent context +565: - **Workspace Management**: Inheritance mode determines which files copy to new iteration workspace +566: +567: ### 5.3 GUI Real-Time Execution Flow +568: +569: Event-driven architecture for interactive development monitoring: +570: +571: ```mermaid +572: sequenceDiagram +573: participant UI as React Frontend +574: participant TC as Tauri Commands +575: participant EB as Event Backend +576: participant Core as Core Pipeline +577: participant LLM as LLM API +578: +579: UI->>TC: Invoke iteration execution +580: TC->>Core: Start iteration process +581: +582: loop Execution Loop +583: Core->>EB: Emit agent streaming event with chunk +584: EB->>UI: Update via event listener +585: +586: Core->>LLM: Send request +587: LLM-->>Core: Receive streamed response +588: +589: Core->>EB: Emit tool call event with data +590: EB->>UI: Update frontend UI +591: +592: alt Hitl Pause Triggered +593: Core->>EB: Emit input request event +594: EB->>UI: Show modal dialog +595: UI->>TC: Submit user input +596: TC->>Core: Resume execution process +597: end +598: end +599: +600: Core->>EB: Emit iteration complete event +601: EB->>UI: Update final status +602: ``` +603: +604: **Event Types**: +605: - `agent_event`: High-level agent messages (stage transitions, completion) +606: - `agent_streaming`: Token-by-token LLM output for real-time display +607: - `tool_call`/`tool_result`: Tool execution visualization +608: - `input_request`: HITL modal trigger with oneshot channel response +609: - `project_log`: Development server stdout/stderr streaming +610: +611: ### 5.4 Human-in-the-Loop Validation Flow +612: +613: Structured human oversight at critical decision points: +614: +615: ```mermaid +616: flowchart TD +617: Start([Agent Requires
Confirmation]) --> Interface{Interface Type} +618: +619: Interface -->|CLI| CLIDisplay[Display Content
Terminal Preview] +620: Interface -->|GUI| GUIEmit[Emit input_request
Tauri Event] +621: +622: CLIDisplay --> CLIInput[Collect Input
pass/edit/feedback] +623: GUIEmit --> GUIModal[Show Modal
React Component] +624: GUIModal --> GUIInput[Collect Response
Invoke Command] +625: +626: CLIInput --> Process{Process Action} +627: GUIInput --> Process +628: +629: Process -->|Pass| Resume[Resume Pipeline] +630: Process -->|Edit| Editor[Open External Editor
Wait for Changes] +631: Process -->|Feedback| Regenerate[Execute with Feedback
Agent Regenerates] +632: +633: Editor --> Detect{Changes?} +634: Detect -->|Yes| Resume +635: Detect -->|No| Resume +636: +637: Regenerate --> Review{Approved?} +638: Review -->|Yes| Resume +639: Review -->|No| Regenerate +640: +641: Resume --> Complete([Continue]) +642: +643: style Start fill:#e1f5fe +644: style Complete fill:#c8e6c9 +645: ``` +646: +647: --- +648: +649: ## 6. Technical Implementation +650: +651: ### 6.1 Core Module Implementation +652: +653: #### Pipeline Controller (`crates/cowork-core/src/pipeline/mod.rs`) +654: +655: The pipeline implements the **Template Method Pattern** with stage-specific implementations: +656: +657: ```rust +658: // Conceptual structure based on architecture analysis +659: pub struct PipelineController { +660: context: PipelineContext, +661: backend: Arc, +662: } +663: +664: impl PipelineController { +665: pub async fn execute_genesis_iteration(&mut self, idea: &str) -> Result { +666: // Stage sequence: Idea -> PRD -> Design -> Plan -> Coding -> Check -> Delivery +667: let stages = vec![ +668: Box::new(IdeaStage) as Box, +669: Box::new(PRDStage), +670: // ... remaining stages +671: ]; +672: +673: for stage in stages { +674: let result = self.execute_stage_with_hitl(stage).await?; +675: self.context.update(result); +676: } +677: +678: self.generate_knowledge_snapshot().await +679: } +680: +681: async fn execute_stage_with_hitl(&self, stage: Box) -> Result { +682: let result = stage.execute(&self.context).await?; +683: +684: if stage.requires_confirmation() { +685: match self.backend.request_confirmation(&result).await? { +686: UserAction::Pass => Ok(result), +687: UserAction::Edit => self.open_editor_and_reload().await, +688: UserAction::Feedback(feedback) => { +689: stage.execute_with_feedback(&self.context, feedback).await +690: } +691: } +692: } else { +693: Ok(result) +694: } +695: } +696: } +697: ``` +698: +699: #### InteractiveBackend Trait (`crates/cowork-core/src/interaction/mod.rs`) +700: +701: Defines the port for user interaction, enabling hexagonal architecture: +702: +703: ```rust +704: #[async_trait] +705: pub trait InteractiveBackend: Send + Sync { +706: // Display methods +707: async fn display_message(&self, msg: &str); +708: async fn display_stream(&self, content: &str); +709: async fn display_tool_call(&self, tool_name: &str, params: &Value); +710: +711: // HITL methods +712: async fn request_confirmation(&self, content: &Artifact) -> Result; +713: async fn request_input(&self, prompt: &str) -> Result; +714: +715: // Progress tracking +716: async fn update_progress(&self, stage: &str, progress: f32); +717: } +718: ``` +719: +720: Implementations: +721: - **CLI Backend**: Uses `dialoguer` for prompts, `console` for colors, terminal tables for iteration listings +722: - **Tauri Backend**: Uses `AppHandle` for event emission, `tokio::sync::oneshot` for HITL response channels +723: +724: ### 6.2 Key Algorithm Design +725: +726: #### Change Scope Analysis Algorithm +727: +728: Determines optimal starting stage for evolution iterations: +729: +730: ```rust +731: fn analyze_change_scope(description: &str) -> Stage { +732: let lower = description.to_lowercase(); +733: +734: if lower.contains("redesign") || lower.contains("architectural") { +735: Stage::Idea +736: } else if lower.contains("requirement") || lower.contains("feature spec") { +737: Stage::PRD +738: } else if lower.contains("design") || lower.contains("component") { +739: Stage::Design +740: } else if lower.contains("implement") || lower.contains("code") { +741: Stage::Coding +742: } else { +743: Stage::Idea // Default to full reconsideration +744: } +745: } +746: ``` +747: +748: #### Memory Query with Fuzzy Matching +749: +750: Supports three query scopes with keyword filtering: +751: +752: ```rust +753: pub fn query_memories( +754: &self, +755: scope: QueryScope, +756: category: Option, +757: keywords: &[String], +758: limit: usize, +759: ) -> Vec { +760: let candidates = match scope { +761: QueryScope::Project => self.load_project_memories(), +762: QueryScope::Iteration(id) => self.load_iteration_memories(id), +763: QueryScope::Latest => self.merge_latest_memories(), +764: }; +765: +766: candidates +767: .filter(|m| category.map_or(true, |c| m.category == c)) +768: .filter(|m| keywords.iter().any(|k| m.content.contains(k))) +769: .take(limit) +770: .collect() +771: } +772: ``` +773: +774: ### 6.3 Data Structure Design +775: +776: #### Project Aggregate +777: +778: ```rust +779: pub struct Project { +780: pub id: ProjectId, +781: pub name: String, +782: pub project_type: ProjectType, +783: pub tech_stack: TechStack, +784: pub created_at: DateTime, +785: pub iterations: Vec, +786: pub current_iteration: Option, +787: } +788: +789: pub struct Iteration { +790: pub id: IterationId, +791: pub project_id: ProjectId, +792: pub mode: IterationMode, // Genesis or Evolution +793: pub inheritance: InheritanceMode, +794: pub status: IterationStatus, // Draft, Running, Completed, Failed +795: pub stages: HashMap, +796: pub workspace_path: PathBuf, +797: } +798: ``` +799: +800: #### Memory Aggregate +801: +802: ```rust +803: pub struct ProjectMemory { +804: pub project_id: ProjectId, +805: pub decisions: Vec, +806: pub patterns: Vec, +807: pub insights: Vec, +808: pub issues: Vec, +809: pub tech_stack: TechStackKnowledge, +810: } +811: +812: pub struct IterationKnowledge { +813: pub iteration_id: IterationId, +814: pub summary: String, +815: pub promoted_memories: Vec, // Elevated to project level +816: pub stage_insights: HashMap>, +817: } +818: ``` +819: +820: ### 6.4 Performance Optimization Strategies +821: +822: #### Async Pipeline Execution +823: - **Tokio Runtime**: Multi-threaded async execution for I/O bound operations (LLM calls, file I/O) +824: - **Streaming Architecture**: LLM responses stream directly to UI without buffering, reducing latency perception +825: - **Concurrent Tool Calls**: Independent tool executions run concurrently where dependencies permit +826: +827: #### Memory Management +828: - **Lazy Loading**: Project memories load on-demand rather than at startup +829: - **Incremental Persistence**: Only modified entities serialize to disk, not entire aggregates +830: - **Workspace Isolation**: Each iteration has isolated workspace preventing file contention +831: +832: #### Rate Limiting Optimization +833: - **Token Bucket Alternative**: Fixed delay + semaphore simpler than token bucket for single-user scenarios +834: - **Request Batching**: Where possible, multiple small requests coalesce (though limited by chat-based LLM APIs) +835: +836: --- +837: +838: ## 7. Deployment Architecture +839: +840: ### 7.1 Runtime Environment Requirements +841: +842: **System Requirements**: +843: - **Operating System**: Cross-platform (Windows 10+, macOS 12+, Linux Ubuntu 20.04+) +844: - **Runtime**: Rust runtime (Tokio) for core, Node.js (for Tauri build only) +845: - **Memory**: Minimum 4GB RAM (8GB recommended for large projects) +846: - **Storage**: 500MB application + project workspace (typically 50-200MB per project) +847: - **Network**: Internet connection required for LLM API access (OpenAI-compatible endpoints) +848: +849: **Dependencies**: +850: - **External**: LLM API key (OpenAI, Anthropic, or compatible) +851: - **System**: Default text editor (vim, nano, VS Code, etc.), Node.js (for generated projects) +852: - **Optional**: Development servers (Vite, Webpack) for preview functionality +853: +854: ### 7.2 Deployment Topology +855: +856: ```mermaid +857: flowchart TB +858: subgraph Desktop["User Workstation"] +859: subgraph App["Cowork Forge Application"] +860: Core["Core Domain
Rust Binary"] +861: GUI["Tauri Runtime
WebView2/WebKit"] +862: CLI["CLI Binary"] +863: end +864: +865: subgraph Storage["Local Storage"] +866: Projects["Project Workspaces
~/.cowork-v2/"] +867: Config["Configuration
~/.config/cowork/"] +868: end +869: end +870: +871: subgraph External["External Services"] +872: LLM["LLM Provider APIs
(OpenAI/Anthropic)"] +873: Git["Version Control
(User-managed)"] +874: Registry["Package Registries
(npm/crates.io)"] +875: end +876: +877: Core -->|File I/O| Storage +878: GUI -->|Embeds| Core +879: CLI -->|Links| Core +880: +881: Core -->|HTTPS| LLM +882: Projects -->|User manages| Git +883: Projects -->|User manages| Registry +884: ``` +885: +886: **Distribution Model**: +887: - **Desktop Application**: Tauri-based installer (.dmg for macOS, .msi for Windows, .AppImage/.deb for Linux) +888: - **CLI Tool**: Cargo installable crate or standalone binary +889: - **Core Library**: Published as Rust crate for potential embedding in other tools +890: +891: ### 7.3 Scalability Design +892: +893: **Current Architecture Limitations**: +894: - **Single-User**: No multi-user collaboration features; designed for individual workstations +895: - **Local-First**: All computation and storage local; no horizontal scaling required +896: - **Synchronous LLM**: Rate limiting (30 req/min) prevents parallel LLM request scaling +897: +898: **Extension Points**: +899: - **Plugin Architecture**: Tool system supports custom ADK tool injection for domain-specific operations +900: - **Custom Stages**: Pipeline trait system allows custom stage implementations (e.g., Security Review stage) +901: - **Multi-Model Support**: LLM configuration supports multiple providers; could extend to local models (Llama, Mistral) +902: - **Memory Backends**: Repository pattern allows replacement of JSON storage with database backends (SQLite, PostgreSQL) for team scenarios +903: +904: ### 7.4 Monitoring and Operations +905: +906: **Observability**: +907: - **Logging**: Structured logging via `tracing` crate with configurable levels (DEBUG for development, INFO for production) +908: - **Metrics**: Pipeline execution duration, stage success rates, LLM token consumption +909: - **Event Tracing**: Tauri events provide real-time execution visibility in GUI mode +910: +911: **Operational Considerations**: +912: - **Backup**: Projects stored as plain text (JSON + Markdown) in user directories; standard backup solutions apply +913: - **Migration**: JSON schema versioning supports forward compatibility; `.cowork-v2` directory naming allows side-by-side version installation +914: - **Security**: Workspace path validation prevents unauthorized file access; no network listeners exposed (outbound-only LLM calls) +915: +916: **Health Checks**: +917: - **LLM Connectivity**: Validation command (`cowork status`) tests API key and connectivity +918: - **Workspace Integrity**: Automatic validation of project structure on load +919: - **Tool Availability**: Runtime checks for required external tools (editors, shell commands) +920: +921: --- +922: +923: ## Configuration System Architecture +924: +925: Cowork Forge introduces a data-driven configuration system that transforms previously hardcoded Agent, Stage, Flow, Skill, and Integration definitions into configurable JSON formats. This makes the system more flexible and extensible, allowing users to customize development workflows without modifying code. +926: +927: ### Configuration Registry +928: +929: At the core of the configuration system is the global Config Registry, managing all configuration definitions: +930: +931: ```mermaid +932: flowchart TB +933: subgraph Registry["Config Registry"] +934: Agents["Agents
(Agent Definitions)"] +935: Stages["Stages
(Stage Definitions)"] +936: Flows["Flows
(Flow Definitions)"] +937: Skills["Skills
(Skill Definitions)"] +938: Integrations["Integrations
(External Integrations)"] +939: Settings["Settings
(User Settings)"] +940: end +941: +942: subgraph Sources["Configuration Sources"] +943: Builtin["Built-in Configs
(Embedded JSON)"] +944: User["User Configs
(~/.cowork/config/)"] +945: end +946: +947: subgraph Consumers["Consumers"] +948: Pipeline["Pipeline Executor"] +949: AgentFactory["Agent Factory"] +950: GUI["GUI Config Panel"] +951: end +952: +953: Sources -->|Load| Registry +954: Registry -->|Query| Consumers +955: ``` +956: +957: ### Configuration Types +958: +959: | Type | Description | Key Fields | +960: |------|-------------|------------| +961: | **Agent** | AI agent definition | id, name, instruction, tools, skills, model | +962: | **Stage** | Development stage definition | id, stage_type, agent/actor_critic, hooks, artifacts | +963: | **Flow** | Workflow definition | id, stages[], start_stage, config, stage_mapping | +964: | **Skill** | Skill extension package | id, category, tools[], prompts[], dependencies | +965: | **Integration** | External system integration | id, integration_type, connection, auth, events | +966: +967: ### Configuration Loading Flow +968: +969: ```mermaid +970: sequenceDiagram +971: participant App as App Startup +972: participant Registry as Config Registry +973: participant Builtin as Built-in Configs +974: participant User as User Configs +975: participant FS as File System +976: +977: App->>Registry: Initialize registry +978: Registry->>Builtin: Load embedded configs +979: Builtin-->>Registry: Agent/Stage/Flow definitions +980: +981: Registry->>FS: Check user config directory +982: FS-->>Registry: Config file list +983: +984: alt User configs exist +985: Registry->>User: Load user configs +986: User-->>Registry: Custom definitions +987: Registry->>Registry: Merge/Override configs +988: end +989: +990: Registry->>Registry: Validate config integrity +991: Registry-->>App: Configuration ready +992: ``` +993: +994: ### Custom Workflows +995: +996: Enterprises can implement standardized development processes through custom Flow configurations: +997: +998: ```json +999: { +1000: "id": "enterprise-web-flow", +1001: "name": "Enterprise Web Application Flow", +1002: "description": "Standard development workflow for enterprise web applications", +1003: "stages": [ +1004: { "stage_id": "idea" }, +1005: { "stage_id": "prd", "overrides": { "needs_confirmation": true } }, +1006: { "stage_id": "design" }, +1007: { "stage_id": "security-review", "overrides": { "needs_confirmation": true } }, +1008: { "stage_id": "plan" }, +1009: { "stage_id": "coding" }, +1010: { "stage_id": "check" }, +1011: { "stage_id": "delivery" } +1012: ], +1013: "config": { +1014: "stop_on_failure": true, +1015: "inheritance": { +1016: "default_mode": "partial", +1017: "stage_mapping": { +1018: "none": "idea", +1019: "partial": "plan", +1020: "full": "idea" +1021: } +1022: } +1023: } +1024: } +1025: ``` +1026: +1027: ### Skill Extension Mechanism +1028: +1029: The Skill system allows injecting domain-specific capabilities into Agents: +1030: +1031: ```mermaid +1032: flowchart LR +1033: subgraph Skill["Skill Package"] +1034: Manifest["manifest.json"] +1035: Tools["Tool Definitions"] +1036: Prompts["Prompt Templates"] +1037: Deps["Dependency Declarations"] +1038: end +1039: +1040: subgraph Agent["Agent"] +1041: BaseTools["Base Tools"] +1042: BaseInst["Base Instructions"] +1043: end +1044: +1045: Skill -->|Inject| Agent +1046: Agent -->|Enhanced Capabilities| Execution["Execution Context"] +1047: ``` +1048: +1049: **Skill Categories**: +1050: - `general`: General-purpose skills +1051: - `web_frontend`: Web frontend development +1052: - `web_backend`: Web backend development +1053: - `mobile`: Mobile application development +1054: - `devops`: DevOps automation +1055: - `testing`: Test automation +1056: - `security`: Security auditing +1057: +1058: ### Skills Module Architecture +1059: +1060: The Skills module implements the [agentskills.io](https://agentskills.io) standard for maximum compatibility: +1061: +1062: ```mermaid +1063: flowchart TB +1064: subgraph Discovery["Skill Discovery"] +1065: FS[".skills/ directory scan"] +1066: Parse["Parse SKILL.md files"] +1067: Index["Build SkillIndex"] +1068: end +1069: +1070: subgraph Selection["Skill Selection"] +1071: Query["User Query"] +1072: Match["Semantic Matching"] +1073: Score["Relevance Scoring"] +1074: end +1075: +1076: subgraph Injection["Context Injection"] +1077: Context["Agent Context"] +1078: Tools["Tool Registration"] +1079: Prompts["Prompt Enhancement"] +1080: end +1081: +1082: FS --> Parse --> Index +1083: Query --> Match --> Score +1084: Index --> Match +1085: Score --> Injection +1086: Injection --> Context +1087: Injection --> Tools +1088: Injection --> Prompts +1089: ``` +1090: +1091: **SkillManager API**: +1092: - `SkillManager::for_project(path)`: Initialize for a project +1093: - `select(query)`: Find matching skills +1094: - `select_best(query)`: Get top match +1095: - `install_skill_from_dir(path)`: Install from local directory +1096: +1097: ### PM Agent Architecture +1098: +1099: The Project Manager Agent provides post-delivery interaction capabilities: +1100: +1101: ```mermaid +1102: flowchart TB +1103: subgraph Trigger["Activation Trigger"] +1104: Complete["Iteration Status = Completed"] +1105: Switch["GUI Switches to Chat Mode"] +1106: end +1107: +1108: subgraph Intent["Intent Recognition"] +1109: Input["User Input"] +1110: NLP["Keyword Analysis"] +1111: Classify["Intent Classification"] +1112: end +1113: +1114: subgraph Actions["Available Actions"] +1115: Goto["goto_stage
Return to previous stage"] +1116: Create["create_iteration
Start evolution iteration"] +1117: Respond["respond_to_user
Answer questions"] +1118: Clarify["ask_clarification
Request details"] +1119: end +1120: +1121: Complete --> Switch --> Input +1122: Input --> NLP --> Classify +1123: Classify -->|bug_fix| Goto +1124: Classify -->|new_feature| Create +1125: Classify -->|consultation| Respond +1126: Classify -->|ambiguous| Clarify +1127: ``` +1128: +1129: **Intent Types**: +1130: | Intent | Trigger Keywords | Action | +1131: |--------|------------------|--------| +1132: | `bug_fix` | bug, error, issue, crash, fail | `goto_stage(coding)` | +1133: | `requirement_change` | modify, change, adjust, update | `goto_stage(appropriate)` | +1134: | `new_feature` | add, new feature, create | `create_iteration()` | +1135: | `consultation` | how, what, why, can you | `respond_to_user()` | +1136: | `ambiguous` | unclear input | `ask_clarification()` | +1137: +1138: **PM Agent Tools**: +1139: - `pm_goto_stage`: Navigate to specified development stage +1140: - `pm_create_iteration`: Create new evolution iteration +1141: - `pm_respond`: Respond to user questions +1142: - `pm_save_decision`: Save project decisions +1143: - `query_memory`: Query project memory for context +1144: +1145: ### Configuration File Locations +1146: +1147: | Platform | User Config Directory | +1148: |----------|----------------------| +1149: | Windows | `%APPDATA%\.cowork\config\` | +1150: | macOS | `~/.cowork/config/` | +1151: | Linux | `~/.cowork/config/` | +1152: +1153: Directory structure: +1154: ``` +1155: config/ +1156: ├── agents/ # Custom agents +1157: │ └── custom_agent.json +1158: ├── stages/ # Custom stages +1159: ├── flows/ # Custom flows +1160: │ └── enterprise-flow.json +1161: ├── skills/ # Skill packages +1162: │ └── my-skill/ +1163: │ ├── manifest.json +1164: │ └── prompts/ +1165: ├── integrations/ # External integrations +1166: └── settings.json # Global settings +1167: ``` +1168: +1169: --- +1170: +1171: ## Architectural Decision Records (ADRs) +1172: +1173: ### ADR-001: Multi-Crate Workspace Structure +1174: **Decision**: Separate CLI, GUI, and Core into distinct crates within a Cargo workspace. +1175: **Rationale**: Enables independent deployment (CLI for automation, GUI for interaction) while sharing domain logic. Prevents GUI dependencies (Tauri, WebView) from bloating CLI binary. +1176: +1177: ### ADR-002: Trait-Based Backend Abstraction +1178: **Decision**: `InteractiveBackend` trait to unify CLI and GUI interactions. +1179: **Rationale**: Single pipeline code path supports both automation and interactive modes without conditional logic throughout domain layer. +1180: +1181: ### ADR-003: JSON-First Persistence +1182: **Decision**: File-based JSON storage instead of database. +1183: **Rationale**: Portability, version control compatibility, and local-first architecture. Enables users to inspect and modify project state with standard tools. +1184: +1185: ### ADR-004: Rate Limiting at Infrastructure Layer +1186: **Decision**: Decorator pattern for LLM rate limiting (30 req/min) with semaphore concurrency control. +1187: **Rationale**: API quota protection and cost control. Global rate limiter ensures compliance regardless of pipeline stage parallelism. +1188: +1189: ### ADR-005: Event-Driven GUI with Asymmetric Communication +1190: **Decision**: Tauri commands for requests, events for streaming responses. +1191: **Rationale**: Commands provide request-response semantics for operations; events enable server-push for streaming LLM tokens and process logs without polling overhead. +1192: +1193: ### ADR-006: Post-Delivery PM Agent +1194: **Decision**: Dedicated PM Agent for post-delivery user interaction with intent recognition. +1195: **Rationale**: Automated pipeline execution needs a bridge to ongoing project maintenance. PM Agent provides natural language interface for bug fixes, requirement changes, and new features without requiring users to understand pipeline internals. +1196: +1197: ### ADR-007: agentskills.io Standard for Skills +1198: **Decision**: Implement agentskills.io standard for skill definitions. +1199: **Rationale**: Industry standard ensures compatibility with external skill packages and community contributions. Simple markdown-based format (SKILL.md) lowers barrier for skill authoring. +1200: +1201: --- +1202: +1203: ### ADR-008: Model Context Protocol (MCP) Integration +1204: **Decision**: Introduce MCP protocol integration to support external tool server connectivity. +1205: **Rationale**: Through standardized Model Context Protocol, system can seamlessly integrate third-party AI services (e.g., Tavily web search, DeepWiki code documentation queries), expanding Agents' tool and capability range without requiring complex local external API adaptation. MCP connects via HTTP transport, and configuration-driven auto-initialization ensures global toolset availability at startup with automatic injection into all Agents. +1206: **Implementation**: +1207: - `McpConfig` configuration structure (`tavily_api_key`, `deepwiki_enabled`) +1208: - `McpManager` for multi-server connection and toolset aggregation +1209: - Global static `GLOBAL_MCP_TOOLSETS` initialized at application startup +1210: - `add_mcp_toolsets_to_builder` automatically injects all MCP tools into Agent builder +1211: - GUI Settings panel with config UI and connection test +1212: +1213: +1214: ## Conclusion +1215: +1216: Cowork Forge demonstrates a mature application of **Hexagonal Architecture** and **Domain-Driven Design** principles to the AI-assisted development domain. The architecture successfully balances AI autonomy with human oversight through the 7-stage pipeline and HITL validation gates. +1217: +1218: Key architectural strengths include: +1219: - **Clean Separation**: Core domain remains pure and testable, isolated from UI and infrastructure concerns +1220: - **Dual Interface Strategy**: Elegant trait-based abstraction enabling both CLI automation and GUI interactivity +1221: - **Knowledge Persistence**: Memory system bridges the gap between stateless AI agents and stateful software development +1222: - **Security-First**: Workspace containment and path validation protect against prompt injection attacks attempting file system traversal +1223: +1224: The system is well-positioned for evolutionary extension, with clear domain boundaries supporting future enhancements such as team collaboration features, additional AI model integrations, or custom pipeline stages. +```` + +### crates/cowork-core/src/agents/mod.rs (134 lines) + +``` +1: create_idea_agent +2: ⋮---- +3: (model: Arc) +4: ⋮---- +5: create_idea_agent_with_id +6: ⋮---- +7: (model: Arc, iteration_id: String) +8: ⋮---- +9: create_prd_loop +10: ⋮---- +11: (model: Arc) +12: ⋮---- +13: create_prd_loop_with_id +14: ⋮---- +15: (model: Arc, iteration_id: String) +16: ⋮---- +17: create_design_loop +18: ⋮---- +19: (model: Arc) +20: ⋮---- +21: create_design_loop_with_id +22: ⋮---- +23: (model: Arc, iteration_id: String) +24: ⋮---- +25: create_plan_loop +26: ⋮---- +27: (model: Arc) +28: ⋮---- +29: create_plan_loop_with_id +30: ⋮---- +31: (model: Arc, iteration_id: String) +32: ⋮---- +33: create_coding_loop +34: ⋮---- +35: (model: Arc) +36: ⋮---- +37: create_coding_loop_with_id +38: ⋮---- +39: (model: Arc, iteration_id: String) +40: ⋮---- +41: create_check_agent +42: ⋮---- +43: (model: Arc) +44: ⋮---- +45: create_check_agent_with_id +46: ⋮---- +47: (model: Arc, iteration_id: String) +48: ⋮---- +49: create_delivery_agent +50: ⋮---- +51: (model: Arc) +52: ⋮---- +53: create_delivery_agent_with_id +54: ⋮---- +55: (model: Arc, iteration_id: String) +56: ⋮---- +57: create_summary_agent +58: ⋮---- +59: (model: Arc, iteration_id: String, iteration_number: u32) +60: ⋮---- +61: create_knowledge_generation_agent +62: ⋮---- +63: ( +64: model: Arc, +65: iteration_id: String, +66: iteration_number: u32, +67: base_iteration_id: Option +68: ) +69: ⋮---- +70: create_project_manager_agent +71: ⋮---- +72: (model: Arc, iteration_id: String) +73: ⋮---- +74: load_artifacts_summary_for_pm +75: ⋮---- +76: (iteration_store: &IterationStore, iteration_id: &str) +77: ⋮---- +78: PMAgentResult +79: ⋮---- +80: { +81: +82: pub message: String, +83: +84: pub actions: Vec, +85: +86: pub parts: Vec, +87: } +88: ⋮---- +89: PMAgentAction +90: ⋮---- +91: { +92: +93: #[serde(rename = "pm_goto_stage")] +94: GotoStage { +95: target_stage: String, +96: reason: String, +97: }, +98: +99: #[serde(rename = "pm_create_iteration")] +100: CreateIteration { +101: iteration_id: String, +102: title: String, +103: description: String, +104: inheritance: String, +105: }, +106: } +107: ⋮---- +108: PMAgentStreamCallback +109: ⋮---- +110: { +111: +112: async fn on_text_chunk(&self, text: &str, is_first: bool, is_last: bool); +113: +114: async fn on_tool_call(&self, tool_name: &str, args: &serde_json::Value); +115: } +116: ⋮---- +117: execute_pm_agent_message_streaming +118: ⋮---- +119: ( +120: model: Arc, +121: iteration_id: String, +122: message: String, +123: history: Vec, +124: stream_callback: Option>, +125: ) +126: ⋮---- +127: execute_pm_agent_message +128: ⋮---- +129: ( +130: model: Arc, +131: iteration_id: String, +132: message: String, +133: history: Vec, +134: ) +``` + +### crates/cowork-core/src/config_definition/mod.rs (17 lines) + +``` +1: pub mod agent_definition; +2: pub mod stage_definition; +3: pub mod flow_definition; +4: pub mod integration_definition; +5: pub mod registry; +6: pub mod validator; +7: pub mod builtin; +8: pub mod agent_factory; +9: +10: pub use agent_definition::*; +11: pub use stage_definition::*; +12: pub use flow_definition::*; +13: pub use integration_definition::*; +14: pub use registry::*; +15: pub use validator::*; +16: pub use builtin::load_builtin_configs; +17: pub use agent_factory::{create_agent_for_stage, create_agent_from_config, initialize_config_registry, initialize_mcp_toolsets, is_mcp_initialized}; +``` + +### crates/cowork-core/src/instructions/check.rs (413 lines) + +```` +1: pub const CHECK_AGENT_INSTRUCTION: &str = r##" +2: # Your Role +3: You are Check Agent. Read README.md and autonomously execute the commands it specifies to verify the project. +4: +5: # 🚨🚨🚨 CRITICAL RULE - BUILD FAILURES MUST RETURN TO CODING 🚨🚨🚨 +6: **This is the MOST IMPORTANT rule - violating this will cause broken code to be deployed!** +7: +8: ## If TypeScript/Build Command FAILS with compilation errors: +9: 1. ❌ DO NOT just save check_report and let pipeline continue +10: 2. ❌ DO NOT proceed to Delivery stage +11: 3. ✅ You MUST call `goto_stage("coding", )` to fix the errors +12: 4. ✅ The build MUST pass before pipeline can proceed +13: +14: ## Example - TypeScript Compilation Error (MANDATORY goto_stage): +15: ``` +16: 1. execute_shell_command("npm run build", "Build project") +17: → status: "failed", stderr: +18: "src/services/performance-service.ts:12:2 - error TS2305: Module has no exported member 'ToolDefinition'" +19: +20: 2. ❌ WRONG: +21: save_check_report("Build failed with 3 errors") +22: // Then do nothing - pipeline continues with broken code! +23: +24: 3. ✅ CORRECT: +25: goto_stage("coding", "构建失败:TypeScript编译错误,共3个错误: +26: +27: 错误1: src/services/performance-service.ts:12:2 +28: - 错误类型: TS2305 +29: - 错误描述: Module '../types/metrics.js' has no exported member 'ToolDefinition' +30: - 修复建议: 在 metrics.ts 中添加 ToolDefinition 类型导出,或从 plugin-impl.ts 导入该类型 +31: +32: 错误2: src/decorators/performance-monitor.ts:73:4 +33: - 错误类型: TS2722 +34: - 错误描述: Cannot invoke an object which is possibly 'undefined' +35: - 修复建议: 添加类型守卫或非空断言 +36: +37: 请修复这些 TypeScript 编译错误后重新执行。") +38: // Pipeline returns to Coding stage to fix the errors +39: ``` +40: +41: # ⚠️ CRITICAL REQUIREMENT - YOU MUST CALL save_check_report() +42: **This is the MOST IMPORTANT requirement - without this tool call, your work will be LOST!** +43: +44: ## ⚠️ MANDATORY WORKFLOW - YOU MUST FOLLOW THIS EXACTLY: +45: +46: ### When ALL checks PASS (dependencies installed, build succeeded): +47: ``` +48: 1. Install dependencies: execute_shell_command("bun install" or "npm install") +49: 2. Build project: execute_shell_command("bun run build" or "npm run build") +50: 3. Verify dist/ output exists +51: 4. ✅ MUST call: save_check_report("# Check Report\n\n## Results\n- Dependencies: ✅\n- Build: ✅\n\n## Conclusion\n项目构建成功,可以正常运行。") +52: 5. STOP - Do NOT continue without calling save_check_report() +53: ``` +54: +55: ### When BUILD FAILS (TypeScript/compilation errors): +56: ``` +57: 1. Try to build: execute_shell_command("bun run build" or "npm run build") +58: 2. If build fails with errors: +59: 3. ✅ MUST call: goto_stage("coding", "构建失败:TypeScript编译错误...") +60: 4. STOP - Do NOT call save_check_report() when build fails +61: ``` +62: +63: ### When PROJECT STRUCTURE is incomplete: +64: ``` +65: 1. Check files with list_files(".") +66: 2. If essential files missing (package.json, src/, etc.): +67: 3. ✅ MUST call: goto_stage("coding", "项目结构不完整...") +68: 4. STOP - Do NOT call save_check_report() when structure is broken +69: ``` +70: +71: ## ⚠️ DO NOT END WITHOUT A TOOL CALL! +72: - ❌ WRONG: Output text and end without calling any tool +73: - ❌ WRONG: Say "Check completed" without calling save_check_report() or goto_stage() +74: - ✅ CORRECT: Always end with either `save_check_report()` OR `goto_stage()` +75: +76: # 🚨🚨🚨 CRITICAL RULE - BUILD FAILURES MUST RETURN TO CODING 🚨🚨🚨 +77: **This is a critical rule - violating this will cause broken code to be deployed!** +78: - **Read README.md**: Check stage starts by reading the project README.md +79: - **Extract commands**: Analyze README to find environment setup, dependency installation, and build/run commands +80: - **Execute autonomously**: Run these commands using execute_shell_command tool +81: - **Make decisions**: Based on command execution results, either approve the project or return to Coding stage with specific feedback +82: +83: # ⚠️ CRITICAL: PROJECT STRUCTURE VALIDATION (NEW - FIRST PRIORITY) +84: **BEFORE checking README or running commands, you MUST verify project file structure:** +85: +86: ## Step 0: Validate Essential Files (NEW - MANDATORY FIRST STEP) +87: **This MUST be done BEFORE reading README.md:** +88: +89: 1. Use `list_files(".")` to see all project files +90: 2. **CRITICAL CHECKS** - Verify these files exist based on project type: +91: +92: ### For Web/Frontend Projects (React/Vue/Vanilla): +93: **REQUIRED FILES:** +94: - [ ] `package.json` - MUST exist and contain dependencies +95: - [ ] Entry HTML (`index.html`) - MUST exist +96: - [ ] Build config (`vite.config.js` or similar) - should exist +97: - [ ] Main entry script (`src/main.js` or `src/main.jsx`) - MUST exist +98: - [ ] `.gitignore` - should exist +99: +100: **IF ANY REQUIRED FILE IS MISSING:** +101: ``` +102: goto_stage("coding", "检查失败:项目结构不完整。缺少必需文件: +103: - [list missing files here] +104: +105: 这是一个Web项目,必须包含: +106: 1. package.json(包含依赖和scripts) +107: 2. index.html(入口HTML文件) +108: 3. src/main.jsx 或 src/main.js(主入口脚本) +109: 4. vite.config.js 或其他构建配置文件 +110: +111: 请在Coding阶段补充这些缺失的文件。") +112: ``` +113: +114: ### For Node.js Tool/Backend: +115: **REQUIRED FILES:** +116: - [ ] `package.json` - MUST exist with "bin" entry (for CLI tools) +117: - [ ] Main entry (`src/index.js` or `index.js`) - MUST exist +118: +119: ### For Rust Projects: +120: **REQUIRED FILES:** +121: - [ ] `Cargo.toml` - MUST exist +122: - [ ] `src/main.rs` or `src/lib.rs` - MUST exist +123: +124: ### For Python Projects: +125: **REQUIRED FILES:** +126: - [ ] `requirements.txt` or `pyproject.toml` - MUST exist +127: - [ ] Main entry (`main.py` or `src/__init__.py`) - MUST exist +128: +129: 3. **IF STRUCTURE IS INCOMPLETE**: +130: - **IMMEDIATELY** call `goto_stage("coding", )` +131: - DO NOT proceed to README check +132: - DO NOT try to run any commands +133: - Provide specific list of missing files in the error message +134: +135: 4. **ONLY IF STRUCTURE IS COMPLETE**: +136: - Proceed to Step 1 (Read README.md) +137: +138: # Workflow - AI 驱动的检查 +139: +140: ## Step 1: 读取 README.md (After Step 0 validation passes) +141: 1. 使用 `read_file("README.md")` 读取项目使用说明 +142: 2. 如果 README.md 不存在: +143: - 使用 `goto_stage("coding", "检查失败:缺少 README.md 文件。请在 Coding 阶段生成 README.md,包含环境要求、依赖安装、运行命令等完整说明。")` +144: - STOP +145: +146: ## Step 2: 分析 README 内容 +147: 分析 README 中的内容,提取关键信息: +148: - **环境要求**:需要哪些软件或环境(如 Node.js、Python、Rust 版本) +149: - **依赖安装命令**:如何安装项目依赖(如 `npm install`, `pip install`, `cargo build`) +150: - **运行/构建命令**:如何启动或构建项目 +151: - **项目类型**:判断是静态网页、Node.js 项目、Rust 项目还是 Python 项目 +152: +153: ## Step 3: 执行检查命令(自主决策) +154: 根据 README 内容,**自主决定执行哪些检查命令**: +155: +156: ### 如果 README 有"依赖安装"部分: +157: - 使用 `execute_shell_command(command, description)` 执行安装命令 +158: - 例如:`execute_shell_command("npm install", "Install Node.js dependencies")` +159: - 例如:`execute_shell_command("pip install -r requirements.txt", "Install Python dependencies")` +160: - 例如:`execute_shell_command("cargo build", "Build Rust project and download dependencies")` +161: +162: ### 如果 README 有"构建命令"部分: +163: - 使用 `execute_shell_command(command, description)` 执行构建命令 +164: - 例如:`execute_shell_command("npm run build", "Build production bundle")` +165: - 例如:`execute_shell_command("cargo build --release", "Build release version")` +166: +167: ### 如果是静态 HTML 项目(无构建命令): +168: - 使用 `list_files(".")` 验证关键文件存在 +169: - 检查 index.html, style.css, script.js 等文件 +170: +171: ## Step 4: 分析结果并决策 +172: +173: ### 成功场景(ALL checks PASS): +174: 如果所有命令执行成功: +175: ``` +176: ✅ 检查通过: +177: - 依赖安装成功 +178: - 构建成功 +179: - 所有必需文件存在 +180: 项目可以正常运行。 +181: ``` +182: **⚠️ CRITICAL: 你必须立即调用 `save_check_report(content)` 保存报告!** +183: **不要只输出文本 - 必须调用工具!** +184: +185: ### 🚨 失败场景(BUILD FAILURE): +186: **如果构建命令失败(TypeScript/编译错误),你 MUST 调用 goto_stage,不能只是保存报告!** +187: +188: ``` +189: ❌ 检查失败: +190: - 具体错误信息(包含文件名、行号、错误类型) +191: - 失败的命令 +192: - 修复建议 +193: ``` +194: +195: **关键:构建失败时的正确处理顺序:** +196: 1. 分析错误信息,提取:文件路径、行号、错误类型、错误描述 +197: 2. 调用 `goto_stage("coding", <详细的错误信息和修复建议>)` +198: 3. **不要**单独调用 `save_check_report`(goto_stage 会处理状态转换) +199: 4. Pipeline 将返回 Coding 阶段修复错误 +200: +201: ## Step 5: 保存检查报告(MANDATORY - CRITICAL!) +202: **这是强制步骤,必须在完成检查后执行!你不能跳过这一步!** +203: +204: ### 如果检查全部通过: +205: **必须调用 `save_check_report(content)` 保存报告:** +206: ``` +207: save_check_report("# Check Report +208: +209: ## 项目信息 +210: - 项目类型: [Web/Node.js/Rust/Python/静态HTML] +211: - 检查时间: [timestamp] +212: +213: ## 检查结果 +214: - 项目结构验证: ✅ +215: - 依赖安装: ✅ +216: - 构建验证: ✅ +217: - 文件完整性: ✅ +218: +219: ## 详细说明 +220: [具体的检查过程和结果描述] +221: +222: ## 结论 +223: ✅ 检查通过,项目构建成功,可以正常运行。 +224: ") +225: ``` +226: +227: ### 如果构建失败: +228: **必须调用 `goto_stage("coding", <错误信息>)` 返回 Coding 阶段修复:** +229: ``` +230: goto_stage("coding", "构建失败:[详细错误信息和修复建议]") +231: ``` +232: +233: **⚠️ 注意**:如果不调用 `save_check_report()` 或 `goto_stage()`,Check 阶段将无法完成! +234: +235: # Tools +236: - read_file(path) ← 读取 README.md +237: - execute_shell_command(command, description, timeout?) ← 执行 README 中的命令 +238: - list_files(path) ← 验证文件存在性 +239: - get_plan() ← 查看任务状态 +240: - goto_stage(stage, reason) ← 返回修复建议 +241: - save_check_report(content) ← **MANDATORY** 保存检查报告(必须在完成检查后调用) +242: +243: # Example 0 - 项目结构验证失败(新增示例) +244: ``` +245: 0. list_files(".") +246: → 只返回:README.md, src/App.jsx, src/components/Button.jsx +247: → 缺少:package.json, index.html, vite.config.js, src/main.jsx +248: +249: 1. 分析:这是Web项目但缺少关键文件 +250: +251: 2. goto_stage("coding", "检查失败:项目结构不完整。 +252: +253: 缺少以下必需文件: +254: - package.json(依赖管理文件) +255: - index.html(入口HTML文件) +256: - vite.config.js(构建配置) +257: - src/main.jsx(主入口脚本) +258: +259: 这是一个React Web项目,必须包含完整的项目结构。请补充这些文件: +260: 1. package.json - 包含react、vite等依赖和dev/build脚本 +261: 2. index.html - 包含
和script标签 +262: 3. vite.config.js - 配置React插件 +263: 4. src/main.jsx - ReactDOM.render入口代码") +264: ``` +265: +266: # Example 1 - 成功检查(Node.js 项目) +267: ``` +268: 1. read_file("README.md") +269: → 内容显示需要 `npm install` 和 `npm run build` +270: +271: 2. execute_shell_command("npm install", "Install dependencies") +272: → status: "success", stdout: "added 123 packages" +273: +274: 3. execute_shell_command("npm run build", "Build project") +275: → status: "success", stdout: "built in 2.3s" +276: +277: 4. save_check_report("# Check Report\n\n## Results\n- Dependencies: ✅ Installed\n- Build: ✅ Success\n\n## Conclusion\n项目可以正常运行。") +278: → status: "success" +279: +280: 5. "✅ 检查通过:依赖安装成功,构建成功,项目可以正常运行。" +281: ``` +282: +283: # Example 2 - 检查失败(缺少 package.json) +284: ``` +285: 1. read_file("README.md") +286: → 内容显示需要 `npm install` 和 `npm run build` +287: +288: 2. execute_shell_command("npm install", "Install dependencies") +289: → status: "failed", stderr: "ENOENT: no such file or package.json" +290: +291: 3. 分析:缺少 package.json 文件 +292: +293: 4. goto_stage("coding", "检查失败:缺少 package.json 文件。README 要求执行 'npm install',但项目根目录下没有 package.json。请在 Coding 阶段生成 package.json 文件并配置正确的依赖。") +294: ``` +295: +296: # Example 3 - 静态 HTML 项目 +297: ``` +298: 1. read_file("README.md") +299: → 内容是静态网页,无需安装依赖,只需在浏览器中打开 index.html +300: +301: 2. list_files(".") +302: → 找到 index.html, style.css, script.js +303: +304: 3. save_check_report("# Check Report\n\n## Project Type\n静态网页项目\n\n## Files Verified\n- index.html ✅\n- style.css ✅\n- script.js ✅\n\n## Conclusion\n所有必需文件存在,可以直接在浏览器中打开 index.html。") +305: → status: "success" +306: +307: 4. "✅ 检查通过:静态网页项目,所有必需文件存在,可以直接在浏览器中打开 index.html。" +308: ``` +309: +310: # Example 4 - 依赖安装失败 +311: ``` +312: 1. read_file("README.md") +313: → 内容显示需要 `pip install -r requirements.txt` +314: +315: 2. execute_shell_command("pip install -r requirements.txt", "Install Python dependencies") +316: → status: "failed", stderr: "ERROR: Could not find a version that satisfies the requirement missing-package==1.0.0" +317: +318: 3. 分析:requirements.txt 中有不存在的依赖 +319: +320: 4. goto_stage("coding", "检查失败:依赖安装失败。错误信息:'ERROR: Could not find a version that satisfies the requirement missing-package==1.0.0'。请检查 requirements.txt 中的依赖名称和版本是否正确,移除不存在的依赖包。") +321: ``` +322: +323: # Example 5 - 构建失败(TypeScript 编译错误)⭐ IMPORTANT +324: ``` +325: 1. read_file("README.md") +326: → 内容显示需要 `npm install` 和 `npm run build` +327: +328: 2. execute_shell_command("npm install", "Install dependencies") +329: → status: "success" +330: +331: 3. execute_shell_command("npm run build", "Build project") +332: → status: "failed", stderr: +333: "src/services/performance-service.ts:12:2 - error TS2305: Module has no exported member 'ToolDefinition' +334: src/decorators/performance-monitor.ts:73:4 - error TS2722: Cannot invoke an object which is possibly 'undefined' +335: src/services/performance-service.ts:54:3 - error TS2722: Cannot invoke an object which is possibly 'undefined'" +336: +337: 4. 分析:TypeScript 类型定义问题,共有 3 个编译错误 +338: +339: 5. ❌ WRONG - 只保存报告,pipeline 继续执行(会导致 broken code 被 deploy): +340: save_check_report("# Check Report\n\n## Results\n- Build: ❌ Failed\n\n## Errors\n[...]") +341: // Pipeline continues to Delivery with broken code! +342: +343: 6. ✅ CORRECT - 调用 goto_stage 返回 Coding 修复: +344: goto_stage("coding", "构建失败:TypeScript 编译错误,共 3 个错误需要修复: +345: +346: ## 错误列表 +347: +348: ### 错误 1: src/services/performance-service.ts:12:2 +349: - 类型: TS2305 - Module has no exported member 'ToolDefinition' +350: - 原因: 从 '../types/metrics.js' 导入 ToolDefinition,但该模块未导出此类型 +351: - 修复: 在 src/types/metrics.ts 中添加 ToolDefinition 类型定义并导出 +352: +353: ### 错误 2: src/decorators/performance-monitor.ts:73:4 +354: - 类型: TS2722 - Cannot invoke an object which is possibly 'undefined' +355: - 原因: 返回值可能为 undefined,但被直接返回 +356: - 修复: 添加类型守卫检查 result !== undefined +357: +358: ### 错误 3: src/services/performance-service.ts:54:3 +359: - 类型: TS2722 - Cannot invoke an object which is possibly 'undefined' +360: - 原因: 同错误 2 +361: - 修复: 添加适当的类型守卫 +362: +363: 请在 Coding 阶段修复这些 TypeScript 类型错误。") +364: +365: // Pipeline 返回 Coding 阶段,修复后再重新执行 Check +366: ``` +367: +368: # Example 6 - TypeScript 类型错误(Evolution 迭代常见问题)⭐ +369: ``` +370: 场景:在已有项目基础上新增功能(Evolution 迭代),但新代码引用了不存在的类型 +371: +372: 1. execute_shell_command("bun run build", "Build project") +373: → status: "failed", stderr: +374: "error TS2305: Module '../types/metrics.js' has no exported member 'ToolDefinition'" +375: +376: 2. 分析:这是 Evolution 迭代,新代码导入的类型在基础迭代的类型文件中不存在 +377: +378: 3. ✅ CORRECT: +379: goto_stage("coding", "构建失败:类型导入错误 +380: +381: 错误: src/services/performance-service.ts 第 12 行 +382: - 尝试从 '../types/metrics.js' 导入 'ToolDefinition' +383: - 但该类型未在 metrics.ts 中定义/导出 +384: +385: 这是 Evolution 迭代的常见问题。请检查: +386: 1. 使用 list_files('.') 查看现有项目结构 +387: 2. 使用 read_file() 检查 metrics.ts 的内容 +388: 3. 确认 ToolDefinition 是否已在其他地方定义(如 plugin-impl.ts) +389: 4. 选项 A: 在 metrics.ts 中添加并导出 ToolDefinition 类型 +390: 5. 选项 B: 从定义该类型的文件导入 +391: +392: 建议优先使用选项 A,将共享类型集中管理。") +393: ``` +394: +395: # 核心原则 +396: - **项目结构验证优先**:在执行任何命令前,先验证必需文件是否存在 +397: - **README 是执行的依据**:AI 根据 README 自主决定如何检查,不依赖硬编码的规则 +398: - **灵活适应不同项目类型**:支持 Web、Node.js、Rust、Python 等多种项目类型 +399: - **提供具体的修复建议**:失败时不仅报告错误,还提供明确的修复方向和缺失文件清单 +400: - **自主决策**:AI 根据项目实际情况决定执行哪些检查命令 +401: +402: **REMEMBER: +403: 1. **ALWAYS start with Step 0: Validate project structure using list_files()** +404: 2. If structure incomplete, immediately goto_stage("coding") with detailed file list +405: 3. Only after structure validation passes, proceed to Step 1: read_file("README.md") +406: 4. Extract commands from README and execute them +407: 5. Analyze results and provide specific feedback if failed +408: 6. For static projects, verify file existence is sufficient +409: 7. **🚨 CRITICAL: If BUILD FAILS (TypeScript/compilation errors), you MUST call goto_stage("coding", ...) - NEVER let broken code proceed to Delivery** +410: 8. **🚨 CRITICAL: If ALL CHECKS PASS, you MUST call save_check_report(content) - your work is LOST without this tool call** +411: 9. **⚠️ NEVER end without calling either save_check_report() OR goto_stage()** +412: "## +413: ; +```` + +### crates/cowork-core/src/tools/artifact_tools.rs (304 lines) + +``` +1: SaveIdeaTool +2: ⋮---- +3: { +4: fn name(&self) -> &str { +5: "save_idea" +6: } +7: +8: fn description(&self) -> &str { +9: "MUST USE THIS TOOL to save the Idea markdown document. Call save_idea(content=) to save your generated idea content to artifacts/idea.md. This is REQUIRED to complete the idea stage." +10: } +11: +12: fn parameters_schema(&self) -> Option { +13: Some(json!({ +14: "type": "object", +15: "properties": { +16: "content": { +17: "type": "string", +18: "description": "Markdown content of the idea document" +19: } +20: }, +21: "required": ["content"] +22: })) +23: } +24: +25: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +26: +27: super::notify_tool_call("save_idea", &json!({"file": "idea.md"})); +28: +29: let content = get_required_string_param(&args, "content")?; +30: +31: match save_idea(content) { +32: Ok(_) => { +33: super::notify_tool_result("save_idea", &Ok(json!({"status": "success"}))); +34: Ok(json!({ +35: "status": "success", +36: "message": "Idea document saved successfully", +37: "file_path": "artifacts/idea.md" +38: })) +39: } +40: Err(e) => { +41: super::notify_tool_result("save_idea", &Err(adk_core::AdkError::tool(e.to_string()))); +42: Err(adk_core::AdkError::tool(e.to_string())) +43: } +44: } +45: } +46: } +47: ⋮---- +48: SaveDeliveryReportTool +49: ⋮---- +50: { +51: fn name(&self) -> &str { +52: "save_delivery_report" +53: } +54: +55: fn description(&self) -> &str { +56: "Save the delivery report markdown document." +57: } +58: +59: fn parameters_schema(&self) -> Option { +60: Some(json!({ +61: "type": "object", +62: "properties": { +63: "content": { +64: "type": "string", +65: "description": "Markdown content of the delivery report" +66: } +67: }, +68: "required": ["content"] +69: })) +70: } +71: +72: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +73: super::notify_tool_call("save_delivery_report", &json!({"file": "delivery_report.md"})); +74: +75: let content = get_required_string_param(&args, "content")?; +76: +77: save_delivery_report(content) +78: .map_err(|e| adk_core::AdkError::tool(e.to_string()))?; +79: +80: Ok(json!({ +81: "status": "success", +82: "message": "Delivery report saved successfully", +83: "file_path": "artifacts/delivery_report.md" +84: })) +85: } +86: } +87: ⋮---- +88: SaveCheckReportTool +89: ⋮---- +90: { +91: fn name(&self) -> &str { +92: "save_check_report" +93: } +94: +95: fn description(&self) -> &str { +96: "MUST USE THIS TOOL to save the Check Report markdown document. Call save_check_report(content=) to save your check results to artifacts/check_report.md. This is REQUIRED to complete the check stage." +97: } +98: +99: fn parameters_schema(&self) -> Option { +100: Some(json!({ +101: "type": "object", +102: "properties": { +103: "content": { +104: "type": "string", +105: "description": "Markdown content of the check report" +106: } +107: }, +108: "required": ["content"] +109: })) +110: } +111: +112: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +113: super::notify_tool_call("save_check_report", &json!({"file": "check_report.md"})); +114: +115: let content = get_required_string_param(&args, "content")?; +116: +117: save_check_report(content) +118: .map_err(|e| adk_core::AdkError::tool(e.to_string()))?; +119: +120: Ok(json!({ +121: "status": "success", +122: "message": "Check report saved successfully", +123: "file_path": "artifacts/check_report.md" +124: })) +125: } +126: } +127: ⋮---- +128: SavePlanDocTool +129: ⋮---- +130: { +131: fn name(&self) -> &str { +132: "save_plan_doc" +133: } +134: +135: fn description(&self) -> &str { +136: "Save the Implementation Plan markdown document." +137: } +138: +139: fn parameters_schema(&self) -> Option { +140: Some(json!({ +141: "type": "object", +142: "properties": { +143: "content": { +144: "type": "string", +145: "description": "Markdown content of the implementation plan document" +146: } +147: }, +148: "required": ["content"] +149: })) +150: } +151: +152: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +153: super::notify_tool_call("save_plan_doc", &json!({"file": "plan.md"})); +154: +155: let content = get_required_string_param(&args, "content")?; +156: +157: save_plan_doc(content) +158: .map_err(|e| adk_core::AdkError::tool(e.to_string()))?; +159: +160: Ok(json!({ +161: "status": "success", +162: "message": "Plan document saved successfully", +163: "file_path": "artifacts/plan.md" +164: })) +165: } +166: } +167: ⋮---- +168: SavePrdDocTool +169: ⋮---- +170: { +171: fn name(&self) -> &str { +172: "save_prd_doc" +173: } +174: +175: fn description(&self) -> &str { +176: "Save the PRD (Product Requirements Document) markdown file." +177: } +178: +179: fn parameters_schema(&self) -> Option { +180: Some(json!({ +181: "type": "object", +182: "properties": { +183: "content": { +184: "type": "string", +185: "description": "Markdown content of the PRD document" +186: } +187: }, +188: "required": ["content"] +189: })) +190: } +191: +192: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +193: +194: super::notify_tool_call("save_prd_doc", &json!({"file": "prd.md"})); +195: +196: let content = get_required_string_param(&args, "content")?; +197: +198: match save_prd_doc(content) { +199: Ok(_) => { +200: super::notify_tool_result("save_prd_doc", &Ok(json!({"status": "success"}))); +201: Ok(json!({ +202: "status": "success", +203: "message": "PRD document saved successfully", +204: "file_path": "artifacts/prd.md" +205: })) +206: } +207: Err(e) => { +208: super::notify_tool_result("save_prd_doc", &Err(adk_core::AdkError::tool(e.to_string()))); +209: Err(adk_core::AdkError::tool(e.to_string())) +210: } +211: } +212: } +213: } +214: ⋮---- +215: SaveDesignDocTool +216: ⋮---- +217: { +218: fn name(&self) -> &str { +219: "save_design_doc" +220: } +221: +222: fn description(&self) -> &str { +223: "Save the Design Document markdown file." +224: } +225: +226: fn parameters_schema(&self) -> Option { +227: Some(json!({ +228: "type": "object", +229: "properties": { +230: "content": { +231: "type": "string", +232: "description": "Markdown content of the design document" +233: } +234: }, +235: "required": ["content"] +236: })) +237: } +238: +239: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +240: super::notify_tool_call("save_design_doc", &json!({"file": "design.md"})); +241: +242: let content = get_required_string_param(&args, "content")?; +243: +244: save_design_doc(content) +245: .map_err(|e| adk_core::AdkError::tool(e.to_string()))?; +246: +247: Ok(json!({ +248: "status": "success", +249: "message": "Design document saved successfully", +250: "file_path": "artifacts/design.md" +251: })) +252: } +253: } +254: ⋮---- +255: LoadFeedbackHistoryTool +256: ⋮---- +257: { +258: fn name(&self) -> &str { +259: "load_feedback_history" +260: } +261: +262: fn description(&self) -> &str { +263: "Load the feedback history from a specific stage. Only returns the most recent feedback entry for that stage." +264: } +265: +266: fn parameters_schema(&self) -> Option { +267: Some(json!({ +268: "type": "object", +269: "properties": { +270: "stage": { +271: "type": "string", +272: "description": "The stage to load feedback for (e.g., 'idea', 'prd', 'design', 'plan', 'coding', 'check', 'delivery')", +273: "enum": ["idea", "prd", "design", "plan", "coding", "check", "delivery"] +274: } +275: }, +276: "required": ["stage"] +277: })) +278: } +279: +280: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +281: let stage = args["stage"].as_str() +282: .ok_or_else(|| adk_core::AdkError::tool("Missing required parameter: stage".to_string()))?; +283: +284: let history = load_feedback_history() +285: .map_err(|e| adk_core::AdkError::tool(e.to_string()))?; +286: +287: +288: let most_recent_feedback = history.feedbacks +289: .into_iter() +290: .filter(|f| f.stage == stage) +291: .max_by_key(|f| f.timestamp); +292: +293: match most_recent_feedback { +294: Some(feedback) => Ok(json!({ +295: "has_feedback": true, +296: "feedback": feedback +297: })), +298: None => Ok(json!({ +299: "has_feedback": false, +300: "message": format!("No feedback found for stage '{}'", stage) +301: })) +302: } +303: } +304: } +``` + +### crates/cowork-core/src/tools/mod.rs (23 lines) + +``` +1: get_required_string_param +2: ⋮---- +3: (args: &'a Value, key: &str) +4: ⋮---- +5: get_optional_string_param +6: ⋮---- +7: (args: &Value, key: &str) +8: ⋮---- +9: get_required_array_param +10: ⋮---- +11: (args: &'a Value, key: &str) +12: ⋮---- +13: set_tool_notify_callback +14: ⋮---- +15: (callback: F) +16: ⋮---- +17: notify_tool_call +18: ⋮---- +19: (tool_name: &str, args: &Value) +20: ⋮---- +21: notify_tool_result +22: ⋮---- +23: (tool_name: &str, result: &Result) +``` + +### crates/cowork-gui/src/components/IterationsPanel.tsx (7 lines) + +``` +1: IterationsPanelProps +2: ⋮---- +3: { +4: onSelectIteration?: (iterationId: string) => void; +5: selectedIterationId?: string | null; +6: onExecuteStatusChange?: (iterationId: string, status: string) => void; +7: } +``` + +### crates/cowork-gui/src/components/common/MarkdownMessage.tsx (5 lines) + +``` +1: MarkdownMessageProps +2: ⋮---- +3: { +4: content: string; +5: } +``` + +### crates/cowork-gui/src/stores/configStore.ts (49 lines) + +``` +1: ConfigState +2: ⋮---- +3: { +4: loading: boolean; +5: error: string | null; +6: selectedFlow: string | null; +7: selectedAgent: string | null; +8: selectedStage: string | null; +9: selectedSkill: string | null; +10: selectedIntegration: string | null; +11: availableTools: ToolInfo[]; +12: +13: +14: loadConfigs: () => Promise; +15: resetConfigs: () => Promise; +16: selectFlow: (id: string | null) => void; +17: selectAgent: (id: string | null) => void; +18: selectStage: (id: string | null) => void; +19: selectSkill: (name: string | null) => void; +20: selectIntegration: (id: string | null) => void; +21: +22: +23: saveAgent: (agent: AgentDefinition) => Promise; +24: deleteAgent: (id: string) => Promise; +25: saveStage: (stage: StageDefinition) => Promise; +26: deleteStage: (id: string) => Promise; +27: saveFlow: (flow: FlowDefinition) => Promise; +28: deleteFlow: (id: string) => Promise; +29: setDefaultFlow: (id: string) => Promise; +30: installSkill: (skillPath: string) => Promise; +31: uninstallSkill: (name: string) => Promise; +32: saveIntegration: (integration: IntegrationDefinition) => Promise; +33: deleteIntegration: (id: string) => Promise; +34: +35: +36: validateAgent: (agent: AgentDefinition) => Promise; +37: validateFlow: (flow: FlowDefinition) => Promise; +38: +39: +40: exportConfig: (type: 'agent' | 'stage' | 'flow', id: string) => Promise; +41: importConfig: (type: 'agent' | 'stage' | 'flow', jsonData: string) => Promise; +42: +43: +44: getBuiltinInstructions: () => Promise; +45: +46: +47: loadAvailableTools: () => Promise; +48: getToolsByCategory: () => Record; +49: } +``` + +### crates/cowork-gui/src/types/index.ts (73 lines) + +``` +1: export type { +2: IterationInfo, +3: IterationStatus, +4: InheritanceMode, +5: CreateIterationRequest, +6: StageDef, +7: } from './iteration'; +8: +9: +10: export type { +11: ProjectMetadata, +12: ProjectData, +13: ProjectStatus, +14: ProjectInfo, +15: CreateProjectRequest, +16: UpdateProjectRequest, +17: CreateProjectResponse, +18: } from './project'; +19: +20: +21: export type { +22: ThinkingMessage, +23: AgentMessage, +24: UserMessage, +25: PMAgentMessage, +26: ToolCallMessage, +27: ToolResultMessage, +28: ChatMessage, +29: ChatMode, +30: PMAction, +31: InputOption, +32: InputRequest, +33: } from './agent'; +34: +35: +36: export type { +37: Knowledge, +38: KnowledgeListResult, +39: } from './knowledge'; +40: +41: +42: export type { +43: AgentType, +44: ModelConfig, +45: ToolReference, +46: IncludeContentsMode, +47: AgentDefinition, +48: StageType, +49: HookPoint, +50: HookConfig, +51: ArtifactConfig, +52: StageRetryConfig, +53: StageDefinition, +54: MemoryScope, +55: +56: InheritanceConfig, +57: FlowConfig, +58: StageOverrides, +59: StageReference, +60: GlobalHookConfig, +61: FlowDefinition, +62: SkillInfo, +63: IntegrationType, +64: AuthType, +65: CredentialSource, +66: AuthConfig, +67: ConnectionConfig, +68: IntegrationEvent, +69: IntegrationDefinition, +70: ValidationIssue, +71: ValidationResult, +72: ConfigRegistryState, +73: } from './config'; +``` + +### crates/cowork-gui/src-tauri/src/commands/import_cmd.rs (88 lines) + +``` +1: ImportProgressEvent +2: ⋮---- +3: { +4: pub step: String, +5: pub message: String, +6: pub progress: u8, +7: } +8: ⋮---- +9: emit_progress +10: ⋮---- +11: (app_handle: &tauri::AppHandle, step: &str, message: &str, progress: u8) +12: ⋮---- +13: PreviewResponse +14: ⋮---- +15: { +16: pub success: bool, +17: pub preview: Option, +18: pub error: Option, +19: } +20: ⋮---- +21: ImportResponse +22: ⋮---- +23: { +24: pub success: bool, +25: pub project_id: Option, +26: pub project_name: Option, +27: pub iteration_id: Option, +28: pub artifacts: Vec, +29: pub used_llm: bool, +30: pub error: Option, +31: } +32: ⋮---- +33: preview_import +34: ⋮---- +35: (path: String) +36: ⋮---- +37: analyze_existing_project +38: ⋮---- +39: (path: String) +40: ⋮---- +41: run_llm_agent +42: ⋮---- +43: ( +44: project_path: &PathBuf, +45: artifacts_dir: &PathBuf, +46: artifact_options: &str, +47: app_handle: &tauri::AppHandle, +48: ) +49: ⋮---- +50: generate_template_artifacts +51: ⋮---- +52: ( +53: analysis: &ProjectAnalysis, +54: options: &cowork_core::importer::ArtifactGenerationOptions, +55: artifacts_dir: &PathBuf, +56: app_handle: &tauri::AppHandle, +57: ) +58: ⋮---- +59: import_project +60: ⋮---- +61: ( +62: path: String, +63: projectName: Option, +64: generateIdea: bool, +65: generatePrd: bool, +66: generateDesign: bool, +67: generatePlan: bool, +68: scanReadme: bool, +69: scanDocs: bool, +70: state: tauri::State<'_, AppState>, +71: app_handle: tauri::AppHandle, +72: ) +73: ⋮---- +74: get_default_artifact_options +75: ⋮---- +76: () +77: ⋮---- +78: copy_project_to_workspace +79: ⋮---- +80: (project_path: &PathBuf, workspace_dir: &PathBuf) +81: ⋮---- +82: should_skip +83: ⋮---- +84: (name: &str) +85: ⋮---- +86: copy_dir_recursive +87: ⋮---- +88: (src: &PathBuf, dst: &PathBuf, skip_hidden: bool) +``` + +### crates/cowork-gui/src-tauri/src/commands/preview.rs (52 lines) + +``` +1: get_code_directory +2: ⋮---- +3: (iteration_id: &str, workspace_path: Option<&str>) +4: ⋮---- +5: install_dependencies_if_needed +6: ⋮---- +7: (workspace: &std::path::Path) +8: ⋮---- +9: try_analyze_runtime +10: ⋮---- +11: (code_dir: &std::path::Path) +12: ⋮---- +13: is_vanilla_html_project +14: ⋮---- +15: (dir: &std::path::Path) +16: ⋮---- +17: has_html_files +18: ⋮---- +19: (dir: &std::path::Path) +20: ⋮---- +21: start_iteration_preview +22: ⋮---- +23: ( +24: iteration_id: String, +25: state: State<'_, AppState>, +26: ) +27: ⋮---- +28: stop_iteration_preview +29: ⋮---- +30: (iteration_id: String) +31: ⋮---- +32: check_preview_status +33: ⋮---- +34: (iteration_id: String) +35: ⋮---- +36: get_project_runtime_info +37: ⋮---- +38: ( +39: iteration_id: String, +40: state: State<'_, AppState>, +41: ) +42: ⋮---- +43: detect_start_command_with_info +44: ⋮---- +45: ( +46: _code_dir: &std::path::Path, +47: config_result: &Result, +48: ) +49: ⋮---- +50: generate_start_command +51: ⋮---- +52: (config: &cowork_core::ProjectRuntimeConfig) +``` + +### crates/cowork-gui/vite.config.js (40 lines) + +``` +1: import { defineConfig } from "vite"; +2: import react from "@vitejs/plugin-react"; +3: import path from "path"; +4: +5: export default defineConfig({ +6: plugins: [react()], +7: resolve: { +8: alias: { +9: "@": path.resolve(__dirname, "src"), +10: }, +11: }, +12: server: { +13: port: 15173, +14: }, +15: build: { +16: rollupOptions: { +17: output: { +18: manualChunks: { +19: +20: "vendor-react": ["react", "react-dom"], +21: +22: "vendor-antd": ["antd", "@ant-design/icons"], +23: +24: "vendor-monaco": ["@monaco-editor/react", "monaco-editor"], +25: +26: "vendor-markdown": [ +27: "react-markdown", +28: "remark-gfm", +29: "rehype-highlight", +30: "rehype-raw", +31: ], +32: +33: "vendor-zustand": ["zustand"], +34: }, +35: }, +36: }, +37: +38: chunkSizeWarningLimit: 1536, +39: }, +40: }); +``` + +### crates/cowork-core/Cargo.toml (63 lines) + +``` +1: [package] +2: name = "cowork-core" +3: version.workspace = true +4: edition.workspace = true +5: authors.workspace = true +6: license.workspace = true +7: repository.workspace = true +8: description = "AI-powered software development system - Core library with adk-rust framework" +9: +10: [dependencies] +11: adk-rust = { workspace = true } +12: adk-core = { workspace = true } +13: adk-agent = { workspace = true } +14: adk-model = { workspace = true, features = ["openai"] } +15: adk-tool = { workspace = true, features = ["http-transport"] } +16: adk-skill = { workspace = true } +17: +18: tokio = { workspace = true } +19: async-trait = "0.1" +20: async-stream = "0.3" +21: futures = { workspace = true } +22: +23: anyhow = { workspace = true } +24: thiserror = { workspace = true } +25: +26: serde = { workspace = true } +27: serde_json = { workspace = true } +28: +29: toml = { workspace = true } +30: +31: chrono = { workspace = true } +32: uuid = { workspace = true } +33: +34: tracing = { workspace = true } +35: +36: walkdir = { workspace = true } +37: ignore = { workspace = true } +38: +39: reqwest = { version = "0.13", features = ["json", "stream"] } +40: +41: dialoguer = { workspace = true } +42: console = { workspace = true } +43: +44: once_cell = "1.21" +45: +46: lazy_static = "1.5" +47: +48: dirs = "5.0" +49: +50: regex = "1" +51: +52: semver = "1.0" +53: +54: schemars = "1.0" +55: +56: include_dir = "0.7" +57: +58: agent-client-protocol = { workspace = true } +59: +60: tokio-util = { workspace = true } +61: +62: [dev-dependencies] +63: tempfile = { workspace = true } +``` + +### crates/cowork-core/src/instructions/plan.rs (391 lines) + +```` +1: pub const PLAN_ACTOR_INSTRUCTION: &str = r##" +2: # Your Role +3: You are Plan Actor. Create or update implementation tasks. +4: +5: # ⚠️ CRITICAL REQUIREMENT - YOU MUST CALL save_plan_doc() +6: **This is the MOST IMPORTANT requirement:** +7: - You MUST call `save_plan_doc(content)` at the END of your work +8: - Without calling this tool, the Plan stage CANNOT complete +9: - Your work will be LOST if you don't save the document +10: - Example: save_plan_doc(content) with your complete plan markdown +11: +12: # CRITICAL: ALWAYS CHECK FEEDBACK FIRST +13: **IMPORTANT**: Before doing anything else, you MUST call `load_feedback_history({"stage": "plan"})` as your VERY FIRST action in every execution. +14: - If feedback exists, you MUST follow the UPDATE MODE workflow +15: - If feedback is empty or not found, you follow the NEW MODE workflow +16: - This is not optional - checking feedback is mandatory +17: +18: # CRITICAL PRINCIPLE: SIMPLE TASKS, NO TESTING/OPTIMIZATION +19: **Tasks MUST focus ONLY on implementing core features:** +20: - ✅ Tasks that implement business logic and user-facing features +21: - ✅ Simple, straightforward implementation tasks +22: - ❌ NO unit test tasks (unless explicitly requested in requirements) +23: - ❌ NO integration test tasks (unless explicitly requested in requirements) +24: - ❌ NO end-to-end test tasks (unless explicitly requested in requirements) +25: - ❌ NO test coverage tasks +26: - ❌ NO performance optimization tasks +27: - ❌ NO deployment/DevOps tasks (unless explicitly in requirements) +28: - ❌ NO monitoring/logging setup tasks +29: - ❌ NO documentation tasks (beyond inline code comments) +30: - ❌ NO code quality/linting setup tasks (unless explicitly in requirements) +31: +32: # ⚠️ CRITICAL: COMPLETE PROJECT FILES (NEW - MANDATORY) +33: **EVERY PLAN MUST INCLUDE TASKS FOR ALL ESSENTIAL PROJECT FILES:** +34: +35: ## For Frontend/Web Projects: +36: **MANDATORY TASKS - Must create tasks for these files:** +37: - ✅ Task for `package.json` - with all dependencies, dev/build scripts +38: - ✅ Task for entry HTML (`index.html`) - with proper structure, script imports +39: - ✅ Task for build tool config (`vite.config.js` or equivalent) +40: - ✅ Task for main entry script (`src/main.js` or similar) +41: - ✅ Task for `.gitignore` file +42: - ✅ Tasks for actual feature implementation +43: +44: ## For Node.js Backend/Tool: +45: **MANDATORY TASKS - Must create tasks for these files:** +46: - ✅ Task for `package.json` - with dependencies, bin entry (for tools) +47: - ✅ Task for main entry (`src/index.js` or `index.js`) +48: - ✅ Task for `.gitignore` file +49: - ✅ Tasks for actual feature implementation +50: +51: ## For Rust Projects: +52: **MANDATORY TASKS - Must create tasks for these files:** +53: - ✅ Task for `Cargo.toml` - with dependencies and metadata +54: - ✅ Task for `src/main.rs` or `src/lib.rs` +55: - ✅ Task for `.gitignore` file +56: - ✅ Tasks for actual feature implementation +57: +58: **VALIDATION CHECK:** +59: Before finalizing the plan, verify: +60: - [ ] Is there a task to create package.json/Cargo.toml/requirements.txt? +61: - [ ] Is there a task to create entry file (index.html/main.rs/main.py)? +62: - [ ] Is there a task to create config files (vite.config.js/tsconfig.json)? +63: - [ ] Are all Design document's "Project Structure" files covered? +64: +65: **Task Count:** +66: - Keep it minimal: 5-12 tasks for simple projects +67: - Each task should be clear and focused on feature implementation +68: - Avoid creating separate tasks for testing/optimization/infrastructure +69: +70: # Workflow - TWO MODES +71: +72: ## Mode Detection (FIRST STEP - MANDATORY) +73: 1. **Call `load_feedback_history({"stage": "plan"})` - THIS IS MANDATORY EVERY TIME** +74: 2. If feedback history exists and has entries → **UPDATE MODE** +75: 3. If no feedback history or empty → **NEW MODE** +76: +77: ## NEW MODE (全新生成) +78: +79: ### Step 1: Load Design (MANDATORY) +80: 1. Call `get_design()` to read all components +81: 2. **STOP** if components are empty - report error and exit +82: 3. (Optional) Call `get_requirements()` for additional context +83: 4. **NEW - CRITICAL**: Read Design document's "Project Structure" section +84: - Identify ALL required files (package.json, entry files, config files) +85: - Note the complete directory structure +86: 5. Analyze design to plan 5-12 **SIMPLE** implementation tasks (core functionality only) +87: +88: ### Step 2: Create Formal Tasks (MANDATORY - INCLUDING ALL ESSENTIAL FILES) +89: 6. **FIRST PRIORITY**: Create tasks for ALL essential project files from Design: +90: - Task for package.json/Cargo.toml/requirements.txt (with all dependencies) +91: - Task for entry file(s) (index.html, main.js, src/main.rs, etc.) +92: - Task for config files (vite.config.js, tsconfig.json, etc.) +93: - Task for .gitignore +94: 7. **SECOND PRIORITY**: Create tasks for feature implementation +95: 8. For EACH task, **MUST** call `create_task(title, description, feature_id, component_id, files_to_create, dependencies, acceptance_criteria)` +96: 9. **CRITICAL**: Focus on core functionality ONLY: +97: - NO unit test tasks (unless explicitly in requirements) +98: - NO integration test tasks +99: - NO performance optimization tasks +100: - NO deployment/DevOps tasks (unless explicitly in requirements) +101: +102: **EXAMPLE TASK BREAKDOWN FOR WEB PROJECT:** +103: ``` +104: TASK-001: Create package.json and project configuration +105: files_to_create: ["package.json", "vite.config.js", ".gitignore"] +106: +107: TASK-002: Create entry HTML and main script +108: files_to_create: ["index.html", "src/main.jsx"] +109: dependencies: ["TASK-001"] +110: +111: TASK-003: Implement [Feature A] +112: files_to_create: ["src/components/FeatureA.jsx"] +113: dependencies: ["TASK-002"] +114: ``` +115: +116: ### Step 3: Save Plan Document (MANDATORY) +117: 7. **CRITICAL**: Generate a complete Implementation Plan markdown that MUST include: +118: - List of all tasks with clear descriptions +119: - **"Required Files Checklist" section** (NEW - MANDATORY): +120: ```markdown +121: ## Required Files Checklist +122: The following files MUST be created during implementation: +123: +124: ### Configuration Files: +125: - [ ] package.json (or Cargo.toml/requirements.txt) - Task: TASK-001 +126: - [ ] vite.config.js (or equivalent build config) - Task: TASK-001 +127: - [ ] .gitignore - Task: TASK-001 +128: +129: ### Entry Files: +130: - [ ] index.html (or src/main.rs/main.py) - Task: TASK-002 +131: - [ ] src/main.jsx (or main entry script) - Task: TASK-002 +132: +133: ### Feature Files: +134: - [ ] src/components/... - Various tasks +135: ``` +136: - Task dependency graph +137: - Implementation notes +138: 8. **MANDATORY**: Call `save_plan_doc(content=)` to save the document - The system will NOT auto-save! +139: +140: ### Step 4: Verify (MANDATORY) +141: 9. Call `get_plan()` to verify all tasks were created +142: 10. Confirm all tasks exist, then report success +143: +144: ## UPDATE MODE (增量更新 - 当 GotoStage 回退到此阶段时) +145: +146: ### Step 1: Analyze Feedback +147: 1. Call `load_feedback_history({"stage": "plan"})` - 获取最近的反馈信息 +148: 2. Read feedback.details to understand what needs to change +149: +150: ### Step 2: Load Context +151: 3. Call `get_design()` to load design components +152: 4. Call `load_design_doc()` to read the full design document +153: 5. Call `get_requirements()` for additional context if needed +154: +155: ### Step 3: CREATE TASKS (CRITICAL - THIS IS YOUR PRIMARY JOB!) +156: 6. **YOU ARE THE ACTOR, NOT THE CRITIC!** +157: - ❌ DO NOT output "Check 1: Verify Plan Data Exists" - that's Critic's job +158: - ❌ DO NOT say "plan.md file does not exist" - your job is to CREATE it +159: - ✅ Your job is to CREATE tasks using `create_task()` +160: - ✅ Your job is to SAVE plan using `save_plan_doc()` +161: +162: 7. **FOR EACH TASK** from design, call: +163: ``` +164: create_task( +165: title="Task title here", +166: description="What this task implements", +167: feature_id="FEAT-XXX", +168: component_id="COMP-XXX", +169: files_to_create=["src/file1.ts", "src/file2.ts"], +170: dependencies=[], +171: acceptance_criteria=["Criteria 1", "Criteria 2"] +172: ) +173: ``` +174: +175: 8. **MANDATORY**: You MUST create at least 5 tasks! +176: - Create tasks for all essential project files +177: - Create tasks for each component from design +178: - Create tasks for feature implementation +179: +180: ### Step 4: Save Plan Document (MANDATORY) +181: 9. Generate a complete Implementation Plan markdown +182: 10. **MANDATORY**: Call `save_plan_doc(content=)` - The system will NOT auto-save! +183: +184: ### Step 5: Verify Your Work +185: 11. Call `get_plan()` to confirm tasks were created +186: 12. If tasks exist, report success: "Created X tasks for [feature description]" +187: +188: ## ⚠️ CRITICAL: ACTOR vs CRITIC CONFUSION +189: +190: **You are the PLAN ACTOR. Your responsibilities:** +191: - ✅ CREATE tasks with `create_task()` +192: - ✅ SAVE plan document with `save_plan_doc()` +193: - ✅ READ design/requirements to understand what to implement +194: - ❌ DO NOT check if plan exists (that's Critic's job) +195: - ❌ DO NOT verify artifacts (that's Critic's job) +196: - ❌ DO NOT output "Check 1: Verify..." (that's Critic's job) +197: +198: **Example of WRONG behavior:** +199: ``` +200: ## Check 1: Verify Plan Data Exists ❌ FAIL +201: - get_plan() returns empty tasks array +202: - FAIL: 0 tasks +203: ``` +204: This is CRITIC output! Actor should NEVER output this! +205: +206: **Example of CORRECT behavior:** +207: ``` +208: I see the feedback says tasks are empty. Let me CREATE tasks now: +209: +210: [Tool Call] create_task: { +211: "title": "Implement performance monitor decorator", +212: "description": "...", +213: ... +214: } +215: +216: [Tool Call] create_task: { +217: "title": "Create performance data service", +218: ... +219: } +220: +221: [Tool Call] save_plan_doc: { +222: "content": "# Implementation Plan\n\n..." +223: } +224: ``` +225: +226: ### Step 4: Document Changes +227: 7. Generate updated plan document with: +228: - What changed and why (based on feedback) +229: - Impact on task dependencies +230: - Any implementation approach changes +231: 8. **MANDATORY**: Call `save_plan_doc(content=)` to save the document - The system will NOT auto-save! +232: +233: Note: Replace {ITERATION_ID} with the actual iteration ID provided in the prompt. +234: +235: # Tools Available +236: +237: ## Core Tools +238: - load_feedback_history() ← **START HERE - 检测是否是 UPDATE MODE** +239: - get_design() - Load design data +240: - get_plan() - Load existing tasks +241: - get_requirements() - Load requirements (optional context) +242: - load_prd_doc() - Load PRD document +243: - load_design_doc() - Load design document +244: - review_with_feedback_content(title, content, prompt) - Get user feedback +245: +246: ## NEW MODE Tools +247: - review_with_feedback_content(title, content, prompt) - Get user feedback +248: - create_task(title, description, feature_id, component_id, files_to_create, dependencies, acceptance_criteria) - Create ONE task +249: +250: ## UPDATE MODE Tools +251: - update_task_status(task_id, new_status) - Update task status +252: - save_plan_doc(content) - Save updated plan document +253: - Tasks are immutable - document changes in plan doc +254: +255: # CRITICAL RULES +256: +257: ## For NEW MODE +258: 1. SIMPLE TASKS ONLY: Focus on core functionality, no testing/optimization +259: 2. STOP if get_design() returns empty components +260: 3. You MUST call review_with_feedback_content in Step 3 +261: 4. **MANDATORY**: If action="feedback", you MUST revise and call review again +262: 5. You MUST use the FINALIZED draft (after all feedback) in Step 4 +263: 6. You MUST call create_task for EACH task in the FINALIZED draft +264: 7. You MUST write plan.md in Step 5 with content matching Step 4 +265: 8. Do NOT create testing/optimization tasks unless explicitly in requirements +266: 9. Do NOT skip steps or say "done" prematurely +267: +268: ## For UPDATE MODE +269: - Tasks are immutable once created - document changes in plan document +270: - Focus on documenting implementation adjustments based on feedback +271: - Preserve existing task definitions, update their descriptions in plan doc +272: - Update task statuses if implementation progress changes +273: - Be efficient - incremental documentation updates are faster than full regeneration +274: +275: **REMEMBER**: +276: - Always start with `load_feedback_history()` to detect mode +277: - **YOU ARE THE ACTOR - YOUR JOB IS TO CREATE TASKS AND SAVE PLAN** +278: - ❌ NEVER output "Check 1: Verify..." - that's Critic's work +279: - ❌ NEVER just analyze without calling create_task() +280: - ✅ MUST call create_task() at least 5 times +281: - ✅ MUST call save_plan_doc() at the end +282: - In UPDATE MODE, focus on creating tasks based on feedback +283: - In NEW MODE, follow the full creation workflow +284: - **If you find yourself saying "Check X: ...", STOP - you're doing Critic's job, not Actor's!** +285: "##; +286: +287: pub const PLAN_CRITIC_INSTRUCTION: &str = r#" +288: # Your Role +289: You are Plan Critic. You MUST verify that Plan Actor completed ALL required steps correctly. +290: +291: # CRITICAL: This is a GATEKEEPER role - you must BLOCK progress if Actor failed! +292: +293: # ⚠️ ANTI-LOOP PROTECTION (HIGHEST PRIORITY) +294: **CRITICAL**: To prevent infinite loops: +295: +296: 1. **Before calling provide_feedback**, ask yourself: +297: - "Have I already reported this EXACT issue before?" +298: +299: 2. **If you're about to give the SAME feedback twice**: +300: - ⛔ **STOP** - call `request_human_review()` instead +301: +302: 3. **Never call provide_feedback twice with same details** +303: +304: # SIMPLE TASKS CHECK - NEW PRIORITY +305: Before other checks, verify that tasks focus on CORE functionality: +306: - ❌ REJECT if tasks include unit test creation (unless explicitly in requirements) +307: - ❌ REJECT if tasks include integration test setup (unless explicitly in requirements) +308: - ❌ REJECT if tasks include E2E test implementation (unless explicitly in requirements) +309: - ❌ REJECT if tasks include test coverage reporting +310: - ❌ REJECT if tasks include performance optimization +311: - ❌ REJECT if tasks include deployment/DevOps work (unless in requirements) +312: - ❌ REJECT if tasks include linting/code quality setup (unless in requirements) +313: - ❌ REJECT if tasks say "Write comprehensive tests for X" +314: - ❌ REJECT if tasks say "Add unit tests for all modules" +315: - ✅ APPROVE only tasks that implement business logic and features +316: +317: ## Mandatory Checks (You MUST perform ALL of these) +318: +319: ### Check 1: Verify Plan Data Exists +320: 1. Call `get_plan()` to load all tasks +321: 2. **FAIL** if tasks array is empty +322: 3. Expected: 5-12 tasks (SIMPLE, core functionality only) +323: 4. **FAIL** if > 15 tasks (too granular) +324: +325: ### Check 2: Verify SIMPLE TASKS (NEW - CRITICAL) +326: 5. For each task, verify it focuses on core functionality: +327: - ❌ Does it say "Write tests for X"? → REJECT (unless explicitly in requirements) +328: - ❌ Does it say "Add unit tests for module X"? → REJECT (unless explicitly in requirements) +329: - ❌ Does it say "Create integration tests"? → REJECT (unless explicitly in requirements) +330: - ❌ Does it say "Implement E2E testing"? → REJECT (unless explicitly in requirements) +331: - ❌ Does it say "Set up test coverage reporting"? → REJECT +332: - ❌ Does it say "Optimize performance of X"? → REJECT +333: - ❌ Does it say "Set up CI/CD pipeline"? → REJECT (unless in requirements) +334: - ❌ Does it say "Create deployment scripts"? → REJECT (unless in requirements) +335: - ❌ Does it say "Set up ESLint/Prettier"? → REJECT (unless in requirements) +336: - ❌ Does it say "Configure logging/monitoring"? → REJECT +337: - ✅ Is it implementing a feature or business logic? → APPROVE +338: +339: 6. If tasks include prohibited work: +340: - **MUST** call `provide_feedback(stage="plan", feedback_type="task_scope_issue", severity="critical", details="Tasks include testing/optimization/deployment/linting work: [list prohibited tasks]", suggested_fix="Remove all non-core tasks. Only keep tasks that implement features and business logic. Examples to remove: 'Write tests', 'Add unit tests', 'Set up CI/CD', 'Configure linting'.")` +341: +342: ### Check 3: Verify Task Dependencies +343: 7. Call `check_task_dependencies()` to verify no circular dependencies +344: 8. **FAIL** if circular dependencies exist +345: +346: ### Check 4: Verify Artifacts Exist (CRITICAL - MUST DO THIS!) +347: 9. **YOU MUST CALL `load_plan_doc()` TO VERIFY THE PLAN MARKDOWN FILE EXISTS** +348: 10. **DO NOT assume anything about tool availability - just call load_plan_doc() and check if it returns content** +349: 11. **If load_plan_doc() returns an error or empty content, THEN report it** +350: 12. **DO NOT report "save_plan_doc tool is not available" - this is incorrect** +351: +352: ## Your Response +353: +354: ### If ALL checks pass: +355: - "✅ Plan approved: [N] simple tasks covering all features, no testing/optimization/deployment tasks." +356: - Provide brief positive feedback on the task breakdown +357: +358: ### If any check FAILS: +359: - Call `provide_feedback(stage="plan", feedback_type, severity, details, suggested_fix)` with specific issues +360: - Use appropriate severity: +361: - "critical" for empty data, missing artifacts, prohibited task types +362: - "major" for circular dependencies +363: - "minor" for documentation issues +364: +365: # Tools Available +366: - get_plan() - Load plan data +367: - check_task_dependencies() - Verify no circular dependencies +368: - load_plan_doc() - Verify plan markdown document (MUST CALL THIS!) +369: - provide_feedback(stage="plan", feedback_type, severity, details, suggested_fix) - Report issues +370: +371: # Anti-Loop Examples +372: +373: ## ✅ CORRECT - Different feedback each time +374: ``` +375: Iteration 1: provide_feedback(stage="plan", feedback_type="task_scope_issue", severity="critical", details="Tasks include unit test creation", suggested_fix="...") +376: Iteration 2: provide_feedback(stage="plan", feedback_type="task_scope_issue", severity="critical", details="Still found test tasks: TASK-003, TASK-007", suggested_fix="...") +377: Iteration 3: request_human_review("Unable to resolve test task issue") +378: ``` +379: +380: ## ❌ WRONG - Same feedback twice +381: ``` +382: Iteration 1: provide_feedback(stage="plan", feedback_type="task_scope_issue", severity="critical", details="Tasks include unit test creation", suggested_fix="...") +383: Iteration 2: provide_feedback(stage="plan", feedback_type="task_scope_issue", severity="critical", details="Tasks include unit test creation", suggested_fix="...") ← PROHIBITED! +384: ``` +385: +386: **REMEMBER**: +387: - SIMPLE TASKS ONLY is your top priority - reject testing/optimization/deployment tasks +388: - Prevent loops by varying feedback or calling request_human_review +389: - Be a GATEKEEPER - don't approve substandard work +390: - **MUST call load_plan_doc() to verify artifacts - DO NOT assume tool availability** +391: "#; +```` + +### crates/cowork-core/src/persistence/iteration_data.rs (135 lines) + +``` +1: set_iteration_id +2: ⋮---- +3: (iteration_id: String) +4: ⋮---- +5: get_iteration_id +6: ⋮---- +7: () +8: ⋮---- +9: clear_iteration_id +10: ⋮---- +11: () +12: ⋮---- +13: get_iteration_dir +14: ⋮---- +15: () +16: ⋮---- +17: data_path +18: ⋮---- +19: (filename: &str) +20: ⋮---- +21: artifact_path +22: ⋮---- +23: (filename: &str) +24: ⋮---- +25: session_path +26: ⋮---- +27: (filename: &str) +28: ⋮---- +29: load_requirements +30: ⋮---- +31: () +32: ⋮---- +33: save_requirements +34: ⋮---- +35: (requirements: &Requirements) +36: ⋮---- +37: load_feature_list +38: ⋮---- +39: () +40: ⋮---- +41: save_feature_list +42: ⋮---- +43: (features: &FeatureList) +44: ⋮---- +45: load_design_spec +46: ⋮---- +47: () +48: ⋮---- +49: save_design_spec +50: ⋮---- +51: (design: &DesignSpec) +52: ⋮---- +53: load_implementation_plan +54: ⋮---- +55: () +56: ⋮---- +57: save_implementation_plan +58: ⋮---- +59: (plan: &ImplementationPlan) +60: ⋮---- +61: load_code_metadata +62: ⋮---- +63: () +64: ⋮---- +65: save_code_metadata +66: ⋮---- +67: (metadata: &CodeMetadata) +68: ⋮---- +69: load_session_meta +70: ⋮---- +71: () +72: ⋮---- +73: save_session_meta +74: ⋮---- +75: (meta: &SessionMeta) +76: ⋮---- +77: load_feedback_history +78: ⋮---- +79: () +80: ⋮---- +81: save_feedback_history +82: ⋮---- +83: (history: &FeedbackHistory) +84: ⋮---- +85: append_feedback +86: ⋮---- +87: (feedback: &Feedback) +88: ⋮---- +89: clear_stage_feedback +90: ⋮---- +91: (stage: &str) +92: ⋮---- +93: clear_all_feedback +94: ⋮---- +95: () +96: ⋮---- +97: load_idea +98: ⋮---- +99: () +100: ⋮---- +101: save_idea +102: ⋮---- +103: (content: &str) +104: ⋮---- +105: save_plan_doc +106: ⋮---- +107: (content: &str) +108: ⋮---- +109: save_prd_doc +110: ⋮---- +111: (content: &str) +112: ⋮---- +113: save_design_doc +114: ⋮---- +115: (content: &str) +116: ⋮---- +117: save_delivery_report +118: ⋮---- +119: (content: &str) +120: ⋮---- +121: save_check_report +122: ⋮---- +123: (content: &str) +124: ⋮---- +125: generate_id +126: ⋮---- +127: (prefix: &str, counter: usize) +128: ⋮---- +129: cowork_dir_exists +130: ⋮---- +131: () +132: ⋮---- +133: iteration_dir_exists +134: ⋮---- +135: () +``` + +### crates/cowork-core/src/persistence/mod.rs (23 lines) + +``` +1: get_global_workspace_lock +2: ⋮---- +3: () +4: ⋮---- +5: set_workspace_path +6: ⋮---- +7: (path: PathBuf) +8: ⋮---- +9: get_workspace_path +10: ⋮---- +11: () +12: ⋮---- +13: get_cowork_dir +14: ⋮---- +15: () +16: ⋮---- +17: is_project_initialized +18: ⋮---- +19: () +20: ⋮---- +21: init_project_structure +22: ⋮---- +23: (_project_name: &str) +``` + +### crates/cowork-core/src/pipeline/executor/knowledge.rs (34 lines) + +``` +1: generate_document_summaries +2: ⋮---- +3: ( +4: iteration_store: &IterationStore, +5: iteration: &Iteration, +6: model: Arc, +7: ) +8: ⋮---- +9: generate_iteration_knowledge +10: ⋮---- +11: ( +12: iteration_store: &IterationStore, +13: iteration: &Iteration, +14: model: Arc, +15: ) +16: ⋮---- +17: inject_project_knowledge +18: ⋮---- +19: ( +20: iteration_store: &IterationStore, +21: iteration: &Iteration, +22: ) +23: ⋮---- +24: regenerate_iteration_knowledge +25: ⋮---- +26: ( +27: iteration_store: &IterationStore, +28: iteration_id: &str, +29: model: Arc, +30: ) +31: ⋮---- +32: extract_summary_from_response +33: ⋮---- +34: (response: &str) +``` + +### crates/cowork-core/src/pipeline/executor/mod.rs (591 lines) + +``` +1: IterationExecutor +2: ⋮---- +3: { +4: project_store: ProjectStore, +5: iteration_store: IterationStore, +6: interaction: Arc, +7: } +8: ⋮---- +9: IterationExecutor +10: ⋮---- +11: { +12: pub fn new(interaction: Arc) -> Self { +13: Self { +14: project_store: ProjectStore::new(), +15: iteration_store: IterationStore::new(), +16: interaction, +17: } +18: } +19: +20: +21: pub fn create_genesis_iteration( +22: &self, +23: project: &mut Project, +24: title: impl Into, +25: description: impl Into, +26: ) -> anyhow::Result { +27: let iteration = crate::domain::Iteration::create_genesis(project, title.into(), description.into()); +28: +29: self.iteration_store.save(&iteration)?; +30: self.project_store +31: .add_iteration(project, iteration.to_summary())?; +32: +33: Ok(iteration) +34: } +35: +36: +37: pub fn create_evolution_iteration( +38: &self, +39: project: &mut Project, +40: title: impl Into, +41: description: impl Into, +42: base_iteration_id: impl Into, +43: ) -> anyhow::Result { +44: let iteration = crate::domain::Iteration::create_evolution( +45: project, +46: title.into(), +47: description.into(), +48: base_iteration_id.into(), +49: crate::domain::InheritanceMode::Full, +50: ); +51: +52: self.iteration_store.save(&iteration)?; +53: self.project_store +54: .add_iteration(project, iteration.to_summary())?; +55: +56: Ok(iteration) +57: } +58: +59: +60: pub async fn execute( +61: &self, +62: project: &mut Project, +63: iteration_id: &str, +64: resume_stage: Option, +65: _model: Option>, +66: ) -> anyhow::Result<()> { +67: let mut iteration = self.iteration_store.load(iteration_id)?; +68: +69: +70: let workspace = workspace::prepare_workspace( +71: &self.iteration_store, +72: &self.interaction, +73: &iteration, +74: ).await?; +75: +76: +77: let start_stage = if let Some(stage) = resume_stage { +78: stage +79: } else if let Some(ref current) = iteration.current_stage { +80: current.clone() +81: } else { +82: iteration.determine_start_stage() +83: }; +84: +85: let stages = get_stages_from_flow(&start_stage); +86: let flow_config = get_flow_config(); +87: +88: println!( +89: "[Executor] Using Flow config: stop_on_failure={}, memory_scope={:?}", +90: flow_config.stop_on_failure, flow_config.memory_scope +91: ); +92: +93: +94: iteration.start(); +95: self.iteration_store.save(&iteration)?; +96: self.project_store +97: .set_current_iteration(project, iteration_id.to_string())?; +98: +99: +100: let memory_store = crate::persistence::MemoryStore::new(); +101: if let Err(e) = memory_store.ensure_iteration_memory(iteration_id) { +102: println!("[Executor] Warning: Failed to create iteration memory: {}", e); +103: } +104: +105: println!( +106: "[Executor] Iteration '{}' started, will execute {} stages starting from '{}'", +107: iteration.title, +108: stages.len(), +109: start_stage +110: ); +111: +112: self.interaction +113: .show_message_with_context( +114: crate::interaction::MessageLevel::Info, +115: format!( +116: "Starting iteration '{}' from stage '{}'", +117: iteration.title, start_stage +118: ), +119: MessageContext::new("Pipeline Controller"), +120: ) +121: .await; +122: +123: +124: if iteration.base_iteration_id.is_some() { +125: if let Err(e) = knowledge::inject_project_knowledge(&self.iteration_store, &iteration).await { +126: println!("[Executor] Warning: Failed to inject project knowledge: {}", e); +127: } +128: } +129: +130: println!("[Executor] Starting stage execution loop..."); +131: self.execute_stages_from(project, &mut iteration, stages, workspace, flow_config).await +132: } +133: +134: +135: async fn execute_stages_from( +136: &self, +137: project: &mut Project, +138: iteration: &mut crate::domain::Iteration, +139: stages: Vec>, +140: workspace: std::path::PathBuf, +141: flow_config: crate::config_definition::flow_definition::FlowConfig, +142: ) -> anyhow::Result<()> { +143: const MAX_STAGE_RETRIES: u32 = 3; +144: const RETRY_DELAY_MS: u64 = 5000; +145: const MAX_FEEDBACK_LOOPS: u32 = 5; +146: +147: let total_stages = stages.len(); +148: let ctx = PipelineContext::new(project.clone(), iteration.clone(), workspace.clone()); +149: +150: crate::persistence::set_iteration_id(iteration.id.clone()); +151: +152: for (stage_idx, stage) in stages.into_iter().enumerate() { +153: let stage_name = stage.name().to_string(); +154: let stage_num = stage_idx + 1; +155: +156: iteration.set_stage(&stage_name); +157: self.iteration_store.save(&iteration)?; +158: +159: println!("[Executor] Stage updated: {} (iteration: {})", stage_name, iteration.id); +160: +161: self.interaction +162: .show_message_with_context( +163: crate::interaction::MessageLevel::Info, +164: format!( +165: "🚀 [{}/{}] Starting stage: {}", +166: stage_num, +167: total_stages, +168: stage.description() +169: ), +170: MessageContext::new("Pipeline Controller").with_stage(&stage_name), +171: ) +172: .await; +173: +174: let mut last_error = None; +175: let mut success = false; +176: +177: for attempt in 0..MAX_STAGE_RETRIES { +178: if attempt > 0 { +179: println!( +180: "[Executor] Retrying stage '{}' (attempt {}/{})", +181: stage_name, attempt + 1, MAX_STAGE_RETRIES +182: ); +183: self.interaction +184: .show_message_with_context( +185: crate::interaction::MessageLevel::Warning, +186: format!( +187: "Retrying stage '{}' (attempt {}/{})", +188: stage_name, attempt + 1, MAX_STAGE_RETRIES +189: ), +190: MessageContext::new("Pipeline Controller").with_stage(&stage_name), +191: ) +192: .await; +193: tokio::time::sleep(tokio::time::Duration::from_millis(RETRY_DELAY_MS)).await; +194: } +195: +196: +197: let mut current_feedback: Option = None; +198: let mut feedback_loop_count: u32 = 0; +199: +200: if let Ok(feedback_history) = crate::persistence::load_feedback_history() { +201: if let Some(fb) = feedback_history +202: .feedbacks +203: .iter() +204: .filter(|f| f.stage == stage_name) +205: .max_by_key(|f| f.timestamp) +206: { +207: tracing::info!("[Executor] Found stored feedback for stage '{}': {}", +208: stage_name, fb.details.chars().take(100).collect::()); +209: current_feedback = Some(fb.details.clone()); +210: } +211: } +212: +213: loop { +214: let result = if let Some(ref feedback) = current_feedback { +215: stage +216: .execute_with_feedback(&ctx, self.interaction.clone(), feedback) +217: .await +218: } else { +219: stage.execute(&ctx, self.interaction.clone()).await +220: }; +221: +222: match result { +223: StageResult::GotoStage(target_stage, reason) => { +224: self.interaction +225: .show_message_with_context( +226: crate::interaction::MessageLevel::Warning, +227: format!( +228: "🔄 Stage jump requested: {} → {}\nReason: {}", +229: stage_name, target_stage, reason +230: ), +231: MessageContext::new("Pipeline Controller").with_stage(&stage_name), +232: ) +233: .await; +234: +235: if let Err(e) = crate::persistence::clear_stage_feedback(&stage_name) { +236: eprintln!("[Warning] Failed to clear feedback for stage '{}': {}", stage_name, e); +237: } +238: +239: iteration.set_stage(&target_stage); +240: self.iteration_store.save(&iteration)?; +241: +242: let new_stages = get_stages_from_flow(&target_stage); +243: +244: self.interaction +245: .show_message_with_context( +246: crate::interaction::MessageLevel::Info, +247: format!( +248: "Restarting pipeline from '{}' stage with {} stages to execute", +249: target_stage, +250: new_stages.len() +251: ), +252: MessageContext::new("Pipeline Controller"), +253: ) +254: .await; +255: +256: return Box::pin(self.execute_stages_from( +257: project, +258: iteration, +259: new_stages, +260: workspace.clone(), +261: flow_config.clone(), +262: )).await; +263: } +264: StageResult::Success(artifact_path) => { +265: let artifact_exists = if let Some(ref path) = artifact_path { +266: std::path::Path::new(path).exists() +267: } else { +268: workspace::check_artifact_exists(&stage_name, &workspace).await +269: }; +270: +271: if !artifact_exists { +272: last_error = Some(format!("Artifacts not generated for stage '{}'", stage_name)); +273: +274: self.interaction +275: .show_message_with_context( +276: crate::interaction::MessageLevel::Error, +277: format!("❌ Stage '{}' completed but artifacts not found. Will retry...", stage_name), +278: MessageContext::new("Pipeline Controller").with_stage(&stage_name), +279: ) +280: .await; +281: beak; +282: } +283: +284: if let Err(e) = crate::persistence::clear_stage_feedback(&stage_name) { +285: eprintln!("[Warning] Failed to clear feedback for stage '{}': {}", stage_name, e); +286: } +287: +288: iteration.complete_stage(&stage_name, artifact_path.clone()); +289: self.iteration_store.save(&iteration)?; +290: +291: let progress_msg = if feedback_loop_count > 0 { +292: format!( +293: "✅ [{}/{}] Stage '{}' completed (revision {})", +294: stage_num, total_stages, stage_name, feedback_loop_count +295: ) +296: } else if attempt > 0 { +297: format!( +298: "✅ [{}/{}] Stage '{}' completed (after {} retries)", +299: stage_num, total_stages, stage_name, attempt +300: ) +301: } else { +302: format!("✅ [{}/{}] Stage '{}' completed", stage_num, total_stages, stage_name) +303: }; +304: +305: self.interaction +306: .show_message_with_context( +307: crate::interaction::MessageLevel::Success, +308: progress_msg, +309: MessageContext::new("Pipeline Controller").with_stage(&stage_name), +310: ) +311: .await; +312: +313: if is_critical_stage(&stage_name) { +314: iteration.pause(); +315: self.iteration_store.save(&iteration)?; +316: +317: let artifact_type = match stage_name.as_str() { +318: "idea" => "idea", +319: "prd" => "requirements", +320: "design" => "design", +321: "plan" => "plan", +322: "coding" => "code", +323: _ => "artifacts", +324: }; +325: +326: let action = self.interaction +327: .request_confirmation_with_feedback( +328: &format!( +329: "Stage '{}' completed. Please review the generated {} document.{}", +330: stage_name, +331: stage_name.to_uppercase(), +332: if feedback_loop_count > 0 { +333: format!(" (Revision {})", feedback_loop_count) +334: } else { +335: String::new() +336: } +337: ), +338: artifact_type +339: ) +340: .await; +341: +342: match action { +343: ConfirmationAction::Continue => { +344: iteration.resume(); +345: self.iteration_store.save(&iteration)?; +346: success = true; +347: beak; +348: } +349: ConfirmationAction::ViewArtifact => { +350: current_feedback = None; +351: continue; +352: } +353: ConfirmationAction::ProvideFeedback(feedback) => { +354: if feedback_loop_count >= MAX_FEEDBACK_LOOPS { +355: self.interaction +356: .show_message_with_context( +357: crate::interaction::MessageLevel::Warning, +358: format!("Maximum revision attempts ({}) reached. Proceeding...", MAX_FEEDBACK_LOOPS), +359: MessageContext::new("Pipeline Controller").with_stage(&stage_name), +360: ) +361: .await; +362: iteration.resume(); +363: self.iteration_store.save(&iteration)?; +364: success = true; +365: beak; +366: } +367: +368: feedback_loop_count += 1; +369: current_feedback = Some(feedback); +370: self.interaction +371: .show_message_with_context( +372: crate::interaction::MessageLevel::Info, +373: format!("Revising stage '{}' based on feedback...", stage_name), +374: MessageContext::new("Pipeline Controller").with_stage(&stage_name), +375: ) +376: .await; +377: continue; +378: } +379: ConfirmationAction::Cancel => { +380: iteration.pause(); +381: self.iteration_store.save(&iteration)?; +382: return Err(anyhow::anyhow!("User cancelled at stage '{}'", stage_name)); +383: } +384: } +385: } else { +386: success = true; +387: beak; +388: } +389: } +390: StageResult::Failed(e) => { +391: last_error = Some(e.clone()); +392: self.interaction +393: .show_message_with_context( +394: crate::interaction::MessageLevel::Error, +395: format!("❌ Stage '{}' failed: {}", stage_name, e), +396: MessageContext::new("Pipeline Controller").with_stage(&stage_name), +397: ) +398: .await; +399: beak; +400: } +401: StageResult::Paused => { +402: iteration.pause(); +403: self.iteration_store.save(&iteration)?; +404: self.interaction +405: .show_message_with_context( +406: crate::interaction::MessageLevel::Info, +407: format!("⏸️ Stage '{}' paused by user", stage_name), +408: MessageContext::new("Pipeline Controller").with_stage(&stage_name), +409: ) +410: .await; +411: return Ok(()); +412: } +413: StageResult::NeedsRevision(e) => { +414: last_error = Some(e.clone()); +415: self.interaction +416: .show_message_with_context( +417: crate::interaction::MessageLevel::Warning, +418: format!("🔄 Stage '{}' needs revision: {}", stage_name, e), +419: MessageContext::new("Pipeline Controller").with_stage(&stage_name), +420: ) +421: .await; +422: beak; +423: } +424: } +425: } +426: +427: if success { +428: beak; +429: } +430: } +431: +432: if !success { +433: if flow_config.stop_on_failure { +434: iteration.fail(); +435: self.iteration_store.save(&iteration)?; +436: +437: return Err(anyhow::anyhow!( +438: "Stage '{}' failed after {} retries: {}", +439: stage_name, +440: MAX_STAGE_RETRIES, +441: last_error.unwrap_or_else(|| "Unknown error".to_string()) +442: )); +443: } else { +444: self.interaction +445: .show_message_with_context( +446: crate::interaction::MessageLevel::Warning, +447: format!("Skipping failed stage '{}' and continuing...", stage_name), +448: MessageContext::new("Pipeline Controller").with_stage(&stage_name), +449: ) +450: .await; +451: } +452: } +453: } +454: +455: +456: iteration.complete(); +457: self.iteration_store.save(&iteration)?; +458: +459: +460: if let Err(e) = crate::persistence::MemoryStore::new().promote_insights_to_decisions(&iteration.id) { +461: println!("[Executor] Warning: Failed to promote insights: {}", e); +462: } +463: +464: project.current_iteration_id = Some(iteration.id.clone()); +465: self.project_store.save(project)?; +466: +467: self.interaction +468: .show_message_with_context( +469: crate::interaction::MessageLevel::Success, +470: format!("Iteration '{}' completed successfully!", iteration.title), +471: MessageContext::new("Pipeline Controller"), +472: ) +473: .await; +474: +475: Ok(()) +476: } +477: +478: +479: pub async fn continue_iteration( +480: &self, +481: project: &mut Project, +482: iteration_id: &str, +483: model: Option>, +484: ) -> anyhow::Result<()> { +485: let mut iteration = self.iteration_store.load(iteration_id)?; +486: +487: println!( +488: "[Executor] Continuing iteration '{}' (status: {:?}, current_stage: {:?})", +489: iteration_id, iteration.status, iteration.current_stage +490: ); +491: +492: if iteration.status != IterationStatus::Paused { +493: return Err(anyhow::anyhow!("Iteration is not paused")); +494: } +495: +496: let resume_stage = iteration.current_stage.clone(); +497: println!("[Executor] Resuming from stage: {:?}", resume_stage); +498: +499: iteration.resume(); +500: self.iteration_store.save(&iteration)?; +501: +502: self.interaction +503: .show_message_with_context( +504: crate::interaction::MessageLevel::Info, +505: format!( +506: "Iteration '{}' resumed from stage: {}", +507: iteration_id, +508: resume_stage.as_ref().unwrap_or(&"unknown".to_string()) +509: ), +510: MessageContext::new("Pipeline Controller") +511: .with_stage(resume_stage.as_deref().unwrap_or("unknown")), +512: ) +513: .await; +514: +515: self.execute(project, iteration_id, resume_stage, model).await +516: } +517: +518: +519: pub async fn retry_iteration( +520: &self, +521: project: &mut Project, +522: iteration_id: &str, +523: ) -> anyhow::Result<()> { +524: let mut iteration = self.iteration_store.load(iteration_id)?; +525: +526: println!( +527: "[Executor] Retrying failed iteration '{}' (status: {:?}, current_stage: {:?})", +528: iteration_id, iteration.status, iteration.current_stage +529: ); +530: +531: if iteration.status != IterationStatus::Failed { +532: return Err(anyhow::anyhow!("Iteration is not failed")); +533: } +534: +535: let retry_stage = if let Some(ref current) = iteration.current_stage { +536: current.clone() +537: } else { +538: println!("[Executor] No current_stage found, defaulting to 'check' for retry"); +539: "check".to_string() +540: }; +541: +542: iteration.resume(); +543: self.iteration_store.save(&iteration)?; +544: +545: self.interaction +546: .show_message_with_context( +547: crate::interaction::MessageLevel::Info, +548: format!("Retrying iteration '{}' from stage: {}", iteration_id, retry_stage), +549: MessageContext::new("Pipeline Controller").with_stage(&retry_stage), +550: ) +551: .await; +552: +553: self.execute(project, iteration_id, Some(retry_stage), None).await +554: } +555: +556: +557: +558: +559: +560: +561: pub async fn generate_document_summaries( +562: &self, +563: iteration: &crate::domain::Iteration, +564: model: Arc, +565: ) -> anyhow::Result<()> { +566: knowledge::generate_document_summaries(&self.iteration_store, iteration, model).await +567: } +568: +569: +570: pub async fn generate_iteration_knowledge( +571: &self, +572: iteration: &crate::domain::Iteration, +573: model: Arc, +574: ) -> anyhow::Result<()> { +575: knowledge::generate_iteration_knowledge(&self.iteration_store, iteration, model).await +576: } +577: +578: +579: pub async fn inject_project_knowledge(&self, iteration: &crate::domain::Iteration) -> anyhow::Result<()> { +580: knowledge::inject_project_knowledge(&self.iteration_store, iteration).await +581: } +582: +583: +584: pub async fn regenerate_iteration_knowledge( +585: &self, +586: iteration_id: &str, +587: model: Arc, +588: ) -> anyhow::Result<()> { +589: knowledge::regenerate_iteration_knowledge(&self.iteration_store, iteration_id, model).await +590: } +591: } +``` + +### crates/cowork-core/src/pipeline/mod.rs (124 lines) + +``` +1: StageResult +2: ⋮---- +3: { +4: Success(Option), +5: Failed(String), +6: Paused, +7: NeedsRevision(String), +8: GotoStage(String, String), +9: } +10: ⋮---- +11: PipelineContext +12: ⋮---- +13: { +14: pub project: Project, +15: pub iteration: Iteration, +16: pub workspace_path: std::path::PathBuf, +17: } +18: ⋮---- +19: PipelineContext +20: ⋮---- +21: { +22: pub fn new(project: Project, iteration: Iteration, workspace_path: std::path::PathBuf) -> Self { +23: Self { +24: project, +25: iteration, +26: workspace_path, +27: } +28: } +29: } +30: ⋮---- +31: Stage +32: ⋮---- +33: { +34: fn name(&self) -> &str; +35: fn description(&self) -> &str; +36: +37: +38: fn needs_confirmation(&self) -> bool { +39: false +40: } +41: +42: +43: async fn execute( +44: &self, +45: ctx: &PipelineContext, +46: interaction: Arc, +47: ) -> StageResult; +48: +49: +50: async fn execute_with_feedback( +51: &self, +52: ctx: &PipelineContext, +53: interaction: Arc, +54: _feedback: &str, +55: ) -> StageResult { +56: +57: +58: self.execute(ctx, interaction).await +59: } +60: } +61: ⋮---- +62: get_all_stages +63: ⋮---- +64: () +65: ⋮---- +66: get_stages_from +67: ⋮---- +68: (start_stage: &str) +69: ⋮---- +70: create_stage_by_id +71: ⋮---- +72: (stage_id: &str) +73: ⋮---- +74: get_stages_from_flow +75: ⋮---- +76: (start_stage: &str) +77: ⋮---- +78: get_flow_config +79: ⋮---- +80: () +81: ⋮---- +82: is_critical_stage +83: ⋮---- +84: (stage_name: &str) +85: ⋮---- +86: test_get_all_stages_order +87: ⋮---- +88: () +89: ⋮---- +90: test_get_stages_from_beginning +91: ⋮---- +92: () +93: ⋮---- +94: test_get_stages_from_middle +95: ⋮---- +96: () +97: ⋮---- +98: test_get_stages_from_end +99: ⋮---- +100: () +101: ⋮---- +102: test_get_stages_from_unknown +103: ⋮---- +104: () +105: ⋮---- +106: test_create_stage_by_id_valid +107: ⋮---- +108: () +109: ⋮---- +110: test_create_stage_by_id_invalid +111: ⋮---- +112: () +113: ⋮---- +114: test_is_critical_stage +115: ⋮---- +116: () +117: ⋮---- +118: test_pipeline_context_new +119: ⋮---- +120: () +121: ⋮---- +122: test_stage_names_and_descriptions +123: ⋮---- +124: () +``` + +### crates/cowork-core/src/pipeline/stages/coding.rs (270 lines) + +``` +1: CodingStage +2: ⋮---- +3: { +4: +5: fn is_external_enabled() -> bool { +6: match load_config() { +7: Ok(config) => config.coding_agent.enabled, +8: Err(_) => false, +9: } +10: } +11: +12: +13: async fn execute_external( +14: ctx: &PipelineContext, +15: interaction: Arc, +16: feedback: Option<&str>, +17: ) -> StageResult { +18: +19: crate::persistence::set_iteration_id(ctx.iteration.id.clone()); +20: +21: let workspace = ctx.workspace_path.clone(); +22: +23: interaction +24: .show_message_with_context( +25: MessageLevel::Info, +26: "🚀 Using External Coding Agent (ACP)".to_string(), +27: MessageContext::new(AGENT_NAME_EXTERNAL).with_stage("coding"), +28: ) +29: .await; +30: +31: +32: +33: +34: let task_description = if let Some(fb) = feedback { +35: +36: println!("[Coding] Using parameter feedback: {}", fb.chars().take(100).collect::()); +37: format!( +38: "## ⚠️ USER REPORTED ISSUE - REQUIRES FIX\n\n\ +39: The user has reported the following problems with the project:\n\n\ +40: \"\"\"\n{}\n\"\"\"\n\n\ +41: ## Your Task\n\ +42: 1. Read and understand the user's issues above\n\ +43: 2. Find the relevant code files\n\ +44: 3. Fix each issue one by one\n\ +45: 4. Verify your fixes work correctly", +46: fb +47: ) +48: } else { +49: +50: let stored_feedback = crate::persistence::load_feedback_history() +51: .ok() +52: .and_then(|history| { +53: history.feedbacks +54: .into_iter() +55: .filter(|f| f.stage == "coding") +56: .max_by_key(|f| f.timestamp) +57: }); +58: +59: if let Some(ref fb) = stored_feedback { +60: println!("[Coding] Found fallback feedback from storage: {}", fb.details.chars().take(100).collect::()); +61: format!("Fix issues based on feedback: {}", fb.details) +62: } else { +63: +64: println!("[Coding] No feedback found, loading plan..."); +65: let iteration_dir = workspace.parent().unwrap_or(&workspace); +66: let plan_artifact = iteration_dir.join("artifacts").join("plan.md"); +67: +68: if let Ok(content) = std::fs::read_to_string(&plan_artifact) { +69: format!("Implement the tasks from the plan:\n\n{}", content) +70: } else { +71: "Implement the planned features.".to_string() +72: } +73: } +74: }; +75: +76: +77: let project_context = format!( +78: "Project: {}\nDescription: {}", +79: ctx.iteration.title, +80: ctx.iteration.description +81: ); +82: +83: +84: eprintln!("DEBUG: Creating ExternalCodingAgent for workspace: {}", workspace.display()); +85: eprintln!("DEBUG: Iteration id={}, base_id={:?}, inheritance={:?}", +86: ctx.iteration.id, ctx.iteration.base_iteration_id, ctx.iteration.inheritance); +87: let agent = match ExternalCodingAgent::new_with_iteration(&workspace, Some(ctx.iteration.clone())).await { +88: Ok(agent) => agent, +89: Err(e) => { +90: interaction +91: .show_message_with_context( +92: MessageLevel::Error, +93: format!("Failed to start external agent: {}", e), +94: MessageContext::new(AGENT_NAME_EXTERNAL).with_stage("coding"), +95: ) +96: .await; +97: +98: tracing::warn!("Falling back to built-in coding agent"); +99: return if let Some(fb) = feedback { +100: execute_stage_with_instruction(ctx, interaction, "coding", CODING_ACTOR_INSTRUCTION, Some(fb)).await +101: } else { +102: execute_stage_with_instruction(ctx, interaction, "coding", CODING_ACTOR_INSTRUCTION, None).await +103: }; +104: } +105: }; +106: +107: +108: let ctx_external = MessageContext::new(AGENT_NAME_EXTERNAL).with_stage("coding"); +109: +110: +111: let StreamingTask { mut messages, result } = agent.execute_task_stream(&task_description, &project_context); +112: +113: +114: let interaction_clone = interaction.clone(); +115: +116: +117: +118: let message_handle = tokio::spawn(async move { +119: let mut thinking_buffer = String::new(); +120: let mut output_buffer = String::new(); +121: +122: loop { +123: tokio::select! { +124: msg = messages.recv() => { +125: match msg { +126: Some(AgentMessage::Thinking(text)) => { +127: +128: thinking_buffer.push_str(&text); +129: +130: if thinking_buffer.chars().count() > 100 { +131: let truncated: String = thinking_buffer.chars().take(100).collect(); +132: let display = format!("💭 Thinking: {}...", truncated); +133: interaction_clone.show_message_with_context(MessageLevel::Info, display, ctx_external.clone()).await; +134: thinking_buffer.clear(); +135: } +136: } +137: Some(AgentMessage::Output(text)) => { +138: output_buffer.push_str(&text); +139: +140: if output_buffer.chars().count() > 200 { +141: let truncated: String = output_buffer.chars().take(200).collect(); +142: let display = format!("📝 Output: {}...", truncated); +143: interaction_clone.show_message_with_context(MessageLevel::Info, display, ctx_external.clone()).await; +144: output_buffer.clear(); +145: } +146: } +147: Some(AgentMessage::Status(text)) => { +148: interaction_clone.show_message_with_context(MessageLevel::Info, format!("⏳ {}", text), ctx_external.clone()).await; +149: } +150: Some(AgentMessage::Error(text)) => { +151: interaction_clone.show_message_with_context(MessageLevel::Error, format!("❌ {}", text), ctx_external.clone()).await; +152: } +153: Some(AgentMessage::Completed) => { +154: interaction_clone.show_message_with_context(MessageLevel::Info, "✅ Task completed".to_string(), ctx_external.clone()).await; +155: } +156: None => { +157: +158: beak; +159: } +160: } +161: } +162: _ = tokio::time::sleep(tokio::time::Duration::from_secs(60)) => { +163: +164: interaction_clone.show_message_with_context(MessageLevel::Info, "⏳ Waiting for agent...".to_string(), ctx_external.clone()).await; +165: } +166: } +167: } +168: }); +169: +170: +171: match result.await { +172: +173: Ok(Ok(_output)) => { +174: +175: let _ = message_handle.await; +176: +177: interaction +178: .show_message_with_context( +179: MessageLevel::Info, +180: "External coding agent completed successfully".to_string(), +181: MessageContext::new(AGENT_NAME_EXTERNAL).with_stage("coding"), +182: ) +183: .await; +184: StageResult::Success(None) +185: } +186: +187: Ok(Err(e)) => { +188: let error_msg = format!("External agent execution error: {}", e); +189: interaction +190: .show_message_with_context( +191: MessageLevel::Error, +192: error_msg.clone(), +193: MessageContext::new(AGENT_NAME_EXTERNAL).with_stage("coding"), +194: ) +195: .await; +196: StageResult::Failed(e.to_string()) +197: } +198: +199: Err(e) => { +200: let error_msg = format!("External agent error: {}", e); +201: interaction +202: .show_message_with_context( +203: MessageLevel::Error, +204: error_msg.clone(), +205: MessageContext::new(AGENT_NAME_EXTERNAL).with_stage("coding"), +206: ) +207: .await; +208: StageResult::Failed(e.to_string()) +209: } +210: } +211: } +212: } +213: ⋮---- +214: CodingStage +215: ⋮---- +216: { +217: fn name(&self) -> &str { +218: "coding" +219: } +220: +221: fn description(&self) -> &str { +222: "Coding - Generate code implementation using Agent with Memory and Tools" +223: } +224: +225: fn needs_confirmation(&self) -> bool { +226: true +227: } +228: +229: async fn execute( +230: &self, +231: ctx: &PipelineContext, +232: interaction: Arc, +233: ) -> StageResult { +234: +235: if Self::is_external_enabled() { +236: return Self::execute_external(ctx, interaction, None).await; +237: } +238: +239: execute_stage_with_instruction(ctx, interaction, "coding", CODING_ACTOR_INSTRUCTION, None).await +240: } +241: +242: async fn execute_with_feedback( +243: &self, +244: ctx: &PipelineContext, +245: interaction: Arc, +246: feedback: &str, +247: ) -> StageResult { +248: +249: let agent_name = if Self::is_external_enabled() { +250: AGENT_NAME_EXTERNAL +251: } else { +252: AGENT_NAME_BUILTIN +253: }; +254: +255: interaction +256: .show_message_with_context( +257: MessageLevel::Info, +258: "Regenerating code based on your feedback...".to_string(), +259: MessageContext::new(agent_name).with_stage("coding"), +260: ) +261: .await; +262: +263: +264: if Self::is_external_enabled() { +265: return Self::execute_external(ctx, interaction, Some(feedback)).await; +266: } +267: +268: execute_stage_with_instruction(ctx, interaction, "coding", CODING_ACTOR_INSTRUCTION, Some(feedback)).await +269: } +270: } +``` + +### crates/cowork-core/src/tools/control_tools.rs (134 lines) + +``` +1: ProvideFeedbackTool +2: ⋮---- +3: { +4: fn name(&self) -> &str { +5: "provide_feedback" +6: } +7: +8: fn description(&self) -> &str { +9: "Provide structured feedback to the Actor agent. \ +10: This feedback will be visible to the Actor in the next iteration." +11: } +12: +13: fn parameters_schema(&self) -> Option { +14: Some(json!({ +15: "type": "object", +16: "properties": { +17: "stage": { +18: "type": "string", +19: "description": "The stage providing this feedback (e.g., 'idea', 'prd', 'design', 'plan', 'coding', 'check', 'delivery')", +20: "enum": ["idea", "prd", "design", "plan", "coding", "check", "delivery"] +21: }, +22: "feedback_type": { +23: "type": "string", +24: "enum": ["build_error", "quality_issue", "missing_requirement", "suggestion"], +25: }, +26: "severity": { +27: "type": "string", +28: "enum": ["critical", "major", "minor"], +29: }, +30: "details": {"type": "string"}, +31: "suggested_fix": {"type": "string"} +32: }, +33: "required": ["stage", "feedback_type", "severity", "details"] +34: })) +35: } +36: +37: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +38: let stage = get_required_string_param(&args, "stage")?; +39: +40: let feedback_type = match get_required_string_param(&args, "feedback_type")? { +41: "build_error" => FeedbackType::BuildError, +42: "quality_issue" => FeedbackType::QualityIssue, +43: "missing_requirement" => FeedbackType::MissingRequirement, +44: _ => FeedbackType::Suggestion, +45: }; +46: +47: let severity = match get_required_string_param(&args, "severity")? { +48: "critical" => Severity::Critical, +49: "major" => Severity::Major, +50: _ => Severity::Minor, +51: }; +52: +53: let feedback = Feedback { +54: stage: stage.to_string(), +55: feedback_type, +56: severity, +57: details: get_required_string_param(&args, "details")?.to_string(), +58: suggested_fix: args +59: .get("suggested_fix") +60: .and_then(|v| v.as_str()) +61: .map(String::from), +62: timestamp: chrono::Utc::now(), +63: }; +64: +65: append_feedback(&feedback).map_err(|e| adk_core::AdkError::tool(e.to_string()))?; +66: +67: Ok(json!({ +68: "status": "feedback_recorded", +69: "message": "Feedback will be available to Actor in next iteration" +70: })) +71: } +72: } +73: ⋮---- +74: AskUserTool +75: ⋮---- +76: { +77: fn name(&self) -> &str { +78: "ask_user" +79: } +80: +81: fn description(&self) -> &str { +82: "Ask the user for confirmation or input via CLI interface." +83: } +84: +85: fn parameters_schema(&self) -> Option { +86: Some(json!({ +87: "type": "object", +88: "properties": { +89: "question": { +90: "type": "string", +91: "description": "The question to ask the user" +92: }, +93: "question_type": { +94: "type": "string", +95: "enum": ["yes_no", "text_input"], +96: "description": "Type of question" +97: } +98: }, +99: "required": ["question", "question_type"] +100: })) +101: } +102: +103: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +104: let question = get_required_string_param(&args, "question")?; +105: let question_type = get_required_string_param(&args, "question_type")?; +106: +107: match question_type { +108: "yes_no" => { +109: let answer = Confirm::new() +110: .with_prompt(question) +111: .default(false) +112: .interact() +113: .map_err(|e| adk_core::AdkError::tool(e.to_string()))?; +114: +115: Ok(json!({ +116: "answer": answer, +117: "answer_type": "boolean" +118: })) +119: } +120: "text_input" => { +121: let answer: String = Input::new() +122: .with_prompt(question) +123: .interact_text() +124: .map_err(|e| adk_core::AdkError::tool(e.to_string()))?; +125: +126: Ok(json!({ +127: "answer": answer, +128: "answer_type": "text" +129: })) +130: } +131: _ => Ok(json!({"error": "Invalid question type"})), +132: } +133: } +134: } +``` + +### crates/cowork-core/src/tools/data_tools.rs (839 lines) + +``` +1: CreateRequirementTool +2: ⋮---- +3: { +4: fn name(&self) -> &str { +5: "create_requirement" +6: } +7: +8: fn description(&self) -> &str { +9: "Create a new requirement in requirements.json. Requirements define what \ +10: the system must do. Each requirement should be SMART (Specific, Measurable, \ +11: Achievable, Relevant, Time-bound) with clear acceptance criteria." +12: } +13: +14: fn parameters_schema(&self) -> Option { +15: Some(json!({ +16: "type": "object", +17: "properties": { +18: "title": { +19: "type": "string", +20: "description": "Brief requirement title" +21: }, +22: "description": { +23: "type": "string", +24: "description": "Detailed description of the requirement" +25: }, +26: "priority": { +27: "type": "string", +28: "enum": ["high", "medium", "low"], +29: "description": "Priority level" +30: }, +31: "category": { +32: "type": "string", +33: "enum": ["functional", "non_functional"], +34: "description": "Requirement category" +35: }, +36: "acceptance_criteria": { +37: "type": "array", +38: "items": {"type": "string"}, +39: "description": "List of acceptance criteria" +40: } +41: }, +42: "required": ["title", "description", "priority", "category", "acceptance_criteria"] +43: })) +44: } +45: +46: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +47: +48: let title = args.get("title").and_then(|v| v.as_str()).unwrap_or(""); +49: super::notify_tool_call("create_requirement", &json!({"title": title})); +50: +51: let mut reqs = load_requirements().map_err(|e| AdkError::tool(e.to_string()))?; +52: +53: let req_id = generate_id("REQ", reqs.requirements.len()); +54: +55: let priority = match get_required_string_param(&args, "priority")? { +56: "high" => Priority::High, +57: "medium" => Priority::Medium, +58: "low" => Priority::Low, +59: _ => Priority::Medium, +60: }; +61: +62: let category = match get_required_string_param(&args, "category")? { +63: "functional" => RequirementCategory::Functional, +64: "non_functional" => RequirementCategory::NonFunctional, +65: _ => RequirementCategory::Functional, +66: }; +67: +68: let requirement = Requirement { +69: id: req_id.clone(), +70: title: get_required_string_param(&args, "title")?.to_string(), +71: description: get_required_string_param(&args, "description")?.to_string(), +72: priority, +73: category, +74: acceptance_criteria: get_required_array_param(&args, "acceptance_criteria")? +75: .iter() +76: .map(|v| v.as_str().unwrap().to_string()) +77: .collect(), +78: related_features: vec![], +79: }; +80: +81: reqs.requirements.push(requirement.clone()); +82: reqs.updated_at = chrono::Utc::now(); +83: save_requirements(&reqs).map_err(|e| AdkError::tool(e.to_string()))?; +84: +85: +86: println!("✅ Created: {} - {}", req_id, requirement.title); +87: +88: +89: super::notify_tool_result("create_requirement", &Ok(json!({"status": "success", "requirement_id": req_id}))); +90: +91: Ok(json!({ +92: "status": "success", +93: "requirement_id": req_id, +94: "message": format!("Requirement {} created successfully", req_id) +95: })) +96: } +97: } +98: ⋮---- +99: AddFeatureTool +100: ⋮---- +101: { +102: fn name(&self) -> &str { +103: "add_feature" +104: } +105: +106: fn description(&self) -> &str { +107: "Add a new feature to feature_list.json. Features are concrete \ +108: functionalities that implement one or more requirements. Each \ +109: feature will later be broken down into implementation tasks." +110: } +111: +112: fn parameters_schema(&self) -> Option { +113: Some(json!({ +114: "type": "object", +115: "properties": { +116: "name": { +117: "type": "string", +118: "description": "Feature name" +119: }, +120: "description": { +121: "type": "string", +122: "description": "Detailed description" +123: }, +124: "requirement_ids": { +125: "type": "array", +126: "items": {"type": "string"}, +127: "description": "IDs of requirements this feature implements" +128: }, +129: "completion_criteria": { +130: "type": "array", +131: "items": {"type": "string"}, +132: "description": "Criteria for feature completion" +133: } +134: }, +135: "required": ["name", "description", "requirement_ids", "completion_criteria"] +136: })) +137: } +138: +139: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +140: let name = get_required_string_param(&args, "name")?; +141: super::notify_tool_call("add_feature", &json!({"name": name})); +142: +143: let mut features = load_feature_list().map_err(|e| AdkError::tool(e.to_string()))?; +144: +145: let feat_id = generate_id("FEAT", features.features.len()); +146: +147: let feature = Feature { +148: id: feat_id.clone(), +149: name: get_required_string_param(&args, "name")?.to_string(), +150: description: get_required_string_param(&args, "description")?.to_string(), +151: requirement_ids: get_required_array_param(&args, "requirement_ids")? +152: .iter() +153: .map(|v| v.as_str().unwrap().to_string()) +154: .collect(), +155: status: FeatureStatus::Pending, +156: assigned_to_tasks: vec![], +157: completion_criteria: get_required_array_param(&args, "completion_criteria")? +158: .iter() +159: .map(|v| v.as_str().unwrap().to_string()) +160: .collect(), +161: created_at: chrono::Utc::now(), +162: completed_at: None, +163: metadata: FeatureMetadata::default(), +164: }; +165: +166: features.features.push(feature); +167: save_feature_list(&features).map_err(|e| AdkError::tool(e.to_string()))?; +168: +169: Ok(json!({ +170: "status": "success", +171: "feature_id": feat_id, +172: "message": format!("Feature {} created successfully", feat_id) +173: })) +174: } +175: } +176: ⋮---- +177: CreateDesignComponentTool +178: ⋮---- +179: { +180: fn name(&self) -> &str { +181: "create_design_component" +182: } +183: +184: fn description(&self) -> &str { +185: "Create a new component in design_spec.json. Components are the \ +186: architectural building blocks (services, modules, UI components) \ +187: that implement features." +188: } +189: +190: fn parameters_schema(&self) -> Option { +191: Some(json!({ +192: "type": "object", +193: "properties": { +194: "name": { +195: "type": "string", +196: "description": "Component name" +197: }, +198: "component_type": { +199: "type": "string", +200: "enum": ["backend_service", "frontend_component", "database", "api_gateway"], +201: "description": "Type of component" +202: }, +203: "responsibilities": { +204: "type": "array", +205: "items": {"type": "string"}, +206: "description": "List of responsibilities" +207: }, +208: "technology": { +209: "type": "string", +210: "description": "Technology stack" +211: }, +212: "related_features": { +213: "type": "array", +214: "items": {"type": "string"}, +215: "description": "Related feature IDs" +216: } +217: }, +218: "required": ["name", "component_type", "responsibilities", "technology"] +219: })) +220: } +221: +222: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +223: let name = args.get("name").and_then(|v| v.as_str()).unwrap_or("unknown"); +224: super::notify_tool_call("create_design_component", &json!({"name": name})); +225: +226: let mut design = load_design_spec().map_err(|e| AdkError::tool(e.to_string()))?; +227: +228: let comp_id = generate_id("COMP", design.architecture.components.len()); +229: +230: +231: let component_type = args.get("component_type") +232: .and_then(|v| v.as_str()) +233: .ok_or_else(|| AdkError::tool("Missing or invalid 'component_type' parameter".to_string()))?; +234: +235: let component_type = match component_type { +236: "backend_service" => ComponentType::BackendService, +237: "frontend_component" => ComponentType::FrontendComponent, +238: "database" => ComponentType::Database, +239: "api_gateway" => ComponentType::ApiGateway, +240: other => ComponentType::Other(other.to_string()), +241: }; +242: +243: +244: let name = args.get("name") +245: .and_then(|v| v.as_str()) +246: .ok_or_else(|| AdkError::tool("Missing or invalid 'name' parameter".to_string()))? +247: .to_string(); +248: +249: let technology = args.get("technology") +250: .and_then(|v| v.as_str()) +251: .ok_or_else(|| AdkError::tool("Missing or invalid 'technology' parameter".to_string()))? +252: .to_string(); +253: +254: +255: let responsibilities = args.get("responsibilities") +256: .and_then(|v| v.as_array()) +257: .ok_or_else(|| AdkError::tool("Missing or invalid 'responsibilities' parameter (must be an array)".to_string()))? +258: .iter() +259: .filter_map(|v| v.as_str().map(|s| s.to_string())) +260: .collect::>(); +261: +262: if responsibilities.is_empty() { +263: return Err(AdkError::tool("'responsibilities' array cannot be empty".to_string())); +264: } +265: +266: +267: let related_features = args.get("related_features") +268: .and_then(|v| v.as_array()) +269: .map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect()) +270: .unwrap_or_default(); +271: +272: let component = DesignComponent { +273: id: comp_id.clone(), +274: name, +275: component_type, +276: responsibilities, +277: technology, +278: interfaces: vec![], +279: related_features, +280: }; +281: +282: design.architecture.components.push(component.clone()); +283: save_design_spec(&design).map_err(|e| AdkError::tool(e.to_string()))?; +284: +285: +286: println!("🏗️ Created component: {} - {}", comp_id, component.name); +287: +288: Ok(json!({ +289: "status": "success", +290: "component_id": comp_id, +291: "message": format!("Component {} created successfully", comp_id) +292: })) +293: } +294: } +295: ⋮---- +296: CreateTaskTool +297: ⋮---- +298: { +299: fn name(&self) -> &str { +300: "create_task" +301: } +302: +303: fn description(&self) -> &str { +304: "Create an implementation task in implementation_plan.json. Tasks \ +305: are concrete coding work items that implement features." +306: } +307: +308: fn parameters_schema(&self) -> Option { +309: Some(json!({ +310: "type": "object", +311: "properties": { +312: "title": {"type": "string"}, +313: "description": {"type": "string"}, +314: "feature_id": {"type": "string"}, +315: "component_id": {"type": "string"}, +316: "files_to_create": { +317: "type": "array", +318: "items": {"type": "string"} +319: }, +320: "dependencies": { +321: "type": "array", +322: "items": {"type": "string"}, +323: "description": "Task IDs that must be completed first" +324: }, +325: "acceptance_criteria": { +326: "type": "array", +327: "items": {"type": "string"} +328: } +329: }, +330: "required": ["title", "description", "feature_id", "component_id", "acceptance_criteria"] +331: })) +332: } +333: +334: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +335: let title = get_required_string_param(&args, "title")?; +336: super::notify_tool_call("create_task", &json!({"title": title})); +337: +338: let mut plan = load_implementation_plan().map_err(|e| AdkError::tool(e.to_string()))?; +339: +340: let task_id = generate_id("TASK", plan.tasks.len()); +341: +342: let task = Task { +343: id: task_id.clone(), +344: title: get_required_string_param(&args, "title")?.to_string(), +345: description: get_required_string_param(&args, "description")?.to_string(), +346: feature_id: get_required_string_param(&args, "feature_id")?.to_string(), +347: component_id: get_required_string_param(&args, "component_id")?.to_string(), +348: status: TaskStatus::Pending, +349: dependencies: args.get("dependencies") +350: .and_then(|v| v.as_array()) +351: .map(|arr| arr.iter().map(|v| v.as_str().unwrap().to_string()).collect()) +352: .unwrap_or_default(), +353: estimated_effort: None, +354: files_to_create: args.get("files_to_create") +355: .and_then(|v| v.as_array()) +356: .map(|arr| arr.iter().map(|v| v.as_str().unwrap().to_string()).collect()) +357: .unwrap_or_default(), +358: acceptance_criteria: get_required_array_param(&args, "acceptance_criteria")? +359: .iter() +360: .map(|v| v.as_str().unwrap().to_string()) +361: .collect(), +362: created_at: chrono::Utc::now(), +363: started_at: None, +364: completed_at: None, +365: }; +366: +367: plan.tasks.push(task); +368: save_implementation_plan(&plan).map_err(|e| AdkError::tool(e.to_string()))?; +369: +370: Ok(json!({ +371: "status": "success", +372: "task_id": task_id, +373: "message": format!("Task {} created successfully", task_id) +374: })) +375: } +376: } +377: ⋮---- +378: UpdateFeatureStatusTool +379: ⋮---- +380: { +381: fn name(&self) -> &str { +382: "update_feature_status" +383: } +384: +385: fn description(&self) -> &str { +386: "Update the status of a feature. Valid transitions: \ +387: pending → in_progress → completed." +388: } +389: +390: fn parameters_schema(&self) -> Option { +391: Some(json!({ +392: "type": "object", +393: "properties": { +394: "feature_id": {"type": "string"}, +395: "new_status": { +396: "type": "string", +397: "enum": ["pending", "in_progress", "completed", "blocked"] +398: } +399: }, +400: "required": ["feature_id", "new_status"] +401: })) +402: } +403: +404: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +405: let feature_id = get_required_string_param(&args, "feature_id")?; +406: let new_status_str = get_required_string_param(&args, "new_status")?; +407: super::notify_tool_call("update_feature_status", &json!({"feature_id": feature_id, "status": new_status_str})); +408: +409: let mut features = load_feature_list().map_err(|e| AdkError::tool(e.to_string()))?; +410: +411: let new_status = match new_status_str { +412: "pending" => FeatureStatus::Pending, +413: "in_progress" => FeatureStatus::InProgress, +414: "completed" => FeatureStatus::Completed, +415: "blocked" => FeatureStatus::Blocked, +416: _ => FeatureStatus::Pending, +417: }; +418: +419: if let Some(feature) = features.features.iter_mut().find(|f| f.id == feature_id) { +420: feature.status = new_status; +421: if new_status_str == "completed" { +422: feature.completed_at = Some(chrono::Utc::now()); +423: } +424: save_feature_list(&features).map_err(|e| AdkError::tool(e.to_string()))?; +425: +426: Ok(json!({ +427: "status": "success", +428: "feature_id": feature_id, +429: "new_status": new_status_str, +430: "message": format!("Feature {} status updated to {}", feature_id, new_status_str) +431: })) +432: } else { +433: Ok(json!({ +434: "status": "error", +435: "message": format!("Feature {} not found", feature_id) +436: })) +437: } +438: } +439: } +440: ⋮---- +441: UpdateTaskStatusTool +442: ⋮---- +443: { +444: fn name(&self) -> &str { +445: "update_task_status" +446: } +447: +448: fn description(&self) -> &str { +449: "Update task status. Call this as you start and complete tasks." +450: } +451: +452: fn parameters_schema(&self) -> Option { +453: Some(json!({ +454: "type": "object", +455: "properties": { +456: "task_id": {"type": "string"}, +457: "new_status": { +458: "type": "string", +459: "enum": ["pending", "in_progress", "completed", "blocked"] +460: } +461: }, +462: "required": ["task_id", "new_status"] +463: })) +464: } +465: +466: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +467: +468: let task_id = args.get("task_id") +469: .and_then(|v| v.as_str()) +470: .ok_or_else(|| AdkError::tool("Missing or invalid 'task_id' parameter".to_string()))?; +471: let new_status_str = args.get("new_status") +472: .and_then(|v| v.as_str()) +473: .ok_or_else(|| AdkError::tool("Missing or invalid 'new_status' parameter".to_string()))?; +474: super::notify_tool_call("update_task_status", &json!({"task_id": task_id, "status": new_status_str})); +475: +476: let mut plan = load_implementation_plan().map_err(|e| AdkError::tool(e.to_string()))?; +477: +478: let new_status = match new_status_str { +479: "pending" => TaskStatus::Pending, +480: "in_progress" => TaskStatus::InProgress, +481: "completed" => TaskStatus::Completed, +482: "blocked" => TaskStatus::Blocked, +483: _ => return Err(AdkError::tool(format!("Invalid status: {}. Must be one of: pending, in_progress, completed, blocked", new_status_str))), +484: }; +485: +486: if let Some(task) = plan.tasks.iter_mut().find(|t| t.id == task_id) { +487: task.status = new_status; +488: match new_status_str { +489: "in_progress" => task.started_at = Some(chrono::Utc::now()), +490: "completed" => task.completed_at = Some(chrono::Utc::now()), +491: _ => {} +492: } +493: save_implementation_plan(&plan).map_err(|e| AdkError::tool(e.to_string()))?; +494: +495: +496: println!("✓ Task {} → {}", task_id, new_status_str); +497: +498: Ok(json!({ +499: "status": "success", +500: "task_id": task_id, +501: "new_status": new_status_str +502: })) +503: } else { +504: Ok(json!({ +505: "status": "error", +506: "message": format!("Task {} not found", task_id) +507: })) +508: } +509: } +510: } +511: ⋮---- +512: GetRequirementsTool +513: ⋮---- +514: { +515: fn name(&self) -> &str { +516: "get_requirements" +517: } +518: +519: fn description(&self) -> &str { +520: "Retrieve all requirements and features." +521: } +522: +523: fn parameters_schema(&self) -> Option { +524: Some(json!({ +525: "type": "object", +526: "properties": {} +527: })) +528: } +529: +530: async fn execute(&self, _ctx: Arc, _args: Value) -> adk_core::Result { +531: let requirements = load_requirements().map_err(|e| AdkError::tool(e.to_string()))?; +532: let features = load_feature_list().map_err(|e| AdkError::tool(e.to_string()))?; +533: +534: Ok(json!({ +535: "requirements": requirements.requirements, +536: "features": features.features +537: })) +538: } +539: } +540: ⋮---- +541: GetDesignTool +542: ⋮---- +543: { +544: fn name(&self) -> &str { +545: "get_design" +546: } +547: +548: fn description(&self) -> &str { +549: "Retrieve the design specification." +550: } +551: +552: fn parameters_schema(&self) -> Option { +553: Some(json!({"type": "object", "properties": {}})) +554: } +555: +556: async fn execute(&self, _ctx: Arc, _args: Value) -> adk_core::Result { +557: let design = load_design_spec().map_err(|e| AdkError::tool(e.to_string()))?; +558: Ok(serde_json::to_value(design).map_err(|e| AdkError::tool(e.to_string()))?) +559: } +560: } +561: ⋮---- +562: GetPlanTool +563: ⋮---- +564: { +565: fn name(&self) -> &str { +566: "get_plan" +567: } +568: +569: fn description(&self) -> &str { +570: "Retrieve the implementation plan with all tasks." +571: } +572: +573: fn parameters_schema(&self) -> Option { +574: Some(json!({ +575: "type": "object", +576: "properties": { +577: "status_filter": { +578: "type": "string", +579: "enum": ["pending", "in_progress", "completed"], +580: "description": "Optional: only return tasks with this status" +581: } +582: } +583: })) +584: } +585: +586: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +587: let plan = load_implementation_plan().map_err(|e| AdkError::tool(e.to_string()))?; +588: +589: if let Some(status_filter) = args.get("status_filter").and_then(|v| v.as_str()) { +590: let status = match status_filter { +591: "pending" => TaskStatus::Pending, +592: "in_progress" => TaskStatus::InProgress, +593: "completed" => TaskStatus::Completed, +594: _ => TaskStatus::Pending, +595: }; +596: +597: let filtered_tasks: Vec<&Task> = plan.tasks.iter() +598: .filter(|t| t.status == status) +599: .collect(); +600: +601: Ok(json!({ +602: "tasks": filtered_tasks, +603: "milestones": plan.milestones +604: })) +605: } else { +606: Ok(serde_json::to_value(plan).map_err(|e| AdkError::tool(e.to_string()))?) +607: } +608: } +609: } +610: ⋮---- +611: UpdateRequirementTool +612: ⋮---- +613: { +614: fn name(&self) -> &str { +615: "update_requirement" +616: } +617: +618: fn description(&self) -> &str { +619: "Update an existing requirement. Use this when restarting from a stage with feedback \ +620: to modify specific requirements without recreating the entire list." +621: } +622: +623: fn parameters_schema(&self) -> Option { +624: Some(json!({ +625: "type": "object", +626: "properties": { +627: "id": { +628: "type": "string", +629: "description": "Requirement ID to update (e.g., REQ-001)" +630: }, +631: "title": { +632: "type": "string", +633: "description": "New title for the requirement" +634: }, +635: "description": { +636: "type": "string", +637: "description": "New description for the requirement" +638: }, +639: "priority": { +640: "type": "string", +641: "enum": ["high", "medium", "low"], +642: "description": "New priority level" +643: }, +644: "acceptance_criteria": { +645: "type": "array", +646: "items": {"type": "string"}, +647: "description": "New list of acceptance criteria" +648: } +649: }, +650: "required": ["id"] +651: })) +652: } +653: +654: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +655: let mut requirements = load_requirements().map_err(|e| AdkError::tool(e.to_string()))?; +656: +657: let id = args.get("id") +658: .and_then(|v| v.as_str()) +659: .ok_or_else(|| AdkError::tool("Missing 'id' parameter".to_string()))?; +660: +661: if let Some(req) = requirements.requirements.iter_mut().find(|r| r.id == id) { +662: +663: if let Some(title) = args.get("title").and_then(|v| v.as_str()) { +664: req.title = title.to_string(); +665: } +666: if let Some(description) = args.get("description").and_then(|v| v.as_str()) { +667: req.description = description.to_string(); +668: } +669: if let Some(priority_str) = args.get("priority").and_then(|v| v.as_str()) { +670: req.priority = match priority_str { +671: "high" => Priority::High, +672: "medium" => Priority::Medium, +673: "low" => Priority::Low, +674: _ => req.priority, +675: }; +676: } +677: if let Some(criteria) = args.get("acceptance_criteria").and_then(|v| v.as_array()) { +678: req.acceptance_criteria = criteria.iter() +679: .filter_map(|v| v.as_str().map(|s| s.to_string())) +680: .collect(); +681: } +682: +683: save_requirements(&requirements).map_err(|e| AdkError::tool(e.to_string()))?; +684: +685: println!("✅ Updated requirement: {}", id); +686: +687: Ok(json!({ +688: "status": "success", +689: "requirement_id": id, +690: "message": format!("Requirement {} updated successfully", id) +691: })) +692: } else { +693: Ok(json!({ +694: "status": "error", +695: "message": format!("Requirement {} not found", id) +696: })) +697: } +698: } +699: } +700: ⋮---- +701: DeleteRequirementTool +702: ⋮---- +703: { +704: fn name(&self) -> &str { +705: "delete_requirement" +706: } +707: +708: fn description(&self) -> &str { +709: "Delete a requirement by ID. Use this when feedback indicates a requirement is no longer needed." +710: } +711: +712: fn parameters_schema(&self) -> Option { +713: Some(json!({ +714: "type": "object", +715: "properties": { +716: "id": { +717: "type": "string", +718: "description": "Requirement ID to delete (e.g., REQ-001)" +719: } +720: }, +721: "required": ["id"] +722: })) +723: } +724: +725: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +726: let mut requirements = load_requirements().map_err(|e| AdkError::tool(e.to_string()))?; +727: +728: let id = args.get("id") +729: .and_then(|v| v.as_str()) +730: .ok_or_else(|| AdkError::tool("Missing 'id' parameter".to_string()))?; +731: +732: let original_len = requirements.requirements.len(); +733: requirements.requirements.retain(|r| r.id != id); +734: +735: if requirements.requirements.len() < original_len { +736: save_requirements(&requirements).map_err(|e| AdkError::tool(e.to_string()))?; +737: +738: println!("🗑️ Deleted requirement: {}", id); +739: +740: Ok(json!({ +741: "status": "success", +742: "requirement_id": id, +743: "message": format!("Requirement {} deleted successfully", id) +744: })) +745: } else { +746: Ok(json!({ +747: "status": "error", +748: "message": format!("Requirement {} not found", id) +749: })) +750: } +751: } +752: } +753: ⋮---- +754: UpdateFeatureTool +755: ⋮---- +756: { +757: fn name(&self) -> &str { +758: "update_feature" +759: } +760: +761: fn description(&self) -> &str { +762: "Update an existing feature. Use this when restarting from a stage with feedback \ +763: to modify specific features without recreating the entire list." +764: } +765: +766: fn parameters_schema(&self) -> Option { +767: Some(json!({ +768: "type": "object", +769: "properties": { +770: "id": { +771: "type": "string", +772: "description": "Feature ID to update (e.g., FEAT-001)" +773: }, +774: "name": { +775: "type": "string", +776: "description": "New name for the feature" +777: }, +778: "description": { +779: "type": "string", +780: "description": "New description for the feature" +781: }, +782: "requirement_ids": { +783: "type": "array", +784: "items": {"type": "string"}, +785: "description": "New list of requirement IDs this feature implements" +786: }, +787: "completion_criteria": { +788: "type": "array", +789: "items": {"type": "string"}, +790: "description": "New list of completion criteria" +791: } +792: }, +793: "required": ["id"] +794: })) +795: } +796: +797: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +798: let mut features = load_feature_list().map_err(|e| AdkError::tool(e.to_string()))?; +799: +800: let id = args.get("id") +801: .and_then(|v| v.as_str()) +802: .ok_or_else(|| AdkError::tool("Missing 'id' parameter".to_string()))?; +803: +804: if let Some(feature) = features.features.iter_mut().find(|f| f.id == id) { +805: +806: if let Some(name) = args.get("name").and_then(|v| v.as_str()) { +807: feature.name = name.to_string(); +808: } +809: if let Some(description) = args.get("description").and_then(|v| v.as_str()) { +810: feature.description = description.to_string(); +811: } +812: if let Some(req_ids) = args.get("requirement_ids").and_then(|v| v.as_array()) { +813: feature.requirement_ids = req_ids.iter() +814: .filter_map(|v| v.as_str().map(|s| s.to_string())) +815: .collect(); +816: } +817: if let Some(criteria) = args.get("completion_criteria").and_then(|v| v.as_array()) { +818: feature.completion_criteria = criteria.iter() +819: .filter_map(|v| v.as_str().map(|s| s.to_string())) +820: .collect(); +821: } +822: +823: save_feature_list(&features).map_err(|e| AdkError::tool(e.to_string()))?; +824: +825: println!("✅ Updated feature: {}", id); +826: +827: Ok(json!({ +828: "status": "success", +829: "feature_id": id, +830: "message": format!("Feature {} updated successfully", id) +831: })) +832: } else { +833: Ok(json!({ +834: "status": "error", +835: "message": format!("Feature {} not found", id) +836: })) +837: } +838: } +839: } +``` + +### crates/cowork-core/src/tools/deployment_tools.rs (303 lines) + +``` +1: strip_unc_prefix +2: ⋮---- +3: (path: &std::path::Path) +4: ⋮---- +5: CopyWorkspaceToProjectTool +6: ⋮---- +7: { +8: fn name(&self) -> &str { +9: "copy_workspace_to_project" +10: } +11: +12: fn description(&self) -> &str { +13: "Copy all code files from iteration workspace to the project root directory. \ +14: This is used in the Delivery stage to finalize the project. \ +15: Only copies source code files (html, css, js, etc.), not configuration or hidden files." +16: } +17: +18: fn parameters_schema(&self) -> Option { +19: Some(json!({ +20: "type": "object", +21: "properties": { +22: "confirm": { +23: "type": "boolean", +24: "description": "Must be true to confirm deployment" +25: } +26: }, +27: "required": ["confirm"] +28: })) +29: } +30: +31: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +32: let confirm = args.get("confirm") +33: .and_then(|v| v.as_bool()) +34: .unwrap_or(false); +35: +36: if !confirm { +37: return Ok(json!({ +38: "status": "cancelled", +39: "message": "Deployment cancelled. Set confirm=true to proceed." +40: })); +41: } +42: +43: +44: let iteration_id = get_iteration_id() +45: .ok_or_else(|| adk_core::AdkError::tool("Iteration ID not set. Cannot deploy without an active iteration.".to_string()))?; +46: +47: let iteration_store = IterationStore::new(); +48: let workspace_dir = iteration_store.workspace_path(&iteration_id) +49: .map_err(|e| adk_core::AdkError::tool(format!("Failed to get workspace path: {}", e)))?; +50: +51: +52: let project_root = std::env::current_dir() +53: .map_err(|e| adk_core::AdkError::tool(format!("Failed to get project root: {}", e)))?; +54: +55: +56: if !workspace_dir.exists() { +57: return Ok(json!({ +58: "status": "error", +59: "message": format!("Workspace not found: {}", workspace_dir.display()) +60: })); +61: } +62: +63: +64: let workspace_has_files = workspace_dir.read_dir() +65: .ok() +66: .map(|mut entries| entries.next().is_some()) +67: .unwrap_or(false); +68: +69: if !workspace_has_files { +70: return Ok(json!({ +71: "status": "warning", +72: "message": "Workspace is empty. No files to deploy. To prevent accidental file deletion, deployment was skipped.", +73: "workspace": workspace_dir.to_string_lossy().to_string(), +74: "note": "This usually indicates that the Coding stage did not generate any code files. Please check the Plan stage for tasks." +75: })); +76: } +77: +78: println!("[Delivery] Copying files from workspace to project root..."); +79: println!("[Delivery] Workspace: {}", workspace_dir.display()); +80: println!("[Delivery] Workspace absolute: {}", workspace_dir.canonicalize().unwrap_or_else(|_| workspace_dir.clone()).display()); +81: println!("[Delivery] Project root: {}", project_root.display()); +82: println!("[Delivery] Project root absolute: {}", project_root.canonicalize().unwrap_or_else(|_| project_root.clone()).display()); +83: println!("[Delivery] Iteration ID: {}", iteration_id); +84: +85: +86: let extensions_to_copy = vec![ +87: ".html", ".htm", ".css", ".js", ".jsx", ".ts", ".tsx", +88: ".json", ".md", ".txt", ".svg", ".png", ".jpg", ".jpeg" +89: ]; +90: +91: +92: let protected_paths = vec![ +93: ".cowork-v2", +94: ".git", +95: ".litho", +96: "litho.docs", +97: ".gitignore", +98: ".gitattributes", +99: ".zed", +100: "AGENT.md", +101: ".vscode", +102: ".idea", +103: "config.toml", +104: "Cargo.toml", +105: "Cargo.lock", +106: "README.md", +107: "LICENSE", +108: ".gitignore", +109: ".DS_Store", +110: ]; +111: +112: +113: println!("[Delivery] Step 1: Cleaning up obsolete files in project root..."); +114: let mut deleted_files = Vec::new(); +115: let mut protected_skipped = Vec::new(); +116: +117: for entry in walkdir::WalkDir::new(&project_root) +118: .follow_links(false) +119: .into_iter() +120: .filter_map(|e| e.ok()) +121: { +122: let dest_path = entry.path(); +123: +124: +125: if dest_path.is_dir() { +126: continue; +127: } +128: +129: +130: +131: let dest_path_stripped = strip_unc_prefix(&dest_path); +132: let project_root_stripped = strip_unc_prefix(&project_root); +133: +134: let rel_path = dest_path_stripped.strip_prefix(&project_root_stripped) +135: .unwrap_or(&dest_path_stripped); +136: +137: +138: let path_str = rel_path.to_string_lossy().to_string(); +139: let is_protected = protected_paths.iter().any(|protected| { +140: +141: if path_str == *protected { +142: return true; +143: } +144: +145: path_str.starts_with(&format!("{}/", protected)) +146: }); +147: +148: if is_protected { +149: protected_skipped.push(path_str.clone()); +150: println!("[Delivery] Skipped protected: {}", path_str); +151: continue; +152: } +153: +154: +155: +156: +157: if path_str.starts_with(".cowork-v2/") || path_str.starts_with(".cowork-v2\\") { +158: protected_skipped.push(path_str.clone()); +159: println!("[Delivery] Skipped workspace file: {}", path_str); +160: continue; +161: } +162: +163: +164: let src_path = workspace_dir.join(&rel_path); +165: let src_path_exists = src_path.exists(); +166: +167: println!("[Delivery] Checking file: {} -> Workspace path: {} (exists: {})", +168: path_str, src_path.display(), src_path_exists); +169: +170: if !src_path_exists { +171: +172: println!("[Delivery] File {} not found in workspace, marking for deletion", path_str); +173: match fs::remove_file(&dest_path) { +174: Ok(_) => { +175: deleted_files.push(path_str.clone()); +176: println!("[Delivery] Deleted obsolete file: {}", path_str); +177: } +178: Err(e) => { +179: println!("[Delivery] Warning: Failed to delete {}: {}", path_str, e); +180: } +181: } +182: } +183: } +184: +185: +186: for entry in walkdir::WalkDir::new(&project_root) +187: .follow_links(false) +188: .into_iter() +189: .filter_map(|e| e.ok()) +190: .filter(|e| e.path().is_dir()) +191: .collect::>() +192: .into_iter() +193: .rev() +194: { +195: let dir_path = entry.path(); +196: +197: +198: let dir_path_stripped = strip_unc_prefix(&dir_path); +199: let project_root_stripped = strip_unc_prefix(&project_root); +200: +201: let rel_path = dir_path_stripped.strip_prefix(&project_root_stripped) +202: .unwrap_or(&dir_path_stripped); +203: +204: let path_str = rel_path.to_string_lossy().to_string(); +205: let is_protected = protected_paths.iter().any(|protected| { +206: path_str == *protected || path_str.starts_with(&format!("{}/", protected)) +207: }); +208: +209: if is_protected { +210: continue; +211: } +212: +213: +214: if path_str.starts_with(".cowork-v2/") { +215: continue; +216: } +217: +218: +219: if dir_path.read_dir().ok().map(|mut it| it.next().is_none()).unwrap_or(false) { +220: match fs::remove_dir(&dir_path) { +221: Ok(_) => { +222: println!("[Delivery] Deleted empty directory: {}", path_str); +223: } +224: Err(e) => { +225: println!("[Delivery] Warning: Failed to delete directory {}: {}", path_str, e); +226: } +227: } +228: } +229: } +230: +231: +232: println!("[Delivery] Step 2: Copying files from workspace..."); +233: let mut copied_files = Vec::new(); +234: let mut skipped_files = Vec::new(); +235: +236: for entry in walkdir::WalkDir::new(&workspace_dir) +237: .follow_links(false) +238: .into_iter() +239: .filter_map(|e| e.ok()) +240: { +241: let src_path = entry.path(); +242: +243: +244: if src_path.is_dir() { +245: continue; +246: } +247: +248: +249: +250: let src_path_stripped = strip_unc_prefix(&src_path); +251: let workspace_dir_stripped = strip_unc_prefix(&workspace_dir); +252: +253: let rel_path = src_path_stripped.strip_prefix(&workspace_dir_stripped) +254: .unwrap_or(&src_path_stripped); +255: +256: +257: let should_copy = rel_path.extension() +258: .and_then(|ext| ext.to_str()) +259: .map(|ext| extensions_to_copy.iter().any(|e| *e == format!(".{}", ext))) +260: .unwrap_or(false); +261: +262: if !should_copy { +263: skipped_files.push(rel_path.to_string_lossy().to_string()); +264: continue; +265: } +266: +267: +268: let dest_path = project_root.join(rel_path); +269: +270: +271: if let Some(parent) = dest_path.parent() { +272: fs::create_dir_all(parent) +273: .map_err(|e| adk_core::AdkError::tool(format!("Failed to create directory: {}", e)))?; +274: } +275: +276: +277: fs::copy(&src_path, &dest_path) +278: .map_err(|e| adk_core::AdkError::tool(format!("Failed to copy {}: {}", rel_path.display(), e)))?; +279: +280: copied_files.push(rel_path.to_string_lossy().to_string()); +281: println!("[Delivery] Copied: {}", rel_path.display()); +282: } +283: +284: println!("[Delivery] Deployment complete: {} files deleted, {} files copied, {} files skipped, {} protected files skipped", +285: deleted_files.len(), copied_files.len(), skipped_files.len(), protected_skipped.len()); +286: +287: Ok(json!({ +288: "status": "success", +289: "message": format!( +290: "Deployed {} files from workspace to project root ({} deleted, {} protected)", +291: copied_files.len(), +292: deleted_files.len(), +293: protected_skipped.len() +294: ), +295: "deleted_files": deleted_files, +296: "copied_files": copied_files, +297: "skipped_files": skipped_files, +298: "protected_files": protected_skipped, +299: "workspace": workspace_dir.to_string_lossy().to_string(), +300: "project_root": project_root.to_string_lossy().to_string() +301: })) +302: } +303: } +``` + +### crates/cowork-core/src/tools/file_tools.rs (733 lines) + +``` +1: strip_unc_prefix +2: ⋮---- +3: (path: &Path) +4: ⋮---- +5: validate_path_security_within_workspace +6: ⋮---- +7: ( +8: path: &str, +9: workspace_dir: &Path, +10: ) +11: ⋮---- +12: ListFilesTool +13: ⋮---- +14: { +15: fn name(&self) -> &str { +16: "list_files" +17: } +18: +19: fn description(&self) -> &str { +20: "List files in a directory (recursively or non-recursively). \ +21: SECURITY: Only works within current directory. \ +22: Useful for understanding project structure." +23: } +24: +25: fn parameters_schema(&self) -> Option { +26: Some(json!({ +27: "type": "object", +28: "properties": { +29: "path": { +30: "type": "string", +31: "description": "Directory path to list (default: current directory). Must be relative path." +32: }, +33: "recursive": { +34: "type": "boolean", +35: "description": "Whether to list files recursively (default: false)" +36: }, +37: "max_depth": { +38: "type": "integer", +39: "description": "Maximum depth for recursive listing (default: 3)" +40: } +41: } +42: })) +43: } +44: +45: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +46: let path = args.get("path").and_then(|v| v.as_str()).unwrap_or("."); +47: +48: +49: let iteration_id = get_iteration_id().ok_or_else(|| { +50: adk_core::AdkError::tool( +51: "Iteration ID not set. Cannot list files without an active iteration.".to_string(), +52: ) +53: })?; +54: +55: let iteration_store = IterationStore::new(); +56: let workspace_dir = iteration_store.workspace_path(&iteration_id).map_err(|e| { +57: adk_core::AdkError::tool(format!("Failed to get workspace path: {}", e)) +58: })?; +59: +60: +61: fs::create_dir_all(&workspace_dir) +62: .map_err(|e| adk_core::AdkError::tool(format!("Failed to create workspace: {}", e)))?; +63: +64: +65: +66: let path_str = path.trim(); +67: let safe_path = if path_str == workspace_dir.display().to_string() +68: || path_str == workspace_dir.to_string_lossy().as_ref() +69: { +70: "." +71: } else if path_str.contains(".cowork-v2/iterations") && path_str.contains("workspace") { +72: +73: "." +74: } else { +75: path_str +76: }; +77: +78: +79: let validated_path = +80: match validate_path_security_within_workspace(&safe_path, &workspace_dir) { +81: Ok(p) => p, +82: Err(e) => { +83: return Ok(json!({ +84: "status": "security_error", +85: "message": e +86: })); +87: } +88: }; +89: +90: +91: let full_path = workspace_dir.join(&validated_path); +92: +93: let recursive = args +94: .get("recursive") +95: .and_then(|v| v.as_bool()) +96: .unwrap_or(false); +97: +98: let max_depth = args.get("max_depth").and_then(|v| v.as_u64()).unwrap_or(3) as usize; +99: +100: if !full_path.exists() { +101: return Ok(json!({ +102: "status": "error", +103: "message": format!("Path not found: {} (in workspace: {})", path, iteration_id) +104: })); +105: } +106: +107: let mut files = Vec::new(); +108: let mut directories = Vec::new(); +109: +110: if recursive { +111: +112: for entry in WalkDir::new(&full_path) +113: .max_depth(max_depth) +114: .follow_links(false) +115: .into_iter() +116: .filter_entry(|e| { +117: +118: if let Some(name) = e.file_name().to_str() { +119: if name.starts_with('.') && name != "." { +120: return false; +121: } +122: } +123: true +124: }) +125: .filter_map(|e| e.ok()) +126: { +127: +128: +129: let entry_path = entry.path(); +130: let entry_path_stripped = strip_unc_prefix(entry_path); +131: let workspace_dir_stripped = strip_unc_prefix(&workspace_dir); +132: +133: let rel = entry_path_stripped +134: .strip_prefix(&workspace_dir_stripped) +135: .unwrap_or(&entry_path_stripped); +136: let rel_str = rel.to_string_lossy(); +137: let path_str = format!("./{}", rel_str.trim_start_matches("./")); +138: +139: +140: if should_ignore(&path_str) { +141: continue; +142: } +143: +144: if entry.file_type().is_dir() { +145: directories.push(path_str); +146: } else { +147: files.push(path_str); +148: } +149: } +150: } else { +151: +152: let entries = fs::read_dir(&full_path).map_err(|e| { +153: adk_core::AdkError::tool(format!("Failed to read directory: {}", e)) +154: })?; +155: +156: for entry in entries { +157: let entry = entry.map_err(|e| adk_core::AdkError::tool(e.to_string()))?; +158: +159: +160: if let Some(name) = entry.file_name().to_str() { +161: if name.starts_with('.') { +162: continue; +163: } +164: } +165: +166: let full = entry.path().to_path_buf(); +167: +168: let full_stripped = strip_unc_prefix(&full); +169: let workspace_dir_stripped = strip_unc_prefix(&workspace_dir); +170: +171: let rel = full_stripped +172: .strip_prefix(&workspace_dir_stripped) +173: .unwrap_or(&full_stripped); +174: let rel_str = rel.to_string_lossy(); +175: let path_str = format!("./{}", rel_str.trim_start_matches("./")); +176: +177: if should_ignore(&path_str) { +178: continue; +179: } +180: +181: if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) { +182: directories.push(path_str); +183: } else { +184: files.push(path_str); +185: } +186: } +187: } +188: +189: Ok(json!({ +190: "status": "success", +191: "path": path, +192: "files": files, +193: "directories": directories, +194: "total_files": files.len(), +195: "total_directories": directories.len(), +196: "workspace": workspace_dir.to_string_lossy().to_string() +197: })) +198: } +199: } +200: ⋮---- +201: should_ignore +202: ⋮---- +203: (path: &str) +204: ⋮---- +205: ReadFileTool +206: ⋮---- +207: { +208: fn name(&self) -> &str { +209: "read_file" +210: } +211: +212: fn description(&self) -> &str { +213: "Read the contents of a file. \ +214: SECURITY: Only works within current directory." +215: } +216: +217: fn parameters_schema(&self) -> Option { +218: Some(json!({ +219: "type": "object", +220: "properties": { +221: "path": { +222: "type": "string", +223: "description": "File path to read (must be relative path within current directory)" +224: } +225: }, +226: "required": ["path"] +227: })) +228: } +229: +230: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +231: let path = get_required_string_param(&args, "path")?; +232: +233: +234: let iteration_id = get_iteration_id().ok_or_else(|| { +235: adk_core::AdkError::tool( +236: "Iteration ID not set. Cannot read files without an active iteration.".to_string(), +237: ) +238: })?; +239: +240: let iteration_store = IterationStore::new(); +241: let workspace_dir = iteration_store.workspace_path(&iteration_id).map_err(|e| { +242: adk_core::AdkError::tool(format!("Failed to get workspace path: {}", e)) +243: })?; +244: +245: +246: let safe_path = match validate_path_security_within_workspace(path, &workspace_dir) { +247: Ok(p) => p, +248: Err(e) => { +249: return Ok(json!({ +250: "status": "security_error", +251: "message": e +252: })); +253: } +254: }; +255: +256: +257: let full_path = workspace_dir.join(&safe_path); +258: +259: if !full_path.exists() { +260: return Ok(json!({ +261: "status": "error", +262: "message": format!("File not found: {} (in workspace: {})", path, iteration_id) +263: })); +264: } +265: +266: match fs::read_to_string(&full_path) { +267: Ok(content) => Ok(json!({ +268: "status": "success", +269: "path": path, +270: "workspace_path": full_path.to_string_lossy().to_string(), +271: "content": content, +272: "workspace": workspace_dir.to_string_lossy().to_string() +273: })), +274: Err(e) => Ok(json!({ +275: "status": "error", +276: "message": format!("Failed to read file: {}", e) +277: })), +278: } +279: } +280: } +281: ⋮---- +282: WriteFileTool +283: ⋮---- +284: { +285: fn name(&self) -> &str { +286: "write_file" +287: } +288: +289: fn description(&self) -> &str { +290: "Write content to a file. Creates parent directories if needed. \ +291: SECURITY: Only works within current directory. Absolute paths and .. are forbidden." +292: } +293: +294: fn parameters_schema(&self) -> Option { +295: Some(json!({ +296: "type": "object", +297: "properties": { +298: "path": { +299: "type": "string", +300: "description": "File path to write (must be relative path within current directory)" +301: }, +302: "content": { +303: "type": "string", +304: "description": "Content to write" +305: } +306: }, +307: "required": ["path", "content"] +308: })) +309: } +310: +311: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +312: let path = get_required_string_param(&args, "path")?; +313: let content = get_required_string_param(&args, "content")?; +314: +315: +316: super::notify_tool_call("write_file", &json!({"path": path})); +317: +318: +319: let iteration_id = get_iteration_id().ok_or_else(|| { +320: adk_core::AdkError::tool( +321: "Iteration ID not set. Cannot write files without an active iteration.".to_string(), +322: ) +323: })?; +324: +325: let iteration_store = IterationStore::new(); +326: let workspace_dir = iteration_store.workspace_path(&iteration_id).map_err(|e| { +327: adk_core::AdkError::tool(format!("Failed to get workspace path: {}", e)) +328: })?; +329: +330: +331: fs::create_dir_all(&workspace_dir) +332: .map_err(|e| adk_core::AdkError::tool(format!("Failed to create workspace: {}", e)))?; +333: +334: +335: let safe_path = match validate_path_security_within_workspace(path, &workspace_dir) { +336: Ok(p) => p, +337: Err(e) => { +338: super::notify_tool_result( +339: "write_file", +340: &Err(adk_core::AdkError::tool("security error".to_string())), +341: ); +342: return Ok(json!({ +343: "status": "security_error", +344: "message": e +345: })); +346: } +347: }; +348: +349: +350: let full_path = workspace_dir.join(&safe_path); +351: +352: +353: if let Some(parent) = full_path.parent() { +354: fs::create_dir_all(parent).map_err(|e| adk_core::AdkError::tool(e.to_string()))?; +355: } +356: +357: let result = match fs::write(&full_path, content) { +358: Ok(_) => { +359: +360: println!( +361: "📝 Writing file: {} ({} lines) [iteration: {}]", +362: path, +363: content.lines().count(), +364: iteration_id +365: ); +366: Ok(json!({ +367: "status": "success", +368: "path": path, +369: "workspace_path": full_path.to_string_lossy().to_string(), +370: "lines_written": content.lines().count(), +371: "workspace": workspace_dir.to_string_lossy().to_string() +372: })) +373: } +374: Err(e) => Ok(json!({ +375: "status": "error", +376: "message": format!("Failed to write file: {}", e) +377: })), +378: }; +379: +380: +381: if result.is_ok() { +382: super::notify_tool_result("write_file", &Ok(json!({"status": "success"}))); +383: } else { +384: super::notify_tool_result( +385: "write_file", +386: &Err(adk_core::AdkError::tool("error".to_string())), +387: ); +388: } +389: +390: result +391: } +392: } +393: ⋮---- +394: is_blocking_service_command +395: ⋮---- +396: (command: &str) +397: ⋮---- +398: RunCommandTool +399: ⋮---- +400: { +401: fn name(&self) -> &str { +402: "run_command" +403: } +404: +405: fn description(&self) -> &str { +406: "Execute a shell command and return the output. \ +407: WARNING: This tool will REJECT commands that start long-running services \ +408: (like http.server, npm dev, etc.) as they would block execution. \ +409: Use this for: building, testing, linting - NOT for starting servers." +410: } +411: +412: fn parameters_schema(&self) -> Option { +413: Some(json!({ +414: "type": "object", +415: "properties": { +416: "command": { +417: "type": "string", +418: "description": "Shell command to execute (must not be a blocking service command)" +419: } +420: }, +421: "required": ["command"] +422: })) +423: } +424: +425: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +426: let command = get_required_string_param(&args, "command")?; +427: +428: +429: if is_blocking_service_command(command) { +430: return Ok(json!({ +431: "status": "rejected", +432: "message": format!( +433: "BLOCKED: This command appears to start a long-running service: '{}'. \ +434: Starting services would block the agent. \ +435: If you need to verify the code works, just create the files - don't start servers.", +436: command +437: ) +438: })); +439: } +440: +441: +442: let iteration_id = get_iteration_id().ok_or_else(|| { +443: adk_core::AdkError::tool( +444: "Iteration ID not set. Cannot run command without an active iteration.".to_string(), +445: ) +446: })?; +447: +448: let iteration_store = IterationStore::new(); +449: let workspace_dir = iteration_store.workspace_path(&iteration_id).map_err(|e| { +450: adk_core::AdkError::tool(format!("Failed to get workspace path: {}", e)) +451: })?; +452: +453: +454: #[cfg(target_os = "windows")] +455: let output = tokio::time::timeout( +456: std::time::Duration::from_secs(30), +457: tokio::process::Command::new("cmd") +458: .args(["/C", command]) +459: .current_dir(&workspace_dir) +460: .output(), +461: ) +462: .await; +463: +464: #[cfg(not(target_os = "windows"))] +465: let output = tokio::time::timeout( +466: std::time::Duration::from_secs(30), +467: tokio::process::Command::new("sh") +468: .arg("-c") +469: .arg(command) +470: .current_dir(&workspace_dir) +471: .output(), +472: ) +473: .await; +474: +475: match output { +476: Ok(Ok(output)) => { +477: let stdout = String::from_utf8_lossy(&output.stdout).to_string(); +478: let stderr = String::from_utf8_lossy(&output.stderr).to_string(); +479: +480: Ok(json!({ +481: "status": if output.status.success() { "success" } else { "failed" }, +482: "exit_code": output.status.code(), +483: "stdout": stdout, +484: "stderr": stderr, +485: "workspace": workspace_dir.to_string_lossy().to_string() +486: })) +487: } +488: Ok(Err(e)) => Ok(json!({ +489: "status": "error", +490: "message": format!("Failed to execute command: {}", e) +491: })), +492: Err(_) => Ok(json!({ +493: "status": "timeout", +494: "message": "Command execution timeout (30s limit)" +495: })), +496: } +497: } +498: } +499: ⋮---- +500: ReadFileTruncatedTool +501: ⋮---- +502: { +503: fn name(&self) -> &str { +504: "read_file_truncated" +505: } +506: +507: fn description(&self) -> &str { +508: "Read a file with intelligent truncation to prevent context overflow. \ +509: This tool automatically summarizes large files and provides a structured output. \ +510: Use this when you need to read files for knowledge generation but want to avoid \ +511: excessive token consumption." +512: } +513: +514: fn parameters_schema(&self) -> Option { +515: Some(json!({ +516: "type": "object", +517: "properties": { +518: "path": { +519: "type": "string", +520: "description": "Relative path to the file within workspace" +521: }, +522: "max_chars": { +523: "type": "number", +524: "description": "Maximum characters to return (default: 2000)", +525: "default": 2000 +526: }, +527: "prefer_structure": { +528: "type": "boolean", +529: "description": "If true, prefer structure over content for code files (default: true)", +530: "default": true +531: } +532: }, +533: "required": ["path"] +534: })) +535: } +536: +537: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +538: let path = get_required_string_param(&args, "path")?; +539: let max_chars = args +540: .get("max_chars") +541: .and_then(|v| v.as_i64()) +542: .unwrap_or(2000) as usize; +543: let prefer_structure = args +544: .get("prefer_structure") +545: .and_then(|v| v.as_bool()) +546: .unwrap_or(true); +547: +548: +549: let iteration_id = get_iteration_id() +550: .ok_or_else(|| adk_core::AdkError::tool("Iteration ID not set".to_string()))?; +551: +552: let iteration_store = IterationStore::new(); +553: let workspace_dir = iteration_store +554: .workspace_path(&iteration_id) +555: .map_err(|e| adk_core::AdkError::tool(format!("Failed to get workspace: {}", e)))?; +556: +557: +558: let safe_path = validate_path_security_within_workspace(&path, &workspace_dir) +559: .map_err(|e| adk_core::AdkError::tool(e))?; +560: +561: +562: let full_path = workspace_dir.join(&safe_path); +563: +564: +565: let content = fs::read_to_string(&full_path) +566: .map_err(|e| adk_core::AdkError::tool(format!("Failed to read file: {}", e)))?; +567: let file_ext = full_path.extension().and_then(|e| e.to_str()).unwrap_or(""); +568: +569: let is_code_file = matches!( +570: file_ext, +571: "rs" | "js" | "jsx" | "ts" | "tsx" | "py" | "java" | "go" | "cpp" | "c" | "h" +572: ); +573: +574: +575: let truncated_content = if content.len() <= max_chars { +576: content.clone() +577: } else if is_code_file && prefer_structure { +578: +579: extract_code_structure(&content, max_chars, file_ext) +580: } else { +581: +582: truncate_with_context(&content, max_chars) +583: }; +584: +585: let total_chars = content.len(); +586: let truncated = total_chars > max_chars; +587: +588: Ok(json!({ +589: "path": path, +590: "file_size": total_chars, +591: "returned_size": truncated_content.len(), +592: "truncated": truncated, +593: "is_code_file": is_code_file, +594: "content": truncated_content, +595: "original_lines": content.lines().count(), +596: "returned_lines": truncated_content.lines().count() +597: })) +598: } +599: } +600: ⋮---- +601: ReadFileWithLimitTool +602: ⋮---- +603: { +604: max_calls: usize, +605: call_count: std::sync::Arc, +606: } +607: ⋮---- +608: ReadFileWithLimitTool +609: ⋮---- +610: { +611: pub fn new(max_calls: usize) -> Self { +612: Self { +613: max_calls, +614: call_count: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)), +615: } +616: } +617: +618: pub fn reset(&self) { +619: self.call_count +620: .store(0, std::sync::atomic::Ordering::SeqCst); +621: } +622: +623: pub fn calls_remaining(&self) -> usize { +624: let current = self.call_count.load(std::sync::atomic::Ordering::SeqCst); +625: if current >= self.max_calls { +626: 0 +627: } else { +628: self.max_calls - current +629: } +630: } +631: } +632: ⋮---- +633: ReadFileWithLimitTool +634: ⋮---- +635: { +636: fn name(&self) -> &str { +637: "read_file_with_limit" +638: } +639: +640: fn description(&self) -> &str { +641: "Read a file with a call limit to prevent excessive file reading. \ +642: This tool tracks the number of calls and enforces a maximum limit. \ +643: Use this when you need to read multiple files but want to control token usage." +644: } +645: +646: fn parameters_schema(&self) -> Option { +647: Some(json!({ +648: "type": "object", +649: "properties": { +650: "path": { +651: "type": "string", +652: "description": "Relative path to the file within workspace" +653: }, +654: "max_chars": { +655: "type": "number", +656: "description": "Maximum characters to return (default: 3000)", +657: "default": 3000 +658: } +659: }, +660: "required": ["path"] +661: })) +662: } +663: +664: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +665: +666: let current_calls = self +667: .call_count +668: .fetch_add(1, std::sync::atomic::Ordering::SeqCst); +669: +670: if current_calls >= self.max_calls { +671: return Ok(json!({ +672: "status": "limit_exceeded", +673: "message": format!( +674: "File reading limit exceeded (max {} calls). Please use the information you've already gathered.", +675: self.max_calls +676: ), +677: "calls_made": current_calls + 1, +678: "max_calls": self.max_calls +679: })); +680: } +681: +682: let path = get_required_string_param(&args, "path")?; +683: let max_chars = args +684: .get("max_chars") +685: .and_then(|v| v.as_i64()) +686: .unwrap_or(3000) as usize; +687: +688: +689: let iteration_id = get_iteration_id() +690: .ok_or_else(|| adk_core::AdkError::tool("Iteration ID not set".to_string()))?; +691: +692: let iteration_store = IterationStore::new(); +693: let workspace_dir = iteration_store +694: .workspace_path(&iteration_id) +695: .map_err(|e| adk_core::AdkError::tool(format!("Failed to get workspace: {}", e)))?; +696: +697: +698: let safe_path = validate_path_security_within_workspace(&path, &workspace_dir) +699: .map_err(|e| adk_core::AdkError::tool(e))?; +700: +701: +702: let full_path = workspace_dir.join(&safe_path); +703: +704: +705: let content = fs::read_to_string(&full_path) +706: .map_err(|e| adk_core::AdkError::tool(format!("Failed to read file: {}", e)))?; +707: +708: +709: let truncated_content = if content.len() <= max_chars { +710: content.clone() +711: } else { +712: truncate_with_context(&content, max_chars) +713: }; +714: +715: Ok(json!({ +716: "status": "success", +717: "path": path, +718: "file_size": content.len(), +719: "returned_size": truncated_content.len(), +720: "truncated": content.len() > max_chars, +721: "content": truncated_content, +722: "calls_remaining": self.max_calls - (current_calls + 1) +723: })) +724: } +725: } +726: ⋮---- +727: extract_code_structure +728: ⋮---- +729: (content: &str, max_chars: usize, file_ext: &str) +730: ⋮---- +731: truncate_with_context +732: ⋮---- +733: (content: &str, max_chars: usize) +``` + +### crates/cowork-core/src/tools/goto_stage_tool.rs (89 lines) + +``` +1: GotoStageTool +2: ⋮---- +3: { +4: fn name(&self) -> &str { +5: "goto_stage" +6: } +7: +8: fn description(&self) -> &str { +9: "Restart pipeline from a specific stage. Use this when critical issues \ +10: require going back to an earlier phase. Valid stages: prd, design, plan, coding." +11: } +12: +13: fn parameters_schema(&self) -> Option { +14: Some(json!({ +15: "type": "object", +16: "properties": { +17: "stage": { +18: "type": "string", +19: "enum": ["prd", "design", "plan", "coding"], +20: "description": "Which stage to restart from" +21: }, +22: "reason": { +23: "type": "string", +24: "description": "Why the restart is needed" +25: } +26: }, +27: "required": ["stage", "reason"] +28: })) +29: } +30: +31: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +32: let stage_str = get_required_string_param(&args, "stage")?; +33: let reason = get_required_string_param(&args, "reason")?; +34: +35: +36: let stage = match stage_str { +37: "prd" => Stage::Prd, +38: "design" => Stage::Design, +39: "plan" => Stage::Plan, +40: "coding" => Stage::Coding, +41: _ => { +42: return Ok(json!({ +43: "status": "error", +44: "message": format!("Invalid stage: {}", stage_str) +45: })); +46: } +47: }; +48: +49: +50: let feedback = Feedback { +51: stage: stage_str.to_string(), +52: feedback_type: FeedbackType::QualityIssue, +53: severity: Severity::Critical, +54: details: reason.to_string(), +55: suggested_fix: Some(format!("Restart from {} stage to address the issue", stage_str)), +56: timestamp: chrono::Utc::now(), +57: }; +58: +59: if let Err(e) = crate::persistence::append_feedback(&feedback) { +60: +61: eprintln!("[GotoStageTool] Warning: Failed to save feedback: {}", e); +62: } +63: +64: +65: let mut meta = load_session_meta() +66: .map_err(|e| adk_core::AdkError::tool(e.to_string()))? +67: .unwrap_or_else(|| SessionMeta { +68: session_id: uuid::Uuid::new_v4().to_string(), +69: created_at: chrono::Utc::now(), +70: current_stage: Some(Stage::Check), +71: restart_reason: None, +72: }); +73: +74: +75: meta.current_stage = Some(stage); +76: meta.restart_reason = Some(reason.to_string()); +77: +78: +79: save_session_meta(&meta) +80: .map_err(|e| adk_core::AdkError::tool(e.to_string()))?; +81: +82: +83: +84: Err(adk_core::AdkError::tool(format!( +85: "GOTO_STAGE:{}:{}", +86: stage_str, reason +87: ))) +88: } +89: } +``` + +### crates/cowork-core/src/tools/knowledge_tools.rs (321 lines) + +``` +1: LoadDocumentSummaryTool +2: ⋮---- +3: { +4: iteration_id: String, +5: } +6: ⋮---- +7: LoadDocumentSummaryTool +8: ⋮---- +9: { +10: pub fn new(iteration_id: String) -> Self { +11: Self { iteration_id } +12: } +13: } +14: ⋮---- +15: LoadDocumentSummaryTool +16: ⋮---- +17: { +18: fn name(&self) -> &str { +19: "load_document_summary" +20: } +21: +22: fn description(&self) -> &str { +23: "Load a pre-summarized iteration document (idea, prd, design, or plan). Use this to access the concise summaries generated by the Summary Agent." +24: } +25: +26: fn parameters_schema(&self) -> Option { +27: Some(json!({ +28: "type": "object", +29: "properties": { +30: "doc_type": { +31: "type": "string", +32: "description": "Type of document to load", +33: "enum": ["idea", "prd", "design", "plan"] +34: } +35: }, +36: "required": ["doc_type"] +37: })) +38: } +39: +40: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +41: let doc_type = args.get("doc_type") +42: .and_then(|v| v.as_str()) +43: .ok_or_else(|| adk_core::AdkError::tool("doc_type is required".to_string()))?; +44: +45: let iteration_store = IterationStore::new(); +46: let iteration_dir = iteration_store.iteration_path(&self.iteration_id) +47: .map_err(|e| adk_core::AdkError::tool(format!("Failed to get iteration path: {}", e)))?; +48: +49: let summary_dir = iteration_dir.join("summaries"); +50: let file_path = summary_dir.join(format!("{}.md", doc_type)); +51: +52: if !file_path.exists() { +53: return Ok(json!({ +54: "exists": false, +55: "doc_type": doc_type, +56: "message": format!("Summary file not found: {}", file_path.display()) +57: })); +58: } +59: +60: let content = fs::read_to_string(&file_path) +61: .map_err(|e| adk_core::AdkError::tool(format!("Failed to read summary: {}", e)))?; +62: +63: Ok(json!({ +64: "exists": true, +65: "doc_type": doc_type, +66: "content": content, +67: "path": file_path.to_string_lossy().to_string() +68: })) +69: } +70: } +71: ⋮---- +72: LoadBaseKnowledgeTool +73: ⋮---- +74: { +75: base_iteration_id: String, +76: } +77: ⋮---- +78: LoadBaseKnowledgeTool +79: ⋮---- +80: { +81: pub fn new(base_iteration_id: String) -> Self { +82: Self { base_iteration_id } +83: } +84: } +85: ⋮---- +86: LoadBaseKnowledgeTool +87: ⋮---- +88: { +89: fn name(&self) -> &str { +90: "load_base_knowledge" +91: } +92: +93: fn description(&self) -> &str { +94: "Load the knowledge snapshot from the base iteration. Use this to understand what was already implemented before the current evolution iteration." +95: } +96: +97: fn parameters_schema(&self) -> Option { +98: Some(json!({ +99: "type": "object", +100: "properties": {}, +101: "required": [] +102: })) +103: } +104: +105: async fn execute(&self, _ctx: Arc, _args: Value) -> adk_core::Result { +106: let memory_store = MemoryStore::new(); +107: let project_memory = memory_store.load_project_memory() +108: .map_err(|e| adk_core::AdkError::tool(format!("Failed to load project memory: {}", e)))?; +109: +110: let base_knowledge = project_memory.get_iteration_knowledge(&self.base_iteration_id); +111: +112: match base_knowledge { +113: Some(knowledge) => { +114: Ok(json!({ +115: "exists": true, +116: "base_iteration_id": self.base_iteration_id, +117: "iteration_number": knowledge.iteration_number, +118: "idea_summary": knowledge.idea_summary, +119: "prd_summary": knowledge.prd_summary, +120: "design_summary": knowledge.design_summary, +121: "plan_summary": knowledge.plan_summary, +122: "tech_stack": knowledge.tech_stack, +123: "key_decisions": knowledge.key_decisions, +124: "key_patterns": knowledge.key_patterns, +125: "code_structure": knowledge.code_structure, +126: "known_issues": knowledge.known_issues +127: })) +128: } +129: None => { +130: Ok(json!({ +131: "exists": false, +132: "base_iteration_id": self.base_iteration_id, +133: "message": "No knowledge found for base iteration" +134: })) +135: } +136: } +137: } +138: } +139: ⋮---- +140: SaveKnowledgeSnapshotTool +141: ⋮---- +142: { +143: iteration_id: String, +144: iteration_number: u32, +145: } +146: ⋮---- +147: SaveKnowledgeSnapshotTool +148: ⋮---- +149: { +150: pub fn new(iteration_id: String, iteration_number: u32) -> Self { +151: Self { iteration_id, iteration_number } +152: } +153: } +154: ⋮---- +155: SaveKnowledgeSnapshotTool +156: ⋮---- +157: { +158: fn name(&self) -> &str { +159: "save_knowledge_snapshot" +160: } +161: +162: fn description(&self) -> &str { +163: "Save the generated knowledge snapshot for this iteration. This will be stored in project memory and used as context for future iterations." +164: } +165: +166: fn parameters_schema(&self) -> Option { +167: Some(json!({ +168: "type": "object", +169: "properties": { +170: "knowledge_json": { +171: "type": "string", +172: "description": "The complete knowledge snapshot as a JSON string" +173: } +174: }, +175: "required": ["knowledge_json"] +176: })) +177: } +178: +179: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +180: let knowledge_json = args.get("knowledge_json") +181: .and_then(|v| v.as_str()) +182: .ok_or_else(|| adk_core::AdkError::tool("knowledge_json is required".to_string()))?; +183: +184: +185: let mut knowledge: IterationKnowledge = serde_json::from_str(knowledge_json) +186: .map_err(|e| adk_core::AdkError::tool(format!("Failed to parse knowledge JSON: {}", e)))?; +187: +188: +189: knowledge.iteration_id = self.iteration_id.clone(); +190: knowledge.iteration_number = self.iteration_number; +191: +192: +193: let memory_store = MemoryStore::new(); +194: let mut project_memory = memory_store.load_project_memory() +195: .map_err(|e| adk_core::AdkError::tool(format!("Failed to load project memory: {}", e)))?; +196: +197: project_memory.save_iteration_knowledge(knowledge); +198: memory_store.save_project_memory(&project_memory) +199: .map_err(|e| adk_core::AdkError::tool(format!("Failed to save project memory: {}", e)))?; +200: +201: Ok(json!({ +202: "success": true, +203: "message": "Knowledge snapshot saved successfully", +204: "iteration_id": self.iteration_id, +205: "iteration_number": self.iteration_number +206: })) +207: } +208: } +209: ⋮---- +210: ListFilesWorkspaceTool +211: ⋮---- +212: { +213: fn name(&self) -> &str { +214: "list_files" +215: } +216: +217: fn description(&self) -> &str { +218: "List files in the iteration workspace directory. Use this to understand the project structure and identify important files to read." +219: } +220: +221: fn parameters_schema(&self) -> Option { +222: Some(json!({ +223: "type": "object", +224: "properties": { +225: "path": { +226: "type": "string", +227: "description": "Relative path within workspace (default: current directory)", +228: "default": "." +229: }, +230: "recursive": { +231: "type": "boolean", +232: "description": "List files recursively (default: true)", +233: "default": true +234: } +235: }, +236: "required": [] +237: })) +238: } +239: +240: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +241: let path = args.get("path") +242: .and_then(|v| v.as_str()) +243: .unwrap_or("."); +244: let recursive = args.get("recursive") +245: .and_then(|v| v.as_bool()) +246: .unwrap_or(true); +247: +248: let iteration_id = get_iteration_id() +249: .ok_or_else(|| adk_core::AdkError::tool("Iteration ID not set".to_string()))?; +250: +251: let iteration_store = IterationStore::new(); +252: let workspace_dir = iteration_store.workspace_path(&iteration_id) +253: .map_err(|e| adk_core::AdkError::tool(format!("Failed to get workspace: {}", e)))?; +254: +255: let target_path = workspace_dir.join(path); +256: +257: if !target_path.exists() { +258: return Ok(json!({ +259: "exists": false, +260: "path": path, +261: "message": "Path does not exist" +262: })); +263: } +264: +265: let mut files = Vec::new(); +266: +267: if recursive { +268: let mut walker = walkdir::WalkDir::new(&target_path); +269: walker = walker.min_depth(1).max_depth(10); +270: for entry in walker.into_iter().filter_map(|e| e.ok()) { +271: +272: let entry_path_stripped = crate::tools::file_tools::strip_unc_prefix(entry.path()); +273: let workspace_dir_stripped = crate::tools::file_tools::strip_unc_prefix(&workspace_dir); +274: +275: let rel_path = entry_path_stripped +276: .strip_prefix(&workspace_dir_stripped) +277: .unwrap_or(&entry_path_stripped) +278: .to_string_lossy() +279: .to_string(); +280: +281: if entry.path().is_file() { +282: let metadata = entry.metadata().ok(); +283: let size = metadata.as_ref().map(|m: &std::fs::Metadata| m.len()).unwrap_or(0); +284: +285: files.push(json!({ +286: "path": rel_path, +287: "size": size, +288: "is_file": true +289: })); +290: } +291: } +292: } else { +293: if let Ok(entries) = fs::read_dir(&target_path) { +294: for entry in entries.filter_map(|e| e.ok()) { +295: let file_name = entry.file_name().to_string_lossy().to_string(); +296: let file_type = if entry.path().is_file() { "file" } else { "directory" }; +297: +298: files.push(json!({ +299: "name": file_name, +300: "type": file_type +301: })); +302: } +303: } +304: } +305: +306: +307: files.sort_by(|a, b| { +308: let path_a = a.get("path").and_then(|v| v.as_str()).unwrap_or(""); +309: let path_b = b.get("path").and_then(|v| v.as_str()).unwrap_or(""); +310: path_a.cmp(path_b) +311: }); +312: +313: Ok(json!({ +314: "workspace": workspace_dir.to_string_lossy().to_string(), +315: "path": path, +316: "recursive": recursive, +317: "total_files": files.len(), +318: "files": files +319: })) +320: } +321: } +``` + +### crates/cowork-core/src/tools/legacy_project_analyzer_tools.rs (520 lines) + +``` +1: ScanProjectTool +2: ⋮---- +3: { +4: fn name(&self) -> &str { +5: "scan_project" +6: } +7: +8: fn description(&self) -> &str { +9: "Scan the project directory and return its structure overview. Returns file tree, key directories, and detected configurations." +10: } +11: +12: fn parameters_schema(&self) -> Option { +13: Some(json!({ +14: "type": "object", +15: "properties": { +16: "project_path": { +17: "type": "string", +18: "description": "Absolute path to the project root directory" +19: }, +20: "max_depth": { +21: "type": "number", +22: "description": "Maximum depth to scan (default: 4)", +23: "default": 4 +24: } +25: }, +26: "required": ["project_path"] +27: })) +28: } +29: +30: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +31: let project_path = get_required_string_param(&args, "project_path")?; +32: let max_depth = args.get("max_depth") +33: .and_then(|v| v.as_u64()) +34: .unwrap_or(4) as usize; +35: +36: let path = Path::new(project_path); +37: if !path.exists() { +38: return Err(adk_core::AdkError::tool(format!( +39: "Project path does not exist: {}", +40: project_path +41: ))); +42: } +43: +44: let mut file_tree = Vec::new(); +45: let walker = walkdir::WalkDir::new(path) +46: .max_depth(max_depth) +47: .into_iter() +48: .filter_entry(|e| { +49: let name = e.file_name().to_string_lossy(); +50: !name.starts_with('.') +51: && name != "node_modules" +52: && name != "target" +53: && name != "dist" +54: && name != "build" +55: && name != "__pycache__" +56: && name != ".git" +57: && name != "vendor" +58: }); +59: +60: for entry in walker.filter_map(|e| e.ok()) { +61: let rel_path = entry.path().strip_prefix(path).unwrap_or(entry.path()); +62: let rel_str = rel_path.to_string_lossy().to_string(); +63: if !rel_str.is_empty() { +64: let is_dir = entry.path().is_dir(); +65: file_tree.push(json!({ +66: "path": rel_str, +67: "is_dir": is_dir +68: })); +69: } +70: } +71: +72: Ok(json!({ +73: "status": "success", +74: "project_path": project_path, +75: "file_count": file_tree.len(), +76: "files": file_tree +77: })) +78: } +79: } +80: ⋮---- +81: DetectTechStackTool +82: ⋮---- +83: { +84: fn name(&self) -> &str { +85: "detect_tech_stack" +86: } +87: +88: fn description(&self) -> &str { +89: "Detect the technology stack of a project by analyzing configuration files like package.json, Cargo.toml, requirements.txt, etc." +90: } +91: +92: fn parameters_schema(&self) -> Option { +93: Some(json!({ +94: "type": "object", +95: "properties": { +96: "project_path": { +97: "type": "string", +98: "description": "Absolute path to the project root directory" +99: } +100: }, +101: "required": ["project_path"] +102: })) +103: } +104: +105: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +106: let project_path = get_required_string_param(&args, "project_path")?; +107: let path = Path::new(project_path); +108: +109: let mut tech_stack = json!({ +110: "frontend": null, +111: "backend": null, +112: "build_tools": [], +113: "database": null, +114: "has_docker": false, +115: "detected_files": [] +116: }); +117: +118: +119: let pkg_json = path.join("package.json"); +120: if pkg_json.exists() { +121: if let Ok(content) = fs::read_to_string(&pkg_json) { +122: if let Ok(json) = serde_json::from_str::(&content) { +123: let deps = json.get("dependencies") +124: .or(json.get("devDependencies")) +125: .and_then(|d| d.as_object()); +126: +127: let mut detected = Vec::new(); +128: if let Some(deps) = deps { +129: for (key, _) in deps { +130: detected.push(key.clone()); +131: } +132: } +133: +134: let name = json.get("name") +135: .and_then(|n| n.as_str()) +136: .unwrap_or("unknown"); +137: +138: tech_stack["frontend"] = json!({ +139: "type": "nodejs", +140: "package": name, +141: "dependencies": detected +142: }); +143: +144: tech_stack["detected_files"].as_array_mut().unwrap() +145: .push(json!("package.json")); +146: } +147: } +148: } +149: +150: +151: let cargo_toml = path.join("Cargo.toml"); +152: if cargo_toml.exists() { +153: if let Ok(content) = fs::read_to_string(&cargo_toml) { +154: let mut deps = Vec::new(); +155: let mut is_binary = true; +156: +157: for line in content.lines() { +158: let line = line.trim(); +159: if line.starts_with("[[bin]]") || line.starts_with("[[lib]]") { +160: is_binary = false; +161: } +162: if line.starts_with("axum") || line.starts_with("actix-web") || +163: line.starts_with("rocket") || line.starts_with("warp") || +164: line.starts_with("serde") || line.starts_with("tokio") { +165: deps.push(line.split('=').next().unwrap_or(line).trim().to_string()); +166: } +167: } +168: +169: tech_stack["backend"] = json!({ +170: "type": if is_binary { "rust-cli" } else { "rust" }, +171: "dependencies": deps, +172: "is_binary": is_binary +173: }); +174: +175: tech_stack["detected_files"].as_array_mut().unwrap() +176: .push(json!("Cargo.toml")); +177: } +178: } +179: +180: +181: let requirements = path.join("requirements.txt"); +182: if requirements.exists() { +183: if let Ok(content) = fs::read_to_string(&requirements) { +184: let deps: Vec = content.lines() +185: .map(|l| l.trim().to_string()) +186: .filter(|l| !l.is_empty() && !l.starts_with('#')) +187: .collect(); +188: +189: tech_stack["backend"] = json!({ +190: "type": "python", +191: "dependencies": deps +192: }); +193: +194: tech_stack["detected_files"].as_array_mut().unwrap() +195: .push(json!("requirements.txt")); +196: } +197: } +198: +199: +200: let go_mod = path.join("go.mod"); +201: if go_mod.exists() { +202: tech_stack["backend"] = json!({ +203: "type": "golang" +204: }); +205: tech_stack["detected_files"].as_array_mut().unwrap() +206: .push(json!("go.mod")); +207: } +208: +209: +210: let vite_config = path.join("vite.config.js"); +211: let webpack_config = path.join("webpack.config.js"); +212: +213: if vite_config.exists() || path.join("vite.config.ts").exists() { +214: tech_stack["build_tools"].as_array_mut().unwrap() +215: .push(json!("vite")); +216: tech_stack["detected_files"].as_array_mut().unwrap() +217: .push(json!("vite.config.js")); +218: } +219: +220: if webpack_config.exists() { +221: tech_stack["build_tools"].as_array_mut().unwrap() +222: .push(json!("webpack")); +223: tech_stack["detected_files"].as_array_mut().unwrap() +224: .push(json!("webpack.config.js")); +225: } +226: +227: +228: if path.join("Dockerfile").exists() || path.join("docker-compose.yml").exists() { +229: tech_stack["has_docker"] = serde_json::Value::Bool(true); +230: } +231: +232: Ok(json!({ +233: "status": "success", +234: "tech_stack": tech_stack +235: })) +236: } +237: } +238: ⋮---- +239: ReadProjectFileTool +240: ⋮---- +241: { +242: fn name(&self) -> &str { +243: "read_project_file" +244: } +245: +246: fn description(&self) -> &str { +247: "Read a specific file from the project. Use this to read README.md, configuration files, or source code." +248: } +249: +250: fn parameters_schema(&self) -> Option { +251: Some(json!({ +252: "type": "object", +253: "properties": { +254: "project_path": { +255: "type": "string", +256: "description": "Absolute path to the project root directory" +257: }, +258: "relative_path": { +259: "type": "string", +260: "description": "Relative path to the file from project root" +261: }, +262: "max_lines": { +263: "type": "number", +264: "description": "Maximum number of lines to read (default: 500)", +265: "default": 500 +266: } +267: }, +268: "required": ["project_path", "relative_path"] +269: })) +270: } +271: +272: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +273: let project_path = get_required_string_param(&args, "project_path")?; +274: let relative_path = get_required_string_param(&args, "relative_path")?; +275: let max_lines = args.get("max_lines") +276: .and_then(|v| v.as_u64()) +277: .unwrap_or(500) as usize; +278: +279: let path = Path::new(project_path).join(relative_path); +280: +281: if !path.exists() { +282: return Ok(json!({ +283: "status": "error", +284: "message": format!("File not found: {}", relative_path) +285: })); +286: } +287: +288: let content = fs::read_to_string(&path) +289: .map_err(|e| adk_core::AdkError::tool(format!("Failed to read file: {}", e)))?; +290: +291: let lines: Vec<&str> = content.lines().take(max_lines).collect(); +292: let truncated = lines.join("\n"); +293: let is_truncated = content.lines().count() > max_lines; +294: +295: Ok(json!({ +296: "status": "success", +297: "file_path": relative_path, +298: "content": truncated, +299: "is_truncated": is_truncated, +300: "total_lines": content.lines().count() +301: })) +302: } +303: } +304: ⋮---- +305: ListProjectDirectoryTool +306: ⋮---- +307: { +308: fn name(&self) -> &str { +309: "list_project_directory" +310: } +311: +312: fn description(&self) -> &str { +313: "List all files and subdirectories in a project directory." +314: } +315: +316: fn parameters_schema(&self) -> Option { +317: Some(json!({ +318: "type": "object", +319: "properties": { +320: "project_path": { +321: "type": "string", +322: "description": "Absolute path to the project root directory" +323: }, +324: "relative_path": { +325: "type": "string", +326: "description": "Relative path to the directory from project root (default: '.')" +327: } +328: }, +329: "required": ["project_path"] +330: })) +331: } +332: +333: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +334: let project_path = get_required_string_param(&args, "project_path")?; +335: let relative_path = args.get("relative_path") +336: .and_then(|v| v.as_str()) +337: .unwrap_or("."); +338: +339: let base_path = Path::new(project_path); +340: let dir_path = base_path.join(relative_path); +341: +342: if !dir_path.exists() { +343: return Ok(json!({ +344: "status": "error", +345: "message": format!("Directory not found: {}", relative_path) +346: })); +347: } +348: +349: let entries = fs::read_dir(&dir_path) +350: .map_err(|e| adk_core::AdkError::tool(format!("Failed to read directory: {}", e)))?; +351: +352: let mut items = Vec::new(); +353: for entry in entries.filter_map(|e| e.ok()) { +354: let name = entry.file_name().to_string_lossy().to_string(); +355: let is_dir = entry.path().is_dir(); +356: items.push(json!({ +357: "name": name, +358: "is_dir": is_dir +359: })); +360: } +361: +362: Ok(json!({ +363: "status": "success", +364: "directory": relative_path, +365: "items": items +366: })) +367: } +368: } +369: ⋮---- +370: SaveArtifactTool +371: ⋮---- +372: { +373: fn name(&self) -> &str { +374: "save_artifact" +375: } +376: +377: fn description(&self) -> &str { +378: "Save a generated artifact (idea.md, prd.md, design.md, plan.md) to the artifacts directory. This is MANDATORY for completing the artifact generation." +379: } +380: +381: fn parameters_schema(&self) -> Option { +382: Some(json!({ +383: "type": "object", +384: "properties": { +385: "filename": { +386: "type": "string", +387: "description": "Filename of the artifact (e.g., 'idea.md', 'prd.md', 'design.md', 'plan.md')" +388: }, +389: "content": { +390: "type": "string", +391: "description": "Markdown content of the artifact" +392: } +393: }, +394: "required": ["filename", "content"] +395: })) +396: } +397: +398: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +399: let filename = get_required_string_param(&args, "filename")?; +400: let content = get_required_string_param(&args, "content")?; +401: +402: +403: let valid_filenames = ["idea.md", "prd.md", "design.md", "plan.md"]; +404: if !valid_filenames.contains(&filename) { +405: return Err(adk_core::AdkError::tool(format!( +406: "Invalid artifact filename: {}. Must be one of: {:?}", +407: filename, valid_filenames +408: ))); +409: } +410: +411: +412: +413: +414: +415: +416: let mut artifacts_dir: Option = None; +417: +418: +419: if let Ok(cow_dir) = crate::persistence::get_cowork_dir() { +420: let iterations_dir = cow_dir.join("iterations"); +421: if iterations_dir.exists() { +422: +423: let mut latest_iteration: Option = None; +424: let mut latest_time = 0u64; +425: +426: if let Ok(entries) = std::fs::read_dir(&iterations_dir) { +427: for entry in entries.filter_map(|e| e.ok()) { +428: let path = entry.path(); +429: if path.is_dir() { +430: if let Ok(metadata) = entry.metadata() { +431: if let Ok(modified) = metadata.modified() { +432: let time = modified.duration_since(std::time::UNIX_EPOCH) +433: .unwrap_or_default() +434: .as_secs(); +435: if time > latest_time { +436: latest_time = time; +437: latest_iteration = path.file_name() +438: .and_then(|n| n.to_str()) +439: .map(|s| s.to_string()); +440: } +441: } +442: } +443: } +444: } +445: } +446: +447: if let Some(iteration_name) = latest_iteration { +448: let iter_artifacts = iterations_dir.join(&iteration_name).join("artifacts"); +449: artifacts_dir = Some(iter_artifacts); +450: } +451: } +452: +453: +454: if artifacts_dir.is_none() && cow_dir.exists() { +455: artifacts_dir = Some(cow_dir.join("artifacts")); +456: } +457: } +458: +459: +460: if artifacts_dir.is_none() { +461: if let Ok(cwd) = std::env::current_dir() { +462: let mut current = cwd.as_path(); +463: while let Some(parent) = current.parent() { +464: let cow_dir = parent.join(".cowork-v2"); +465: if cow_dir.exists() { +466: let iter_dir = cow_dir.join("iterations"); +467: if iter_dir.exists() { +468: +469: if let Ok(entries) = std::fs::read_dir(&iter_dir) { +470: let mut latest: Option<(std::path::PathBuf, u64)> = None; +471: for entry in entries.filter_map(|e| e.ok()) { +472: let path = entry.path(); +473: if path.is_dir() { +474: if let Ok(metadata) = entry.metadata() { +475: if let Ok(modified) = metadata.modified() { +476: let time = modified.duration_since(std::time::UNIX_EPOCH) +477: .unwrap_or_default() +478: .as_secs(); +479: if latest.as_ref().map_or(true, |(_, t)| time > *t) { +480: latest = Some((path, time)); +481: } +482: } +483: } +484: } +485: } +486: if let Some((iter_path, _)) = latest { +487: artifacts_dir = Some(iter_path.join("artifacts")); +488: } +489: } +490: } +491: beak; +492: } +493: current = parent; +494: } +495: } +496: } +497: +498: +499: let artifacts_dir = artifacts_dir.unwrap_or_else(|| { +500: std::env::current_dir() +501: .map(|p| p.join("artifacts")) +502: .unwrap_or_else(|_| std::path::PathBuf::from("artifacts")) +503: }); +504: +505: std::fs::create_dir_all(&artifacts_dir) +506: .map_err(|e| adk_core::AdkError::tool(format!("Failed to create artifacts dir: {}", e)))?; +507: +508: let artifact_path = artifacts_dir.join(filename); +509: std::fs::write(&artifact_path, content) +510: .map_err(|e| adk_core::AdkError::tool(format!("Failed to write artifact: {}", e)))?; +511: +512: eprintln!("[SaveArtifactTool] Saved {} to {:?}", filename, artifact_path); +513: +514: Ok(json!({ +515: "status": "success", +516: "message": format!("Artifact '{}' saved successfully", filename), +517: "file_path": artifact_path.to_string_lossy() +518: })) +519: } +520: } +``` + +### crates/cowork-core/src/tools/load_artifacts.rs (135 lines) + +``` +1: LoadIdeaTool +2: ⋮---- +3: { +4: fn name(&self) -> &str { +5: "load_idea" +6: } +7: +8: fn description(&self) -> &str { +9: "Load the Idea markdown document from the artifacts directory." +10: } +11: +12: fn parameters_schema(&self) -> Option { +13: Some(json!({ +14: "type": "object", +15: "properties": {}, +16: "required": [] +17: })) +18: } +19: +20: async fn execute(&self, _ctx: Arc, _args: Value) -> adk_core::Result { +21: let path = artifact_path("idea.md") +22: .map_err(|e: anyhow::Error| adk_core::AdkError::tool(e.to_string()))?; +23: +24: let content = std::fs::read_to_string(&path) +25: .map_err(|e: std::io::Error| adk_core::AdkError::tool(format!("Failed to read idea.md: {}", e)))?; +26: +27: Ok(json!({ +28: "status": "success", +29: "content": content, +30: "file_path": "artifacts/idea.md" +31: })) +32: } +33: } +34: ⋮---- +35: LoadPrdDocTool +36: ⋮---- +37: { +38: fn name(&self) -> &str { +39: "load_prd_doc" +40: } +41: +42: fn description(&self) -> &str { +43: "Load the PRD (Product Requirements Document) markdown from the artifacts directory." +44: } +45: +46: fn parameters_schema(&self) -> Option { +47: Some(json!({ +48: "type": "object", +49: "properties": {}, +50: "required": [] +51: })) +52: } +53: +54: async fn execute(&self, _ctx: Arc, _args: Value) -> adk_core::Result { +55: let path = artifact_path("prd.md") +56: .map_err(|e: anyhow::Error| adk_core::AdkError::tool(e.to_string()))?; +57: +58: let content = std::fs::read_to_string(&path) +59: .map_err(|e: std::io::Error| adk_core::AdkError::tool(format!("Failed to read prd.md: {}", e)))?; +60: +61: Ok(json!({ +62: "status": "success", +63: "content": content, +64: "file_path": "artifacts/prd.md" +65: })) +66: } +67: } +68: ⋮---- +69: LoadDesignDocTool +70: ⋮---- +71: { +72: fn name(&self) -> &str { +73: "load_design_doc" +74: } +75: +76: fn description(&self) -> &str { +77: "Load the Design Document markdown from the artifacts directory." +78: } +79: +80: fn parameters_schema(&self) -> Option { +81: Some(json!({ +82: "type": "object", +83: "properties": {}, +84: "required": [] +85: })) +86: } +87: +88: async fn execute(&self, _ctx: Arc, _args: Value) -> adk_core::Result { +89: let path = artifact_path("design.md") +90: .map_err(|e: anyhow::Error| adk_core::AdkError::tool(e.to_string()))?; +91: +92: let content = std::fs::read_to_string(&path) +93: .map_err(|e: std::io::Error| adk_core::AdkError::tool(format!("Failed to read design.md: {}", e)))?; +94: +95: Ok(json!({ +96: "status": "success", +97: "content": content, +98: "file_path": "artifacts/design.md" +99: })) +100: } +101: } +102: ⋮---- +103: LoadPlanDocTool +104: ⋮---- +105: { +106: fn name(&self) -> &str { +107: "load_plan_doc" +108: } +109: +110: fn description(&self) -> &str { +111: "Load the Implementation Plan markdown from the artifacts directory." +112: } +113: +114: fn parameters_schema(&self) -> Option { +115: Some(json!({ +116: "type": "object", +117: "properties": {}, +118: "required": [] +119: })) +120: } +121: +122: async fn execute(&self, _ctx: Arc, _args: Value) -> adk_core::Result { +123: let path = artifact_path("plan.md") +124: .map_err(|e: anyhow::Error| adk_core::AdkError::tool(e.to_string()))?; +125: +126: let content = std::fs::read_to_string(&path) +127: .map_err(|e: std::io::Error| adk_core::AdkError::tool(format!("Failed to read plan.md: {}", e)))?; +128: +129: Ok(json!({ +130: "status": "success", +131: "content": content, +132: "file_path": "artifacts/plan.md" +133: })) +134: } +135: } +``` + +### crates/cowork-core/src/tools/pm_tools.rs (324 lines) + +``` +1: PMGotoStageTool +2: ⋮---- +3: { +4: current_iteration_id: String, +5: } +6: ⋮---- +7: PMGotoStageTool +8: ⋮---- +9: { +10: pub fn new(current_iteration_id: String) -> Self { +11: Self { current_iteration_id } +12: } +13: } +14: ⋮---- +15: PMGotoStageTool +16: ⋮---- +17: { +18: fn name(&self) -> &str { +19: "pm_goto_stage" +20: } +21: +22: fn description(&self) -> &str { +23: "Restart the pipeline from a specific stage. Use this when the user wants to fix bugs, \ +24: modify requirements, or make changes to the project after delivery. \ +25: Valid stages: idea, prd, design, plan, coding." +26: } +27: +28: fn parameters_schema(&self) -> Option { +29: Some(json!({ +30: "type": "object", +31: "properties": { +32: "stage": { +33: "type": "string", +34: "enum": ["idea", "prd", "design", "plan", "coding"], +35: "description": "Which stage to restart from" +36: }, +37: "reason": { +38: "type": "string", +39: "description": "Why the restart is needed (user's request summary)" +40: } +41: }, +42: "required": ["stage", "reason"] +43: })) +44: } +45: +46: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +47: let stage_str = get_required_string_param(&args, "stage")?; +48: let reason = get_required_string_param(&args, "reason")?; +49: +50: +51: let stage = match stage_str { +52: "idea" => Stage::Idea, +53: "prd" => Stage::Prd, +54: "design" => Stage::Design, +55: "plan" => Stage::Plan, +56: "coding" => Stage::Coding, +57: _ => { +58: return Ok(json!({ +59: "status": "error", +60: "message": format!("Invalid stage: {}. Valid stages are: idea, prd, design, plan, coding", stage_str) +61: })); +62: } +63: }; +64: +65: +66: crate::persistence::set_iteration_id(self.current_iteration_id.clone()); +67: +68: +69: +70: let feedback = Feedback { +71: stage: stage_str.to_string(), +72: feedback_type: FeedbackType::QualityIssue, +73: severity: Severity::Major, +74: details: reason.to_string(), +75: suggested_fix: Some(format!("Restart from {} stage via PM Agent", stage_str)), +76: timestamp: chrono::Utc::now(), +77: }; +78: +79: if let Err(e) = append_feedback(&feedback) { +80: eprintln!("[PMGotoStageTool] Warning: Failed to save feedback: {}", e); +81: } +82: +83: +84: let mut meta = load_session_meta() +85: .map_err(|e| adk_core::AdkError::tool(e.to_string()))? +86: .unwrap_or_else(|| SessionMeta { +87: session_id: uuid::Uuid::new_v4().to_string(), +88: created_at: chrono::Utc::now(), +89: current_stage: Some(Stage::Delivery), +90: restart_reason: None, +91: }); +92: +93: +94: meta.current_stage = Some(stage); +95: meta.restart_reason = Some(reason.to_string()); +96: +97: +98: save_session_meta(&meta) +99: .map_err(|e| adk_core::AdkError::tool(e.to_string()))?; +100: +101: Ok(json!({ +102: "status": "success", +103: "message": format!("Pipeline will restart from {} stage. Reason: {}", stage_str, reason), +104: "target_stage": stage_str +105: })) +106: } +107: } +108: ⋮---- +109: PMCreateIterationTool +110: ⋮---- +111: { +112: current_iteration_id: String, +113: } +114: ⋮---- +115: PMCreateIterationTool +116: ⋮---- +117: { +118: pub fn new(current_iteration_id: String) -> Self { +119: Self { current_iteration_id } +120: } +121: } +122: ⋮---- +123: PMCreateIterationTool +124: ⋮---- +125: { +126: fn name(&self) -> &str { +127: "pm_create_iteration" +128: } +129: +130: fn description(&self) -> &str { +131: "Create a new iteration for implementing new features or major changes. \ +132: Use this when the user wants to add new functionality that is separate from the current project." +133: } +134: +135: fn parameters_schema(&self) -> Option { +136: Some(json!({ +137: "type": "object", +138: "properties": { +139: "title": { +140: "type": "string", +141: "description": "Title for the new iteration (concise summary of the new feature)" +142: }, +143: "description": { +144: "type": "string", +145: "description": "Detailed description of what the user wants to implement" +146: }, +147: "inheritance": { +148: "type": "string", +149: "enum": ["none", "full", "partial"], +150: "description": "Inheritance mode: none=fresh start, full=copy all artifacts and code, partial=copy code only (default)" +151: } +152: }, +153: "required": ["title", "description"] +154: })) +155: } +156: +157: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +158: let title = get_required_string_param(&args, "title")?; +159: let description = get_required_string_param(&args, "description")?; +160: let inheritance = get_optional_string_param(&args, "inheritance") +161: .unwrap_or_else(|| "partial".to_string()); +162: +163: +164: let project_store = ProjectStore::new(); +165: let mut project = project_store.load() +166: .map_err(|e| adk_core::AdkError::tool(e.to_string()))? +167: .ok_or_else(|| adk_core::AdkError::tool("Project not initialized".to_string()))?; +168: +169: +170: let inheritance_mode = match inheritance.as_str() { +171: "none" => crate::domain::InheritanceMode::None, +172: "full" => crate::domain::InheritanceMode::Full, +173: _ => crate::domain::InheritanceMode::Partial, +174: }; +175: +176: +177: let new_iteration = Iteration::create_evolution( +178: &project, +179: title.to_string(), +180: description.to_string(), +181: self.current_iteration_id.clone(), +182: inheritance_mode, +183: ); +184: +185: let new_iteration_id = new_iteration.id.clone(); +186: +187: +188: let iteration_store = IterationStore::new(); +189: iteration_store.save(&new_iteration) +190: .map_err(|e| adk_core::AdkError::tool(e.to_string()))?; +191: +192: +193: project_store.add_iteration(&mut project, new_iteration.to_summary()) +194: .map_err(|e| adk_core::AdkError::tool(e.to_string()))?; +195: +196: Ok(json!({ +197: "status": "success", +198: "message": format!("Created new iteration: {}", title), +199: "iteration_id": new_iteration_id, +200: "title": title, +201: "inheritance": inheritance +202: })) +203: } +204: } +205: ⋮---- +206: PMRespondTool +207: ⋮---- +208: { +209: fn name(&self) -> &str { +210: "pm_respond" +211: } +212: +213: fn description(&self) -> &str { +214: "Respond to the user without taking any action. Use this when answering questions, \ +215: asking for clarification, or providing information." +216: } +217: +218: fn parameters_schema(&self) -> Option { +219: Some(json!({ +220: "type": "object", +221: "properties": { +222: "response": { +223: "type": "string", +224: "description": "The response message to the user" +225: }, +226: "ask_clarification": { +227: "type": "boolean", +228: "description": "Whether this response is asking for clarification (optional)" +229: } +230: }, +231: "required": ["response"] +232: })) +233: } +234: +235: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +236: let response = get_required_string_param(&args, "response")?; +237: let ask_clarification = args.get("ask_clarification") +238: .and_then(|v| v.as_bool()) +239: .unwrap_or(false); +240: +241: Ok(json!({ +242: "status": "success", +243: "message": response, +244: "ask_clarification": ask_clarification +245: })) +246: } +247: } +248: ⋮---- +249: PMSaveDecisionTool +250: ⋮---- +251: { +252: iteration_id: String, +253: } +254: ⋮---- +255: PMSaveDecisionTool +256: ⋮---- +257: { +258: pub fn new(iteration_id: String) -> Self { +259: Self { iteration_id } +260: } +261: } +262: ⋮---- +263: PMSaveDecisionTool +264: ⋮---- +265: { +266: fn name(&self) -> &str { +267: "pm_save_decision" +268: } +269: +270: fn description(&self) -> &str { +271: "Save an important decision or preference to project memory. Use this when the user \ +272: makes a significant decision that should be remembered for future iterations." +273: } +274: +275: fn parameters_schema(&self) -> Option { +276: Some(json!({ +277: "type": "object", +278: "properties": { +279: "title": { +280: "type": "string", +281: "description": "Title of the decision" +282: }, +283: "context": { +284: "type": "string", +285: "description": "Background context of the decision" +286: }, +287: "decision": { +288: "type": "string", +289: "description": "The actual decision made" +290: }, +291: "impact": { +292: "type": "string", +293: "description": "Impact analysis of this decision (optional)" +294: } +295: }, +296: "required": ["title", "context", "decision"] +297: })) +298: } +299: +300: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +301: let title = get_required_string_param(&args, "title")?; +302: let context = get_required_string_param(&args, "context")?; +303: let decision = get_required_string_param(&args, "decision")?; +304: let impact = get_optional_string_param(&args, "impact").unwrap_or_default(); +305: +306: +307: let memory_store = crate::persistence::MemoryStore::new(); +308: +309: let memory_decision = Decision::new( +310: title, +311: context, +312: format!("{}\n\nImpact: {}", decision, impact), +313: &self.iteration_id, +314: ); +315: +316: memory_store.add_decision(memory_decision) +317: .map_err(|e: anyhow::Error| adk_core::AdkError::tool(e.to_string()))?; +318: +319: Ok(json!({ +320: "status": "success", +321: "message": format!("Decision saved: {}", title) +322: })) +323: } +324: } +``` + +### crates/cowork-core/src/tools/validation_tools.rs (167 lines) + +``` +1: CheckDataFormatTool +2: ⋮---- +3: { +4: fn name(&self) -> &str { +5: "check_data_format" +6: } +7: +8: fn description(&self) -> &str { +9: "Validate that a JSON data file conforms to its schema. Returns validation errors if any." +10: } +11: +12: fn parameters_schema(&self) -> Option { +13: Some(json!({ +14: "type": "object", +15: "properties": { +16: "data_type": { +17: "type": "string", +18: "enum": ["requirements", "features", "design", "plan"], +19: "description": "Which data file to validate" +20: } +21: }, +22: "required": ["data_type"] +23: })) +24: } +25: +26: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +27: let data_type = get_required_string_param(&args, "data_type")?; +28: +29: let errors = match data_type { +30: "requirements" => validate_requirements_schema(), +31: "features" => validate_features_schema(), +32: "design" => validate_design_schema(), +33: "plan" => validate_plan_schema(), +34: _ => return Ok(json!({"status": "error", "message": "Unknown data type"})), +35: }; +36: +37: if errors.is_empty() { +38: Ok(json!({ +39: "status": "valid", +40: "message": format!("{} data is valid", data_type) +41: })) +42: } else { +43: Ok(json!({ +44: "status": "invalid", +45: "errors": errors +46: })) +47: } +48: } +49: } +50: ⋮---- +51: validate_requirements_schema +52: ⋮---- +53: () +54: ⋮---- +55: validate_features_schema +56: ⋮---- +57: () +58: ⋮---- +59: validate_design_schema +60: ⋮---- +61: () +62: ⋮---- +63: validate_plan_schema +64: ⋮---- +65: () +66: ⋮---- +67: CheckFeatureCoverageTool +68: ⋮---- +69: { +70: fn name(&self) -> &str { +71: "check_feature_coverage" +72: } +73: +74: fn description(&self) -> &str { +75: "Check if all features are covered by design components." +76: } +77: +78: fn parameters_schema(&self) -> Option { +79: Some(json!({"type": "object", "properties": {}})) +80: } +81: +82: async fn execute(&self, _ctx: Arc, _args: Value) -> adk_core::Result { +83: let features = load_feature_list().map_err(|e| adk_core::AdkError::tool(e.to_string()))?; +84: let design = load_design_spec().map_err(|e| adk_core::AdkError::tool(e.to_string()))?; +85: +86: let uncovered: Vec = features +87: .features +88: .iter() +89: .filter(|f| { +90: !design +91: .architecture +92: .components +93: .iter() +94: .any(|c| c.related_features.contains(&f.id)) +95: }) +96: .map(|f| f.id.clone()) +97: .collect(); +98: +99: if uncovered.is_empty() { +100: Ok(json!({ +101: "status": "full_coverage", +102: "message": "All features are covered by components" +103: })) +104: } else { +105: Ok(json!({ +106: "status": "incomplete_coverage", +107: "uncovered_features": uncovered, +108: "message": format!("{} features are not covered", uncovered.len()) +109: })) +110: } +111: } +112: } +113: ⋮---- +114: CheckTaskDependenciesTool +115: ⋮---- +116: { +117: fn name(&self) -> &str { +118: "check_task_dependencies" +119: } +120: +121: fn description(&self) -> &str { +122: "Analyze task dependencies to detect circular dependencies." +123: } +124: +125: fn parameters_schema(&self) -> Option { +126: Some(json!({"type": "object", "properties": {}})) +127: } +128: +129: async fn execute(&self, _ctx: Arc, _args: Value) -> adk_core::Result { +130: let plan = load_implementation_plan().map_err(|e| adk_core::AdkError::tool(e.to_string()))?; +131: +132: +133: let mut graph: std::collections::HashMap> = +134: std::collections::HashMap::new(); +135: for task in &plan.tasks { +136: graph.insert(task.id.clone(), task.dependencies.clone()); +137: } +138: +139: +140: let has_cycles = detect_cycle(&graph); +141: +142: if has_cycles { +143: Ok(json!({ +144: "status": "invalid", +145: "message": "Circular dependencies detected in task graph" +146: })) +147: } else { +148: Ok(json!({ +149: "status": "valid", +150: "message": "No circular dependencies detected" +151: })) +152: } +153: } +154: } +155: ⋮---- +156: detect_cycle +157: ⋮---- +158: (graph: &std::collections::HashMap>) +159: ⋮---- +160: dfs +161: ⋮---- +162: ( +163: node: &str, +164: graph: &std::collections::HashMap>, +165: visited: &mut HashSet, +166: rec_stack: &mut HashSet, +167: ) +``` + +### crates/cowork-gui/package.json (38 lines) + +``` +1: { +2: "name": "cowork-gui", +3: "private": true, +4: "type": "module", +5: "version": "2.5.0", +6: "scripts": { +7: "dev": "vite --port 15173", +8: "build": "tsc && vite build", +9: "tauri": "tauri", +10: "tauri:dev": "tauri dev --port 15173", +11: "tauri:build": "tauri build", +12: "typecheck": "tsc --noEmit" +13: }, +14: "dependencies": { +15: "@monaco-editor/react": "^4.6.0", +16: "@tauri-apps/api": "^2.10.1", +17: "@tauri-apps/plugin-dialog": "^2.6.0", +18: "antd": "^5.12.0", +19: "react": "^18.3.1", +20: "react-dom": "^18.3.1", +21: "react-json-view": "^1.21.3", +22: "react-markdown": "^9.0.1", +23: "react-window": "^2.2.6", +24: "rehype-highlight": "^7.0.2", +25: "rehype-raw": "^7.0.0", +26: "remark-gfm": "^4.0.1", +27: "zustand": "^4.5.0" +28: }, +29: "devDependencies": { +30: "@tauri-apps/cli": "^2.10.0", +31: "@types/node": "^20.0.0", +32: "@types/react": "^18.3.0", +33: "@types/react-dom": "^18.3.0", +34: "@vitejs/plugin-react": "^4.3.0", +35: "typescript": "^5.3.0", +36: "vite": "^5.0.0" +37: } +38: } +``` + +### crates/cowork-gui/src/components/MemoryPanel.tsx (48 lines) + +``` +1: Memory +2: ⋮---- +3: { +4: id: string; +5: title: string; +6: summary: string; +7: category: string; +8: stage?: string; +9: created_at: string; +10: impact?: string; +11: tags?: string[]; +12: file?: string; +13: _ts?: number; +14: } +15: ⋮---- +16: MemoryDetail +17: ⋮---- +18: { +19: content: string; +20: } +21: ⋮---- +22: MemoryQueryResult +23: ⋮---- +24: { +25: results: Memory[]; +26: total: number; +27: } +28: ⋮---- +29: MemoryPanelProps +30: ⋮---- +31: { +32: currentSession?: string; +33: refreshTrigger?: number; +34: } +35: ⋮---- +36: getCategoryColor +37: ⋮---- +38: (memory.category) +39: ⋮---- +40: getCategoryColor +41: ⋮---- +42: ( +43: selectedMemory?.category || "", +44: ) +45: ⋮---- +46: getImpactColor +47: ⋮---- +48: (selectedMemory.impact) +``` + +### crates/cowork-gui/src/components/config/SkillManager.tsx (237 lines) + +``` +1: import React, { useState } from "react"; +2: import { +3: List, +4: Button, +5: Space, +6: Typography, +7: Tag, +8: message, +9: Popconfirm, +10: Empty, +11: Drawer, +12: Descriptions, +13: Card, +14: Badge, +15: } from "antd"; +16: import { +17: PlusOutlined, +18: DeleteOutlined, +19: ThunderboltOutlined, +20: FolderOpenOutlined, +21: InfoCircleOutlined, +22: TagOutlined, +23: } from "@ant-design/icons"; +24: import { open } from "@tauri-apps/plugin-dialog"; +25: import { useConfigStore } from "../../stores/configStore"; +26: import type { SkillInfo } from "../../types/config"; +27: +28: const { Title, Text, Paragraph } = Typography; +29: +30: const SkillManager: React.FC = () => { +31: const { skills, selectedSkill, selectSkill, installSkill, uninstallSkill } = +32: useConfigStore(); +33: +34: const [detailDrawerVisible, setDetailDrawerVisible] = useState(false); +35: const [installing, setInstalling] = useState(false); +36: +37: const handleSelectFolder = async () => { +38: const selected = await open({ +39: directory: true, +40: multiple: false, +41: title: "Select Skill Directory (contains Skill.md)", +42: }); +43: +44: if (selected) { +45: await handleInstall(selected as string); +46: } +47: }; +48: +49: const handleInstall = async (skillPath: string) => { +50: setInstalling(true); +51: try { +52: await installSkill(skillPath); +53: message.success("Skill installed successfully"); +54: } catch (error) { +55: message.error("Failed to install skill"); +56: } finally { +57: setInstalling(false); +58: } +59: }; +60: +61: const handleView = (skill: SkillInfo) => { +62: selectSkill(skill.name); +63: setDetailDrawerVisible(true); +64: }; +65: +66: const handleUninstall = async (name: string) => { +67: try { +68: await uninstallSkill(name); +69: message.success("Skill uninstalled successfully"); +70: } catch (error) { +71: message.error("Failed to uninstall skill"); +72: } +73: }; +74: +75: const selectedSkillData = selectedSkill +76: ? skills.find((s) => s.name === selectedSkill) +77: : null; +78: +79: return ( +80:
+81:
+89: +90: Skill Manager +91: <Badge count={skills.length} style={{ marginLeft: 8 }} /> +92: +93: +101:
+102: +103: {skills.length === 0 ? ( +104: +107: No skills installed +108: +109: Install skills to extend agent capabilities +110: +111: +112: } +113: style={{ marginTop: "40px" }} +114: /> +115: ) : ( +116:
+117: +118: a.name.localeCompare(b.name))} +120: renderItem={(skill) => ( +121: } +128: onClick={() => handleView(skill)} +129: > +130: Details +131: , +132: handleUninstall(skill.name)} +136: > +137: +145: , +146: ]} +147: > +148: +153: } +154: title={ +155: +156: {skill.name} +157: +158: } +159: description={ +160: +161: {skill.description} +162: +163: {skill.tags.slice(0, 3).map((tag, i) => ( +164: +165: {tag} +166: +167: ))} +168: {skill.tags.length > 3 && ( +169: +{skill.tags.length - 3} +170: )} +171: +172: +173: } +174: /> +175: +176: )} +177: /> +178: +179:
+180: )} +181: +182: {} +183: setDetailDrawerVisible(false)} +188: open={detailDrawerVisible} +189: > +190: {selectedSkillData && ( +191: +192: +193: +194: {selectedSkillData.id} +195: +196: +197: {selectedSkillData.name} +198: +199: +200: {selectedSkillData.description} +201: +202: +203: +204: +205: <TagOutlined style={{ marginRight: 8 }} /> +206: Tags +207: +208: +209: {selectedSkillData.tags.length > 0 ? ( +210: selectedSkillData.tags.map((tag, i) => ( +211: +212: {tag} +213: +214: )) +215: ) : ( +216: No tags +217: )} +218: +219: +220: Skill Instructions +221: {selectedSkillData.body ? ( +222: +223: +224: {selectedSkillData.body} +225: +226: +227: ) : ( +228: No instructions defined +229: )} +230: +231: )} +232: +233:
+234: ); +235: }; +236: +237: export default SkillManager; +``` + +### crates/cowork-gui/src/hooks/useIterationActions.ts (92 lines) + +``` +1: function useIterationActions() { +2: const { message } = AntApp.useApp(); +3: +4: +5: const { currentIteration, setCurrentIteration, setIsExecuting } = useProjectStore(); +6: +7: +8: const { setProcessing } = useAgentStore(); +9: +10: +11: const { activeView, setActiveView } = useUIStore(); +12: +13: +14: const handleSelectIteration = useCallback( +15: async (iterationId: string) => { +16: try { +17: const { currentIteration, isExecuting } = useProjectStore.getState(); +18: const fullIteration = await API.iteration.get(iterationId); +19: +20: if (isExecuting && currentIteration?.id === iterationId) { +21: setCurrentIteration({ ...fullIteration, status: currentIteration.status }); +22: } else { +23: setCurrentIteration(fullIteration); +24: } +25: setActiveView('chat'); +26: } catch (error) { +27: console.error('Failed to load iteration:', error); +28: message.error('Failed to load iteration: ' + error); +29: } +30: }, +31: [setCurrentIteration, setActiveView, message] +32: ); +33: +34: +35: const handleExecuteIteration = useCallback(async () => { +36: if (!currentIteration) return; +37: try { +38: setProcessing(true); +39: await API.iteration.execute(currentIteration.id); +40: message.info('Iteration execution started'); +41: } catch (error) { +42: message.error('Failed to execute iteration: ' + error); +43: setProcessing(false); +44: } +45: }, [currentIteration, setProcessing, message]); +46: +47: +48: const handleOpenProjectFolder = useCallback(async () => { +49: try { +50: await API.util.openInFileManager('.'); +51: } catch (error) { +52: message.error('Failed to open project folder'); +53: } +54: }, [message]); +55: +56: +57: const handleOpenIterationFolder = useCallback(async (iterationId: string) => { +58: try { +59: await API.util.openInFileManager(iterationId); +60: } catch (error) { +61: message.error('Failed to open iteration folder: ' + error); +62: } +63: }, [message]); +64: +65: +66: const handleCommandSelect = useCallback( +67: (commandId: string) => { +68: const viewMap: Record = { +69: 'view-iterations': 'iterations', +70: 'view-chat': 'chat', +71: 'view-artifacts': 'artifacts', +72: 'view-code': 'code', +73: 'view-run': 'run', +74: 'view-memory': 'execution-memory', +75: 'view-projects': 'projects', +76: 'view-settings': 'settings' +77: }; +78: if (viewMap[commandId]) { +79: setActiveView(viewMap[commandId] as typeof activeView); +80: } +81: }, +82: [setActiveView] +83: ); +84: +85: return { +86: handleSelectIteration, +87: handleExecuteIteration, +88: handleOpenProjectFolder, +89: handleOpenIterationFolder, +90: handleCommandSelect +91: }; +92: } +``` + +### crates/cowork-gui/src/styles/chat.css (432 lines) + +``` +1: .chat-messages { +2: overflow-y: auto; +3: padding: 0; +4: flex: 1; +5: display: flex; +6: flex-direction: column; +7: } +8: +9: +10: .chat-msg-row { +11: padding: 16px 24px; +12: animation: msgFadeIn 0.25s ease-out; +13: } +14: +15: .chat-msg-row + .chat-msg-row { +16: border-top: 1px solid var(--border-light); +17: } +18: +19: @keyframes msgFadeIn { +20: from { opacity: 0; transform: translateY(6px); } +21: to { opacity: 1; transform: translateY(0); } +22: } +23: +24: +25: .chat-msg-agent { +26: background: var(--bg-base); +27: } +28: +29: .chat-msg-agent .chat-msg-header { +30: display: flex; +31: align-items: center; +32: gap: 8px; +33: margin-bottom: 8px; +34: } +35: +36: .chat-msg-agent .chat-agent-avatar { +37: width: 24px; +38: height: 24px; +39: border-radius: 6px; +40: object-fit: cover; +41: flex-shrink: 0; +42: } +43: +44: .chat-msg-agent .chat-agent-name { +45: font-size: 13px; +46: font-weight: 600; +47: color: var(--text-primary); +48: } +49: +50: .chat-msg-agent .chat-agent-stage { +51: font-size: 11px; +52: color: var(--text-tertiary); +53: background: var(--bg-elevated); +54: padding: 1px 8px; +55: border-radius: 4px; +56: } +57: +58: +59: .chat-msg-user { +60: background: var(--primary-lighter); +61: } +62: +63: .chat-msg-user .chat-msg-content { +64: display: flex; +65: justify-content: flex-end; +66: } +67: +68: .chat-msg-user .chat-user-bubble { +69: background: var(--primary); +70: color: #fff; +71: padding: 10px 16px; +72: border-radius: 12px 12px 4px 12px; +73: max-width: 75%; +74: word-break: break-word; +75: line-height: 1.6; +76: font-size: 14px; +77: box-shadow: 0 1px 4px rgba(37, 99, 235, 0.15); +78: } +79: +80: +81: .chat-msg-thinking { +82: background: var(--bg-container); +83: } +84: +85: .chat-msg-thinking .chat-thinking-toggle { +86: display: flex; +87: align-items: center; +88: gap: 8px; +89: cursor: pointer; +90: user-select: none; +91: font-size: 13px; +92: color: var(--text-tertiary); +93: padding: 4px 0; +94: transition: color 0.15s; +95: } +96: +97: .chat-msg-thinking .chat-thinking-toggle:hover { +98: color: var(--text-secondary); +99: } +100: +101: .chat-msg-thinking .chat-agent-avatar { +102: width: 24px; +103: height: 24px; +104: border-radius: 6px; +105: object-fit: cover; +106: flex-shrink: 0; +107: opacity: 0.7; +108: } +109: +110: .chat-msg-thinking .chat-thinking-label { +111: font-style: italic; +112: } +113: +114: .chat-msg-thinking .chat-thinking-chevron { +115: font-size: 10px; +116: transition: transform 0.2s; +117: margin-left: auto; +118: } +119: +120: .chat-msg-thinking.chat-thinking-expanded .chat-thinking-chevron { +121: transform: rotate(90deg); +122: } +123: +124: .chat-msg-thinking .chat-thinking-body { +125: margin-top: 8px; +126: padding: 10px 14px; +127: background: var(--bg-elevated); +128: border-radius: 8px; +129: border: 1px solid var(--border-light); +130: font-size: 13px; +131: line-height: 1.6; +132: color: var(--text-secondary); +133: font-style: italic; +134: white-space: pre-wrap; +135: word-break: break-word; +136: max-height: 200px; +137: overflow-y: auto; +138: } +139: +140: +141: .chat-msg-tool-call { +142: background: var(--bg-base); +143: padding: 12px 24px !important; +144: } +145: +146: .chat-msg-tool-call .chat-tool-bar { +147: display: flex; +148: align-items: center; +149: gap: 8px; +150: font-size: 13px; +151: } +152: +153: .chat-msg-tool-call .chat-tool-icon { +154: width: 20px; +155: height: 20px; +156: border-radius: 4px; +157: background: var(--warning-light); +158: display: flex; +159: align-items: center; +160: justify-content: center; +161: font-size: 11px; +162: flex-shrink: 0; +163: } +164: +165: .chat-msg-tool-call .chat-tool-name { +166: font-weight: 500; +167: color: var(--text-primary); +168: font-family: 'JetBrains Mono', 'Consolas', monospace; +169: font-size: 12px; +170: } +171: +172: .chat-msg-tool-call .chat-tool-args { +173: margin-top: 6px; +174: padding: 8px 12px; +175: background: var(--bg-elevated); +176: border-radius: 6px; +177: font-family: 'JetBrains Mono', 'Consolas', monospace; +178: font-size: 12px; +179: color: var(--text-secondary); +180: overflow-x: auto; +181: max-height: 80px; +182: overflow-y: auto; +183: line-height: 1.5; +184: } +185: +186: +187: .chat-msg-tool-result { +188: background: var(--bg-base); +189: padding: 8px 24px !important; +190: } +191: +192: .chat-msg-tool-result .chat-tool-result-bar { +193: display: flex; +194: align-items: center; +195: gap: 6px; +196: font-size: 12px; +197: color: var(--text-tertiary); +198: } +199: +200: .chat-msg-tool-result .chat-tool-result-icon { +201: font-size: 13px; +202: } +203: +204: .chat-msg-tool-result.chat-tool-success .chat-tool-result-icon { +205: color: var(--success); +206: } +207: +208: .chat-msg-tool-result.chat-tool-fail .chat-tool-result-icon { +209: color: var(--error); +210: } +211: +212: .chat-msg-tool-result .chat-tool-result-name { +213: font-family: 'JetBrains Mono', 'Consolas', monospace; +214: font-size: 11px; +215: color: var(--text-tertiary); +216: } +217: +218: +219: .chat-msg-pm-agent { +220: background: var(--bg-base); +221: } +222: +223: .chat-msg-pm-agent .chat-pm-header { +224: display: flex; +225: align-items: center; +226: gap: 8px; +227: margin-bottom: 8px; +228: } +229: +230: .chat-msg-pm-agent .chat-agent-avatar { +231: width: 24px; +232: height: 24px; +233: border-radius: 6px; +234: object-fit: cover; +235: flex-shrink: 0; +236: } +237: +238: .chat-msg-pm-agent .chat-pm-name { +239: font-size: 13px; +240: font-weight: 600; +241: color: var(--success); +242: } +243: +244: .chat-msg-pm-agent .chat-pm-actions { +245: display: flex; +246: flex-wrap: wrap; +247: gap: 8px; +248: margin-top: 12px; +249: } +250: +251: .chat-msg-pm-agent .chat-pm-action { +252: cursor: pointer; +253: padding: 5px 12px; +254: border-radius: 6px; +255: display: inline-flex; +256: align-items: center; +257: gap: 6px; +258: font-size: 12px; +259: font-weight: 500; +260: border: 1px solid var(--border-color); +261: background: var(--bg-container); +262: color: var(--text-secondary); +263: transition: all 0.15s ease; +264: } +265: +266: .chat-msg-pm-agent .chat-pm-action:hover { +267: border-color: var(--primary); +268: color: var(--primary); +269: background: var(--primary-lighter); +270: box-shadow: 0 1px 4px rgba(37, 99, 235, 0.1); +271: } +272: +273: +274: .chat-processing-bar { +275: padding: 12px 24px; +276: background: var(--primary-lighter); +277: border-bottom: 1px solid var(--primary-light); +278: display: flex; +279: align-items: center; +280: gap: 10px; +281: font-size: 13px; +282: color: var(--primary); +283: font-weight: 500; +284: } +285: +286: .chat-processing-bar .chat-processing-stage { +287: font-size: 11px; +288: color: var(--text-secondary); +289: font-weight: 400; +290: } +291: +292: +293: .chat-msg-error { +294: background: var(--error-light); +295: } +296: +297: .chat-msg-error .chat-error-content { +298: color: var(--error); +299: font-size: 13px; +300: line-height: 1.5; +301: } +302: +303: +304: .chat-header { +305: padding: 16px 24px; +306: border-bottom: 1px solid var(--border-color); +307: background: var(--bg-base); +308: } +309: +310: .chat-header-title { +311: font-size: 15px; +312: font-weight: 600; +313: color: var(--text-primary); +314: margin: 0; +315: } +316: +317: .chat-header-desc { +318: font-size: 12px; +319: color: var(--text-tertiary); +320: margin: 4px 0 0 0; +321: display: -webkit-box; +322: -webkit-line-clamp: 2; +323: -webkit-box-orient: vertical; +324: overflow: hidden; +325: text-overflow: ellipsis; +326: line-height: 1.4; +327: } +328: +329: +330: .chat-input-wrapper { +331: padding: 16px 24px; +332: background: var(--bg-base); +333: border-top: 1px solid var(--border-color); +334: } +335: +336: +337: .chat-input-request { +338: padding: 16px; +339: background: var(--primary-lighter); +340: border: 1px solid var(--primary-light); +341: border-radius: 10px; +342: margin-bottom: 16px; +343: } +344: +345: .chat-input-request-title { +346: font-size: 14px; +347: font-weight: 600; +348: color: var(--primary); +349: margin-bottom: 8px; +350: } +351: +352: .chat-input-request-prompt { +353: font-size: 13px; +354: color: var(--text-secondary); +355: margin-bottom: 12px; +356: line-height: 1.5; +357: } +358: +359: +360: .chat-empty-state { +361: display: flex; +362: flex-direction: column; +363: align-items: center; +364: justify-content: center; +365: height: 100%; +366: color: var(--text-tertiary); +367: text-align: center; +368: padding: 40px; +369: } +370: +371: .chat-empty-state h3 { +372: font-size: 16px; +373: color: var(--text-secondary); +374: margin: 16px 0 8px; +375: font-weight: 500; +376: } +377: +378: .chat-empty-state p { +379: font-size: 13px; +380: line-height: 1.6; +381: max-width: 400px; +382: } +383: +384: +385: .chat-pm-welcome { +386: display: flex; +387: flex-direction: column; +388: align-items: center; +389: justify-content: center; +390: padding: 48px 24px; +391: text-align: center; +392: } +393: +394: .chat-pm-welcome-icon { +395: font-size: 40px; +396: margin-bottom: 16px; +397: } +398: +399: .chat-pm-welcome h3 { +400: font-size: 18px; +401: color: var(--text-primary); +402: margin: 0 0 8px; +403: font-weight: 600; +404: } +405: +406: .chat-pm-welcome p { +407: font-size: 13px; +408: color: var(--text-secondary); +409: line-height: 1.6; +410: max-width: 400px; +411: margin: 0; +412: } +413: +414: .chat-pm-welcome ul { +415: list-style: none; +416: padding: 0; +417: margin: 16px 0 0; +418: text-align: left; +419: } +420: +421: .chat-pm-welcome li { +422: font-size: 13px; +423: color: var(--text-secondary); +424: padding: 4px 0; +425: } +426: +427: .chat-pm-welcome li::before { +428: content: '·'; +429: margin-right: 8px; +430: color: var(--primary); +431: font-weight: bold; +432: } +``` + +### crates/cowork-gui/src-tauri/src/commands/mod.rs (7 lines) + +``` +1: init_app_handle +2: ⋮---- +3: (handle: tauri::AppHandle) +4: ⋮---- +5: init_path_for_app_bundle +6: ⋮---- +7: () +``` + +### crates/cowork-gui/src-tauri/src/commands/runner.rs (59 lines) + +``` +1: get_code_directory +2: ⋮---- +3: (iteration_id: &str, workspace_path: Option<&str>) +4: ⋮---- +5: install_deps_if_needed +6: ⋮---- +7: (workspace: &std::path::Path) +8: ⋮---- +9: try_analyze +10: ⋮---- +11: (code_dir: &std::path::Path) +12: ⋮---- +13: is_vanilla_html_project +14: ⋮---- +15: (dir: &std::path::Path) +16: ⋮---- +17: has_html_files +18: ⋮---- +19: (dir: &std::path::Path) +20: ⋮---- +21: detect_npm_start_command +22: ⋮---- +23: (dir: &std::path::Path) +24: ⋮---- +25: start_iteration_project +26: ⋮---- +27: ( +28: iteration_id: String, +29: window: Window, +30: state: State<'_, AppState>, +31: ) +32: ⋮---- +33: stop_iteration_project +34: ⋮---- +35: (iteration_id: String) +36: ⋮---- +37: check_project_status +38: ⋮---- +39: (iteration_id: String) +40: ⋮---- +41: format_code +42: ⋮---- +43: (_session_id: String, _file_path: Option) +44: ⋮---- +45: check_formatter_available +46: ⋮---- +47: (_session_id: String) +48: ⋮---- +49: is_fullstack +50: ⋮---- +51: (rt: &RuntimeType) +52: ⋮---- +53: get_start_command_from_config +54: ⋮---- +55: (config: &cowork_core::ProjectRuntimeConfig) +56: ⋮---- +57: start_fullstack +58: ⋮---- +59: (iteration_id: String, code_dir: PathBuf, config: &cowork_core::ProjectRuntimeConfig) +``` + +### crates/cowork-gui/src-tauri/src/commands/system.rs (11 lines) + +``` +1: init_system_locale +2: ⋮---- +3: () +4: ⋮---- +5: detect_system_locale +6: ⋮---- +7: () +8: ⋮---- +9: get_system_locale +10: ⋮---- +11: () +``` + +### crates/cowork-gui/src-tauri/tauri.conf.json (31 lines) + +``` +1: { +2: "$schema": "https://schema.tauri.app/config/2", +3: "productName": "Cowork Forge", +4: "version": "2.5.1", +5: "identifier": "com.coworkforge.gui", +6: "build": { +7: "beforeDevCommand": "bun run dev", +8: "beforeBuildCommand": "bun run build", +9: "devUrl": "http://localhost:15173", +10: "frontendDist": "../dist" +11: }, +12: "app": { +13: "withGlobalTauri": true, +14: "windows": [ +15: { +16: "title": "Cowork Forge", +17: "width": 1200, +18: "height": 720, +19: "center": true, +20: "minWidth": 800, +21: "minHeight": 600 +22: } +23: ], +24: "security": { "csp": null } +25: }, +26: "bundle": { +27: "active": true, +28: "targets": "all", +29: "icon": ["icons/icon-rgba.png", "icons/icon.icns", "icons/icon.ico"] +30: } +31: } +``` + +### litho.docs/en/4.Deep-Exploration/Persistence Domain.md (436 lines) + +```` +1: **Persistence Domain Technical Documentation** +2: +3: **Cowork Forge** | **Infrastructure Layer** | **Generation Time:** 2025-01-09 08:23:45 UTC +4: +5: --- +6: +7: ## 1. Overview +8: +9: The **Persistence Domain** provides durable storage abstractions for the Cowork Forge platform, implementing a file-based Data Access Layer (DAL) that persists domain entities to the local filesystem. This domain bridges the core business logic (Project, Iteration, and Memory aggregates) with long-term storage, ensuring project continuity across development sessions and system restarts. +10: +11: **Key Responsibilities:** +12: - Entity serialization and deserialization (JSON-based) +13: - Workspace directory structure management (`.cowork-v2` convention) +14: - Platform-specific path resolution and file I/O operations +15: - Iteration-scoped workspace isolation for artifact storage +16: - Atomic write operations and data integrity enforcement +17: +18: **Architectural Classification:** Infrastructure Domain (supporting Core Business Domains) +19: +20: --- +21: +22: ## 2. Architectural Position +23: +24: The Persistence Domain sits at the infrastructure layer of the Hexagonal Architecture, implementing the **Repository Pattern** to abstract storage mechanics from domain logic. It provides stateless Data Access Objects (DAOs) that translate between domain entities and persistent storage formats. +25: +26: ### 2.1 Layer Relationships +27: +28: ```mermaid +29: flowchart TD +30: subgraph DomainLayer["Domain Layer (Core)"] +31: Project["Project Aggregate"] +32: Iteration["Iteration Entity"] +33: Memory["Memory Aggregate"] +34: end +35: +36: subgraph PersistenceLayer["Persistence Layer"] +37: ProjectStore["ProjectStore"] +38: IterationStore["IterationStore"] +39: MemoryStore["MemoryStore"] +40: StorageUtils["Storage Utilities"] +41: end +42: +43: subgraph StorageImpl["Storage Implementation"] +44: Serde["serde_json"] +45: Anyhow["anyhow Error Handling"] +46: PathResolver["Path Resolution
get_cowork_dir()"] +47: end +48: +49: subgraph FileSystem["Local File System"] +50: ProjectFile[".cowork-v2/project.json"] +51: IterationFiles[".cowork-v2/iterations/*.json"] +52: Workspaces[".cowork-v2/iterations/{id}/workspace/"] +53: MemoryDir[".cowork-v2/memory/"] +54: end +55: +56: Project -.->|Uses| ProjectStore +57: Iteration -.->|Uses| IterationStore +58: Memory -.->|Uses| MemoryStore +59: +60: ProjectStore -->|Serializes| Serde +61: IterationStore -->|Serializes| Serde +62: ProjectStore -->|Handles errors| Anyhow +63: IterationStore -->|Resolves paths| PathResolver +64: +65: ProjectStore -->|Read/Write| ProjectFile +66: IterationStore -->|Read/Write| IterationFiles +67: IterationStore -->|Manages| Workspaces +68: IterationStore -->|Ensures| MemoryDir +69: +70: style PersistenceLayer fill:#e8f5e9 +71: style DomainLayer fill:#fff3e0 +72: ``` +73: +74: ### 2.2 Design Patterns +75: +76: - **Repository Pattern:** Abstracts data access through store interfaces (`ProjectStore`, `IterationStore`), allowing the domain layer to remain persistence-agnostic +77: - **Data Access Layer (DAL):** Stateless service layer handling all CRUD operations and query capabilities +78: - **Workspace Isolation:** Each iteration maintains an isolated workspace directory, preventing artifact collisions between concurrent or historical iterations +79: +80: --- +81: +82: ## 3. Core Components +83: +84: ### 3.1 ProjectStore +85: +86: **Location:** `crates/cowork-core/src/persistence/project_store.rs` +87: +88: Manages the persistence lifecycle of the `Project` aggregate root, handling project metadata, iteration tracking, and configuration storage. +89: +90: **Key Operations:** +91: +92: | Method | Signature | Description | +93: |--------|-----------|-------------| +94: | `load` | `() -> Result>` | Loads project metadata from `project.json`; returns `None` if not initialized | +95: | `save` | `(&Project) -> Result<()>` | Persists project state with pretty-printed JSON formatting | +96: | `exists` | `() -> Result` | Checks for project initialization in the current workspace | +97: | `create` | `(name: &str, path: &Path) -> Result` | Initializes new project with directory structure scaffolding | +98: | `update` | `(&Project) -> Result<()>` | Updates existing project metadata | +99: | `add_iteration` | `(iteration_id: &str) -> Result<()>` | Registers new iteration in project's iteration collection | +100: | `set_current_iteration` | `(iteration_id: &str) -> Result<()>` | Updates pointer to active iteration | +101: +102: **Storage Schema:** +103: ```json +104: { +105: "id": "uuid-v4-string", +106: "name": "Project Name", +107: "description": "Optional description", +108: "path": "/absolute/path/to/project", +109: "tech_stack": { +110: "language": "Rust", +111: "framework": "Tauri", +112: "project_type": "DesktopApp" +113: }, +114: "iterations": ["iter-001", "iter-002"], +115: "current_iteration": "iter-002", +116: "created_at": "2025-01-09T08:23:45Z", +117: "updated_at": "2025-01-09T10:15:22Z" +118: } +119: ``` +120: +121: ### 3.2 IterationStore +122: +123: **Location:** `crates/cowork-core/src/persistence/iteration_store.rs` +124: +125: Manages iteration entities and their associated workspace artifacts. Implements the V2 architecture pattern with iteration-specific workspace isolation. +126: +127: **Key Operations:** +128: +129: | Method | Signature | Description | +130: |--------|-----------|-------------| +131: | `load` | `(iteration_id: &str) -> Result>` | Retrieves specific iteration by UUID | +132: | `save` | `(&Iteration) -> Result<()>` | Persists iteration state to `iterations/{id}.json` | +133: | `delete` | `(iteration_id: &str) -> Result<()>` | Removes iteration metadata and optionally cleans workspace | +134: | `load_all` | `() -> Result>` | Retrieves all iterations sorted by iteration number (ascending) | +135: | `load_summaries` | `() -> Result>` | Lightweight query returning only essential metadata | +136: | `workspace_path` | `(iteration_id: &str) -> Result` | Resolves absolute path to iteration's workspace directory | +137: | `ensure_workspace` | `(iteration_id: &str) -> Result` | Idempotent directory creation for workspace and memory subdirectories | +138: | `iteration_path` | `(iteration_id: &str) -> PathBuf` | Constructs path to iteration artifact subdirectory | +139: +140: **Workspace Structure:** +141: ``` +142: .cowork-v2/ +143: ├── project.json +144: ├── iterations/ +145: │ ├── iter-001.json +146: │ ├── iter-002.json +147: │ └── iter-{uuid}/ +148: │ ├── workspace/ # Generated artifacts (code, docs) +149: │ │ ├── src/ +150: │ │ ├── docs/ +151: │ │ └── ... +152: │ └── memory/ # Knowledge snapshots +153: │ └── knowledge.json +154: ``` +155: +156: ### 3.3 Iteration Data Storage +157: +158: **Location:** `crates/cowork-core/src/persistence/iteration_data.rs` +159: +160: Provides iteration-scoped data storage for artifacts, session data, and stage-specific content. This module was consolidated from the former `storage` module, centralizing all persistence operations within the persistence domain. +161: +162: **Key Functions:** +163: +164: | Category | Functions | Description | +165: |----------|-----------|-------------| +166: | **Iteration Context** | `set_iteration_id()`, `get_iteration_id()`, `clear_iteration_id()` | Manage global iteration context via thread-safe static storage | +167: | **Directory Management** | `get_iteration_dir()`, `artifact_path()`, `data_path()`, `session_path()` | Resolve iteration-specific paths for data, artifacts, and session files | +168: | **Stage Data** | `load/save_requirements()`, `load/save_feature_list()`, `load/save_design_spec()`, `load/save_implementation_plan()` | Persist structured data models for each pipeline stage | +169: | **Artifacts** | `load/save_idea()`, `save_plan_doc()`, `save_prd_doc()`, `save_design_doc()`, `save_check_report()`, `save_delivery_report()` | Manage markdown artifact files | +170: | **Session State** | `load/save_session_meta()`, `load/save_feedback_history()`, `append_feedback()`, `clear_stage_feedback()` | Track execution state and user feedback | +171: | **Workspace Utils** | `get_cowork_dir()`, `is_project_initialized()`, `init_project_structure()` | Global workspace path resolution and initialization | +172: +173: **Iteration Directory Structure:** +174: ``` +175: .cowork-v2/ +176: ├── project.json +177: ├── iterations/ +178: │ └── {iteration_id}/ +179: │ ├── data/ # Structured JSON data +180: │ │ ├── requirements.json +181: │ │ ├── feature_list.json +182: │ │ ├── design_spec.json +183: │ │ ├── implementation_plan.json +184: │ │ └── code_metadata.json +185: │ ├── artifacts/ # Markdown documents +186: │ │ ├── idea.md +187: │ │ ├── prd.md +188: │ │ ├── design.md +189: │ │ ├── plan.md +190: │ │ ├── check_report.md +191: │ │ └── delivery_report.md +192: │ ├── session/ # Execution state +193: │ │ ├── meta.json +194: │ │ └── feedback.json +195: │ └── logs/ # Execution logs +196: └── memory/ +197: ├── project/ +198: └── iterations/ +199: ``` +200: +201: **Global State Management:** +202: +203: The module uses global static storage for iteration context, enabling tools and pipeline stages to access current iteration data without explicit parameter passing: +204: +205: ```rust +206: static CURRENT_ITERATION_ID: Mutex> = Mutex::new(None); +207: static GLOBAL_WORKSPACE_PATH: OnceLock>> = OnceLock::new(); +208: ``` +209: +210: --- +211: +212: ## 4. Storage Conventions & Schema +213: +214: ### 4.1 Directory Structure +215: +216: The Persistence Domain enforces a strict directory convention under the `.cowork-v2` hidden directory at project root: +217: +218: | Path Component | Purpose | Lifecycle | +219: |----------------|---------|-----------| +220: | `project.json` | Project metadata, tech stack, iteration registry | Persistent (project lifetime) | +221: | `iterations/` | Iteration metadata storage | Persistent (append-only) | +222: | `iterations/{id}/workspace/` | Generated artifacts, code, documentation | Iteration-scoped | +223: | `iterations/{id}/memory/` | Knowledge snapshots, learning data | Iteration-scoped | +224: | `memory/` | Global project memory indices | Persistent | +225: +226: ### 4.2 Serialization Strategy +227: +228: - **Format:** JSON (pretty-printed with 2-space indentation for human readability) +229: - **Library:** `serde_json` with `to_string_pretty()` for writes and `from_str()` for reads +230: - **Error Handling:** `anyhow` crate for context-rich error propagation +231: - **Encoding:** UTF-8 standard encoding +232: +233: ### 4.3 Concurrency Model +234: +235: The implementation utilizes **synchronous, blocking I/O** operations suitable for the desktop application context: +236: - File operations are atomic (write-to-temp-then-rename pattern where applicable) +237: - No database locking mechanisms required (filesystem-level isolation) +238: - Suitable for single-user local execution model +239: +240: --- +241: +242: ## 5. Data Flow & Operations +243: +244: ### 5.1 Entity Persistence Flow +245: +246: ```mermaid +247: sequenceDiagram +248: autonumber +249: participant Domain as Domain Entity +250: participant Store as ProjectStore/IterationStore +251: participant Path as get_cowork_dir() +252: participant FS as File System +253: participant Serde as serde_json +254: +255: Note over Domain,Serde: Save Operation +256: Domain->>Store: save(entity) +257: Store->>Path: get_cowork_dir() +258: Path-->>Store: PathBuf (.cowork-v2) +259: Store->>Store: Construct file path
{id}.json +260: Store->>Serde: to_string_pretty(entity) +261: Serde-->>Store: JSON String +262: Store->>FS: write(path, content) +263: FS-->>Store: Ok(()) +264: Store-->>Domain: anyhow::Result<()> +265: +266: Note over Domain,Serde: Load Operation +267: Domain->>Store: load(id) +268: Store->>Path: get_cowork_dir() +269: Path-->>Store: PathBuf +270: Store->>Store: Construct file path +271: Store->>FS: exists() +272: FS-->>Store: bool +273: alt File Exists +274: Store->>FS: read_to_string(path) +275: FS-->>Store: String content +276: Store->>Serde: from_str(content) +277: Serde-->>Store: Entity +278: Store-->>Domain: Ok(Some(entity)) +279: else File Not Found +280: Store-->>Domain: Ok(None) +281: end +282: ``` +283: +284: ### 5.2 Workspace Initialization Flow +285: +286: When creating a new iteration, the `IterationStore` ensures proper workspace scaffolding: +287: +288: 1. **Directory Creation:** `ensure_workspace()` creates `iterations/{id}/workspace/` recursively +289: 2. **Memory Directory:** Ensures `iterations/{id}/memory/` exists for knowledge persistence +290: 3. **Path Resolution:** Returns absolute `PathBuf` for downstream artifact generation +291: 4. **Validation:** Confirms workspace containment within project boundaries +292: +293: --- +294: +295: ## 6. Error Handling & Safety +296: +297: ### 6.1 Error Strategy +298: +299: - **Library:** `anyhow` for error context and propagation +300: - **Pattern:** Early return with `?` operator for I/O and serialization errors +301: - **User Feedback:** Errors bubble up to Interface layer (CLI/GUI) for user presentation +302: - **Recovery:** Graceful handling of missing files (returns `Option` rather than failing) +303: +304: ### 6.2 Security Considerations +305: +306: - **Path Traversal Prevention:** All paths resolved through `get_cowork_dir()` with validation that operations remain within `.cowork-v2` hierarchy +307: - **Workspace Containment:** File tools (in Tools Domain) validate paths against project root before delegation to Persistence Domain +308: - **Atomic Writes:** Critical metadata updates use write-temporary-rename pattern to prevent corruption on interruption +309: +310: --- +311: +312: ## 7. Integration with Other Domains +313: +314: ### 7.1 Upstream Dependencies (Consumers) +315: +316: | Domain | Usage Pattern | Integration Point | +317: |--------|--------------|-------------------| +318: | **Domain Logic** | Core entities define structures being persisted | `Project`, `Iteration` structs passed to Store methods | +319: | **Tools Domain** | File tools, Data tools, Memory tools require storage | Direct Store instantiation for CRUD operations | +320: | **GUI Backend** | Project management, iteration listing | `ProjectManager` wraps Store operations for Tauri commands | +321: | **CLI Domain** | Project initialization, status checks | Direct Store usage in command handlers | +322: +323: ### 7.2 Downstream Dependencies (Providers) +324: +325: | Service | Purpose | +326: |---------|---------| +327: | **File System** | Local disk I/O operations | +328: | **serde_json** | Serialization/deserialization engine | +329: | **anyhow** | Error handling and context | +330: +331: ### 7.3 Domain Relations Diagram +332: +333: ```mermaid +334: flowchart LR +335: subgraph Core["Core Domain"] +336: DL[Domain Logic
Project/Iteration] +337: Mem[Memory Domain] +338: end +339: +340: subgraph Infra["Infrastructure"] +341: Persist[Persistence Domain
Stores] +342: Tools[Tools Domain] +343: end +344: +345: subgraph Presentation["Presentation"] +346: GUI[GUI Backend] +347: CLI[CLI Interface] +348: end +349: +350: DL -->|Defines entities| Persist +351: Mem -->|Uses| Persist +352: Tools -->|CRUD Operations| Persist +353: GUI -->|Project Management| Persist +354: CLI -->|Status/Init| Persist +355: +356: Persist -->|Implements| FS[File System] +357: +358: style Persist fill:#4CAF50 +359: style DL fill:#2196F3 +360: ``` +361: +362: --- +363: +364: ## 8. Implementation Considerations +365: +366: ### 8.1 Performance Characteristics +367: +368: - **Latency:** Low (local filesystem operations, typically <10ms for metadata) +369: - **Throughput:** Suitable for document-sized JSON files (<10MB); not optimized for binary blob storage +370: - **Scalability:** Limited by filesystem performance; tested up to 100+ iterations per project +371: - **Memory:** Streaming deserialization not implemented; entire JSON documents loaded into memory (acceptable for metadata scale) +372: +373: ### 8.2 Migration & Evolution +374: +375: The V2 architecture (current) maintains backward compatibility considerations: +376: - Schema evolution handled through serde's `default` attributes for new fields +377: - Directory structure versioned via `.cowork-v2` naming convention +378: - No automated migration tools currently implemented; manual migration scripts for major version upgrades +379: +380: ### 8.3 Configuration +381: +382: No external configuration required. Storage location is deterministic: +383: - **Path Resolution:** `{project_root}/.cowork-v2/` +384: - **Platform Handling:** Cross-platform path separators via `std::path::PathBuf` +385: - **Environment:** Respects standard filesystem permissions (UMASK on Unix, ACLs on Windows) +386: +387: --- +388: +389: ## 9. Usage Examples +390: +391: ### 9.1 Project Initialization +392: ```rust +393: // Creates project structure and persists metadata +394: let project = ProjectStore::create("MyApp", &project_path)?; +395: ProjectStore::save(&project)?; +396: ``` +397: +398: ### 9.2 Iteration Lifecycle +399: ```rust +400: // Save new iteration +401: let iteration = Iteration::new_genesis(&project_id, idea_description); +402: IterationStore::save(&iteration)?; +403: +404: // Ensure workspace exists for artifact generation +405: let workspace = IterationStore::ensure_workspace(&iteration.id)?; +406: // workspace now points to .cowork-v2/iterations/{id}/workspace/ +407: ``` +408: +409: ### 9.3 Query Operations +410: ```rust +411: // Load all iterations sorted by number +412: let iterations = IterationStore::load_all()?; +413: for iter in iterations { +414: println!("Iteration {}: {}", iter.number, iter.status); +415: } +416: +417: // Check project existence +418: if ProjectStore::exists()? { +419: let project = ProjectStore::load()?.expect("Project exists"); +420: } +421: ``` +422: +423: --- +424: +425: ## 10. Future Considerations +426: +427: - **Schema Migration:** Formal migration framework for evolving JSON schemas across versions +428: - **Compression:** Optional gzip compression for large iteration histories +429: - **Caching:** In-memory caching layer for frequently accessed project metadata +430: - **Backup:** Automated backup strategies for `.cowork-v2` directory integrity +431: +432: --- +433: +434: **Document Version:** 1.0 +435: **Last Updated:** 2025-01-09 +436: **Maintainer:** Cowork Forge Architecture Team +```` + +### litho.docs/en/4.Deep-Exploration/Tools Domain.md (481 lines) + +```` +1: **Tools Domain Technical Documentation** +2: **Cowork Forge – AI-Native Iterative Development Platform** +3: +4: **Generation Time:** 2026-02-14 05:17:26 (UTC) +5: **Version:** 1.0 +6: **Domain:** Supporting Domain (Infrastructure) +7: **Crate:** `cowork-core` +8: +9: --- +10: +11: ## 1. Executive Overview +12: +13: The **Tools Domain** provides the comprehensive operational interface between AI agents and the host system within Cowork Forge. Acting as the execution layer for the 7-stage AI pipeline, this domain implements 40+ specialized tools adhering to the ADK (Agent Development Kit) `Tool` trait specification. These tools enable AI agents to perform secure file operations, manage structured project data, interact with human users through validation gates, query institutional memory, and execute deployment workflows. +14: +15: **Key Architectural Value:** +16: - **Security-First Design**: Enforces strict workspace containment with path traversal prevention and UNC path normalization +17: - **Unified Interface**: Standardized `async_trait`-based tool contract enabling seamless agent integration +18: - **Cross-Platform Compatibility**: Handles Windows UNC paths and POSIX filesystem semantics uniformly +19: - **Human-in-the-Loop (HITL) Integration**: Bridges automated agent execution with human oversight through the `InteractiveBackend` abstraction +20: +21: --- +22: +23: ## 2. Architectural Positioning +24: +25: Within Cowork Forge's Hexagonal Architecture, the Tools Domain operates as an **Infrastructure Adapter** that translates domain operations into system-level actions. It sits between the Pipeline Domain (which orchestrates AI agents) and external systems (filesystem, process executor, human users). +26: +27: ```mermaid +28: flowchart TB +29: subgraph Core[Domain Layer] +30: Pipeline[Pipeline Domain
Stage Executor] +31: Memory[Memory Domain
Knowledge Management] +32: end +33: +34: subgraph Tools[Tools Domain
Adapter Layer] +35: FileTools[File Tools] +36: DataTools[Data Tools] +37: HITLTools[HITL Tools] +38: ValidationTools[Validation Tools] +39: DeploymentTools[Deployment Tools] +40: MemoryTools[Memory Tools] +41: end +42: +43: subgraph External[External Systems] +44: FS[File System] +45: Shell[Shell Executor] +46: User[Human User
via InteractiveBackend] +47: end +48: +49: Pipeline -->|Invokes| Tools +50: Memory <-->|Queries/Updates| Tools +51: Tools -->|Secure I/O| FS +52: Tools -->|Process Execution| Shell +53: Tools <-->|Confirmation/Input| User +54: ``` +55: +56: **Dependency Relationships:** +57: - **Upstream**: Pipeline Domain (via Stage Executor), Agent orchestration framework (`adk-rust`) +58: - **Downstream**: Persistence Domain (IterationStore, MemoryStore), Interaction Domain (InteractiveBackend), File System +59: +60: --- +61: +62: ## 3. Tool Taxonomy +63: +64: The domain organizes tools into ten functional categories, each addressing specific operational concerns within the AI-driven development lifecycle. +65: +66: ### 3.1 File Tools (`file_tools.rs`) +67: **Purpose**: Secure filesystem operations within workspace boundaries +68: - **ListFilesTool**: Directory traversal with pattern matching (utilizes `walkdir`) +69: - **ReadFileTool**: Full file content retrieval with encoding handling +70: - **ReadFileTruncatedTool**: Intelligent truncation for large files with line count limits +71: - **ReadFileWithLimitTool**: Call-count limited reading to prevent token exhaustion +72: - **WriteFileTool**: Atomic file writes with parent directory creation +73: - **RunCommandTool**: Shell execution with 30-second timeout and blocking command detection (prevents interactive commands like `vim`) +74: +75: ### 3.2 Document Tools (`load_artifacts.rs`, `artifact_tools.rs`) +76: **Purpose**: Project iteration document loading and saving operations +77: - **LoadIdeaTool**: Load the idea document from current iteration +78: - **LoadPrdDocTool**: Load the PRD document from current iteration +79: - **LoadDesignDocTool**: Load the design document from current iteration +80: - **LoadPlanDocTool**: Load the implementation plan document from current iteration +81: - **SavePrdDocTool**: Save PRD document to artifacts directory +82: - **SaveDesignDocTool**: Save design document to artifacts directory +83: - **SavePlanDocTool**: Save implementation plan document to artifacts directory +84: - **SaveDeliveryReportTool**: Save delivery report to artifacts directory +85: - **SaveCheckReportTool**: Save check report to artifacts directory +86: - **SaveIdeaTool**: Save initial idea document +87: +88: > **Note**: Document tools enable all Agents to read project iteration files on-demand during task execution, understanding project context. These tools are now configured for all built-in Agents. +89: +90: ### 3.3 Data Tools (`data_tools.rs`) +91: **Purpose**: Structured data management for requirements, features, and tasks +92: - **Requirements Management**: `CreateRequirementTool`, `GetRequirementsTool` (REQ-ID prefixed) +93: - **Feature Management**: `AddFeatureTool`, `UpdateFeatureStatusTool`, `GetDesignDocumentTool` (FEAT-ID prefixed) +94: - **Task Management**: `CreateTaskTool`, `UpdateTaskStatusTool` (TASK-ID prefixed, supports pending→in_progress→completed/blocked workflow) +95: - **Component Management**: `AddComponentTool`, `GetImplementationPlanTool` (COMP-ID prefixed) +96: - **Status Workflow**: Enforces valid state transitions and dependency tracking +97: +98: ### 3.4 HITL Tools (`hitl_tools.rs`, `hitl_content_tools.rs`) +99: **Purpose**: Human-agent interaction for validation and refinement +100: - **ReviewAndEditFileTool**: Binary workflow (pass/edit) for file review with external editor integration +101: - **ReviewWithFeedbackFileTool**: Ternary workflow (pass/edit/feedback) enabling agent regeneration with human comments +102: - **ReviewAndEditContentTool**: Content-level review for generated artifacts (PRD, Design docs) +103: - **ReviewWithFeedbackContentTool**: Feedback-driven content refinement +104: +105: ### 3.5 Validation Tools (`validation_tools.rs`) +106: **Purpose**: Data integrity and consistency verification +107: - **CheckDataFormatTool**: JSON Schema validation for structured data files +108: - **CheckFeatureCoverageTool**: Bidirectional coverage analysis between features and requirements +109: - **CheckTaskDependenciesTool**: Circular dependency detection using Depth-First Search (DFS) algorithm +110: +111: ### 3.6 Deployment Tools (`deployment_tools.rs`) +112: **Purpose**: Safe promotion of workspace artifacts to project root +113: - **CopyWorkspaceToProjectTool**: Two-phase deployment strategy: +114: - **Phase 1**: Orphaned file cleanup (removes files not in workspace, with protected paths: `.git/`, `config.toml`, `README.md`) +115: - **Phase 2**: Extension-filtered copy (whitelist: `.html`, `.css`, `.js`, `.ts`, `.tsx`, `.json`, `.md`, images, fonts) +116: +117: ### 3.7 Memory Tools (`memory_tools.rs`) +118: **Purpose**: Knowledge persistence and retrieval across iterations +119: - **QueryMemoryTool**: Fuzzy keyword search across three scopes (project, iteration, smart-merged) +120: - **SaveInsightTool**: Capture iteration insights with categorization +121: - **SaveIssueTool**: Record technical debt and known issues +122: - **SaveLearningTool**: Document architectural learnings +123: - **PromoteToDecisionTool**: Elevate insights to architectural decisions +124: - **PromoteToPatternTool**: Elevate insights to reusable design patterns +125: +126: ### 3.8 PM Tools (`pm_tools.rs`) +127: **Purpose**: Project Manager Agent operational tools, supporting post-delivery user interaction +128: - **PMGotoStageTool**: Allows user to return to previous stages (idea, prd, design, plan, coding) for re-execution +129: - **PMCreateIterationTool**: Create new iteration for handling new requirements or modifications +130: - **PMRespondTool**: Send text response to user +131: - **PMSaveDecisionTool**: Save user decisions to project memory +132: +133: > **Note**: PM tools are only available when iteration status is `Completed` (after Delivery stage), used for post-delivery project maintenance and requirement change scenarios. +134: +135: ### 3.9 Test & Lint Tools (`test_lint_tools.rs`) +136: **Purpose**: Code quality verification and testing automation +137: - **ExecuteShellCommandTool**: Run shell commands for testing and linting with timeout control +138: +139: ### 3.10 Control Tools (`control_tools.rs`) +140: **Purpose**: Pipeline flow control and stage management +141: - **GotoStageTool**: Control flow for jumping to specific pipeline stages +142: +143: ### 3.11 Artifact Tools (`artifact_tools.rs`) +144: **Purpose**: Artifact loading and management +145: - **LoadArtifactSummaryTool**: Load summaries of generated artifacts +146: - **LoadArtifactTool**: Load full artifact content +147: +148: ### 3.12 Load Artifacts Tools (`load_artifacts.rs`) +149: **Purpose**: Document and artifact loading utilities +150: - **LoadDocumentSummaryTool**: Load compressed stage summaries (idea, PRD, design, plan) +151: - **LoadBaseKnowledgeTool**: Load historical knowledge from base iterations +152: +153: ### 3.13 Goto Stage Tool (`goto_stage_tool.rs`) +154: **Purpose**: Stage navigation for PM Agent +155: - **PMGotoStageTool**: Navigate to specific stages for re-execution +156: +157: --- +158: ### 3.14 MCP Tools (`mcp_tools.rs`) +159: **Purpose**: Integration with external Model Context Protocol (MCP) servers to extend agent capabilities with remote tool sets +160: - **McpManager**: Manages connections to configured MCP servers, handles toolset aggregation +161: - **McpServerConfig**: Configuration for remote MCP server endpoints and timeouts +162: - **Connection Lifecycle**: Asynchronous initialization at application startup, global toolset storage, automatic injection into all agents via `add_mcp_toolsets_to_builder` +163: - **Supported Servers**: +164: - **Tavily**: Web search and AI-powered research (requires API key) +165: - **DeepWiki**: Code documentation and knowledge base queries (enable flag) +166: +167: > **Note**: MCP toolsets are not directly invocable as individual tools; instead, their toolsets are merged into the agent's tool set during agent construction, making remote tools appear as native tools to the agent. +168: +169: +170: ## 4. Core Implementation Patterns +171: +172: ### 4.1 ADK Tool Trait Contract +173: All tools implement the standardized `Tool` trait from `adk_core`, ensuring interoperability with the agent framework: +174: +175: ```rust +176: #[async_trait] +177: pub trait Tool: Send + Sync { +178: fn name(&self) -> &str; +179: fn description(&self) -> &str; +180: fn parameters_schema(&self) -> Value; // JSON Schema for validation +181: +182: async fn execute( +183: &self, +184: ctx: Arc, +185: args: Value +186: ) -> adk_core::Result; +187: } +188: ``` +189: +190: **Key Implementation Details:** +191: - **Async Execution**: All tool operations are non-blocking using `async_trait` and Tokio runtime +192: - **JSON Schema Validation**: Parameters validated against declared schema before execution +193: - **Structured Output**: Results returned as `serde_json::Value` with consistent error wrapping +194: - **Context Injection**: `ToolContext` provides access to iteration ID and global state +195: +196: ### 4.2 Workspace Containment Architecture +197: **Security Model**: All file-system-touching tools enforce **workspace containment** to prevent directory traversal attacks and ensure project isolation. +198: +199: **Validation Pipeline:** +200: 1. **UNC Normalization**: Strips Windows UNC prefixes (`\\?\`) using `strip_unc_prefix()` for cross-platform path consistency +201: 2. **Traversal Detection**: Rejects paths containing `..` components (directory escape attempts) +202: 3. **Absolute Path Blocking**: Blocks absolute paths (e.g., `/etc/passwd`, `C:\Windows`) forcing relative path usage +203: 4. **Boundary Verification**: Resolves canonical paths and verifies they reside within the iteration workspace +204: +205: ```rust +206: pub fn validate_path_security_within_workspace( +207: path: &Path, +208: workspace: &Path +209: ) -> Result { +210: let normalized = strip_unc_prefix(path); +211: if normalized.components().any(|c| c == Component::ParentDir) { +212: return Err(ToolError::SecurityViolation("Path traversal detected".into())); +213: } +214: // Additional absolute path and boundary checks... +215: } +216: ``` +217: +218: ### 4.3 Global Backend Integration +219: HITL tools require access to the user interface layer (CLI or GUI) through the `InteractiveBackend` trait. The domain uses a **global singleton pattern** (thread-safe via `Lazy`) to provide backend access without polluting tool signatures: +220: +221: ```rust +222: static INTERACTION_BACKEND: Lazy>>> = +223: Lazy::new(|| Mutex::new(None)); +224: +225: pub fn set_interaction_backend(backend: Arc) { +226: *INTERACTION_BACKEND.lock().unwrap() = Some(backend); +227: } +228: ``` +229: +230: --- +231: +232: ## 5. Tool Execution Lifecycle +233: +234: The following sequence illustrates the complete execution flow when an AI agent invokes a tool: +235: +236: ```mermaid +237: sequenceDiagram +238: participant Agent as AI Agent (ADK) +239: participant Tool as Concrete Tool +240: participant Schema as JSON Schema Validator +241: participant Security as Path Security Layer +242: participant Store as IterationStore +243: participant FS as File System +244: +245: Agent->>Tool: execute(ctx, args) +246: +247: Tool->>Schema: Validate args against parameters_schema() +248: alt Invalid Parameters +249: Schema-->>Tool: ValidationError +250: Tool-->>Agent: Err(InvalidArguments) +251: end +252: +253: alt File Operation Required +254: Tool->>Store: get_iteration_id() +255: Store-->>Tool: iteration_id: String +256: +257: Tool->>Store: workspace_path(iteration_id) +258: Store-->>Tool: workspace_dir: PathBuf +259: +260: Tool->>Security: validate_path_security_within_workspace(target_path, workspace_dir) +261: Security->>Security: strip_unc_prefix() +262: Security->>Security: detect_traversal_attempts() +263: Security->>Security: verify_workspace_boundary() +264: +265: alt Security Violation +266: Security-->>Tool: SecurityError +267: Tool-->>Agent: Err(AccessDenied) +268: end +269: +270: Security-->>Tool: canonical_safe_path +271: Tool->>FS: Perform I/O operation +272: FS-->>Tool: Result +273: end +274: +275: alt Data Operation +276: Tool->>FS: load_requirements() / load_feature_list() +277: FS-->>Tool: StructuredData +278: Tool->>Tool: Business logic (CRUD, validation) +279: Tool->>FS: save_modified_data() +280: end +281: +282: Tool->>Tool: Format JSON response +283: Tool-->>Agent: Ok(serde_json::Value) +284: ``` +285: +286: --- +287: +288: ## 6. Domain-Specific Behaviors +289: +290: ### 6.1 Data Tools: ID Generation and Workflows +291: Data tools implement **structured ID generation** using the `generate_id()` utility: +292: - Requirements: `REQ-001`, `REQ-002` +293: - Features: `FEAT-001` +294: - Components: `COMP-001` +295: - Tasks: `TASK-001` +296: +297: **Status Workflow Enforcement:** +298: Tasks enforce a finite state machine: `pending` → `in_progress` → (`completed` | `blocked`). Invalid transitions are rejected with descriptive errors. +299: +300: ### 6.2 Validation Tools: Dependency Analysis +301: The `CheckTaskDependenciesTool` implements **cycle detection** using DFS: +302: 1. Builds adjacency list from task dependencies +303: 2. Tracks visited nodes and recursion stack +304: 3. Detects back-edges indicating circular dependencies +305: 4. Returns detailed error messages identifying the cyclic chain +306: +307: ### 6.3 Deployment Tools: Safety Protocols +308: The deployment tool implements **destructive operation safety**: +309: - **Protected Paths**: Hardcoded exclusion of `.git/`, `README.md`, and `config.toml` from deletion +310: - **Extension Whitelisting**: Only copies file types explicitly approved for deployment (source maps, config files, and documentation excluded unless whitelisted) +311: - **Two-Phase Commit**: Cleanup phase executes only after successful workspace validation; copy phase executes only after successful cleanup +312: +313: --- +314: +315: ## 7. Integration with Adjacent Domains +316: +317: ### 7.1 Persistence Domain +318: Tools interact with persistence through **Store abstractions**: +319: - **IterationStore**: Resolves workspace paths and manages iteration metadata +320: - **MemoryStore**: Handles knowledge persistence for Memory Tools +321: - **Direct FS**: File Tools bypass stores for raw I/O but maintain path validation +322: +323: ### 7.2 Memory Domain +324: Memory Tools act as the **write path** for the Memory Domain: +325: - **Query Operations**: Delegate to `MemoryStore` with `MemoryQuery` filters (scope, category, keyword) +326: - **Write Operations**: Append to `IterationKnowledge` structures with automatic timestamping (`chrono`) +327: - **Promotion Logic**: Elevate insights to decisions/patterns using domain logic in `memory_tools.rs` +328: +329: ### 7.3 Interaction Domain +330: HITL Tools consume the `InteractiveBackend` trait: +331: - **CLI Implementation**: Terminal-based prompts using `dialoguer` with colored output (`colored` crate) +332: - **GUI Implementation**: Tauri event emission (`input_request` events) with oneshot channel response handling +333: - **Timeout Handling**: GUI backend implements 3000-second timeout for human responses to prevent indefinite blocking +334: +335: --- +336: +337: ## 8. Error Handling and Observability +338: +339: ### 8.1 Error Taxonomy +340: Tools return structured errors categorized as: +341: - **SecurityError**: Path violations, workspace escapes +342: - **ValidationError**: Schema mismatches, invalid state transitions +343: - **IOError**: Filesystem failures, permission denied +344: - **TimeoutError**: Command execution exceeding limits +345: - **UserCancelledError**: HITL interaction aborted by user +346: +347: ### 8.2 Logging and Transparency +348: All tool executions emit structured console output via `println!` macros for user visibility: +349: ```rust +350: println!("[Tool] Writing file: {}", path.display()); +351: println!("[Tool] Command output: {}", stdout); +352: ``` +353: +354: This ensures transparent agent behavior in both CLI and GUI contexts (GUI captures stdout via Tauri process streams). +355: +356: --- +357: +358: ## 9. Extension Guidelines +359: +360: To implement a new tool within this domain: +361: +362: 1. **Implement the Tool Trait**: Define `name()`, `description()`, and `parameters_schema()` +363: 2. **Security Validation**: For filesystem operations, always use `validate_path_security_within_workspace()` +364: 3. **Context Access**: Retrieve `iteration_id` from global storage for workspace-relative operations +365: 4. **Error Mapping**: Convert internal errors to `adk_core::Result` with descriptive messages +366: 5. **Registration**: Add to the tool registry in `tools/mod.rs` for agent discovery +367: +368: **Example Pattern:** +369: ```rust +370: pub struct MyNewTool; +371: +372: #[async_trait] +373: impl Tool for MyNewTool { +374: fn name(&self) -> &str { "my_new_tool" } +375: +376: async fn execute(&self, ctx: Arc, args: Value) -> Result { +377: // Parameter extraction +378: // Security validation (if FS involved) +379: // Business logic execution +380: // JSON result formatting +381: } +382: } +383: ``` +384: +385: --- +386: +387: ## 10. Performance Considerations +388: +389: - **Async Concurrency**: Tool execution is non-blocking, allowing the pipeline to handle multiple concurrent tool calls (subject to LLM rate limits) +390: - **File Truncation**: Large file handling uses `ReadFileTruncatedTool` to prevent token overflow in LLM contexts +391: - **Call Limits**: `ReadFileWithLimitTool` implements call-count tracking to prevent recursive file reading attacks +392: - **Caching**: IterationStore paths are resolved once per execution context and reused across tool calls +393: +394: --- +395: +396: **End of Documentation** +397: --- +398: +399: ## 11. PM Tools (Project Manager Agent Tools) +400: +401: **Purpose**: Support post-delivery interactions through the Project Manager Agent, enabling users to continue working with completed projects. +402: +403: **Available Tools**: +404: +405: ### 11.1 PMGotoStageTool +406: Restarts the pipeline from a specific stage. Used when users want to fix bugs, modify requirements, or make changes after delivery. +407: +408: **Parameters**: +409: - `stage` (required): Target stage - one of `idea`, `prd`, `design`, `plan`, `coding` +410: - `reason` (required): Why the restart is needed (user's request summary) +411: +412: **Use Cases**: +413: - User: "Fix the login bug" → Jump to `coding` stage +414: - User: "Change the database schema" → Jump to `design` stage +415: - User: "Add a new requirement" → Jump to `prd` stage +416: +417: ### 11.2 PMCreateIterationTool +418: Creates a new iteration for implementing new features or major changes. +419: +420: **Parameters**: +421: - `title` (required): Title for the new iteration +422: - `description` (required): Detailed description of what to implement +423: - `inheritance` (optional): Inheritance mode - `none`, `full`, or `partial` (default: `partial`) +424: +425: **Inheritance Modes**: +426: | Mode | Description | +427: |------|-------------| +428: | `none` | Fresh start, no inheritance from current iteration | +429: | `full` | Copy all artifacts and code from current iteration | +430: | `partial` | Copy code only, regenerate documentation (recommended) | +431: +432: ### 11.3 PMRespondTool +433: Responds to the user without taking any action. Used for answering questions or asking for clarification. +434: +435: **Parameters**: +436: - `response` (required): The response message to the user +437: - `ask_clarification` (optional): Whether this response is asking for clarification +438: +439: ### 11.4 PMSaveDecisionTool +440: Saves important decisions or preferences to project memory for future reference. +441: +442: **Parameters**: +443: - `title` (required): Title of the decision +444: - `context` (required): Background context of the decision +445: - `decision` (required): The actual decision made +446: - `impact` (optional): Impact analysis of this decision +447: +448: > **Note**: PM Tools are only available when iteration status is `Completed` (after Delivery stage). They enable a conversational interface for post-delivery project maintenance and requirement changes. +449: +450: --- +451: +452: ## 12. ACP Integration +453: +454: The Tools Domain includes support for external coding agents via the Agent Communication Protocol (ACP): +455: +456: **Configuration** (in `config.toml`): +457: ```toml +458: [coding_agent] +459: enabled = true +460: agent_type = "opencode" # opencode, iflow, codex, gemini, claude +461: command = "bun" +462: args = ["x", "opencode-ai", "acp"] +463: transport = "stdio" # stdio or websocket +464: workspace_path = "" # optional +465: ``` +466: +467: **Supported External Agents**: +468: - **OpenCode**: OpenCode AI agent via `bun x opencode-ai acp` +469: - **iFlow**: iFlow CLI agent +470: - **Codex**: OpenAI Codex CLI +471: - **Gemini CLI**: Google's Gemini command-line agent +472: - **Claude CLI**: Anthropic's Claude command-line agent +473: +474: **Integration Flow**: +475: 1. During Coding stage, if external agent is enabled, the system spawns the configured agent process +476: 2. Communication occurs via stdio or WebSocket using ACP protocol +477: 3. The external agent receives the coding task with full project context +478: 4. Streaming responses are forwarded to the GUI for real-time display +479: 5. On completion, results are integrated back into the pipeline +480: +481: This allows users to leverage specialized coding agents while maintaining the full 7-stage workflow orchestration. +```` + +### crates/cowork-cli/src/commands/config.rs (3 lines) + +``` +1: execute +2: ⋮---- +3: () +``` + +### crates/cowork-cli/src/commands/continue_cmd.rs (3 lines) + +``` +1: execute +2: ⋮---- +3: (iteration_id: Option) +``` + +### crates/cowork-cli/src/commands/delete.rs (3 lines) + +``` +1: execute +2: ⋮---- +3: (iteration_id: String) +``` + +### crates/cowork-cli/src/commands/import.rs (33 lines) + +``` +1: execute +2: ⋮---- +3: ( +4: path: String, +5: name: Option, +6: generate_idea: bool, +7: generate_prd: bool, +8: generate_design: bool, +9: generate_plan: bool, +10: template_only: bool, +11: ) +12: ⋮---- +13: run_llm_agent_import +14: ⋮---- +15: ( +16: project_path: &PathBuf, +17: artifacts_dir: &PathBuf, +18: generate_idea: bool, +19: generate_prd: bool, +20: generate_design: bool, +21: generate_plan: bool, +22: ) +23: ⋮---- +24: generate_template_artifacts +25: ⋮---- +26: ( +27: analysis: &cowork_core::importer::ProjectAnalysis, +28: artifacts_dir: &PathBuf, +29: generate_idea: bool, +30: generate_prd: bool, +31: generate_design: bool, +32: generate_plan: bool, +33: ) +``` + +### crates/cowork-cli/src/commands/init.rs (3 lines) + +``` +1: execute +2: ⋮---- +3: (name: Option) +``` + +### crates/cowork-cli/src/commands/iter.rs (8 lines) + +``` +1: execute +2: ⋮---- +3: ( +4: title: String, +5: description: Option, +6: base: Option, +7: inherit: String, +8: ) +``` + +### crates/cowork-cli/src/commands/knowledge.rs (3 lines) + +``` +1: execute +2: ⋮---- +3: (iteration_id: String) +``` + +### crates/cowork-cli/src/commands/list.rs (3 lines) + +``` +1: execute +2: ⋮---- +3: (all: bool) +``` + +### crates/cowork-cli/src/commands/mod.rs (22 lines) + +``` +1: pub mod iter; +2: pub mod list; +3: pub mod show; +4: pub mod continue_cmd; +5: pub mod init; +6: pub mod status; +7: pub mod delete; +8: pub mod knowledge; +9: pub mod import; +10: pub mod config; +11: +12: +13: pub use iter::execute as iter; +14: pub use list::execute as list; +15: pub use show::execute as show; +16: pub use continue_cmd::execute as continue_iteration; +17: pub use init::execute as init; +18: pub use status::execute as status; +19: pub use delete::execute as delete; +20: pub use knowledge::execute as regenerate_knowledge; +21: pub use import::execute as import; +22: pub use config::execute as config; +``` + +### crates/cowork-cli/src/commands/show.rs (3 lines) + +``` +1: execute +2: ⋮---- +3: (iteration_id: Option) +``` + +### crates/cowork-cli/src/commands/status.rs (3 lines) + +``` +1: execute +2: ⋮---- +3: () +``` + +### crates/cowork-cli/src/main.rs (110 lines) + +``` +1: Cli +2: ⋮---- +3: { +4: #[command(subcommand)] +5: command: Commands, +6: +7: +8: #[arg(short, long, global = true)] +9: verbose: bool, +10: } +11: ⋮---- +12: Commands +13: ⋮---- +14: { +15: +16: Iter { +17: +18: title: String, +19: +20: +21: #[arg(short, long)] +22: description: Option, +23: +24: +25: #[arg(short, long)] +26: base: Option, +27: +28: +29: #[arg(short, long, default_value = "full")] +30: inherit: String, +31: }, +32: +33: +34: List { +35: +36: #[arg(short, long)] +37: all: bool, +38: }, +39: +40: +41: Show { +42: +43: iteration_id: Option, +44: }, +45: +46: +47: Continue { +48: +49: iteration_id: Option, +50: }, +51: +52: +53: Init { +54: +55: #[arg(short, long)] +56: name: Option, +57: }, +58: +59: +60: Status, +61: +62: +63: Delete { +64: +65: iteration_id: String, +66: }, +67: +68: +69: RegenerateKnowledge { +70: +71: iteration_id: String, +72: }, +73: +74: +75: Import { +76: +77: path: String, +78: +79: +80: #[arg(short, long)] +81: name: Option, +82: +83: +84: #[arg(long, default_value = "true")] +85: idea: bool, +86: +87: +88: #[arg(long, default_value = "true")] +89: prd: bool, +90: +91: +92: #[arg(long, default_value = "true")] +93: design: bool, +94: +95: +96: #[arg(long, default_value = "true")] +97: plan: bool, +98: +99: +100: #[arg(long)] +101: template_only: bool, +102: }, +103: +104: +105: Config, +106: } +107: ⋮---- +108: main +109: ⋮---- +110: () +``` + +### crates/cowork-cli/src/utils.rs (3 lines) + +``` +1: truncate +2: ⋮---- +3: (s: &str, max_len: usize) +``` + +### crates/cowork-core/src/agents/external_coding_agent.rs (210 lines) + +``` +1: ExternalCodingAgent +2: ⋮---- +3: { +4: +5: config: CodingAgentConfig, +6: +7: workspace: PathBuf, +8: +9: ready: bool, +10: +11: iteration: Option, +12: } +13: ⋮---- +14: StreamingTask +15: ⋮---- +16: { +17: +18: pub messages: mpsc::UnboundedReceiver, +19: +20: pub result: std::pin::Pin>> + Send>>, +21: } +22: ⋮---- +23: ExternalCodingAgent +24: ⋮---- +25: { +26: +27: pub async fn new(workspace: &PathBuf) -> Result { +28: Self::new_with_iteration(workspace, None).await +29: } +30: +31: +32: pub async fn new_with_iteration(workspace: &PathBuf, iteration: Option) -> Result { +33: eprintln!("DEBUG: ExternalCodingAgent::new_with_iteration called with workspace: {}", workspace.display()); +34: if let Some(ref iter) = iteration { +35: eprintln!("DEBUG: Iteration context: id={}, base_id={:?}, inheritance={:?}", +36: iter.id, iter.base_iteration_id, iter.inheritance); +37: } +38: +39: let config = load_config() +40: .context("Failed to load config")?; +41: +42: eprintln!("DEBUG: Config loaded, coding_agent.enabled: {}", config.coding_agent.enabled); +43: +44: if !config.coding_agent.enabled { +45: anyhow::bail!("External coding agent is not enabled in config"); +46: } +47: +48: Ok(Self { +49: config: config.coding_agent, +50: workspace: workspace.clone(), +51: ready: false, +52: iteration, +53: }) +54: } +55: +56: +57: pub fn is_enabled() -> Result { +58: let config = load_config() +59: .context("Failed to load config")?; +60: Ok(config.coding_agent.enabled) +61: } +62: +63: +64: +65: +66: +67: pub fn execute_task_stream( +68: self, +69: task_description: &str, +70: project_context: &str, +71: ) -> StreamingTask { +72: let prompt = self.build_prompt(task_description, project_context); +73: +74: +75: let (messages, result) = crate::acp::execute_with_external_agent( +76: self.config, +77: self.workspace, +78: prompt, +79: ); +80: +81: StreamingTask { +82: messages, +83: result: Box::pin(result), +84: } +85: } +86: +87: +88: +89: +90: +91: +92: +93: +94: pub async fn execute_task( +95: &mut self, +96: task_description: &str, +97: project_context: &str, +98: ) -> Result { +99: +100: let prompt = self.build_prompt(task_description, project_context); +101: +102: tracing::info!("Executing coding task via external agent: {}", &prompt[..prompt.len().min(200)]); +103: +104: +105: let mut client = AcpClient::from_config(&self.config, &self.workspace).await?; +106: +107: +108: match client.execute_task(&prompt).await { +109: Ok(result) => { +110: self.ready = true; +111: Ok(AcpTaskResult::new(result, true)) +112: } +113: Err(e) => { +114: tracing::error!("External agent execution failed: {}", e); +115: Ok(AcpTaskResult::error(e.to_string())) +116: } +117: } +118: } +119: +120: +121: fn build_prompt(&self, task_description: &str, project_context: &str) -> String { +122: let mut prompt = String::new(); +123: +124: +125: let is_evolution = self.iteration.as_ref() +126: .map(|i| i.base_iteration_id.is_some()) +127: .unwrap_or(false); +128: +129: let inheritance_mode = self.iteration.as_ref() +130: .map(|i| i.inheritance) +131: .unwrap_or(InheritanceMode::None); +132: +133: if is_evolution { +134: prompt.push_str("═══════════════════════════════════════════════════════════════\n"); +135: prompt.push_str("🚨 CRITICAL: THIS IS AN EVOLUTION ITERATION\n"); +136: prompt.push_str("═══════════════════════════════════════════════════════════════\n"); +137: prompt.push_str("\n"); +138: prompt.push_str("⚠️ DO NOT DELETE EXISTING CODE! ⚠️\n"); +139: prompt.push_str("\n"); +140: prompt.push_str("This iteration builds upon an EXISTING project.\n"); +141: prompt.push_str("The workspace directory already contains code from a previous iteration.\n"); +142: prompt.push_str("\n"); +143: +144: match inheritance_mode { +145: InheritanceMode::Partial => { +146: prompt.push_str("📋 INHERITANCE MODE: PARTIAL\n"); +147: prompt.push_str("- Code files from the base iteration have been copied to the workspace\n"); +148: prompt.push_str("- Artifacts (PRD, Design, Plan) are regenerated fresh\n"); +149: prompt.push_str("- You MUST preserve existing code and add new features incrementally\n"); +150: } +151: InheritanceMode::Full => { +152: prompt.push_str("📋 INHERITANCE MODE: FULL\n"); +153: prompt.push_str("- All files (code + artifacts) from base iteration are available\n"); +154: prompt.push_str("- You MUST preserve existing code and only make necessary modifications\n"); +155: } +156: InheritanceMode::None => {} +157: } +158: +159: prompt.push_str("\n"); +160: prompt.push_str("🎯 YOUR TASK:\n"); +161: prompt.push_str("1. FIRST, list the existing files in the workspace to understand the current structure\n"); +162: prompt.push_str("2. Read relevant existing code files before making changes\n"); +163: prompt.push_str("3. Add new features incrementally - DO NOT rewrite from scratch\n"); +164: prompt.push_str("4. Only modify files that need changes for the new features\n"); +165: prompt.push_str("5. Preserve all existing functionality\n"); +166: prompt.push_str("\n"); +167: prompt.push_str("═══════════════════════════════════════════════════════════════\n\n"); +168: } +169: +170: prompt.push_str(&format!( +171: r#"# Coding Task +172: +173: ## Project Context +174: {} +175: +176: ## Base Instruction +177: {} +178: +179: ## Task Description +180: {} +181: +182: ## Working Directory +183: {} +184: +185: ## Requirements +186: 1. Implement the task according to the description +187: 2. Write clean, maintainable code +188: 3. Ensure the code compiles and runs correctly +189: 4. If you encounter any issues, report them clearly +190: +191: Please start implementing the task."#, +192: project_context, +193: CODING_ACTOR_INSTRUCTION, +194: task_description, +195: self.workspace.display() +196: )); +197: +198: prompt +199: } +200: +201: +202: pub fn is_ready(&self) -> bool { +203: self.ready +204: } +205: +206: +207: pub fn agent_type(&self) -> &str { +208: &self.config.agent_type +209: } +210: } +``` + +### crates/cowork-core/src/agents/legacy_project_analyzer.rs (18 lines) + +``` +1: create_legacy_project_analyzer +2: ⋮---- +3: (model: Arc) +4: ⋮---- +5: create_legacy_project_analyzer_with_id +6: ⋮---- +7: ( +8: model: Arc, +9: iteration_id: String, +10: ) +11: ⋮---- +12: create_legacy_project_analyzer_with_context +13: ⋮---- +14: ( +15: model: Arc, +16: project_path: String, +17: artifact_options: String, +18: ) +``` + +### crates/cowork-core/src/config_definition/agent_definition.rs (175 lines) + +``` +1: AgentType +2: ⋮---- +3: { +4: +5: #[default] +6: Simple, +7: +8: Loop { +9: +10: max_iterations: Option, +11: }, +12: } +13: ⋮---- +14: ModelConfig +15: ⋮---- +16: { +17: +18: pub model_id: Option, +19: +20: pub temperature: Option, +21: +22: pub max_tokens: Option, +23: +24: pub top_p: Option, +25: } +26: ⋮---- +27: ModelConfig +28: ⋮---- +29: { +30: fn default() -> Self { +31: Self { +32: model_id: None, +33: temperature: Some(0.7), +34: max_tokens: None, +35: top_p: None, +36: } +37: } +38: } +39: ⋮---- +40: ToolReference +41: ⋮---- +42: { +43: +44: pub tool_id: String, +45: +46: pub config: Option>, +47: } +48: ⋮---- +49: AgentDefinition +50: ⋮---- +51: { +52: +53: pub id: String, +54: +55: pub name: String, +56: +57: pub description: Option, +58: +59: pub version: Option, +60: +61: +62: #[serde(default)] +63: pub agent_type: AgentType, +64: +65: +66: +67: +68: +69: +70: pub instruction: String, +71: +72: +73: #[serde(default)] +74: pub tools: Vec, +75: +76: +77: #[serde(default)] +78: pub model: ModelConfig, +79: +80: +81: #[serde(default)] +82: pub include_contents: IncludeContentsMode, +83: +84: +85: #[serde(default)] +86: pub tags: Vec, +87: +88: +89: #[serde(default)] +90: pub metadata: HashMap, +91: } +92: ⋮---- +93: IncludeContentsMode +94: ⋮---- +95: { +96: +97: #[default] +98: None, +99: +100: All, +101: +102: Selected(Vec), +103: } +104: ⋮---- +105: ActorCriticDefinition +106: ⋮---- +107: { +108: +109: pub actor: AgentDefinition, +110: +111: pub critic: AgentDefinition, +112: +113: pub max_iterations: Option, +114: } +115: ⋮---- +116: AgentDefinition +117: ⋮---- +118: { +119: +120: pub fn new(id: impl Into, name: impl Into, instruction: impl Into) -> Self { +121: Self { +122: id: id.into(), +123: name: name.into(), +124: description: None, +125: version: None, +126: agent_type: AgentType::Simple, +127: instruction: instruction.into(), +128: tools: Vec::new(), +129: model: ModelConfig::default(), +130: include_contents: IncludeContentsMode::None, +131: tags: Vec::new(), +132: metadata: HashMap::new(), +133: } +134: } +135: +136: +137: pub fn with_tool(mut self, tool_id: impl Into) -> Self { +138: self.tools.push(ToolReference { +139: tool_id: tool_id.into(), +140: config: None, +141: }); +142: self +143: } +144: +145: +146: pub fn with_tool_config(mut self, tool_id: impl Into, config: HashMap) -> Self { +147: self.tools.push(ToolReference { +148: tool_id: tool_id.into(), +149: config: Some(config), +150: }); +151: self +152: } +153: +154: +155: pub fn with_tag(mut self, tag: impl Into) -> Self { +156: self.tags.push(tag.into()); +157: self +158: } +159: +160: +161: pub fn as_loop(mut self, max_iterations: Option) -> Self { +162: self.agent_type = AgentType::Loop { max_iterations }; +163: self +164: } +165: +166: +167: pub fn with_model(mut self, model: ModelConfig) -> Self { +168: self.model = model; +169: self +170: } +171: } +172: ⋮---- +173: test_agent_definition_serialization +174: ⋮---- +175: () +``` + +### crates/cowork-core/src/config_definition/default_configs/agents/built-in/check_agent.json (48 lines) + +``` +1: { +2: "id": "check_agent", +3: "name": "Check Agent", +4: "description": "Performs quality validation on the implemented code", +5: "version": "1.0.0", +6: "agent_type": "simple", +7: "instruction": "builtin://check_agent", +8: "tools": [ +9: { +10: "tool_id": "get_implementation_plan" +11: }, +12: { +13: "tool_id": "read_file" +14: }, +15: { +16: "tool_id": "list_files" +17: }, +18: { +19: "tool_id": "run_command" +20: }, +21: { +22: "tool_id": "check_tests" +23: }, +24: { +25: "tool_id": "check_lint" +26: }, +27: { +28: "tool_id": "check_data_format" +29: }, +30: { +31: "tool_id": "query_memory" +32: }, +33: { +34: "tool_id": "save_insight" +35: }, +36: { +37: "tool_id": "save_issue" +38: }, +39: { +40: "tool_id": "save_check_report" +41: } +42: ], +43: "model": { +44: "temperature": 0.3 +45: }, +46: "include_contents": "none", +47: "tags": ["built-in", "quality", "validation"] +48: } +``` + +### crates/cowork-core/src/config_definition/default_configs/agents/built-in/delivery_agent.json (36 lines) + +``` +1: { +2: "id": "delivery_agent", +3: "name": "Delivery Agent", +4: "description": "Generates delivery report and deploys the project", +5: "version": "1.0.0", +6: "agent_type": "simple", +7: "instruction": "builtin://delivery_agent", +8: "tools": [ +9: { +10: "tool_id": "copy_workspace_to_project" +11: }, +12: { +13: "tool_id": "read_file" +14: }, +15: { +16: "tool_id": "list_files" +17: }, +18: { +19: "tool_id": "read_file_truncated" +20: }, +21: { +22: "tool_id": "save_delivery_report" +23: }, +24: { +25: "tool_id": "query_memory" +26: }, +27: { +28: "tool_id": "save_insight" +29: } +30: ], +31: "model": { +32: "temperature": 0.5 +33: }, +34: "include_contents": "none", +35: "tags": ["built-in", "delivery", "deployment"] +36: } +``` + +### crates/cowork-core/src/config_definition/default_configs/agents/built-in/design_actor.json (54 lines) + +``` +1: { +2: "id": "design_actor", +3: "name": "Design Actor", +4: "description": "Generates system design specification based on requirements", +5: "version": "1.0.0", +6: "agent_type": "simple", +7: "instruction": "builtin://design_actor", +8: "tools": [ +9: { +10: "tool_id": "load_feedback_history" +11: }, +12: { +13: "tool_id": "get_requirements" +14: }, +15: { +16: "tool_id": "get_design" +17: }, +18: { +19: "tool_id": "load_prd_doc" +20: }, +21: { +22: "tool_id": "create_design_component" +23: }, +24: { +25: "tool_id": "save_design_doc" +26: }, +27: { +28: "tool_id": "read_file" +29: }, +30: { +31: "tool_id": "list_files" +32: }, +33: { +34: "tool_id": "read_file_truncated" +35: }, +36: { +37: "tool_id": "query_memory" +38: }, +39: { +40: "tool_id": "save_insight" +41: }, +42: { +43: "tool_id": "save_issue" +44: }, +45: { +46: "tool_id": "save_learning" +47: } +48: ], +49: "model": { +50: "temperature": 0.7 +51: }, +52: "include_contents": "none", +53: "tags": ["built-in", "design", "actor"] +54: } +``` + +### crates/cowork-core/src/config_definition/default_configs/agents/built-in/design_critic.json (45 lines) + +``` +1: { +2: "id": "design_critic", +3: "name": "Design Critic", +4: "description": "Reviews and validates the system design specification", +5: "version": "1.0.0", +6: "agent_type": "simple", +7: "instruction": "builtin://design_critic", +8: "tools": [ +9: { +10: "tool_id": "get_requirements" +11: }, +12: { +13: "tool_id": "get_design" +14: }, +15: { +16: "tool_id": "load_design_doc" +17: }, +18: { +19: "tool_id": "check_feature_coverage" +20: }, +21: { +22: "tool_id": "provide_feedback" +23: }, +24: { +25: "tool_id": "read_file" +26: }, +27: { +28: "tool_id": "list_files" +29: }, +30: { +31: "tool_id": "read_file_truncated" +32: }, +33: { +34: "tool_id": "query_memory" +35: }, +36: { +37: "tool_id": "save_issue" +38: } +39: ], +40: "model": { +41: "temperature": 0.3 +42: }, +43: "include_contents": "none", +44: "tags": ["built-in", "design", "critic"] +45: } +``` + +### crates/cowork-core/src/config_definition/default_configs/agents/built-in/idea_agent.json (30 lines) + +``` +1: { +2: "id": "idea_agent", +3: "name": "Idea Agent", +4: "description": "Captures and structures the initial project idea into a formal idea document", +5: "version": "1.0.0", +6: "agent_type": "simple", +7: "instruction": "builtin://idea_agent", +8: "tools": [ +9: { +10: "tool_id": "save_idea" +11: }, +12: { +13: "tool_id": "read_file" +14: }, +15: { +16: "tool_id": "list_files" +17: }, +18: { +19: "tool_id": "query_memory" +20: }, +21: { +22: "tool_id": "save_insight" +23: } +24: ], +25: "model": { +26: "temperature": 0.7 +27: }, +28: "include_contents": "none", +29: "tags": ["built-in", "ideation"] +30: } +``` + +### crates/cowork-core/src/config_definition/default_configs/agents/built-in/plan_actor.json (60 lines) + +``` +1: { +2: "id": "plan_actor", +3: "name": "Plan Actor", +4: "description": "Generates implementation plan with detailed tasks", +5: "version": "1.0.0", +6: "agent_type": "simple", +7: "instruction": "builtin://plan_actor", +8: "tools": [ +9: { +10: "tool_id": "load_feedback_history" +11: }, +12: { +13: "tool_id": "get_requirements" +14: }, +15: { +16: "tool_id": "get_design" +17: }, +18: { +19: "tool_id": "load_design_doc" +20: }, +21: { +22: "tool_id": "create_task" +23: }, +24: { +25: "tool_id": "add_component" +26: }, +27: { +28: "tool_id": "get_implementation_plan" +29: }, +30: { +31: "tool_id": "save_plan_doc" +32: }, +33: { +34: "tool_id": "read_file" +35: }, +36: { +37: "tool_id": "list_files" +38: }, +39: { +40: "tool_id": "read_file_truncated" +41: }, +42: { +43: "tool_id": "query_memory" +44: }, +45: { +46: "tool_id": "save_insight" +47: }, +48: { +49: "tool_id": "save_issue" +50: }, +51: { +52: "tool_id": "save_learning" +53: } +54: ], +55: "model": { +56: "temperature": 0.7 +57: }, +58: "include_contents": "none", +59: "tags": ["built-in", "planning", "actor"] +60: } +``` + +### crates/cowork-core/src/config_definition/default_configs/agents/built-in/plan_critic.json (42 lines) + +``` +1: { +2: "id": "plan_critic", +3: "name": "Plan Critic", +4: "description": "Reviews and validates the implementation plan", +5: "version": "1.0.0", +6: "agent_type": "simple", +7: "instruction": "builtin://plan_critic", +8: "tools": [ +9: { +10: "tool_id": "get_implementation_plan" +11: }, +12: { +13: "tool_id": "load_plan_doc" +14: }, +15: { +16: "tool_id": "check_task_dependencies" +17: }, +18: { +19: "tool_id": "provide_feedback" +20: }, +21: { +22: "tool_id": "read_file" +23: }, +24: { +25: "tool_id": "list_files" +26: }, +27: { +28: "tool_id": "read_file_truncated" +29: }, +30: { +31: "tool_id": "query_memory" +32: }, +33: { +34: "tool_id": "save_issue" +35: } +36: ], +37: "model": { +38: "temperature": 0.3 +39: }, +40: "include_contents": "none", +41: "tags": ["built-in", "planning", "critic"] +42: } +``` + +### crates/cowork-core/src/config_definition/default_configs/agents/built-in/pm_agent.json (39 lines) + +``` +1: { +2: "id": "pm_agent", +3: "name": "Project Manager Agent", +4: "description": "Handles post-delivery user interactions and project management", +5: "version": "1.0.0", +6: "agent_type": "simple", +7: "instruction": "builtin://project_manager", +8: "tools": [ +9: { +10: "tool_id": "pm_goto_stage" +11: }, +12: { +13: "tool_id": "pm_create_iteration" +14: }, +15: { +16: "tool_id": "pm_respond" +17: }, +18: { +19: "tool_id": "pm_save_decision" +20: }, +21: { +22: "tool_id": "read_file" +23: }, +24: { +25: "tool_id": "list_files" +26: }, +27: { +28: "tool_id": "read_file_truncated" +29: }, +30: { +31: "tool_id": "query_memory" +32: } +33: ], +34: "model": { +35: "temperature": 0.7 +36: }, +37: "include_contents": "none", +38: "tags": ["built-in", "management", "pm"] +39: } +``` + +### crates/cowork-core/src/config_definition/default_configs/agents/built-in/prd_actor.json (57 lines) + +``` +1: { +2: "id": "prd_actor", +3: "name": "PRD Actor", +4: "description": "Generates Product Requirements Document based on the idea document", +5: "version": "1.0.0", +6: "agent_type": "simple", +7: "instruction": "builtin://prd_actor", +8: "tools": [ +9: { +10: "tool_id": "load_feedback_history" +11: }, +12: { +13: "tool_id": "load_idea" +14: }, +15: { +16: "tool_id": "create_requirement" +17: }, +18: { +19: "tool_id": "add_feature" +20: }, +21: { +22: "tool_id": "update_requirement" +23: }, +24: { +25: "tool_id": "update_feature" +26: }, +27: { +28: "tool_id": "delete_requirement" +29: }, +30: { +31: "tool_id": "get_requirements" +32: }, +33: { +34: "tool_id": "save_prd_doc" +35: }, +36: { +37: "tool_id": "read_file" +38: }, +39: { +40: "tool_id": "list_files" +41: }, +42: { +43: "tool_id": "read_file_truncated" +44: }, +45: { +46: "tool_id": "query_memory" +47: }, +48: { +49: "tool_id": "save_insight" +50: } +51: ], +52: "model": { +53: "temperature": 0.7 +54: }, +55: "include_contents": "none", +56: "tags": ["built-in", "requirements", "actor"] +57: } +``` + +### crates/cowork-core/src/config_definition/default_configs/agents/built-in/prd_critic.json (39 lines) + +``` +1: { +2: "id": "prd_critic", +3: "name": "PRD Critic", +4: "description": "Reviews and validates the Product Requirements Document", +5: "version": "1.0.0", +6: "agent_type": "simple", +7: "instruction": "builtin://prd_critic", +8: "tools": [ +9: { +10: "tool_id": "get_requirements" +11: }, +12: { +13: "tool_id": "load_idea" +14: }, +15: { +16: "tool_id": "provide_feedback" +17: }, +18: { +19: "tool_id": "read_file" +20: }, +21: { +22: "tool_id": "list_files" +23: }, +24: { +25: "tool_id": "read_file_truncated" +26: }, +27: { +28: "tool_id": "query_memory" +29: }, +30: { +31: "tool_id": "save_issue" +32: } +33: ], +34: "model": { +35: "temperature": 0.3 +36: }, +37: "include_contents": "none", +38: "tags": ["built-in", "requirements", "critic"] +39: } +``` + +### crates/cowork-core/src/config_definition/default_configs/agents/built-in/summary_agent.json (27 lines) + +``` +1: { +2: "id": "summary_agent", +3: "name": "Summary Agent", +4: "description": "Generates document summaries for each stage", +5: "version": "1.0.0", +6: "agent_type": "simple", +7: "instruction": "builtin://summary_agent", +8: "tools": [ +9: { +10: "tool_id": "read_file" +11: }, +12: { +13: "tool_id": "write_file" +14: }, +15: { +16: "tool_id": "list_files" +17: }, +18: { +19: "tool_id": "read_file_truncated" +20: } +21: ], +22: "model": { +23: "temperature": 0.5 +24: }, +25: "include_contents": "none", +26: "tags": ["built-in", "documentation", "summary"] +27: } +``` + +### crates/cowork-core/src/config_definition/default_configs/flows/default.json (65 lines) + +``` +1: { +2: "id": "default", +3: "name": "Default Development Flow", +4: "description": "Standard 7-stage software development workflow matching V2 behavior", +5: "version": "1.0.0", +6: "stages": [ +7: { +8: "stage_id": "idea", +9: "alias": null, +10: "overrides": {}, +11: "on_success": "prd" +12: }, +13: { +14: "stage_id": "prd", +15: "alias": null, +16: "overrides": {}, +17: "on_success": "design" +18: }, +19: { +20: "stage_id": "design", +21: "alias": null, +22: "overrides": {}, +23: "on_success": "plan" +24: }, +25: { +26: "stage_id": "plan", +27: "alias": null, +28: "overrides": {}, +29: "on_success": "coding" +30: }, +31: { +32: "stage_id": "coding", +33: "alias": null, +34: "overrides": {}, +35: "on_success": "check" +36: }, +37: { +38: "stage_id": "check", +39: "alias": null, +40: "overrides": {}, +41: "on_success": "delivery" +42: }, +43: { +44: "stage_id": "delivery", +45: "alias": null, +46: "overrides": {} +47: } +48: ], +49: "start_stage": "idea", +50: "global_hooks": [], +51: "config": { +52: "stop_on_failure": true, +53: "save_state_on_interrupt": true, +54: "memory_scope": "merged", +55: "inheritance": { +56: "default_mode": "partial", +57: "stage_mapping": { +58: "none": "idea", +59: "partial": "idea", +60: "full": "idea" +61: } +62: } +63: }, +64: "tags": ["built-in", "default"] +65: } +``` + +### crates/cowork-core/src/config_definition/validator.rs (70 lines) + +``` +1: ValidationResult +2: ⋮---- +3: { +4: pub is_valid: bool, +5: pub errors: Vec, +6: pub warnings: Vec, +7: } +8: ⋮---- +9: ValidationResult +10: ⋮---- +11: { +12: pub fn new() -> Self { +13: Self { +14: is_valid: true, +15: errors: Vec::new(), +16: warnings: Vec::new(), +17: } +18: } +19: +20: pub fn error(&mut self, message: impl Into) { +21: self.errors.push(message.into()); +22: self.is_valid = false; +23: } +24: +25: pub fn warning(&mut self, message: impl Into) { +26: self.warnings.push(message.into()); +27: } +28: +29: pub fn merge(&mut self, other: ValidationResult) { +30: if !other.is_valid { +31: self.is_valid = false; +32: } +33: self.errors.extend(other.errors); +34: self.warnings.extend(other.warnings); +35: } +36: } +37: ⋮---- +38: ConfigValidator +39: ⋮---- +40: { +41: registry: &'a ConfigRegistry, +42: } +43: ⋮---- +44: new +45: ⋮---- +46: (registry: &'a ConfigRegistry) +47: ⋮---- +48: validate_all +49: ⋮---- +50: (&self) +51: ⋮---- +52: validate_agent +53: ⋮---- +54: (&self, agent: &AgentDefinition) +55: ⋮---- +56: validate_stage +57: ⋮---- +58: (&self, stage: &StageDefinition) +59: ⋮---- +60: validate_flow +61: ⋮---- +62: (&self, flow: &FlowDefinition) +63: ⋮---- +64: validate_integration +65: ⋮---- +66: (&self, integration: &IntegrationDefinition) +67: ⋮---- +68: test_validate_agent +69: ⋮---- +70: () +``` + +### crates/cowork-core/src/data/mod.rs (2 lines) + +``` +1: pub mod models; +2: pub use models::*; +``` + +### crates/cowork-core/src/importer/artifact_generator.rs (109 lines) + +``` +1: GeneratedArtifact +2: ⋮---- +3: { +4: pub filename: String, +5: pub content: String, +6: pub artifact_type: ArtifactType, +7: } +8: ⋮---- +9: ArtifactType +10: ⋮---- +11: { +12: Idea, +13: PRD, +14: Design, +15: Plan, +16: } +17: ⋮---- +18: ArtifactGenerationOptions +19: ⋮---- +20: { +21: pub generate_idea: bool, +22: pub generate_prd: bool, +23: pub generate_design: bool, +24: pub generate_plan: bool, +25: pub scan_readme: bool, +26: pub scan_docs: bool, +27: } +28: ⋮---- +29: ArtifactGenerationOptions +30: ⋮---- +31: { +32: fn default() -> Self { +33: Self { +34: generate_idea: true, +35: generate_prd: true, +36: generate_design: true, +37: generate_plan: true, +38: scan_readme: true, +39: scan_docs: true, +40: } +41: } +42: } +43: ⋮---- +44: generate_artifacts +45: ⋮---- +46: ( +47: analysis: &ProjectAnalysis, +48: options: &ArtifactGenerationOptions, +49: ) +50: ⋮---- +51: generate_idea +52: ⋮---- +53: (analysis: &ProjectAnalysis) +54: ⋮---- +55: read_dependencies_info +56: ⋮---- +57: (project_path: &str) +58: ⋮---- +59: generate_prd +60: ⋮---- +61: (analysis: &ProjectAnalysis) +62: ⋮---- +63: generate_design +64: ⋮---- +65: (analysis: &ProjectAnalysis) +66: ⋮---- +67: generate_plan +68: ⋮---- +69: (analysis: &ProjectAnalysis) +70: ⋮---- +71: format_features +72: ⋮---- +73: (analysis: &ProjectAnalysis) +74: ⋮---- +75: format_tech_stack +76: ⋮---- +77: (analysis: &ProjectAnalysis) +78: ⋮---- +79: format_tech_stack_table +80: ⋮---- +81: (analysis: &ProjectAnalysis) +82: ⋮---- +83: format_entry_points +84: ⋮---- +85: (analysis: &ProjectAnalysis) +86: ⋮---- +87: format_entry_points_detail +88: ⋮---- +89: (analysis: &ProjectAnalysis) +90: ⋮---- +91: format_architecture +92: ⋮---- +93: (analysis: &ProjectAnalysis) +94: ⋮---- +95: format_feature_list +96: ⋮---- +97: (analysis: &ProjectAnalysis) +98: ⋮---- +99: format_config_files +100: ⋮---- +101: (root_files: &[String]) +102: ⋮---- +103: format_directory_tree +104: ⋮---- +105: (directories: &[crate::importer::project_analyzer::DirectoryInfo]) +106: ⋮---- +107: format_layers +108: ⋮---- +109: (layers: &[String]) +``` + +### crates/cowork-core/src/importer/import_config.rs (209 lines) + +``` +1: ImportConfig +2: ⋮---- +3: { +4: +5: pub project_path: PathBuf, +6: +7: pub artifact_options: ArtifactOptions, +8: +9: pub initialize_cowork_dir: bool, +10: +11: pub project_name: Option, +12: } +13: ⋮---- +14: ImportConfig +15: ⋮---- +16: { +17: +18: pub fn new(project_path: PathBuf) -> Self { +19: Self { +20: project_path, +21: artifact_options: ArtifactOptions::default(), +22: initialize_cowork_dir: true, +23: project_name: None, +24: } +25: } +26: +27: +28: pub fn with_artifact_options(mut self, options: ArtifactOptions) -> Self { +29: self.artifact_options = options; +30: self +31: } +32: +33: +34: pub fn skip_cowork_init(mut self) -> Self { +35: self.initialize_cowork_dir = false; +36: self +37: } +38: +39: +40: pub fn with_project_name(mut self, name: impl Into) -> Self { +41: self.project_name = Some(name.into()); +42: self +43: } +44: } +45: ⋮---- +46: ArtifactOptions +47: ⋮---- +48: { +49: +50: pub generate_idea: bool, +51: +52: pub generate_prd: bool, +53: +54: pub generate_design: bool, +55: +56: pub generate_plan: bool, +57: +58: pub scan_readme: bool, +59: +60: pub scan_docs: bool, +61: +62: pub scan_comments: bool, +63: } +64: ⋮---- +65: ArtifactOptions +66: ⋮---- +67: { +68: fn default() -> Self { +69: Self { +70: generate_idea: true, +71: generate_prd: true, +72: generate_design: true, +73: generate_plan: true, +74: scan_readme: true, +75: scan_docs: true, +76: scan_comments: false, +77: } +78: } +79: } +80: ⋮---- +81: ImportResult +82: ⋮---- +83: { +84: +85: pub success: bool, +86: +87: pub project_name: String, +88: +89: pub project_path: PathBuf, +90: +91: pub generated_artifacts: Vec, +92: +93: pub detected_technologies: Vec, +94: +95: pub error: Option, +96: } +97: ⋮---- +98: ImportResult +99: ⋮---- +100: { +101: +102: pub fn success( +103: project_name: String, +104: project_path: PathBuf, +105: artifacts: Vec, +106: technologies: Vec, +107: ) -> Self { +108: Self { +109: success: true, +110: project_name, +111: project_path, +112: generated_artifacts: artifacts, +113: detected_technologies: technologies, +114: error: None, +115: } +116: } +117: +118: +119: pub fn failure(message: impl Into) -> Self { +120: Self { +121: success: false, +122: project_name: String::new(), +123: project_path: PathBuf::new(), +124: generated_artifacts: Vec::new(), +125: detected_technologies: Vec::new(), +126: error: Some(message.into()), +127: } +128: } +129: } +130: ⋮---- +131: ImportPreview +132: ⋮---- +133: { +134: +135: pub name: String, +136: +137: pub path: PathBuf, +138: +139: pub technologies: Vec, +140: +141: pub files_to_scan: Vec, +142: +143: pub artifacts_to_generate: Vec, +144: +145: pub warnings: Vec, +146: } +147: ⋮---- +148: ImportPreview +149: ⋮---- +150: { +151: +152: pub fn from_path(path: &PathBuf) -> Self { +153: use crate::importer::project_analyzer::analyze_project; +154: +155: let name = path +156: .file_name() +157: .and_then(|n| n.to_str()) +158: .unwrap_or("Unknown") +159: .to_string(); +160: +161: match analyze_project(path) { +162: Ok(analysis) => { +163: let technologies = analysis.technologies +164: .iter() +165: .map(|t| t.name.clone()) +166: .collect(); +167: +168: let files_to_scan: Vec = std::fs::read_dir(path) +169: .map(|entries| { +170: entries +171: .filter_map(|e| e.ok()) +172: .map(|e| e.file_name().to_string_lossy().to_string()) +173: .collect() +174: }) +175: .unwrap_or_default(); +176: +177: let artifacts_to_generate = vec![ +178: "idea.md".to_string(), +179: "prd.md".to_string(), +180: "design.md".to_string(), +181: "plan.md".to_string(), +182: ]; +183: +184: let warnings = if analysis.documentation.is_empty() { +185: vec!["No README.md found - limited documentation available".to_string()] +186: } else { +187: Vec::new() +188: }; +189: +190: Self { +191: name, +192: path: path.clone(), +193: technologies, +194: files_to_scan, +195: artifacts_to_generate, +196: warnings, +197: } +198: } +199: Err(e) => Self { +200: name, +201: path: path.clone(), +202: technologies: Vec::new(), +203: files_to_scan: Vec::new(), +204: artifacts_to_generate: Vec::new(), +205: warnings: vec![format!("Analysis error: {}", e)], +206: }, +207: } +208: } +209: } +``` + +### crates/cowork-core/src/importer/mod.rs (7 lines) + +``` +1: pub mod project_analyzer; +2: pub mod artifact_generator; +3: pub mod import_config; +4: +5: pub use project_analyzer::*; +6: pub use artifact_generator::*; +7: pub use import_config::*; +``` + +### crates/cowork-core/src/importer/project_analyzer.rs (131 lines) + +``` +1: DetectedTechnology +2: ⋮---- +3: { +4: pub name: String, +5: pub version: Option, +6: pub category: TechCategory, +7: } +8: ⋮---- +9: TechCategory +10: ⋮---- +11: { +12: Frontend, +13: Backend, +14: Database, +15: BuildTool, +16: Test, +17: Lint, +18: Container, +19: Other, +20: } +21: ⋮---- +22: ProjectAnalysis +23: ⋮---- +24: { +25: +26: pub project_path: String, +27: +28: pub name: String, +29: +30: pub technologies: Vec, +31: +32: pub structure: ProjectStructure, +33: +34: pub documentation: Vec, +35: +36: pub architecture_hints: ArchitectureHints, +37: } +38: ⋮---- +39: ProjectStructure +40: ⋮---- +41: { +42: pub root_files: Vec, +43: pub directories: Vec, +44: pub entry_points: Vec, +45: } +46: ⋮---- +47: DirectoryInfo +48: ⋮---- +49: { +50: pub path: String, +51: pub purpose: Option, +52: pub file_count: usize, +53: } +54: ⋮---- +55: EntryPoint +56: ⋮---- +57: { +58: pub path: String, +59: pub file_type: EntryPointType, +60: pub description: String, +61: } +62: ⋮---- +63: EntryPointType +64: ⋮---- +65: { +66: Frontend, +67: Backend, +68: CLI, +69: Config, +70: } +71: ⋮---- +72: DocumentationFile +73: ⋮---- +74: { +75: pub path: String, +76: pub title: Option, +77: pub file_type: DocType, +78: } +79: ⋮---- +80: DocType +81: ⋮---- +82: { +83: Readme, +84: Changelog, +85: Contributing, +86: License, +87: API, +88: Architecture, +89: Other, +90: } +91: ⋮---- +92: ArchitectureHints +93: ⋮---- +94: { +95: pub pattern: Option, +96: pub layers: Vec, +97: pub is_monolithic: bool, +98: pub is_distributed: bool, +99: } +100: ⋮---- +101: analyze_project +102: ⋮---- +103: (project_path: &Path) +104: ⋮---- +105: derive_project_name +106: ⋮---- +107: (project_path: &Path) +108: ⋮---- +109: detect_technologies +110: ⋮---- +111: (project_path: &Path) +112: ⋮---- +113: analyze_structure +114: ⋮---- +115: (project_path: &Path) +116: ⋮---- +117: infer_directory_purpose +118: ⋮---- +119: (name: &str) +120: ⋮---- +121: find_documentation +122: ⋮---- +123: (project_path: &Path) +124: ⋮---- +125: extract_title_from_markdown +126: ⋮---- +127: (path: &Path) +128: ⋮---- +129: infer_architecture +130: ⋮---- +131: (structure: &ProjectStructure, technologies: &[DetectedTechnology]) +``` + +### crates/cowork-core/src/instructions/delivery.rs (130 lines) + +```` +1: pub const DELIVERY_AGENT_INSTRUCTION: &str = r##" +2: # ⚠️ CRITICAL RULE - READ FIRST ⚠️ +3: **This is the FINAL agent. But ONLY generate report if project is TRULY complete!** +4: +5: # ⚠️ CRITICAL REQUIREMENT - YOU MUST CALL save_delivery_report() +6: **This is the MOST IMPORTANT requirement:** +7: - You MUST call `save_delivery_report(content)` at the END of your work +8: - Without calling this tool, the Delivery stage CANNOT complete +9: - Your work will be LOST if you don't save the document +10: - Example: save_delivery_report(content) with your complete delivery report +11: +12: # Your Role +13: You are Delivery Agent. Create a comprehensive delivery report **ONLY IF** the project is actually done. +14: +15: # CRITICAL Pre-Check (DO THIS FIRST!) +16: **Before generating the report, you MUST verify the project is complete:** +17: +18: 1. Call `get_plan()` to check task status +19: 2. **CRITICAL**: Use `list_files(".")` to verify actual code files exist +20: 3. **If NO code files exist** (e.g., no index.html, no .js files): +21: - DO NOT generate delivery report +22: - Instead, output: "❌ Project incomplete: No code files found. Tasks marked complete but implementation missing." +23: - STOP immediately +24: +25: # Workflow (Only if pre-check passes) +26: 1. Load project data: +27: - `get_requirements()` +28: - `get_design()` +29: - `get_plan()` +30: - `load_feedback_history()` +31: 2. Generate a markdown report summarizing everything +32: 3. **MANDATORY**: Save it: +33: - `save_delivery_report(content=)` - The system will NOT auto-save! +34: 4. **CRITICAL**: Deploy to project root: +35: - `copy_workspace_to_project(confirm=true)` - This copies all source files from workspace to project root +36: 5. **DONE** - This is the last stage, pipeline completes automatically +37: +38: # Tools +39: - get_requirements() +40: - get_design() +41: - get_plan() +42: - load_feedback_history() +43: - load_idea() ← Load idea document +44: - load_prd_doc() ← Load PRD document +45: - load_design_doc() ← Load design document +46: - list_files(path) ← **USE THIS to verify files exist!** +47: - save_delivery_report(content) +48: - copy_workspace_to_project(confirm=true) ← **Deploy files to project root** +49: +50: # Report Structure (Markdown) +51: ```markdown +52: # Delivery Report +53: +54: ## Project Summary +55: [Brief overview] +56: +57: ## Requirements (X total) +58: - REQ-001: [Title] ✅ +59: - REQ-002: [Title] ✅ +60: +61: ## Features (X total) +62: - FEAT-001: [Name] - [Description] ✅ +63: - FEAT-002: [Name] - [Description] ✅ +64: +65: ## Architecture +66: - Component 1: [Tech stack] +67: - Component 2: [Tech stack] +68: +69: ## Tasks Completed +70: Total: X tasks +71: Status: All completed +72: +73: ## Project Files Generated +74: - index.html +75: - style.css +76: - script.js +77: [List all generated files] +78: +79: ## Quality Checks +80: - Build: ✅ Passing +81: - Tests: ✅ Passed (or N/A for pure frontend) +82: - Lint: ✅ Clean (or N/A for pure frontend) +83: +84: ## Deployment +85: ✅ All files deployed to project root directory +86: +87: ## Getting Started +88: \`\`\`bash +89: # How to run the project +90: \`\`\` +91: +92: ## Next Steps +93: [What user should do next] +94: ``` +95: +96: # Example - Complete Project +97: ``` +98: 1. get_plan() +99: 2. # Returns: 49 tasks, all completed +100: 3. list_files(".") +101: 4. # Returns: ["index.html", "style.css", "script.js", "data.json"] ✅ +102: 5. # Files exist! Proceed with report +103: 6. get_requirements() +104: 7. get_design() +105: 8. # Generate report markdown +106: 9. save_delivery_report(report_content) +107: 10. copy_workspace_to_project(confirm=true) +108: 11. # Returns: {"status": "success", "copied_files": [...]} +109: # Done! +110: ``` +111: +112: # Example - Incomplete Project (STOP!) +113: ``` +114: 1. get_plan() +115: 2. # Returns: 49 tasks, all marked "completed" +116: 3. list_files(".") +117: 4. # Returns: [] or only [".cowork-v2", ".config.toml"] ← NO code files! +118: 5. # STOP! Do NOT generate report! +119: 6. Output: "❌ Project incomplete: Tasks marked complete but no code files found (index.html, etc.). Cannot generate delivery report." +120: # STOP here, do not call save_delivery_report() or copy_workspace_to_project() +121: ``` +122: +123: **REMEMBER: +124: 1. ALWAYS check for actual files BEFORE generating report +125: 2. If files don't exist, DO NOT generate delivery_report.md +126: 3. Task status alone is NOT enough - verify actual implementation! +127: 4. After saving report, MUST call copy_workspace_to_project(confirm=true) to deploy files +128: 5. This copies code from .cowork-v2/iterations/{ITERATION_ID}/workspace to project root +129: 6. Only source code files are copied (html, css, js, etc.), not config or hidden files +130: "##; +```` + +### crates/cowork-core/src/instructions/design.rs (400 lines) + +```` +1: pub const DESIGN_ACTOR_INSTRUCTION: &str = r##" +2: # Your Role +3: You are Design Actor. Create or update system architecture components. +4: +5: # ⚠️ CRITICAL REQUIREMENT - YOU MUST CALL save_design_doc() +6: **This is the MOST IMPORTANT requirement:** +7: - You MUST call `save_design_doc(content)` at the END of your work +8: - Without calling this tool, the Design stage CANNOT complete +9: - Your work will be LOST if you don't save the document +10: - Example: save_design_doc(content) with your complete design markdown +11: +12: # CRITICAL PRINCIPLE: SIMPLICITY & MINIMAL ARCHITECTURE +13: **The architecture MUST be simple and use minimal components:** +14: - ✅ Use simplest tech stack that works (prefer built-in/standard tools) +15: - ✅ Minimize number of components (2-4 is ideal, 6 is maximum) +16: - ✅ Use monolithic architecture when appropriate (don't over-split) +17: - ✅ **Prefer standard libraries over external dependencies** (e.g., native fetch over axios, built-in sqlite over complex ORMs) +18: - ✅ **Choose batteries-included frameworks** (e.g., Django over Flask+extensions, Next.js over React+manual routing) +19: - ✅ **Avoid framework soup** - stick to ONE main framework per layer +20: - ❌ NO microservices unless explicitly required +21: - ❌ NO complex caching layers (Redis/Memcached) unless critical +22: - ❌ NO message queues unless explicitly required +23: - ❌ NO service mesh, API gateway unless explicitly required +24: - ❌ NO separate monitoring/logging infrastructure +25: - ❌ NO ORM frameworks when simple SQL queries suffice +26: - ❌ NO state management libraries (Redux/MobX) for simple apps - use component state +27: +28: # ⚠️ CRITICAL: FULLSTACK PROXY CONFIGURATION (FOR FULLSTACK PROJECTS ONLY) +29: **For Fullstack projects (Frontend + Backend), you MUST configure API proxy:** +30: +31: ## When Backend API is Needed +32: If the project has a backend API component, the frontend MUST be able to communicate with it during development. +33: +34: ## Vite Proxy Configuration (REQUIRED for Fullstack) +35: When using Vite for frontend, add proxy configuration in `vite.config.js`: +36: +37: ```javascript +38: // vite.config.js for Fullstack projects +39: import { defineConfig } from 'vite' +40: import react from '@vitejs/plugin-react' +41: +42: export default defineConfig({ +43: plugins: [react()], +44: server: { +45: port: 5173, +46: proxy: { +47: '/api': { +48: target: 'http://localhost:3000', // Backend server address +49: changeOrigin: true, +50: secure: false, +51: } +52: } +53: } +54: }) +55: ``` +56: +57: ## Key Points for Fullstack Design: +58: - Frontend dev server runs on port 5173 (Vite default) +59: - Backend API server runs on port 3000 (or as specified) +60: - All `/api/*` requests from frontend are proxied to backend +61: - This enables seamless frontend-backend communication during development +62: - **Document the proxy configuration in the Design Document** +63: +64: ## Example Design Document Section for Fullstack: +65: ```markdown +66: ## API Communication +67: - Frontend dev server: http://localhost:5173 +68: - Backend API server: http://localhost:3000 +69: - Proxy: /api/* → http://localhost:3000/api/* +70: +71: ## vite.config.js +72: \`\`\`javascript +73: import { defineConfig } from 'vite' +74: import react from '@vitejs/plugin-react' +75: +76: export default defineConfig({ +77: plugins: [react()], +78: server: { +79: port: 5173, +80: proxy: { +81: '/api': { +82: target: 'http://localhost:3000', +83: changeOrigin: true, +84: } +85: } +86: } +87: }) +88: \`\`\` +89: ``` +90: +91: # ⚠️ CRITICAL: PROJECT STRUCTURE & FILES (NEW - MANDATORY) +92: **You MUST design a COMPLETE and RUNNABLE project structure with ALL necessary files:** +93: +94: ## For Frontend/Web Projects (React/Vue/Vanilla JS): +95: **MANDATORY FILES - Must be explicitly mentioned in design document:** +96: - ✅ `package.json` - with ALL dependencies, scripts (dev, build, preview) +97: - ✅ Entry HTML file - `index.html` with proper structure +98: - ✅ Build tool config - `vite.config.js` (for Vite) or equivalent +99: - ✅ Main entry script - `src/main.js` or `src/index.js` +100: - ✅ TypeScript config - `tsconfig.json` (if using TypeScript) +101: - ✅ `.gitignore` - to exclude node_modules, dist, etc. +102: +103: ## For Node.js Backend/Tool Projects: +104: **MANDATORY FILES - Must be explicitly mentioned in design document:** +105: - ✅ `package.json` - with dependencies, bin entry (for tools), start script +106: - ✅ Main entry - `src/index.js` or `index.js` +107: - ✅ `.gitignore` - to exclude node_modules +108: - ✅ Config files - if needed for the tool (e.g., `.eslintrc`, `tsconfig.json`) +109: +110: ## For Rust Projects: +111: **MANDATORY FILES - Must be explicitly mentioned in design document:** +112: - ✅ `Cargo.toml` - with all dependencies and [package] metadata +113: - ✅ `src/main.rs` (for binaries) or `src/lib.rs` (for libraries) +114: - ✅ `.gitignore` - to exclude target/, Cargo.lock (for libraries) +115: +116: ## For Python Projects: +117: **MANDATORY FILES - Must be explicitly mentioned in design document:** +118: - ✅ `requirements.txt` or `pyproject.toml` - with all dependencies +119: - ✅ Main entry - `main.py` or `src/__init__.py` +120: - ✅ `.gitignore` - to exclude __pycache__, venv, etc. +121: +122: **YOUR DESIGN DOCUMENT MUST INCLUDE A "Project Structure" SECTION:** +123: ```markdown +124: ## Project Structure +125: \`\`\` +126: project-root/ +127: ├── package.json # Dependencies: react, vite, etc. Scripts: dev, build +128: ├── index.html # Entry HTML with root div +129: ├── vite.config.js # Vite configuration +130: ├── .gitignore # Exclude node_modules, dist +131: ├── src/ +132: │ ├── main.jsx # React app entry point +133: │ ├── App.jsx # Main app component +134: │ └── components/ # UI components +135: \`\`\` +136: +137: **Key Files:** +138: - `package.json`: Contains react@18, vite@5, dev/build scripts +139: - `index.html`: Entry point with
+140: - `vite.config.js`: React plugin configuration +141: - `src/main.jsx`: ReactDOM.render setup +142: ``` +143: +144: # Workflow - TWO MODES +145: +146: ## Mode Detection (FIRST STEP) +147: 1. Call `load_feedback_history({"stage": "design"})` to check if this is a restart +148: 2. If feedback history exists and has entries → **UPDATE MODE** +149: 3. If no feedback history or empty → **NEW MODE** +150: +151: ## NEW MODE (全新生成) +152: +153: ### Step 1: Load Requirements (MANDATORY) +154: 1. Call `get_requirements()` to read all requirements and features +155: 2. **STOP** if requirements or features are empty - report error and exit +156: 3. Analyze requirements to plan 2-4 **SIMPLE** components (avoid over-splitting) +157: +158: ### Step 2: Create Formal Design (MANDATORY) +159: 4. For EACH component, **MUST** call `create_design_component(name, component_type, responsibilities, technology, related_features)` +160: 5. **CRITICAL**: Keep architecture SIMPLE and MINIMAL: +161: - Use 2-4 components maximum +162: - Prefer monolithic architecture +163: - Avoid microservices unless explicitly required +164: - Use simplest tech stack possible +165: +166: ### Step 3: Save Design Document (MANDATORY - INCLUDING PROJECT STRUCTURE) +167: 6. **CRITICAL**: Generate a complete Design Document markdown that MUST include: +168: - Architecture components (as usual) +169: - **"Project Structure" section** (NEW - MANDATORY): +170: - Complete directory tree with ALL files +171: - Explicit listing of package.json/Cargo.toml/requirements.txt +172: - Entry files (index.html, main.js, src/main.rs, etc.) +173: - Config files (vite.config.js, tsconfig.json, etc.) +174: - .gitignore file +175: - Brief description of each key file's purpose +176: - Example structure format (see above in "Project Structure" section) +177: 7. **MANDATORY**: Call `save_design_doc(content=)` to save the document - The system will NOT auto-save! +178: +179: ### Step 4: Verify (MANDATORY) +180: 8. Call `get_design()` to verify all components were created +181: 9. Confirm all components exist, then report success +182: +183: ## UPDATE MODE (增量更新 - 当 GotoStage 回退到此阶段时) +184: +185: ### Step 1: Analyze Feedback +186: 1. Call `load_feedback_history({"stage": "design"})` - 获取最近的反馈信息 +187: 2. Read feedback.details to understand what needs to change +188: +189: ### Step 2: Load Existing Design +190: 3. Call `get_design()` to read existing components +191: 4. Design document is saved automatically - no need to read it directly +192: +193: ### Step 3: Incremental Updates +194: 5. Analyze feedback and determine what to modify: +195: - Which components need to be updated? +196: - What technology changes are needed? +197: - What architectural adjustments are required? +198: +199: 6. Apply targeted updates: +200: - **IMPORTANT**: Components are immutable once created +201: - If feedback requires architectural changes, document them in the design document +202: - Update the design document to reflect the changes +203: - Use `save_design_doc()` to save the updated design +204: +205: ### Step 4: Document Changes +206: 7. Generate updated design document with: +207: - What changed and why (based on feedback) +208: - Impact on architecture +209: - Any technology stack changes +210: 8. **MANDATORY**: Call `save_design_doc(content=)` to save the document - The system will NOT auto-save! +211: +212: ### UPDATE MODE Example +213: +214: ``` +215: # 假设 feedback 显示: "API架构需要从REST改为GraphQL,需要认证中间件" +216: +217: 1. load_feedback_history() +218: → feedbacks: [{ +219: feedback_type: "QualityIssue", +220: severity: "Critical", +221: details: "API架构需要从REST改为GraphQL,需要认证中间件" +222: }] +223: +224: 2. get_design() +225: → Returns existing components +226: +227: 3. Design document is saved automatically - no need to read it directly +228: +229: 4. 分析需要修改的内容: +230: - Backend API 架构需要调整 +231: - 需要添加认证中间件组件 +232: - 组件接口需要更新 +233: +234: 5. 由于组件不可变,更新设计文档: +235: save_design_doc(content=" +236: # Updated Architecture Design +237: +238: ## Changes Based on Feedback +239: - API Architecture: REST → GraphQL +240: - New Component: Authentication Middleware +241: +242: ## Updated Components +243: [列出现有组件,说明它们如何适应新架构] +244: +245: ## Technology Stack Updates +246: - Backend: Express.js + Apollo Server (GraphQL) +247: - Authentication: JWT middleware +248: ") +249: +250: 6. save_design_doc(updated_content) +251: +252: 7. 完成!Critic 将审查更新后的设计 +253: ``` +254: +255: Note: Replace {ITERATION_ID} with the actual iteration ID provided in the prompt. +256: +257: # Tools Available +258: +259: ## Core Tools +260: - load_feedback_history() ← **START HERE - 检测是否是 UPDATE MODE** +261: - get_requirements() - Load requirements and features +262: - get_design() - Verify created components +263: - load_prd_doc() - Load PRD document +264: - review_with_feedback_content(title, content, prompt) - Get user feedback +265: +266: ## NEW MODE Tools +267: - review_with_feedback_content(title, content, prompt) - Get user feedback +268: - create_design_component(name, component_type, responsibilities, technology, related_features) - Create ONE component +269: - save_design_doc(content) - Save design markdown document +270: +271: ## UPDATE MODE Tools +272: - save_design_doc(content) - Save updated design document +273: - Components are immutable - document changes in design doc +274: +275: # Component Types +276: - frontend_component, backend_service, database, api_gateway, other +277: +278: # CRITICAL RULES +279: +280: ## For NEW MODE +281: 1. SIMPLICITY FIRST: Use minimal components, simplest tech stack +282: 2. STOP if get_requirements() returns empty arrays +283: 3. You MUST call review_with_feedback_content in Step 3 +284: 4. **MANDATORY**: If action="feedback", you MUST revise and call review again +285: 5. You MUST use the FINALIZED draft (after all feedback) in Step 4 +286: 6. You MUST call create_design_component for EACH component in the FINALIZED draft +287: 7. You MUST call save_design_doc in Step 5 with content matching Step 4 +288: 8. Do NOT over-engineer: No microservices, complex caching, message queues unless critical +289: 9. Do NOT skip steps or say "done" prematurely +290: +291: ## For UPDATE MODE +292: - Components are immutable once created - document changes in design document +293: - Focus on documenting architectural adjustments based on feedback +294: - Preserve existing component definitions, update their descriptions in design doc +295: - Be efficient - incremental documentation updates are faster than full regeneration +296: +297: **REMEMBER**: +298: - Always start with `load_feedback_history()` to detect mode +299: - In UPDATE MODE, components are immutable - document changes instead +300: - In NEW MODE, follow the full creation workflow +301: - **MANDATORY**: ALWAYS call `save_design_doc(content)` at the end - this is REQUIRED to complete the design stage +302: "##; +303: +304: pub const DESIGN_CRITIC_INSTRUCTION: &str = r#" +305: # Your Role +306: You are Design Critic. You MUST verify that Design Actor completed ALL required steps correctly. +307: +308: # CRITICAL: This is a GATEKEEPER role - you must BLOCK progress if Actor failed! +309: +310: # ⚠️ ANTI-LOOP PROTECTION (HIGHEST PRIORITY) +311: **CRITICAL**: To prevent infinite loops: +312: +313: 1. **Before calling provide_feedback**, ask yourself: +314: - "Have I already reported this EXACT issue before?" +315: +316: 2. **If you're about to give the SAME feedback twice**: +317: - ⛔ **STOP** - call `request_human_review()` instead +318: +319: 3. **Never call provide_feedback twice with same details** +320: +321: # SIMPLICITY CHECK - NEW PRIORITY +322: Before other checks, verify that architecture is SIMPLE and MINIMAL: +323: - ❌ REJECT if > 4 components (too complex) +324: - ❌ REJECT if you see: microservices, service mesh, complex caching, message queues (unless critical) +325: - ❌ REJECT if tech stack is overly complex (multiple frameworks, many dependencies) +326: - ❌ REJECT if using heavyweight ORMs when simple SQL suffices (e.g., TypeORM for basic CRUD) +327: - ❌ REJECT if using external HTTP libraries when native fetch/requests available +328: - ❌ REJECT if using state management (Redux/MobX) for simple apps +329: - ✅ APPROVE only SIMPLE, monolithic-friendly architectures with minimal dependencies +330: +331: ## Mandatory Checks (You MUST perform ALL of these) +332: +333: ### Check 1: Verify Design Data Exists +334: 1. Call `get_design()` to load all components +335: 2. **FAIL** if components array is empty +336: 3. Expected: 2-4 components (SIMPLE architecture) +337: 4. **FAIL** if > 4 components (over-engineered) +338: +339: ### Check 2: Verify SIMPLICITY (NEW - CRITICAL) +340: 5. For each component and overall architecture: +341: - ❌ Does it use microservices architecture? → REJECT (unless explicitly required) +342: - ❌ Does it include Redis/Memcached for caching? → REJECT (unless critical) +343: - ❌ Does it include message queue (RabbitMQ/Kafka)? → REJECT (unless critical) +344: - ❌ Does it have separate monitoring/logging infrastructure? → REJECT +345: - ❌ Does tech stack have many frameworks/libraries? → REJECT (keep it simple) +346: - ❌ Does it use heavyweight ORMs (e.g., TypeORM, Hibernate) for simple CRUD? → REJECT +347: - ❌ Does it use external HTTP clients when standard library available? → REJECT +348: - ❌ Does it use Redux/MobX for state management in simple apps? → REJECT +349: - ✅ Is it simple, monolithic, with minimal dependencies? → APPROVE +350: +351: 6. If architecture is too complex: +352: - **MUST** call `provide_feedback(stage="design", feedback_type="architecture_issue", severity="critical", details="Architecture is over-engineered: [list issues]", suggested_fix="Simplify to 2-4 components, use monolithic approach, prefer standard libaries, remove unnecessary dependencies")` +353: +354: ### Check 3: Verify Feature Coverage +355: 7. Call `check_feature_coverage()` to verify all features are mapped to components +356: 8. **FAIL** if any feature is not covered by at least one component +357: +358: ### Check 4: Verify Artifacts Exist +359: 9. Call `load_design_doc()` to check if Design markdown was saved +360: 10. **FAIL** if design.md does not exist or is empty +361: +362: ## Your Response +363: +364: ### If ALL checks pass: +365: - "✅ Design approved: [N] simple components covering all features, architecture follows minimal principles." +366: - Provide brief positive feedback on the architecture +367: +368: ### If any check FAILS: +369: - Call `provide_feedback(stage="design", feedback_type, severity, details, suggested_fix)` with specific issues +370: - Use appropriate severity: +371: - "critical" for empty data, missing artifacts, over-engineering +372: - "major" for feature coverage issues +373: - "minor" for documentation issues +374: +375: # Tools Available +376: - get_design() - Load design data +377: - check_feature_coverage() - Verify all features covered +378: - load_design_doc() - Verify design markdown document +379: - provide_feedback(stage="design", feedback_type, severity, details, suggested_fix) - Report issues +380: +381: # Anti-Loop Examples +382: +383: ## ✅ CORRECT - Different feedback each time +384: ``` +385: Iteration 1: provide_feedback(stage="design", feedback_type="quality_issue", severity="critical", details="Missing component for user auth", suggested_fix="...") +386: Iteration 2: provide_feedback(stage="design", feedback_type="quality_issue", severity="critical", details="Still missing: authentication mechanism", suggested_fix="...") +387: Iteration 3: request_human_review("Unable to resolve auth component issue") +388: ``` +389: +390: ## ❌ WRONG - Same feedback twice +391: ``` +392: Iteration 1: provide_feedback(stage="design", feedback_type="quality_issue", severity="critical", details="Missing component for user auth", suggested_fix="...") +393: Iteration 2: provide_feedback(stage="design", feedback_type="quality_issue", severity="critical", details="Missing component for user auth", suggested_fix="...") ← PROHIBITED! +394: ``` +395: +396: **REMEMBER**: +397: - SIMPLICITY is your top priority - reject over-engineered designs +398: - Prevent loops by varying feedback or calling request_human_review +399: - Be a GATEKEEPER - don't approve substandard work +400: "#; +```` + +### crates/cowork-core/src/instructions/idea.rs (95 lines) + +```` +1: pub const IDEA_AGENT_INSTRUCTION: &str = r##" +2: You are the Idea Agent, the first step in the Cowork Forge system. +3: +4: # Your Role +5: Your job is to understand the user's initial idea (already provided in the prompt), generate a structured idea document, and save it using the save_idea tool. +6: +7: # ⚠️ CRITICAL REQUIREMENT - YOU MUST CALL save_idea() +8: **This is the MOST IMPORTANT requirement:** +9: - You MUST call `save_idea(content)` at the END of your work +10: - Without calling this tool, the Idea stage CANNOT complete +11: - Your work will be LOST if you don't save the document +12: - Example: save_idea(content) with your complete idea markdown +13: +14: # CRITICAL: READ THE USER'S IDEA FROM THE PROMPT +15: The user's project idea is provided in the prompt under the section: +16: "USER'S PROJECT IDEA (ALREADY PROVIDED):" +17: +18: YOU MUST USE THIS EXACT IDEA from the prompt. DO NOT generate your own idea. DO NOT use any example ideas from your training data. +19: +20: # Task Workflow +21: 1. **Read** the user's project idea from the prompt (look for "USER'S PROJECT IDEA (ALREADY PROVIDED):" section) +22: 2. **Understand** the idea that the user provided +23: 3. **Generate** idea content based on THAT SPECIFIC IDEA (not your own examples) +24: 4. **Save** using `save_idea(content=)` - THIS IS MANDATORY +25: 5. **Done** - the idea is ready for the PRD team +26: +27: # CRITICAL: You MUST call save_idea() tool +28: - The system will NOT automatically save the idea document +29: - You MUST call `save_idea(content=)` to save it +30: - This ensures the idea is stored in the artifacts directory +31: - WITHOUT calling save_idea(), your work will be lost +32: +33: # Tool Usage +34: To save your idea, you MUST use the tool like this: +35: ``` +36: save_idea(content="# Project Idea +37: +38: ## Problem Statement +39: ...") +40: ``` +41: +42: # Output Format for Idea Content +43: +44: ```markdown +45: # Project Idea +46: +47: ## Problem Statement +48: [What problem does this solve? - based on the user's idea] +49: +50: ## Target Users +51: [Who will use this? - based on the user's idea] +52: +53: ## Key Goals +54: - Goal 1 [from the user's idea] +55: - Goal 2 [from the user's idea] +56: - ... +57: +58: ## Initial Thoughts +59: [Any additional context or constraints from user's idea] +60: +61: ## Technical Considerations +62: [Any technical requirements or preferences from user's idea] +63: +64: ## Next Steps +65: This idea will be passed to the PRD team for requirement analysis. +66: ``` +67: +68: # Tools Available +69: - `save_idea(content)` - This is the ONLY tool you need to use. Save the idea markdown document (MANDATORY for saving) +70: - `query_memory(query)` - Query iteration memory (optional) +71: - `save_insight(content, importance, stage)` - Save insights to memory (optional) +72: +73: # CRITICAL REMINDER +74: You MUST use the `save_idea` tool to save your idea. Without calling this tool, your work will be lost and the stage will fail. +75: The user's idea is already provided in the prompt - DO NOT ask for it again! +76: DO NOT use example ideas from your training data - use the EXACT idea provided in the prompt! +77: +78: # Example Workflow (DO NOT COPY THIS EXAMPLE - USE THE USER'S IDEA FROM THE PROMPT!) +79: +80: User's project idea (from prompt): "实现文章管理、分类标签、评论功能、用户认证" +81: +82: Step 1: Read the idea from the prompt: "实现文章管理、分类标签、评论功能、用户认证" +83: Step 2: Understand this is about a personal blog system +84: Step 3: Generate idea content based on THIS idea (not the math exam example above) +85: Step 4: Call `save_idea(content=)` to save it (MANDATORY!) +86: Step 5: Done - pass to next stage +87: +88: **Remember**: +89: - You MUST call `save_idea()` to save the idea +90: - The user's idea is already provided in the prompt - do NOT ask for it again! +91: - Do NOT engage in Q&A dialogue. Generate the idea, save it, done. +92: - The save_idea tool requires a single parameter: content (string) +93: - The save_idea tool is the ONLY required tool for this stage +94: - USE THE EXACT IDEA FROM THE PROMPT - NOT EXAMPLES FROM TRAINING DATA! +95: "##; +```` + +### crates/cowork-core/src/instructions/legacy_project_analyzer.rs (83 lines) + +```` +1: pub const LEGACY_PROJECT_ANALYZER_INSTRUCTION: &str = r##" +2: # Legacy Project Analyzer Agent +3: +4: You are the Legacy Project Analyzer, a specialized agent responsible for analyzing existing (legacy) projects and generating Artifacts. +5: +6: ## CRITICAL: You MUST Complete All Phases +7: +8: You are NOT done until you have called `save_artifact()` for EACH requested artifact. +9: DO NOT STOP after analysis - you MUST generate and save all artifacts. +10: +11: ## Workflow (Must Complete ALL Steps) +12: +13: ### Step 1: Analyze Project (Use Tools) +14: - Call `scan_project(project_path)` to get directory structure +15: - Call `detect_tech_stack(project_path)` to identify technologies +16: - Call `read_project_file(project_path, relative_path)` to read key files (README.md, package.json, etc.) +17: +18: ### Step 2: Generate Artifacts (MANDATORY) +19: Based on artifact_options, you MUST generate and save: +20: +21: **For idea.md:** +22: Generate a comprehensive project idea document including: +23: - Project Overview (what the project does) +24: - Background (why it exists) +25: - Key Features (extracted from code/docs) +26: - Technical Stack +27: - Project Structure +28: +29: **For prd.md:** +30: Generate product requirements including: +31: - Functional Requirements +32: - Non-Functional Requirements +33: - User Interactions +34: - Constraints +35: +36: **For design.md:** +37: Generate technical design including: +38: - Architecture Overview +39: - Technology Stack Table +40: - Directory Structure +41: - Key Modules +42: +43: **For plan.md:** +44: Generate implementation plan including: +45: - Phase breakdown +46: - Task list with checkboxes +47: - Next steps +48: +49: ### Step 3: Save Artifacts (CRITICAL - You MUST do this) +50: For EACH artifact, you MUST call: +51: ``` +52: save_artifact(filename="idea.md", content="...") +53: save_artifact(filename="prd.md", content="...") +54: save_artifact(filename="design.md", content="...") +55: save_artifact(filename="plan.md", content="...") +56: ``` +57: +58: ## Tool Reference +59: +60: - `scan_project(project_path, max_depth?)` - Scan project structure +61: - `detect_tech_stack(project_path)` - Detect technologies +62: - `read_project_file(project_path, relative_path, max_lines?)` - Read a file +63: - `list_project_directory(project_path, relative_path?)` - List directory +64: - `save_artifact(filename, content)` - Save artifact (MANDATORY for completion) +65: +66: ## Project Information +67: +68: - Project Path: {project_path} +69: - Artifact Options: {artifact_options} +70: +71: ## Example Execution +72: +73: 1. scan_project("D:/path/to/project") +74: 2. detect_tech_stack("D:/path/to/project") +75: 3. read_project_file("D:/path/to/project", "README.md") +76: 4. [Generate idea.md content in your thinking] +77: 5. save_artifact("idea.md", "# Project Idea\n\n...") +78: 6. [Generate prd.md content in your thinking] +79: 7. save_artifact("prd.md", "# PRD\n\n...") +80: 8. ... continue for all requested artifacts +81: +82: REMEMBER: You are NOT finished until save_artifact() has been called for ALL requested artifacts. +83: "##; +```` + +### crates/cowork-core/src/instructions/mod.rs (23 lines) + +``` +1: pub mod idea; +2: pub mod prd; +3: pub mod design; +4: pub mod plan; +5: pub mod coding; +6: pub mod check; +7: pub mod delivery; +8: pub mod summary; +9: pub mod knowledge_gen; +10: pub mod project_manager; +11: pub mod legacy_project_analyzer; +12: +13: pub use idea::*; +14: pub use prd::*; +15: pub use design::*; +16: pub use plan::*; +17: pub use coding::*; +18: pub use check::*; +19: pub use delivery::*; +20: pub use summary::*; +21: pub use knowledge_gen::*; +22: pub use project_manager::*; +23: pub use legacy_project_analyzer::*; +``` + +### crates/cowork-core/src/instructions/prd.rs (276 lines) + +```` +1: pub const PRD_ACTOR_INSTRUCTION: &str = r##" +2: # Your Role +3: You are PRD Actor. Create or update requirements and features. +4: +5: # ⚠️ CRITICAL REQUIREMENT - YOU MUST CALL save_prd_doc() +6: **This is the MOST IMPORTANT requirement:** +7: - You MUST call `save_prd_doc(content)` at the END of your work +8: - Without calling this tool, the PRD stage CANNOT complete +9: - Your work will be LOST if you don't save the document +10: - Example: save_prd_doc(content) with your complete PRD markdown +11: +12: # Workflow - TWO MODES +13: +14: ## Mode Detection (FIRST STEP) +15: 1. Call `load_feedback_history({"stage": "prd"})` to check if this is a restart +16: 2. If feedback history exists and has entries → **UPDATE MODE** +17: 3. If no feedback history or empty → **NEW MODE** +18: +19: ## NEW MODE (全新生成) +20: +21: ### Step 1: Initial Analysis +22: 1. Load idea using `load_idea()` to understand the project +23: 2. Analyze the project scope and goals +24: +25: ### Step 2: Generate Formal Requirements and Save PRD Document (MANDATORY) +26: 3. Based on the analysis, create formal requirements: +27: - Call `create_requirement(...)` for each requirement +28: - Call `add_feature(...)` for each feature +29: 4. **CRITICAL**: Generate a complete PRD markdown document: +30: - Include all requirements with their IDs, titles, descriptions, priorities, and acceptance criteria +31: - Include all features with their IDs, names, descriptions, and linked requirements +32: 5. **MANDATORY**: Call `save_prd_doc(content=)` to save the document - The system will NOT auto-save! +33: 6. Done! Critic will review next. +34: +35: ## UPDATE MODE (增量更新 - 当 GotoStage 回退到此阶段时) +36: +37: ### Step 1: Analyze Feedback +38: 1. Call `load_feedback_history({"stage": "prd"})` - 获取最近的反馈信息 +39: 2. Read feedback.details to understand what needs to change +40: +41: ### Step 2: Load Existing Content +42: 3. Read existing artifacts: +43: - PRD document is saved automatically - no need to read it directly +44: - Use `get_requirements()` to get structured data (requirements and features) +45: +46: ### Step 3: Incremental Updates +47: 4. Analyze feedback and determine what to modify: +48: - Identify which requirements/features are affected +49: - What needs to be added, modified, or deleted +50: +51: 5. Apply targeted updates: +52: - Use `update_requirement(id, ...)` to modify existing requirements +53: - Use `update_feature(id, ...)` to modify existing features +54: - Use `delete_requirement(id)` to remove requirements +55: - Use `create_requirement(...)` for new requirements +56: - Use `add_feature(...)` for new features +57: +58: ### Step 4: Save Updated PRD (MANDATORY) +59: 6. Generate updated PRD document from modified requirements/features +60: 7. **MANDATORY**: Call `save_prd_doc(content=)` to save the document - The system will NOT auto-save! +61: +62: ### UPDATE MODE Example +63: +64: ``` +65: # 假设 feedback 显示: "API架构需要从REST改为GraphQL,添加认证需求" +66: +67: 1. load_feedback_history() +68: → feedbacks: [{ +69: feedback_type: "QualityIssue", +70: severity: "Critical", +71: details: "API架构需要从REST改为GraphQL,添加认证需求" +72: }] +73: +74: 2. get_requirements() +75: → Returns existing requirements and features +76: +77: 3. 分析需要修改的内容: +78: - 修改 API 相关需求 (REQ-003) +79: - 添加认证需求 (REQ-006) +80: - 更新相关功能 (FEAT-002) +81: +82: 4. 增量更新: +83: update_requirement( +84: id="REQ-003", +85: new_title="GraphQL API", +86: new_description="使用GraphQL提供灵活的数据查询接口" +87: ) +88: +89: create_requirement( +90: title="用户认证", +91: description="支持JWT token认证", +92: priority="high", +93: category="functional", +94: acceptance_criteria=["用户可以登录", "支持token刷新"] +95: ) +96: +97: update_feature( +98: id="FEAT-002", +99: new_description="GraphQL API + 认证功能" +100: ) +101: +102: 5. 保存更新后的 PRD 文档 +103: save_prd_doc(content=updated_content) +104: +105: 6. 完成!Critic 将审查更新后的需求 +106: ``` +107: +108: Note: Replace {ITERATION_ID} with the actual iteration ID provided in the prompt. +109: +110: # Tools +111: +112: ## Core Tools +113: - load_feedback_history() ← **START HERE - 检测是否是 UPDATE MODE** +114: - load_idea() ← Load idea document +115: - get_requirements() ← 读取现有需求和功能 +116: +117: ## NEW MODE Tools +118: - review_with_feedback_content(title, content, prompt) ← **HITL tool (content-based)** +119: - create_requirement(title, description, priority, category, acceptance_criteria) +120: - add_feature(name, description, requirement_ids, completion_criteria) +121: - save_prd_doc(content) ← **Save final PRD document (MANDATORY)** +122: +123: ## UPDATE MODE Tools +124: - update_requirement(id, title, description, priority, acceptance_criteria) +125: - update_feature(id, name, description, requirement_ids, completion_criteria) +126: - delete_requirement(id) +127: - create_requirement(...) ← 用于新需求 +128: - add_feature(...) ← 用于新功能 +129: - save_prd_doc(content) ← **Save updated PRD document (MANDATORY)** +130: +131: ## UPDATE MODE Tools +132: - update_requirement(id, title, description, priority, acceptance_criteria) +133: - update_feature(id, name, description, requirement_ids, completion_criteria) +134: - delete_requirement(id) +135: - create_requirement(...) ← 用于新需求 +136: - add_feature(...) ← 用于新功能 +137: +138: # Important Principles +139: +140: ## For NEW MODE +141: - Always create draft → review_with_feedback → revise if needed → create formal +142: - Respect user feedback - adjust requirements based on their input +143: - Max 2 review iterations to avoid infinite loops +144: +145: ## For UPDATE MODE +146: - **Don't recreate everything** - only modify what's affected by feedback +147: - Preserve unchanged requirements and features +148: - Focus on the specific issues mentioned in feedback +149: - Be efficient - incremental updates are faster than full regeneration +150: +151: **REMEMBER**: +152: - Always start with `load_feedback_history()` to detect mode +153: - In UPDATE MODE, be surgical - only change what needs changing +154: - In NEW MODE, follow the full creation workflow +155: "##; +156: +157: pub const PRD_CRITIC_INSTRUCTION: &str = r#" +158: # Your Role +159: You are PRD Critic. Review the generated requirements. +160: +161: # CRITICAL: This is a GATEKEEPER role - you must BLOCK progress if Actor failed! +162: +163: # ⚠️ ANTI-LOOP PROTECTION (HIGHEST PRIORITY) +164: **CRITICAL**: To prevent infinite loops: +165: +166: 1. **Before calling provide_feedback**, ask yourself: +167: - "Have I already reported this EXACT issue before?" +168: +169: 2. **If you're about to give the SAME feedback twice**: +170: - ⛔ **STOP** - call `request_human_review()` instead +171: +172: 3. **Never call provide_feedback twice with same details** +173: +174: ## Mandatory Checks (You MUST perform ALL of these) +175: +176: ### Check 1: Verify Requirements Data Exists +177: 1. Call `get_requirements()` to see what Actor created +178: - This returns: {requirements: [...], features: [...]} +179: - **FAIL** if requirements array is empty +180: +181: ### Check 2: Verify PRD Document Exists (CRITICAL - MUST DO THIS!) +182: 2. **YOU MUST CALL `load_prd_doc()` TO VERIFY THE PRD MARKDOWN FILE EXISTS** +183: 3. **If load_prd_doc() returns an error or empty content**: +184: - This is a CRITICAL failure - the Actor forgot to call save_prd_doc() +185: - **MUST** call `provide_feedback(stage="prd", feedback_type="missing_artifact", severity="critical", details="PRD document (prd.md) was not saved. The Actor must call save_prd_doc() to save the document.", suggested_fix="Call save_prd_doc(content) with the complete PRD markdown document.")` +186: +187: ### Check 3: Quick Analysis +188: 4. Count and assess: +189: - How many requirements? (Aim for 3-8) +190: - How many features? (Aim for 2-5) +191: - Do they seem reasonable for the project scope? +192: +193: # Workflow - SIMPLE AND DIRECT +194: +195: ## Step 1: Get Requirements Data +196: 1. Call `get_requirements()` to see what Actor created +197: - This returns: {requirements: [...], features: [...]} +198: - You get ALL the data you need from this one call +199: +200: ## Step 2: Verify PRD Document (MANDATORY!) +201: 2. **MUST call `load_prd_doc()` to verify the markdown document exists** +202: 3. If it returns empty or error, provide feedback immediately +203: +204: ## Step 3: Quick Analysis +205: 4. Count and assess: +206: - How many requirements? (Aim for 3-8) +207: - How many features? (Aim for 2-5) +208: - Do they seem reasonable for the project scope? +209: +210: ## Step 4: Respond +211: 5. **Just respond with your assessment**: +212: - If good: "✅ X requirements and Y features cover the project scope well. PRD document saved." +213: - If issues: Describe what's wrong +214: +215: ## Important Notes +216: +217: - **DON'T try to read files directly** - Use the provided tools +218: - **If you really need idea.md**: Use `load_idea()` to load the idea document +219: - **File not found?** Just skip it and work with requirements data +220: - **Actor already got user feedback**, so usually requirements are OK +221: - **BUT YOU MUST VERIFY prd.md EXISTS!** +222: +223: Note: Replace {ITERATION_ID} with the actual iteration ID provided in the prompt. +224: +225: # Tools +226: - get_requirements() ← **START HERE - Get structured data** +227: - load_prd_doc() ← **MANDATORY - Verify document was saved!** +228: - load_idea() ← Load idea document if you need additional context +229: - provide_feedback(stage="prd", feedback_type, severity, details, suggested_fix) ← If issues found +230: +231: # Example - Normal Case +232: ``` +233: 1. get_requirements() +234: 2. # Returns: 3 requirements, 3 features +235: 3. load_prd_doc() +236: 4. # Returns: PRD markdown content (success) +237: 5. "✅ 3 requirements and 3 features cover core functionality well. PRD document saved." +238: ``` +239: +240: # Example - Missing Artifact (Critical!) +241: ``` +242: 1. get_requirements() +243: 2. # Returns: 3 requirements, 3 features (looks good) +244: 3. load_prd_doc() +245: 4. # Returns: Error or empty content +246: 5. # MUST provide feedback! +247: provide_feedback( +248: stage="prd", +249: feedback_type="missing_artifact", +250: severity="critical", +251: details="PRD document (prd.md) was not saved. Only requirements.json exists.", +252: suggested_fix="Call save_prd_doc(content) with the complete PRD markdown document." +253: ) +254: ``` +255: +256: # Anti-Loop Examples +257: +258: ## ✅ CORRECT - Different feedback each time +259: ``` +260: Iteration 1: provide_feedback(stage="prd", feedback_type="missing_artifact", severity="critical", details="PRD document not saved", suggested_fix="...") +261: Iteration 2: provide_feedback(stage="prd", feedback_type="missing_artifact", severity="critical", details="PRD document still not saved after retry", suggested_fix="...") +262: Iteration 3: request_human_review("Unable to resolve PRD document saving issue") +263: ``` +264: +265: ## ❌ WRONG - Same feedback twice +266: ``` +267: Iteration 1: provide_feedback(stage="prd", feedback_type="missing_artifact", severity="critical", details="PRD document not saved", suggested_fix="...") +268: Iteration 2: provide_feedback(stage="prd", feedback_type="missing_artifact", severity="critical", details="PRD document not saved", suggested_fix="...") ← PROHIBITED! +269: ``` +270: +271: **REMEMBER**: +272: - Start with `get_requirements()` - it has the structured data +273: - **MUST verify prd.md exists with load_prd_doc()** +274: - Don't loop on file errors - just proceed +275: - Keep it simple! +276: "#; +```` + +### crates/cowork-core/src/instructions/project_manager.rs (156 lines) + +```` +1: pub const PROJECT_MANAGER_AGENT_INSTRUCTION: &str = r#" +2: # 你的角色 +3: 你是项目经理 Agent。你负责理解用户在项目交付后的想法和需求,并采取适当的行动。 +4: +5: # 当前项目上下文 +6: - 迭代 ID: {ITERATION_ID} +7: - 当前状态:已完成交付 (Delivery) +8: - 项目已可运行 +9: +10: # 你的能力 +11: +12: ## MCP 外部工具能力 +13: **如果配置了 MCP 服务,你可以使用以下外部工具:** +14: +15: ### Tavily 网络搜索(如果已配置) +16: - 用途:搜索互联网获取实时信息 +17: - 使用场景:用户要求查询在线信息、搜索项目、查找文档等 +18: - 示例:用户说"帮我查一下 xxx 项目"时,你可以使用 Tavily 搜索 +19: +20: ### DeepWiki 代码文档(如果已配置) +21: - 用途:查询 GitHub 仓库的技术文档 +22: - 使用场景:用户要求了解某个开源项目、查询 API 文档等 +23: - 示例:用户说"帮我看看 github 上的 xxx 项目"时,你可以使用 DeepWiki +24: +25: **重要:如果你的工具列表中包含这些 MCP 工具,请主动使用它们来帮助用户!** +26: +27: ## 1. 识别用户意图 +28: 分析用户的输入,判断是以下哪种情况: +29: +30: ### bug_fix - 修复问题/缺陷 +31: 典型触发词:bug, 错误, 问题, 不工作, 失败, 异常, 崩溃, 报错, 修复 +32: 示例: +33: - "发现一个 bug,按钮点击没反应" +34: - "首页加载失败" +35: - "登录功能有问题" +36: - "点击提交按钮报错了" +37: +38: ### requirement_change - 需求变更 +39: 典型触发词:修改, 改成, 变成, 调整, 更改, 换成, 改一下 +40: 示例: +41: - "把首页的标题改一下" +42: - "把按钮颜色改成红色" +43: - "调整一下布局" +44: - "把登录框移到右边" +45: +46: ### new_feature - 新功能 +47: 典型触发词:添加, 新增, 加上, 增加, 新功能, 还需要一个, 再做一个 +48: 示例: +49: - "添加一个搜索功能" +50: - "新增用户注册页面" +51: - "加上分享按钮" +52: - "需要一个导出功能" +53: +54: ### consultation - 咨询问题 +55: 典型触发词:怎么, 如何, 为什么, 是什么, 能否, 可以, 帮我看看 +56: 示例: +57: - "怎么部署这个项目?" +58: - "数据库在哪里?" +59: - "可以支持多语言吗?" +60: - "帮我看看这个功能是怎么实现的" +61: +62: ### ambiguous - 意图不明确 +63: 示例: +64: - 单字或短语 +65: - 无法判断具体需求 +66: - 需要更多信息 +67: +68: ## 2. 可执行的操作 +69: +70: ### goto_stage - 返回之前阶段 +71: 使用场景: +72: - Bug 修复需要修改代码 → coding +73: - 需求变更涉及设计调整 → design +74: - 需求变更涉及功能规格 → prd +75: - 重大产品决策变更需要重新设计 → idea +76: +77: ### create_iteration - 创建新迭代 +78: 使用场景: +79: - 新需求(添加新功能) +80: - 重大变更(需要全新的迭代) +81: - 用户明确表示要做一个新项目 +82: +83: ### answer_question - 回答问题 +84: 使用场景: +85: - 用户咨询项目相关问题 +86: - 需要解释项目架构或功能 +87: +88: ### ask_clarification - 请求澄清 +89: 使用场景: +90: - 意图不明确 +91: - 需要更多信息才能执行 +92: +93: ## 3. 工作流程 +94: +95: 1. **分析用户输入**:理解用户想要做什么 +96: 2. **识别意图**:判断意图分类 +97: 3. **选择操作**:决定使用哪个工具 +98: 4. **执行操作**:调用相应的工具 +99: 5. **反馈结果**:告诉用户操作结果 +100: +101: ## 4. 对话原则 +102: +103: - **友好专业**:使用项目经理的语气,像一个有经验的同事 +104: - **明确确认**:执行重要操作前先告诉用户你要做什么 +105: - **提供选项**:当不确定时,给用户多个选项 +106: - **简洁明了**:不要过于冗长,直接切入重点 +107: +108: ## 5. 输出格式 +109: +110: ### 对于需要执行操作的情况: +111: 先向用户说明你要做什么,然后调用工具。 +112: +113: 示例: +114: ``` +115: 我理解你发现了登录功能的问题。我会让系统重新进入 Coding 阶段来修复这个 bug。 +116: +117: [调用 goto_stage 工具] +118: ``` +119: +120: ### 对于需要澄清的情况: +121: 提出明确的问题,帮助用户提供更多信息。 +122: +123: 示例: +124: ``` +125: 你想对项目做什么修改呢?请告诉我具体需求: +126: 1. 修复某个 bug? +127: 2. 调整现有功能? +128: 3. 添加新功能? +129: ``` +130: +131: ### 对于咨询问题: +132: 直接回答用户的问题。 +133: +134: # 工具使用规则 +135: +136: ## goto_stage +137: - stage 参数必须是以下之一:idea, prd, design, plan, coding +138: - reason 参数要清楚说明为什么需要返回这个阶段 +139: +140: ## create_iteration +141: - title 要简洁明了,概括用户需求 +142: - description 要详细说明用户的具体需求 +143: - base_iteration_id 使用当前迭代 ID +144: - inheritance 默认使用 "partial"(部分继承代码,重新生成文档) +145: +146: ## respond_to_user +147: - 当只需要回答问题或请求澄清时使用 +148: - response 参数是你的回复内容 +149: +150: # 重要提醒 +151: +152: 1. **不要猜测**:如果不确定用户意图,使用 ask_clarification +153: 2. **确认重要操作**:创建新迭代是重要操作,先告诉用户 +154: 3. **保持上下文**:记住之前的对话内容 +155: 4. **使用中文**:与用户对话时使用中文 +156: "#; +```` + +### crates/cowork-core/src/integration/adapters.rs (75 lines) + +``` +1: AdapterError +2: ⋮---- +3: { +4: #[error("Connection failed: {0}")] +5: ConnectionFailed(String), +6: +7: #[error("Authentication failed: {0}")] +8: AuthenticationFailed(String), +9: +10: #[error("Request failed: {0}")] +11: RequestFailed(String), +12: +13: #[error("Timeout exceeded")] +14: TimeoutExceeded, +15: +16: #[error("Invalid response: {0}")] +17: InvalidResponse(String), +18: +19: #[error("Retry limit exceeded: {0}")] +20: RetryLimitExceeded(String), +21: +22: #[error("Configuration error: {0}")] +23: ConfigurationError(String), +24: +25: #[error("IO error: {0}")] +26: IoError(#[from] std::io::Error), +27: +28: #[error("HTTP error: {0}")] +29: HttpError(#[from] reqwest::Error), +30: +31: #[error("JSON error: {0}")] +32: JsonError(#[from] serde_json::Error), +33: } +34: ⋮---- +35: IntegrationAdapter +36: ⋮---- +37: { +38: +39: async fn execute( +40: &self, +41: integration: &IntegrationDefinition, +42: event: IntegrationEvent, +43: ) -> Result; +44: +45: +46: async fn test_connection(&self, integration: &IntegrationDefinition) -> Result; +47: +48: +49: fn adapter_type(&self) -> &str; +50: } +51: ⋮---- +52: NoOpAdapter +53: ⋮---- +54: { +55: async fn execute( +56: &self, +57: _integration: &IntegrationDefinition, +58: _event: IntegrationEvent, +59: ) -> Result { +60: Ok(IntegrationResponse { +61: success: true, +62: data: None, +63: error: None, +64: actions: vec![], +65: }) +66: } +67: +68: async fn test_connection(&self, _integration: &IntegrationDefinition) -> Result { +69: Ok(true) +70: } +71: +72: fn adapter_type(&self) -> &str { +73: "noop" +74: } +75: } +``` + +### crates/cowork-core/src/integration/hooks.rs (251 lines) + +``` +1: HookExecutionContext +2: ⋮---- +3: { +4: +5: pub stage_id: String, +6: +7: pub iteration_id: String, +8: +9: pub project_id: String, +10: +11: pub hook_point: HookPoint, +12: +13: pub data: HashMap, +14: } +15: ⋮---- +16: HookExecutionContext +17: ⋮---- +18: { +19: pub fn new( +20: stage_id: impl Into, +21: iteration_id: impl Into, +22: project_id: impl Into, +23: hook_point: HookPoint, +24: ) -> Self { +25: Self { +26: stage_id: stage_id.into(), +27: iteration_id: iteration_id.into(), +28: project_id: project_id.into(), +29: hook_point, +30: data: HashMap::new(), +31: } +32: } +33: +34: +35: pub fn with_data(mut self, key: impl Into, value: serde_json::Value) -> Self { +36: self.data.insert(key.into(), value); +37: self +38: } +39: +40: +41: pub fn to_event(&self, integration_id: impl Into) -> IntegrationEvent { +42: IntegrationEvent { +43: integration_id: integration_id.into(), +44: hook_point: format!("{:?}", self.hook_point), +45: stage_id: self.stage_id.clone(), +46: iteration_id: self.iteration_id.clone(), +47: project_id: self.project_id.clone(), +48: timestamp: chrono::Utc::now().to_rfc3339(), +49: data: self.data.clone(), +50: event_type: match self.hook_point { +51: HookPoint::PreExecute => IntegrationEventType::StageStarted, +52: HookPoint::PostExecute => IntegrationEventType::StageCompleted, +53: HookPoint::OnFailure => IntegrationEventType::StageFailed, +54: HookPoint::PreConfirmation => IntegrationEventType::ConfirmationRequested, +55: HookPoint::PostConfirmation => IntegrationEventType::ConfirmationReceived, +56: }, +57: } +58: } +59: } +60: ⋮---- +61: HookExecutionResult +62: ⋮---- +63: { +64: +65: pub integration_id: String, +66: +67: pub success: bool, +68: +69: pub response: Option, +70: +71: pub error: Option, +72: } +73: ⋮---- +74: HookManager +75: ⋮---- +76: { +77: +78: integrations: HashMap, +79: +80: rest_adapter: Arc, +81: } +82: ⋮---- +83: HookManager +84: ⋮---- +85: { +86: +87: pub fn new() -> Self { +88: let http_client = reqwest::Client::builder() +89: .timeout(std::time::Duration::from_secs(30)) +90: .build() +91: .unwrap_or_else(|_| reqwest::Client::new()); +92: +93: Self { +94: integrations: HashMap::new(), +95: rest_adapter: Arc::new(RestAdapter::new(http_client)), +96: } +97: } +98: +99: +100: pub fn register_integration(&mut self, integration: IntegrationDefinition) { +101: self.integrations.insert(integration.id.clone(), integration); +102: } +103: +104: +105: pub fn remove_integration(&mut self, integration_id: &str) { +106: self.integrations.remove(integration_id); +107: } +108: +109: +110: pub fn get_integration(&self, integration_id: &str) -> Option<&IntegrationDefinition> { +111: self.integrations.get(integration_id) +112: } +113: +114: +115: pub fn list_integrations(&self) -> Vec<&IntegrationDefinition> { +116: self.integrations.values().collect() +117: } +118: +119: +120: pub async fn execute_hook( +121: &self, +122: integration_id: &str, +123: context: HookExecutionContext, +124: ) -> Result { +125: let integration = self.integrations.get(integration_id) +126: .with_context(|| format!("Integration not found: {}", integration_id))?; +127: +128: if !integration.enabled { +129: return Ok(HookExecutionResult { +130: integration_id: integration_id.to_string(), +131: success: true, +132: response: None, +133: error: Some("Integration is disabled".to_string()), +134: }); +135: } +136: +137: +138: let event = context.to_event(integration_id); +139: +140: +141: let response = match integration.integration_type { +142: crate::config_definition::IntegrationType::RestApi => { +143: self.rest_adapter.execute(integration, event).await +144: } +145: crate::config_definition::IntegrationType::Webhook => { +146: +147: self.rest_adapter.execute(integration, event).await +148: } +149: _ => { +150: +151: return Ok(HookExecutionResult { +152: integration_id: integration_id.to_string(), +153: success: false, +154: response: None, +155: error: Some(format!("Unsupported integration type: {:?}", integration.integration_type)), +156: }); +157: } +158: }; +159: +160: match response { +161: Ok(resp) => Ok(HookExecutionResult { +162: integration_id: integration_id.to_string(), +163: success: resp.success, +164: response: Some(resp), +165: error: None, +166: }), +167: Err(e) => Ok(HookExecutionResult { +168: integration_id: integration_id.to_string(), +169: success: false, +170: response: None, +171: error: Some(e.to_string()), +172: }), +173: } +174: } +175: +176: +177: pub async fn execute_hooks( +178: &self, +179: hooks: &[HookConfig], +180: context: HookExecutionContext, +181: ) -> Vec { +182: let mut results = Vec::new(); +183: +184: for hook in hooks { +185: +186: if hook.point != context.hook_point { +187: continue; +188: } +189: +190: +191: let result = self.execute_hook(&hook.integration_id, context.clone()).await; +192: +193: match result { +194: Ok(r) => results.push(r), +195: Err(e) => results.push(HookExecutionResult { +196: integration_id: hook.integration_id.clone(), +197: success: false, +198: response: None, +199: error: Some(e.to_string()), +200: }), +201: } +202: } +203: +204: results +205: } +206: +207: +208: pub async fn execute_and_process( +209: &self, +210: hooks: &[HookConfig], +211: context: HookExecutionContext, +212: ) -> Result> { +213: let results = self.execute_hooks(hooks, context).await; +214: +215: let mut actions = Vec::new(); +216: +217: for result in results { +218: if !result.success { +219: if let Some(error) = result.error { +220: tracing::warn!( +221: "Hook execution failed for integration {}: {}", +222: result.integration_id, error +223: ); +224: } +225: continue; +226: } +227: +228: if let Some(response) = result.response { +229: actions.extend(response.actions); +230: } +231: } +232: +233: Ok(actions) +234: } +235: } +236: ⋮---- +237: HookManager +238: ⋮---- +239: { +240: fn default() -> Self { +241: Self::new() +242: } +243: } +244: ⋮---- +245: test_hook_context +246: ⋮---- +247: () +248: ⋮---- +249: test_hook_manager_creation +250: ⋮---- +251: () +``` + +### crates/cowork-core/src/persistence/iteration_store.rs (127 lines) + +``` +1: IterationStore +2: ⋮---- +3: { +4: pub fn new() -> Self { +5: Self +6: } +7: +8: +9: pub fn load(&self, iteration_id: &str) -> anyhow::Result { +10: let path = self.iteration_file_path(iteration_id)?; +11: if !path.exists() { +12: anyhow::bail!("Iteration not found: {}", iteration_id); +13: } +14: let content = std::fs::read_to_string(&path)?; +15: let iteration: Iteration = serde_json::from_str(&content)?; +16: Ok(iteration) +17: } +18: +19: +20: pub fn save(&self, iteration: &Iteration) -> anyhow::Result<()> { +21: let path = self.iteration_file_path(&iteration.id)?; +22: +23: +24: if let Some(parent) = path.parent() { +25: std::fs::create_dir_all(parent)?; +26: } +27: +28: let content = serde_json::to_string_pretty(iteration)?; +29: std::fs::write(&path, content)?; +30: Ok(()) +31: } +32: +33: +34: pub fn exists(&self, iteration_id: &str) -> bool { +35: self.iteration_file_path(iteration_id) +36: .map(|p| p.exists()) +37: .unwrap_or(false) +38: } +39: +40: +41: pub fn delete(&self, iteration_id: &str) -> anyhow::Result<()> { +42: let path = self.iteration_file_path(iteration_id)?; +43: if path.exists() { +44: std::fs::remove_file(&path)?; +45: } +46: Ok(()) +47: } +48: +49: +50: pub fn load_all(&self) -> anyhow::Result> { +51: let dir = get_cowork_dir()?.join("iterations"); +52: if !dir.exists() { +53: return Ok(Vec::new()); +54: } +55: +56: let mut iterations = Vec::new(); +57: for entry in std::fs::read_dir(&dir)? { +58: let entry = entry?; +59: if entry +60: .path() +61: .extension() +62: .map(|e| e == "json") +63: .unwrap_or(false) +64: { +65: if let Ok(content) = std::fs::read_to_string(entry.path()) { +66: if let Ok(iteration) = serde_json::from_str::(&content) { +67: iterations.push(iteration); +68: } +69: } +70: } +71: } +72: +73: +74: iterations.sort_by_key(|i| i.number); +75: Ok(iterations) +76: } +77: +78: +79: pub fn load_summaries(&self) -> anyhow::Result> { +80: let iterations = self.load_all()?; +81: Ok(iterations.into_iter().map(|i| i.to_summary()).collect()) +82: } +83: +84: +85: +86: pub fn workspace_path(&self, iteration_id: &str) -> anyhow::Result { +87: let cowork_dir = get_cowork_dir()?; +88: Ok(cowork_dir +89: .join("iterations") +90: .join(iteration_id) +91: .join("workspace")) +92: } +93: +94: +95: pub fn ensure_workspace(&self, iteration_id: &str) -> anyhow::Result { +96: let workspace = self.workspace_path(iteration_id)?; +97: std::fs::create_dir_all(&workspace)?; +98: +99: +100: let memory_dir = get_cowork_dir()?.join("memory/iterations"); +101: std::fs::create_dir_all(&memory_dir)?; +102: +103: Ok(workspace) +104: } +105: +106: +107: +108: pub fn iteration_path(&self, iteration_id: &str) -> anyhow::Result { +109: let cowork_dir = get_cowork_dir()?; +110: Ok(cowork_dir.join("iterations").join(iteration_id)) +111: } +112: +113: fn iteration_file_path(&self, iteration_id: &str) -> anyhow::Result { +114: let cowork_dir = get_cowork_dir()?; +115: Ok(cowork_dir +116: .join("iterations") +117: .join(format!("{}.json", iteration_id))) +118: } +119: } +120: ⋮---- +121: IterationStore +122: ⋮---- +123: { +124: fn default() -> Self { +125: Self::new() +126: } +127: } +``` + +### crates/cowork-core/src/pipeline/executor/interaction_ext.rs (36 lines) + +``` +1: ConfirmationAction +2: ⋮---- +3: { +4: Continue, +5: ViewArtifact, +6: ProvideFeedback(String), +7: Cancel, +8: } +9: ⋮---- +10: InteractionExt +11: ⋮---- +12: { +13: async fn request_confirmation(&self, prompt: &str) -> bool; +14: async fn request_confirmation_with_artifact(&self, prompt: &str, artifact_type: &str) -> bool; +15: async fn request_confirmation_with_feedback( +16: &self, +17: prompt: &str, +18: artifact_type: &str, +19: ) -> ConfirmationAction; +20: } +21: ⋮---- +22: request_confirmation +23: ⋮---- +24: (&self, prompt: &str) +25: ⋮---- +26: request_confirmation_with_artifact +27: ⋮---- +28: (&self, prompt: &str, artifact_type: &str) +29: ⋮---- +30: request_confirmation_with_feedback +31: ⋮---- +32: ( +33: &self, +34: prompt: &str, +35: artifact_type: &str, +36: ) +``` + +### crates/cowork-core/src/pipeline/executor/workspace.rs (61 lines) + +``` +1: prepare_workspace +2: ⋮---- +3: ( +4: iteration_store: &IterationStore, +5: interaction: &Arc, +6: iteration: &Iteration, +7: ) +8: ⋮---- +9: inherit_from_base +10: ⋮---- +11: ( +12: iteration_store: &IterationStore, +13: interaction: &Arc, +14: workspace: &std::path::PathBuf, +15: base_iteration_id: &str, +16: inheritance_mode: InheritanceMode, +17: ) +18: ⋮---- +19: check_artifact_exists +20: ⋮---- +21: (stage_name: &str, workspace: &std::path::Path) +22: ⋮---- +23: copy_dir_all +24: ⋮---- +25: (src: &std::path::Path, dst: &std::path::Path) +26: ⋮---- +27: copy_code_files +28: ⋮---- +29: (src: &std::path::Path, dst: &std::path::Path) +30: ⋮---- +31: create_test_structure +32: ⋮---- +33: () +34: ⋮---- +35: test_check_artifact_exists_file_present +36: ⋮---- +37: () +38: ⋮---- +39: test_check_artifact_exists_file_missing +40: ⋮---- +41: () +42: ⋮---- +43: test_check_artifact_exists_file_empty +44: ⋮---- +45: () +46: ⋮---- +47: test_check_artifact_exists_coding_stage +48: ⋮---- +49: () +50: ⋮---- +51: test_check_artifact_exists_unknown_stage +52: ⋮---- +53: () +54: ⋮---- +55: test_copy_dir_all +56: ⋮---- +57: () +58: ⋮---- +59: test_copy_code_files_skips_artifacts +60: ⋮---- +61: () +``` + +### crates/cowork-core/src/runtime_security.rs (269 lines) + +``` +1: RuntimeSecurityChecker +2: ⋮---- +3: { +4: +5: allowed_package_managers: Vec, +6: +7: allowed_base_commands: Vec, +8: +9: dangerous_patterns: Vec, +10: +11: project_root: Option, +12: } +13: ⋮---- +14: RuntimeSecurityChecker +15: ⋮---- +16: { +17: +18: pub fn new() -> Self { +19: Self { +20: project_root: None, +21: allowed_package_managers: vec![ +22: "npm".to_string(), +23: "bun".to_string(), +24: "yarn".to_string(), +25: "pnpm".to_string(), +26: "cargo".to_string(), +27: "pip".to_string(), +28: "uv".to_string(), +29: "python".to_string(), +30: "python3".to_string(), +31: "uvicorn".to_string(), +32: "flask".to_string(), +33: ], +34: allowed_base_commands: vec![ +35: "npm".to_string(), +36: "bun".to_string(), +37: "yarn".to_string(), +38: "pnpm".to_string(), +39: "cargo".to_string(), +40: "pip".to_string(), +41: "uv".to_string(), +42: "python".to_string(), +43: "python3".to_string(), +44: "uvicorn".to_string(), +45: "flask".to_string(), +46: ], +47: dangerous_patterns: vec![ +48: +49: Regex::new(r"(?i)rm\s+-rf\s+/").unwrap(), +50: Regex::new(r"(?i)rmdir\s+/").unwrap(), +51: Regex::new(r"(?i)format\s+[a-zA-Z]:").unwrap(), +52: Regex::new(r"(?i)del\s+/[sq]\s+/[a-zA-Z]:").unwrap(), +53: Regex::new(r"(?i)rm\s+-rf?\s+\.\.").unwrap(), +54: +55: Regex::new(r"(?i)chmod\s+-R\s+777").unwrap(), +56: Regex::new(r"(?i)chown\s+-R").unwrap(), +57: +58: Regex::new(r"(?i)curl\s+.*\|\s*(sh|bash|powershell)").unwrap(), +59: Regex::new(r"(?i)wget\s+.*\|\s*(sh|bash|powershell)").unwrap(), +60: Regex::new(r"(?i)Invoke-WebRequest.*\|").unwrap(), +61: +62: Regex::new(r"(?i)mkfs").unwrap(), +63: Regex::new(r"(?i)dd\s+if=").unwrap(), +64: Regex::new(r"(?i)fdisk").unwrap(), +65: +66: Regex::new(r"(?i)nc\s+-e").unwrap(), +67: Regex::new(r"(?i)ncat\s+-e").unwrap(), +68: Regex::new(r"(?i)ssh\s+.*-o\s+ProxyCommand").unwrap(), +69: +70: Regex::new(r"(?i)Remove-Item\s+-Recurse\s+-Force\s+C:\\").unwrap(), +71: Regex::new(r"(?i)Stop-Computer").unwrap(), +72: Regex::new(r"(?i)Restart-Computer").unwrap(), +73: Regex::new(r"(?i)Set-ExecutionPolicy\s+-ExecutionPolicy\s+Bypass").unwrap(), +74: +75: Regex::new(r"(?i)sysctl").unwrap(), +76: Regex::new(r"(?i)modprobe").unwrap(), +77: Regex::new(r"(?i)insmod").unwrap(), +78: +79: Regex::new(r"(?i)npm\s+publish").unwrap(), +80: Regex::new(r"(?i)npm\s+deploy").unwrap(), +81: Regex::new(r"(?i)npm\s+(run\s+)?eject").unwrap(), +82: Regex::new(r"(?i)cargo\s+publish").unwrap(), +83: Regex::new(r"(?i)pip\s+upload").unwrap(), +84: Regex::new(r"(?i)twine\s+upload").unwrap(), +85: ], +86: } +87: } +88: +89: +90: pub fn with_project_root(mut self, root: std::path::PathBuf) -> Self { +91: self.project_root = Some(root); +92: self +93: } +94: +95: +96: pub fn check_config(&self, config: &ProjectRuntimeConfig) -> SecurityCheckResult { +97: let mut warnings = Vec::new(); +98: let mut errors = Vec::new(); +99: +100: +101: if let Some(deps) = config.dependencies.package_manager.to_string().split_whitespace().next() { +102: if !self.allowed_package_managers.contains(&deps.to_string()) { +103: errors.push(format!( +104: "不允许的包管理器: {}. 允许: {:?}", +105: deps, +106: self.allowed_package_managers +107: )); +108: } +109: } +110: +111: +112: if !config.dependencies.install_command.is_empty() { +113: if !self.is_command_safe(&config.dependencies.install_command) { +114: errors.push(format!( +115: "危险的安装命令: {}. 只允许标准包管理器命令", +116: config.dependencies.install_command +117: )); +118: } +119: } +120: +121: +122: if let Some(frontend) = &config.frontend { +123: if !frontend.dev_command.is_empty() && !self.is_command_safe(&frontend.dev_command) { +124: errors.push(format!("危险的前端 dev 命令: {}", frontend.dev_command)); +125: } +126: if !frontend.build_command.is_empty() && !self.is_command_safe(&frontend.build_command) { +127: errors.push(format!("危险的前端 build 命令: {}", frontend.build_command)); +128: } +129: } +130: +131: +132: if let Some(backend) = &config.backend { +133: if !backend.dev_command.is_empty() && !self.is_command_safe(&backend.dev_command) { +134: errors.push(format!("危险的后端 dev 命令: {}", backend.dev_command)); +135: } +136: if !backend.build_command.is_empty() && !self.is_command_safe(&backend.build_command) { +137: errors.push(format!("危险的后端 build 命令: {}", backend.build_command)); +138: } +139: if let Some(start_cmd) = &backend.start_command { +140: if !start_cmd.is_empty() && !self.is_command_safe(start_cmd) { +141: errors.push(format!("危险的启动命令: {}", start_cmd)); +142: } +143: } +144: } +145: +146: +147: if let Some(fullstack) = &config.fullstack { +148: if !fullstack.frontend_dev_command.is_empty() && !self.is_command_safe(&fullstack.frontend_dev_command) { +149: errors.push(format!("危险的全栈前端命令: {}", fullstack.frontend_dev_command)); +150: } +151: if !fullstack.backend_dev_command.is_empty() && !self.is_command_safe(&fullstack.backend_dev_command) { +152: errors.push(format!("危险的全栈后端命令: {}", fullstack.backend_dev_command)); +153: } +154: } +155: +156: +157: if config.dependencies.install_command.contains("--global") { +158: warnings.push("全局安装可能影响系统环境".to_string()); +159: } +160: +161: if config.dependencies.install_command.contains("sudo") { +162: warnings.push("使用 sudo 安装可能需要管理员权限".to_string()); +163: } +164: +165: SecurityCheckResult { +166: is_safe: errors.is_empty(), +167: warnings, +168: errors, +169: } +170: } +171: +172: +173: pub fn is_command_safe(&self, command: &str) -> bool { +174: if command.is_empty() { +175: return true; +176: } +177: +178: let cmd_lower = command.to_lowercase(); +179: +180: +181: for pattern in &self.dangerous_patterns { +182: if pattern.is_match(&cmd_lower) { +183: return false; +184: } +185: } +186: +187: +188: let parts: Vec<&str> = cmd_lower.split_whitespace().collect(); +189: if let Some(first) = parts.first() { +190: let is_allowed = self.allowed_base_commands.iter().any(|c| first.starts_with(c)); +191: +192: if !is_allowed { +193: +194: let has_acceptable = parts.iter().skip(1).any(|p| { +195: self.allowed_base_commands.iter().any(|c| p.starts_with(c)) +196: || p.starts_with("&&") +197: || p.starts_with("||") +198: || p.starts_with(";") +199: || p.starts_with("cd") +200: }); +201: +202: if !has_acceptable { +203: return false; +204: } +205: } +206: } +207: +208: +209: if cmd_lower.contains('|') && ( +210: cmd_lower.contains("sh") +211: || cmd_lower.contains("bash") +212: || cmd_lower.contains("powershell") +213: || cmd_lower.contains("cmd") +214: ) { +215: return false; +216: } +217: +218: +219: if cmd_lower.contains("${") || cmd_lower.contains("$(") { +220: +221: } +222: +223: true +224: } +225: +226: +227: pub fn is_path_safe(&self, path: &Path) -> bool { +228: let root = match &self.project_root { +229: Some(r) => r, +230: None => return true, +231: }; +232: +233: +234: let canonical_root = match root.canonicalize() { +235: Ok(p) => p, +236: Err(_) => return false, +237: }; +238: +239: let canonical_path = match path.canonicalize() { +240: Ok(p) => p, +241: Err(_) => { +242: +243: if let Some(parent) = path.parent() { +244: return self.is_path_safe(parent); +245: } +246: return false; +247: } +248: }; +249: +250: +251: canonical_path.starts_with(&canonical_root) +252: } +253: } +254: ⋮---- +255: RuntimeSecurityChecker +256: ⋮---- +257: { +258: fn default() -> Self { +259: Self::new() +260: } +261: } +262: ⋮---- +263: test_dangerous_commands +264: ⋮---- +265: () +266: ⋮---- +267: test_safe_commands +268: ⋮---- +269: () +``` + +### crates/cowork-core/src/skills/manager.rs (198 lines) + +``` +1: SkillManagerConfig +2: ⋮---- +3: { +4: +5: pub root_path: PathBuf, +6: +7: pub selection_policy: SelectionPolicy, +8: +9: pub max_injected_chars: usize, +10: } +11: ⋮---- +12: SkillManagerConfig +13: ⋮---- +14: { +15: fn default() -> Self { +16: Self { +17: root_path: PathBuf::from("."), +18: selection_policy: SelectionPolicy::default(), +19: max_injected_chars: 2000, +20: } +21: } +22: } +23: ⋮---- +24: SkillManagerConfig +25: ⋮---- +26: { +27: +28: pub fn new(root_path: impl Into) -> Self { +29: Self { +30: root_path: root_path.into(), +31: ..Default::default() +32: } +33: } +34: +35: +36: pub fn with_policy(mut self, policy: SelectionPolicy) -> Self { +37: self.selection_policy = policy; +38: self +39: } +40: +41: +42: pub fn with_max_injected_chars(mut self, max: usize) -> Self { +43: self.max_injected_chars = max; +44: self +45: } +46: } +47: ⋮---- +48: SkillManager +49: ⋮---- +50: { +51: config: SkillManagerConfig, +52: index: SkillIndex, +53: } +54: ⋮---- +55: SkillManager +56: ⋮---- +57: { +58: +59: pub fn new(config: SkillManagerConfig) -> Result { +60: let index = load_skill_index(&config.root_path) +61: .map_err(|e| anyhow::anyhow!("Failed to load skill index: {}", e))?; +62: +63: tracing::info!( +64: "SkillManager initialized with {} skills from {:?}", +65: index.len(), +66: config.root_path +67: ); +68: +69: Ok(Self { config, index }) +70: } +71: +72: +73: pub fn for_project(project_path: impl AsRef) -> Result { +74: let config = SkillManagerConfig::new(project_path.as_ref()); +75: Self::new(config) +76: } +77: +78: +79: pub fn index(&self) -> &SkillIndex { +80: &self.index +81: } +82: +83: +84: pub fn list_skills(&self) -> &[SkillDocument] { +85: self.index.skills() +86: } +87: +88: +89: pub fn skill_count(&self) -> usize { +90: self.index.len() +91: } +92: +93: +94: pub fn is_empty(&self) -> bool { +95: self.index.is_empty() +96: } +97: +98: +99: pub fn find_skill(&self, name: &str) -> Option<&SkillDocument> { +100: self.index.skills().iter().find(|s| s.name == name) +101: } +102: +103: +104: pub fn find_skill_by_id(&self, id: &str) -> Option<&SkillDocument> { +105: self.index.skills().iter().find(|s| s.id == id) +106: } +107: +108: +109: pub fn select(&self, query: &str) -> Vec { +110: select_skills(&self.index, query, &self.config.selection_policy) +111: } +112: +113: +114: pub fn select_best(&self, query: &str) -> Option { +115: self.select(query).into_iter().next() +116: } +117: +118: +119: pub fn select_with_policy(&self, query: &str, policy: &SelectionPolicy) -> Vec { +120: select_skills(&self.index, query, policy) +121: } +122: +123: +124: pub fn get_summaries(&self) -> Vec { +125: self.index.summaries() +126: } +127: +128: +129: pub fn reload(&mut self) -> Result<()> { +130: self.index = load_skill_index(&self.config.root_path) +131: .map_err(|e| anyhow::anyhow!("Failed to reload skill index: {}", e))?; +132: +133: tracing::info!( +134: "SkillManager reloaded with {} skills", +135: self.index.len() +136: ); +137: +138: Ok(()) +139: } +140: +141: +142: +143: +144: pub fn install_skill_from_dir(&self, source_dir: &Path) -> Result { +145: +146: let skill_md_path = source_dir.join("SKILL.md"); +147: if !skill_md_path.exists() { +148: anyhow::bail!("Source directory does not contain SKILL.md: {:?}", source_dir); +149: } +150: +151: let content = std::fs::read_to_string(&skill_md_path) +152: .with_context(|| format!("Failed to read {:?}", skill_md_path))?; +153: +154: +155: let parsed = adk_skill::parse_skill_markdown(&skill_md_path, &content) +156: .map_err(|e| anyhow::anyhow!("Failed to parse SKILL.md: {}", e))?; +157: +158: let skill_name = parsed.name.clone(); +159: +160: +161: let target_dir = self.config.root_path.join(".skills").join(&skill_name); +162: std::fs::create_dir_all(&target_dir) +163: .with_context(|| format!("Failed to create target directory: {:?}", target_dir))?; +164: +165: +166: copy_dir_all(source_dir, &target_dir)?; +167: +168: tracing::info!("Installed skill '{}' to {:?}", skill_name, target_dir); +169: +170: Ok(skill_name) +171: } +172: +173: +174: pub fn skills_directory(&self) -> PathBuf { +175: self.config.root_path.join(".skills") +176: } +177: +178: +179: pub fn has_skill(&self, name: &str) -> bool { +180: self.index.skills().iter().any(|s| s.name == name) +181: } +182: } +183: ⋮---- +184: copy_dir_all +185: ⋮---- +186: (src: &Path, dst: &Path) +187: ⋮---- +188: test_skill_manager_empty +189: ⋮---- +190: () +191: ⋮---- +192: test_skill_manager_with_skill +193: ⋮---- +194: () +195: ⋮---- +196: test_skill_selection +197: ⋮---- +198: () +``` + +### crates/cowork-core/src/skills/mod.rs (20 lines) + +``` +1: mod manager; +2: +3: pub use manager::{SkillManager, SkillManagerConfig}; +4: +5: +6: pub use adk_skill::{ +7: +8: SkillDocument, SkillIndex, SkillSummary, SkillMatch, +9: +10: SelectionPolicy, +11: +12: SkillInjector, SkillInjectorConfig, +13: apply_skill_injection, select_skill_prompt_block, +14: +15: load_skill_index, parse_skill_markdown, parse_instruction_markdown, +16: +17: discover_skill_files, discover_instruction_files, +18: +19: SkillError, SkillResult, +20: }; +``` + +### crates/cowork-core/src/tools/hitl_content_tools.rs (205 lines) + +``` +1: set_interaction_backend +2: ⋮---- +3: (backend: Arc) +4: ⋮---- +5: get_interaction_backend +6: ⋮---- +7: () +8: ⋮---- +9: ReviewAndEditContentTool +10: ⋮---- +11: { +12: fn name(&self) -> &str { +13: "review_and_edit_content" +14: } +15: +16: fn description(&self) -> &str { +17: "Let the user review content and choose: edit, pass, or provide feedback." +18: } +19: +20: fn parameters_schema(&self) -> Option { +21: Some(json!({ +22: "type": "object", +23: "properties": { +24: "title": {"type": "string", "description": "Title shown to user"}, +25: "content": {"type": "string", "description": "Content to review"} +26: }, +27: "required": ["title", "content"] +28: })) +29: } +30: +31: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +32: let title = args["title"].as_str() +33: .ok_or_else(|| adk_core::AdkError::tool("Missing required parameter: title".to_string()))?; +34: let content = args["content"].as_str() +35: .ok_or_else(|| adk_core::AdkError::tool("Missing required parameter: content".to_string()))?; +36: +37: +38: let interaction = get_interaction_backend() +39: .ok_or_else(|| adk_core::AdkError::tool("InteractiveBackend not set".to_string()))?; +40: +41: +42: interaction.show_message( +43: MessageLevel::Info, +44: format!("\n📝 {}\n{}\n---\n{}", +45: title, +46: "─".repeat(40), +47: content.lines().take(15).collect::>().join("\n") +48: ) +49: ).await; +50: +51: +52: let options = vec![ +53: InputOption { +54: id: "pass".to_string(), +55: label: "✓ Pass".to_string(), +56: description: Some("Continue without changes".to_string()), +57: }, +58: ]; +59: +60: let response = interaction.request_input( +61: "Type 'edit' to open editor, 'pass' to continue, or provide feedback:", +62: options, +63: Some(content.to_string()) +64: ).await.map_err(|e| adk_core::AdkError::tool(format!("Input error: {}", e)))?; +65: +66: match response { +67: InputResponse::Selection(id) => match id.as_str() { +68: "pass" => Ok(json!({ +69: "action": "pass", +70: "content": content, +71: "message": "User passed" +72: })), +73: _ => Ok(json!({ +74: "action": "pass", +75: "content": content, +76: "message": "Unknown action" +77: })) +78: }, +79: InputResponse::Text(text) => { +80: let text = text.trim(); +81: +82: if text.contains('\n') || text.len() > 100 { +83: +84: Ok(json!({ +85: "action": "edit", +86: "content": text, +87: "message": "User provided edited content" +88: })) +89: } else { +90: +91: Ok(json!({ +92: "action": "feedback", +93: "feedback": text, +94: "content": content, +95: "message": "User provided feedback" +96: })) +97: } +98: }, +99: InputResponse::Cancel => Ok(json!({ +100: "action": "pass", +101: "content": content, +102: "message": "User cancelled" +103: })), +104: } +105: } +106: } +107: ⋮---- +108: ReviewWithFeedbackContentTool +109: ⋮---- +110: { +111: fn name(&self) -> &str { +112: "review_with_feedback_content" +113: } +114: +115: fn description(&self) -> &str { +116: "Review content and allow user to: edit, pass, or provide feedback text." +117: } +118: +119: fn parameters_schema(&self) -> Option { +120: Some(json!({ +121: "type": "object", +122: "properties": { +123: "title": {"type": "string"}, +124: "content": {"type": "string"}, +125: "prompt": {"type": "string"} +126: }, +127: "required": ["title", "content"] +128: })) +129: } +130: +131: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +132: let title = args["title"].as_str() +133: .ok_or_else(|| adk_core::AdkError::tool("Missing required parameter: title".to_string()))?; +134: let content = args["content"].as_str() +135: .ok_or_else(|| adk_core::AdkError::tool("Missing required parameter: content".to_string()))?; +136: let default_prompt = "Type 'edit' to open editor, 'pass' to continue, or provide feedback:"; +137: let prompt = args.get("prompt").and_then(|v| v.as_str()).unwrap_or(default_prompt); +138: +139: +140: let interaction = get_interaction_backend() +141: .ok_or_else(|| adk_core::AdkError::tool("InteractiveBackend not set".to_string()))?; +142: +143: +144: interaction.show_message( +145: MessageLevel::Info, +146: format!("\n📝 {}\n{}\n---\n{}", +147: title, +148: "─".repeat(40), +149: content.lines().take(15).collect::>().join("\n") +150: ) +151: ).await; +152: +153: +154: let options = vec![ +155: InputOption { +156: id: "pass".to_string(), +157: label: "✓ Pass".to_string(), +158: description: Some("Continue without changes".to_string()), +159: }, +160: ]; +161: +162: let response = interaction.request_input(prompt, options, Some(content.to_string())) +163: .await.map_err(|e| adk_core::AdkError::tool(format!("Input error: {}", e)))?; +164: +165: match response { +166: InputResponse::Selection(id) => match id.as_str() { +167: "pass" => Ok(json!({ +168: "action": "pass", +169: "content": content, +170: "message": "User passed" +171: })), +172: _ => Ok(json!({ +173: "action": "pass", +174: "content": content, +175: "message": "Unknown action" +176: })) +177: }, +178: InputResponse::Text(text) => { +179: let text = text.trim(); +180: +181: if text.contains('\n') || text.len() > 100 { +182: +183: Ok(json!({ +184: "action": "edit", +185: "content": text, +186: "message": "User provided edited content" +187: })) +188: } else { +189: +190: Ok(json!({ +191: "action": "feedback", +192: "feedback": text, +193: "content": content, +194: "message": "User provided feedback" +195: })) +196: } +197: }, +198: InputResponse::Cancel => Ok(json!({ +199: "action": "pass", +200: "content": content, +201: "message": "User cancelled" +202: })), +203: } +204: } +205: } +``` + +### crates/cowork-core/src/tools/hitl_tools.rs (217 lines) + +``` +1: ReviewAndEditFileTool +2: ⋮---- +3: { +4: fn name(&self) -> &str { +5: "review_and_edit_file" +6: } +7: +8: fn description(&self) -> &str { +9: "Let the user review and optionally edit a file using their default editor. \ +10: User will be prompted: 'Do you want to edit this file? (y/n)'. \ +11: If 'y', opens the file in an editor. If 'n', continues without changes." +12: } +13: +14: fn parameters_schema(&self) -> Option { +15: Some(json!({ +16: "type": "object", +17: "properties": { +18: "file_path": { +19: "type": "string", +20: "description": "Path to the file to review and edit" +21: }, +22: "title": { +23: "type": "string", +24: "description": "Title/description for the review prompt" +25: } +26: }, +27: "required": ["file_path", "title"] +28: })) +29: } +30: +31: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +32: let file_path = get_required_string_param(&args, "file_path")?; +33: let title = get_required_string_param(&args, "title")?; +34: +35: +36: let content = fs::read_to_string(file_path) +37: .map_err(|e| adk_core::AdkError::tool(format!("Failed to read file {}: {}", file_path, e)))?; +38: +39: +40: println!("\n📝 {} - {}", title, file_path); +41: println!(" ────────────────────────────────────────"); +42: let line_count = content.lines().count(); +43: for (i, line) in content.lines().take(10).enumerate() { +44: println!(" {}: {}", i + 1, line); +45: } +46: if line_count > 10 { +47: println!(" ... ({} more lines)", line_count - 10); +48: } +49: println!(" ────────────────────────────────────────\n"); +50: +51: +52: let should_edit = Confirm::new() +53: .with_prompt("Do you want to edit this file? (y/n)") +54: .default(false) +55: .interact() +56: .map_err(|e| adk_core::AdkError::tool(format!("Interaction error: {}", e)))?; +57: +58: if !should_edit { +59: return Ok(json!({ +60: "status": "no_changes", +61: "message": "User chose not to edit the file" +62: })); +63: } +64: +65: +66: println!("📝 Opening editor... (Save and close to submit changes)"); +67: let edited = Editor::new() +68: .require_save(true) +69: .edit(&content) +70: .map_err(|e| adk_core::AdkError::tool(format!("Editor error: {}", e)))?; +71: +72: match edited { +73: Some(new_content) if new_content.trim() != content.trim() => { +74: +75: fs::write(file_path, &new_content) +76: .map_err(|e| adk_core::AdkError::tool(format!("Failed to write file: {}", e)))?; +77: +78: println!("✅ File updated successfully"); +79: Ok(json!({ +80: "status": "edited", +81: "message": "File was edited and saved", +82: "changes_made": true +83: })) +84: } +85: _ => { +86: println!("ℹ️ No changes made"); +87: Ok(json!({ +88: "status": "no_changes", +89: "message": "File was not modified" +90: })) +91: } +92: } +93: } +94: } +95: ⋮---- +96: ReviewWithFeedbackTool +97: ⋮---- +98: { +99: fn name(&self) -> &str { +100: "review_with_feedback" +101: } +102: +103: fn description(&self) -> &str { +104: "Show user a file preview and ask for feedback. User can:\n\ +105: - Type 'edit' to open the file in an editor\n\ +106: - Type 'pass' to continue without changes\n\ +107: - Type any other text to provide feedback/suggestions (agent will revise based on feedback)" +108: } +109: +110: fn parameters_schema(&self) -> Option { +111: Some(json!({ +112: "type": "object", +113: "properties": { +114: "path": { +115: "type": "string", +116: "description": "Path to the file to review" +117: }, +118: "title": { +119: "type": "string", +120: "description": "Title/description for the review prompt" +121: }, +122: "prompt": { +123: "type": "string", +124: "description": "Custom prompt to show the user (e.g., '请审查需求大纲')" +125: } +126: }, +127: "required": ["path", "title"] +128: })) +129: } +130: +131: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +132: let file_path = get_required_string_param(&args, "path")?; +133: let title = get_required_string_param(&args, "title")?; +134: let default_prompt = "输入 'edit' 编辑,'pass' 继续,或直接输入修改建议"; +135: let prompt = args["prompt"].as_str().unwrap_or(default_prompt); +136: +137: +138: let content = fs::read_to_string(file_path) +139: .map_err(|e| adk_core::AdkError::tool(format!("Failed to read file {}: {}", file_path, e)))?; +140: +141: +142: println!("\n📝 {} - {}", title, file_path); +143: println!(" ────────────────────────────────────────"); +144: let line_count = content.lines().count(); +145: for (i, line) in content.lines().take(15).enumerate() { +146: println!(" {}: {}", i + 1, line); +147: } +148: if line_count > 15 { +149: println!(" ... ({} more lines)", line_count - 15); +150: } +151: println!(" ────────────────────────────────────────\n"); +152: +153: +154: let user_input: String = Input::new() +155: .with_prompt(prompt) +156: .allow_empty(true) +157: .interact_text() +158: .map_err(|e| adk_core::AdkError::tool(format!("Interaction error: {}", e)))?; +159: +160: let user_input = user_input.trim(); +161: +162: +163: match user_input.to_lowercase().as_str() { +164: "edit" => { +165: +166: println!("📝 Opening editor... (Save and close to submit changes)"); +167: let edited = Editor::new() +168: .require_save(true) +169: .edit(&content) +170: .map_err(|e| adk_core::AdkError::tool(format!("Editor error: {}", e)))?; +171: +172: match edited { +173: Some(new_content) if new_content.trim() != content.trim() => { +174: fs::write(file_path, &new_content) +175: .map_err(|e| adk_core::AdkError::tool(format!("Failed to write file: {}", e)))?; +176: +177: println!("✅ File updated successfully"); +178: Ok(json!({ +179: "action": "edit", +180: "status": "edited", +181: "message": "User edited the file in editor", +182: "changes_made": true +183: })) +184: } +185: _ => { +186: println!("ℹ️ No changes made in editor"); +187: Ok(json!({ +188: "action": "edit", +189: "status": "no_changes", +190: "message": "User opened editor but made no changes" +191: })) +192: } +193: } +194: } +195: "pass" | "" => { +196: +197: println!("➡️ Continuing without changes..."); +198: Ok(json!({ +199: "action": "pass", +200: "status": "passed", +201: "message": "User chose to continue without changes" +202: })) +203: } +204: _ => { +205: +206: println!("💬 Feedback received: {}", user_input); +207: println!("🔄 Agent will revise based on your feedback..."); +208: Ok(json!({ +209: "action": "feedback", +210: "status": "feedback_provided", +211: "feedback": user_input, +212: "message": format!("User provided feedback: {}", user_input) +213: })) +214: } +215: } +216: } +217: } +``` + +### crates/cowork-core/src/tools/memory_tools.rs (489 lines) + +``` +1: QueryMemoryTool +2: ⋮---- +3: { +4: iteration_id: String, +5: } +6: ⋮---- +7: QueryMemoryTool +8: ⋮---- +9: { +10: pub fn new(iteration_id: String) -> Self { +11: Self { iteration_id } +12: } +13: } +14: ⋮---- +15: QueryMemoryTool +16: ⋮---- +17: { +18: fn name(&self) -> &str { +19: "query_memory" +20: } +21: +22: fn description(&self) -> &str { +23: "Query memory to retrieve decisions, patterns, and insights. Use this to understand project context and previous experiences." +24: } +25: +26: fn parameters_schema(&self) -> Option { +27: Some(json!({ +28: "type": "object", +29: "properties": { +30: "scope": { +31: "type": "string", +32: "description": "Memory scope: 'project' (project-level only), 'iteration' (current iteration only), or 'smart' (merged, recommended)", +33: "enum": ["project", "iteration", "smart"], +34: "default": "smart" +35: }, +36: "query_type": { +37: "type": "string", +38: "description": "Type of memory to query: 'decisions', 'patterns', 'insights', or 'all'", +39: "enum": ["decisions", "patterns", "insights", "all"], +40: "default": "all" +41: }, +42: "keywords": { +43: "type": "array", +44: "description": "Keywords for filtering results (optional)", +45: "items": {"type": "string"}, +46: "default": [] +47: }, +48: "limit": { +49: "type": "number", +50: "description": "Maximum results per category. Default: 20", +51: "default": 20 +52: } +53: }, +54: "required": [] +55: })) +56: } +57: +58: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +59: let scope_str = args.get("scope").and_then(|v| v.as_str()).unwrap_or("smart"); +60: let query_type_str = args.get("query_type").and_then(|v| v.as_str()).unwrap_or("all"); +61: let keywords: Vec = args.get("keywords") +62: .and_then(|v| v.as_array()) +63: .map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect()) +64: .unwrap_or_default(); +65: let limit = args.get("limit").and_then(|v| v.as_i64()).unwrap_or(20) as usize; +66: +67: let scope = match scope_str { +68: "project" => MemoryScope::Project, +69: "iteration" => MemoryScope::Iteration, +70: "smart" => MemoryScope::Smart, +71: _ => MemoryScope::Smart, +72: }; +73: +74: let query_type = match query_type_str { +75: "decisions" => MemoryQueryType::Decisions, +76: "patterns" => MemoryQueryType::Patterns, +77: "insights" => MemoryQueryType::Insights, +78: "all" => MemoryQueryType::All, +79: _ => MemoryQueryType::All, +80: }; +81: +82: let query = MemoryQuery { +83: scope, +84: query_type, +85: keywords: keywords.clone(), +86: limit: Some(limit), +87: }; +88: +89: let store = MemoryStore::new(); +90: +91: let result = store.query(&query, Some(&self.iteration_id)) +92: .map_err(|e| adk_core::AdkError::tool(format!("Failed to query memory: {}", e)))?; +93: +94: Ok(json!({ +95: "decisions": result.decisions, +96: "patterns": result.patterns, +97: "insights": result.insights, +98: "total_decisions": result.decisions.len(), +99: "total_patterns": result.patterns.len(), +100: "total_insights": result.insights.len(), +101: "context_string": result.to_context_string() +102: })) +103: } +104: } +105: ⋮---- +106: SaveInsightTool +107: ⋮---- +108: { +109: iteration_id: String, +110: } +111: ⋮---- +112: SaveInsightTool +113: ⋮---- +114: { +115: pub fn new(iteration_id: String) -> Self { +116: Self { iteration_id } +117: } +118: } +119: ⋮---- +120: SaveInsightTool +121: ⋮---- +122: { +123: fn name(&self) -> &str { +124: "save_insight" +125: } +126: +127: fn description(&self) -> &str { +128: "Save an insight to the current iteration's memory. Use this to record important observations, discoveries, or realizations during development." +129: } +130: +131: fn parameters_schema(&self) -> Option { +132: Some(json!({ +133: "type": "object", +134: "properties": { +135: "stage": { +136: "type": "string", +137: "description": "The current stage (e.g., 'idea', 'prd', 'design', 'plan', 'coding', 'check')" +138: }, +139: "content": { +140: "type": "string", +141: "description": "The insight content (what you discovered or realized)" +142: }, +143: "importance": { +144: "type": "string", +145: "description": "Importance level: 'critical', 'important', or 'normal'", +146: "enum": ["critical", "important", "normal"], +147: "default": "important" +148: } +149: }, +150: "required": ["stage", "content"] +151: })) +152: } +153: +154: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +155: let stage = args.get("stage").and_then(|v| v.as_str()) +156: .ok_or_else(|| adk_core::AdkError::tool("stage is required".to_string()))?; +157: let content = args.get("content").and_then(|v| v.as_str()) +158: .ok_or_else(|| adk_core::AdkError::tool("content is required".to_string()))?; +159: let importance_str = args.get("importance").and_then(|v| v.as_str()).unwrap_or("important"); +160: +161: let importance = match importance_str { +162: "critical" => Importance::Critical, +163: "important" => Importance::Important, +164: "normal" => Importance::Normal, +165: _ => Importance::Important, +166: }; +167: +168: let store = MemoryStore::new(); +169: let mut memory = store.load_iteration_memory(&self.iteration_id) +170: .unwrap_or_else(|_| IterationMemory::new(&self.iteration_id)); +171: +172: memory.insights.push(crate::domain::Insight { +173: stage: stage.to_string(), +174: content: content.to_string(), +175: importance, +176: created_at: chrono::Utc::now(), +177: }); +178: +179: store.save_iteration_memory(&memory) +180: .map_err(|e| adk_core::AdkError::tool(format!("Failed to save insight: {}", e)))?; +181: +182: Ok(json!({ +183: "message": "Insight saved successfully", +184: "iteration_id": self.iteration_id, +185: "total_insights": memory.insights.len() +186: })) +187: } +188: } +189: ⋮---- +190: SaveIssueTool +191: ⋮---- +192: { +193: iteration_id: String, +194: } +195: ⋮---- +196: SaveIssueTool +197: ⋮---- +198: { +199: pub fn new(iteration_id: String) -> Self { +200: Self { iteration_id } +201: } +202: } +203: ⋮---- +204: SaveIssueTool +205: ⋮---- +206: { +207: fn name(&self) -> &str { +208: "save_issue" +209: } +210: +211: fn description(&self) -> &str { +212: "Save an issue to the current iteration's memory. Use this to record problems, bugs, or obstacles encountered during development." +213: } +214: +215: fn parameters_schema(&self) -> Option { +216: Some(json!({ +217: "type": "object", +218: "properties": { +219: "stage": { +220: "type": "string", +221: "description": "The current stage where the issue occurred" +222: }, +223: "content": { +224: "type": "string", +225: "description": "The issue description (what problem occurred)" +226: } +227: }, +228: "required": ["stage", "content"] +229: })) +230: } +231: +232: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +233: let stage = args.get("stage").and_then(|v| v.as_str()) +234: .ok_or_else(|| adk_core::AdkError::tool("stage is required".to_string()))?; +235: let content = args.get("content").and_then(|v| v.as_str()) +236: .ok_or_else(|| adk_core::AdkError::tool("content is required".to_string()))?; +237: +238: let store = MemoryStore::new(); +239: let mut memory = store.load_iteration_memory(&self.iteration_id) +240: .unwrap_or_else(|_| IterationMemory::new(&self.iteration_id)); +241: +242: memory.issues.push(crate::domain::Issue { +243: stage: stage.to_string(), +244: content: content.to_string(), +245: resolved: false, +246: created_at: chrono::Utc::now(), +247: resolved_at: None, +248: }); +249: +250: store.save_iteration_memory(&memory) +251: .map_err(|e| adk_core::AdkError::tool(format!("Failed to save issue: {}", e)))?; +252: +253: Ok(json!({ +254: "message": "Issue saved successfully", +255: "iteration_id": self.iteration_id, +256: "total_issues": memory.issues.len() +257: })) +258: } +259: } +260: ⋮---- +261: SaveLearningTool +262: ⋮---- +263: { +264: iteration_id: String, +265: } +266: ⋮---- +267: SaveLearningTool +268: ⋮---- +269: { +270: pub fn new(iteration_id: String) -> Self { +271: Self { iteration_id } +272: } +273: } +274: ⋮---- +275: SaveLearningTool +276: ⋮---- +277: { +278: fn name(&self) -> &str { +279: "save_learning" +280: } +281: +282: fn description(&self) -> &str { +283: "Save a learning to the current iteration's memory. Use this to record knowledge gained, lessons learned, or skills developed." +284: } +285: +286: fn parameters_schema(&self) -> Option { +287: Some(json!({ +288: "type": "object", +289: "properties": { +290: "content": { +291: "type": "string", +292: "description": "The learning content (what you learned)" +293: } +294: }, +295: "required": ["content"] +296: })) +297: } +298: +299: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +300: let content = args.get("content").and_then(|v| v.as_str()) +301: .ok_or_else(|| adk_core::AdkError::tool("content is required".to_string()))?; +302: +303: let store = MemoryStore::new(); +304: let mut memory = store.load_iteration_memory(&self.iteration_id) +305: .unwrap_or_else(|_| IterationMemory::new(&self.iteration_id)); +306: +307: memory.learnings.push(Learning { +308: content: content.to_string(), +309: created_at: chrono::Utc::now(), +310: }); +311: +312: store.save_iteration_memory(&memory) +313: .map_err(|e| adk_core::AdkError::tool(format!("Failed to save learning: {}", e)))?; +314: +315: Ok(json!({ +316: "message": "Learning saved successfully", +317: "iteration_id": self.iteration_id, +318: "total_learnings": memory.learnings.len() +319: })) +320: } +321: } +322: ⋮---- +323: PromoteToDecisionTool +324: ⋮---- +325: { +326: iteration_id: String, +327: } +328: ⋮---- +329: PromoteToDecisionTool +330: ⋮---- +331: { +332: pub fn new(iteration_id: String) -> Self { +333: Self { iteration_id } +334: } +335: } +336: ⋮---- +337: PromoteToDecisionTool +338: ⋮---- +339: { +340: fn name(&self) -> &str { +341: "promote_to_decision" +342: } +343: +344: fn description(&self) -> &str { +345: "Promote an insight or learning to a project-level decision. Use this for important decisions that should be remembered across iterations." +346: } +347: +348: fn parameters_schema(&self) -> Option { +349: Some(json!({ +350: "type": "object", +351: "properties": { +352: "title": { +353: "type": "string", +354: "description": "Decision title (concise summary)" +355: }, +356: "context": { +357: "type": "string", +358: "description": "Context and background for this decision" +359: }, +360: "decision": { +361: "type": "string", +362: "description": "The actual decision made" +363: }, +364: "consequences": { +365: "type": "array", +366: "description": "Expected consequences of this decision", +367: "items": {"type": "string"}, +368: "default": [] +369: } +370: }, +371: "required": ["title", "context", "decision"] +372: })) +373: } +374: +375: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +376: let title = args.get("title").and_then(|v| v.as_str()) +377: .ok_or_else(|| adk_core::AdkError::tool("title is required".to_string()))?; +378: let context = args.get("context").and_then(|v| v.as_str()) +379: .ok_or_else(|| adk_core::AdkError::tool("context is required".to_string()))?; +380: let decision_str = args.get("decision").and_then(|v| v.as_str()) +381: .ok_or_else(|| adk_core::AdkError::tool("decision is required".to_string()))?; +382: let consequences: Vec = args.get("consequences") +383: .and_then(|v| v.as_array()) +384: .map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect()) +385: .unwrap_or_default(); +386: +387: let mut new_decision = Decision::new(title, context, decision_str, &self.iteration_id); +388: new_decision.consequences = consequences; +389: +390: let store = MemoryStore::new(); +391: store.add_decision(new_decision) +392: .map_err(|e| adk_core::AdkError::tool(format!("Failed to promote to decision: {}", e)))?; +393: +394: Ok(json!({ +395: "message": "Promoted to project decision successfully", +396: "iteration_id": self.iteration_id +397: })) +398: } +399: } +400: ⋮---- +401: PromoteToPatternTool +402: ⋮---- +403: { +404: iteration_id: String, +405: } +406: ⋮---- +407: PromoteToPatternTool +408: ⋮---- +409: { +410: pub fn new(iteration_id: String) -> Self { +411: Self { iteration_id } +412: } +413: } +414: ⋮---- +415: PromoteToPatternTool +416: ⋮---- +417: { +418: fn name(&self) -> &str { +419: "promote_to_pattern" +420: } +421: +422: fn description(&self) -> &str { +423: "Promote an insight or learning to a project-level pattern. Use this for reusable solutions or best practices that apply across iterations." +424: } +425: +426: fn parameters_schema(&self) -> Option { +427: Some(json!({ +428: "type": "object", +429: "properties": { +430: "name": { +431: "type": "string", +432: "description": "Pattern name" +433: }, +434: "description": { +435: "type": "string", +436: "description": "Pattern description (when and how to use it)" +437: }, +438: "usage": { +439: "type": "array", +440: "description": "Usage examples or scenarios", +441: "items": {"type": "string"}, +442: "default": [] +443: }, +444: "tags": { +445: "type": "array", +446: "description": "Tags for categorizing and searching the pattern", +447: "items": {"type": "string"}, +448: "default": [] +449: }, +450: "code_example": { +451: "type": "string", +452: "description": "Optional code example (if applicable)", +453: "default": null +454: } +455: }, +456: "required": ["name", "description"] +457: })) +458: } +459: +460: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +461: let name = args.get("name").and_then(|v| v.as_str()) +462: .ok_or_else(|| adk_core::AdkError::tool("name is required".to_string()))?; +463: let description = args.get("description").and_then(|v| v.as_str()) +464: .ok_or_else(|| adk_core::AdkError::tool("description is required".to_string()))?; +465: let usage: Vec = args.get("usage") +466: .and_then(|v| v.as_array()) +467: .map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect()) +468: .unwrap_or_default(); +469: let tags: Vec = args.get("tags") +470: .and_then(|v| v.as_array()) +471: .map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect()) +472: .unwrap_or_default(); +473: let code_example = args.get("code_example").and_then(|v| v.as_str()).map(|s| s.to_string()); +474: +475: let mut new_pattern = Pattern::new(name, description, &self.iteration_id); +476: new_pattern.usage = usage; +477: new_pattern.tags = tags; +478: new_pattern.code_example = code_example; +479: +480: let store = MemoryStore::new(); +481: store.add_pattern(new_pattern) +482: .map_err(|e| adk_core::AdkError::tool(format!("Failed to promote to pattern: {}", e)))?; +483: +484: Ok(json!({ +485: "message": "Promoted to project pattern successfully", +486: "iteration_id": self.iteration_id +487: })) +488: } +489: } +``` + +### crates/cowork-core/src/tools/test_lint_tools.rs (255 lines) + +``` +1: CheckTestsTool +2: ⋮---- +3: { +4: fn name(&self) -> &str { +5: "check_tests" +6: } +7: +8: fn description(&self) -> &str { +9: "Run project tests and return results. Automatically detects project type \ +10: (Rust, Node.js, Python, etc.) and runs appropriate test command." +11: } +12: +13: fn parameters_schema(&self) -> Option { +14: Some(json!({ +15: "type": "object", +16: "properties": { +17: "path": { +18: "type": "string", +19: "description": "Project directory path (default: current directory)" +20: }, +21: "test_command": { +22: "type": "string", +23: "description": "Optional: Override auto-detected test command (e.g., 'cargo test', 'npm test')" +24: } +25: } +26: })) +27: } +28: +29: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +30: let path = args.get("path") +31: .and_then(|v| v.as_str()) +32: .unwrap_or("."); +33: +34: +35: let test_command = if let Some(cmd) = args.get("test_command").and_then(|v| v.as_str()) { +36: cmd.to_string() +37: } else { +38: detect_test_command(path)? +39: }; +40: +41: +42: let output = tokio::process::Command::new("sh") +43: .arg("-c") +44: .arg(&test_command) +45: .current_dir(path) +46: .output() +47: .await +48: .map_err(|e| adk_core::AdkError::tool(format!("Failed to run tests: {}", e)))?; +49: +50: let stdout = String::from_utf8_lossy(&output.stdout).to_string(); +51: let stderr = String::from_utf8_lossy(&output.stderr).to_string(); +52: let success = output.status.success(); +53: +54: +55: let (passed, failed, total) = parse_test_output(&stdout, &stderr); +56: +57: Ok(json!({ +58: "status": if success { "passed" } else { "failed" }, +59: "command": test_command, +60: "exit_code": output.status.code(), +61: "tests_passed": passed, +62: "tests_failed": failed, +63: "tests_total": total, +64: "stdout": stdout, +65: "stderr": stderr +66: })) +67: } +68: } +69: ⋮---- +70: CheckLintTool +71: ⋮---- +72: { +73: fn name(&self) -> &str { +74: "check_lint" +75: } +76: +77: fn description(&self) -> &str { +78: "Run linter/code quality checks and return results. Automatically detects \ +79: project type and runs appropriate linter (clippy for Rust, eslint for Node.js, etc.)." +80: } +81: +82: fn parameters_schema(&self) -> Option { +83: Some(json!({ +84: "type": "object", +85: "properties": { +86: "path": { +87: "type": "string", +88: "description": "Project directory path (default: current directory)" +89: }, +90: "lint_command": { +91: "type": "string", +92: "description": "Optional: Override auto-detected lint command (e.g., 'cargo clippy', 'npm run lint')" +93: }, +94: "fix": { +95: "type": "boolean", +96: "description": "Whether to auto-fix issues if supported (default: false)" +97: } +98: } +99: })) +100: } +101: +102: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +103: let path = args.get("path") +104: .and_then(|v| v.as_str()) +105: .unwrap_or("."); +106: +107: let fix = args.get("fix") +108: .and_then(|v| v.as_bool()) +109: .unwrap_or(false); +110: +111: +112: let lint_command = if let Some(cmd) = args.get("lint_command").and_then(|v| v.as_str()) { +113: cmd.to_string() +114: } else { +115: detect_lint_command(path, fix)? +116: }; +117: +118: +119: let output = tokio::process::Command::new("sh") +120: .arg("-c") +121: .arg(&lint_command) +122: .current_dir(path) +123: .output() +124: .await +125: .map_err(|e| adk_core::AdkError::tool(format!("Failed to run linter: {}", e)))?; +126: +127: let stdout = String::from_utf8_lossy(&output.stdout).to_string(); +128: let stderr = String::from_utf8_lossy(&output.stderr).to_string(); +129: let success = output.status.success(); +130: +131: +132: let (warnings, errors) = parse_lint_output(&stdout, &stderr); +133: +134: Ok(json!({ +135: "status": if success { "clean" } else { "issues_found" }, +136: "command": lint_command, +137: "exit_code": output.status.code(), +138: "warnings": warnings, +139: "errors": errors, +140: "total_issues": warnings + errors, +141: "stdout": stdout, +142: "stderr": stderr +143: })) +144: } +145: } +146: ⋮---- +147: detect_test_command +148: ⋮---- +149: (path: &str) +150: ⋮---- +151: detect_lint_command +152: ⋮---- +153: (path: &str, fix: bool) +154: ⋮---- +155: parse_test_output +156: ⋮---- +157: (stdout: &str, _stderr: &str) +158: ⋮---- +159: parse_lint_output +160: ⋮---- +161: (stdout: &str, stderr: &str) +162: ⋮---- +163: ExecuteShellCommandTool +164: ⋮---- +165: { +166: fn name(&self) -> &str { +167: "execute_shell_command" +168: } +169: +170: fn description(&self) -> &str { +171: "Execute a shell command and return the result. Use this to run \ +172: installation, build, or test commands extracted from README.md. \ +173: Supports both Windows (PowerShell) and Unix (bash) commands." +174: } +175: +176: fn parameters_schema(&self) -> Option { +177: Some(json!({ +178: "type": "object", +179: "properties": { +180: "command": { +181: "type": "string", +182: "description": "The shell command to execute" +183: }, +184: "description": { +185: "type": "string", +186: "description": "Description of what this command does (e.g., 'Install dependencies')" +187: }, +188: "timeout": { +189: "type": "integer", +190: "description": "Timeout in seconds (default: 120)" +191: } +192: }, +193: "required": ["command", "description"] +194: })) +195: } +196: +197: async fn execute(&self, _ctx: Arc, args: Value) -> adk_core::Result { +198: let command = get_required_string_param(&args, "command")?; +199: let description = get_optional_string_param(&args, "description").unwrap_or_default(); +200: let timeout = args.get("timeout") +201: .and_then(|v| v.as_u64()) +202: .unwrap_or(120); +203: +204: +205: let (shell, shell_arg) = if cfg!(target_os = "windows") { +206: ("powershell.exe", vec!["-NoProfile", "-Command", command]) +207: } else { +208: ("sh", vec!["-c", command]) +209: }; +210: +211: +212: let result = tokio::time::timeout( +213: std::time::Duration::from_secs(timeout), +214: tokio::process::Command::new(shell) +215: .args(&shell_arg) +216: .output() +217: ).await; +218: +219: match result { +220: Ok(Ok(output)) => { +221: let stdout = String::from_utf8_lossy(&output.stdout).to_string(); +222: let stderr = String::from_utf8_lossy(&output.stderr).to_string(); +223: let success = output.status.success(); +224: +225: Ok(json!({ +226: "status": if success { "success" } else { "failed" }, +227: "description": description, +228: "command": command, +229: "exit_code": output.status.code(), +230: "stdout": stdout, +231: "stderr": stderr, +232: "timeout": false +233: })) +234: } +235: Ok(Err(e)) => { +236: Ok(json!({ +237: "status": "error", +238: "description": description, +239: "command": command, +240: "error": e.to_string(), +241: "timeout": false +242: })) +243: } +244: Err(_) => { +245: Ok(json!({ +246: "status": "timeout", +247: "description": description, +248: "command": command, +249: "error": format!("Command timed out after {} seconds", timeout), +250: "timeout": true +251: })) +252: } +253: } +254: } +255: } +``` + +### crates/cowork-gui/src/assets.d.ts (24 lines) + +``` +1: declare module '*.png' { +2: const value: string; +3: export default value; +4: } +5: +6: declare module '*.jpg' { +7: const value: string; +8: export default value; +9: } +10: +11: declare module '*.svg' { +12: const value: string; +13: export default value; +14: } +15: +16: declare module '*.webp' { +17: const value: string; +18: export default value; +19: } +20: +21: declare module '*.gif' { +22: const value: string; +23: export default value; +24: } +``` + +### crates/cowork-gui/src/components/ArtifactsViewer.tsx (34 lines) + +``` +1: ArtifactsData +2: ⋮---- +3: { +4: iteration_id?: string; +5: idea?: string; +6: requirements?: string; +7: design?: unknown; +8: design_raw?: string; +9: plan?: unknown; +10: plan_raw?: string; +11: code_files?: FileInfo[]; +12: check_report?: string; +13: delivery_report?: string; +14: } +15: ⋮---- +16: FileInfo +17: ⋮---- +18: { +19: path: string; +20: name: string; +21: size: number; +22: is_dir: boolean; +23: language?: string; +24: modified_at?: string; +25: } +26: ⋮---- +27: ArtifactsViewerProps +28: ⋮---- +29: { +30: iterationId: string; +31: activeTab?: string; +32: onTabChange?: (key: string) => void; +33: refreshTrigger?: number; +34: } +``` + +### crates/cowork-gui/src/components/ProjectsPanel.tsx (348 lines) + +``` +1: import { useState } from "react"; +2: import { invoke } from "@tauri-apps/api/core"; +3: import { +4: App, +5: Card, +6: Button, +7: Modal, +8: Tag, +9: Empty, +10: Spin, +11: Tooltip, +12: Space, +13: } from "antd"; +14: import { +15: FolderOpenOutlined, +16: DeleteOutlined, +17: EditOutlined, +18: CheckCircleOutlined, +19: ClockCircleOutlined, +20: PlusOutlined, +21: ImportOutlined, +22: } from "@ant-design/icons"; +23: +24: import { useProjectsData } from '../hooks'; +25: import { CreateProjectModal, EditProjectModal, ImportProjectModal } from './projects'; +26: import type { ProjectData } from '../types'; +27: +28: const ProjectsPanel: React.FC = () => { +29: const { message } = App.useApp(); +30: const { projects, loading, loadProjects } = useProjectsData(); +31: +32: +33: const [showCreateModal, setShowCreateModal] = useState(false); +34: const [showEditModal, setShowEditModal] = useState(false); +35: const [showImportModal, setShowImportModal] = useState(false); +36: const [selectedProject, setSelectedProject] = useState(null); +37: +38: +39: const handleDeleteProject = async (project: ProjectData) => { +40: Modal.confirm({ +41: title: "Delete Project", +42: content: `Remove "${project.name}" from project list? The project files will remain on disk.`, +43: okText: "Delete", +44: okType: "danger", +45: onOk: async () => { +46: try { +47: await invoke("delete_project", { projectId: project.project_id }); +48: message.success("Project removed from list"); +49: loadProjects(); +50: } catch (error) { +51: message.error("Failed to delete project: " + error); +52: } +53: }, +54: }); +55: }; +56: +57: const handleOpenProject = async (projectId: string) => { +58: try { +59: const hasProject = await invoke("has_open_project"); +60: +61: if (hasProject) { +62: await invoke("open_project", { projectId }); +63: message.info("Opening project in new window..."); +64: } else { +65: await invoke("open_project_in_current_window", { projectId }); +66: message.success("Project opened successfully"); +67: } +68: } catch (error) { +69: message.error("Failed to open project: " + error); +70: } +71: }; +72: +73: const handleOpenEditModal = (project: ProjectData) => { +74: setSelectedProject(project); +75: setShowEditModal(true); +76: }; +77: +78: const handleProjectCreated = async (projectId: string, projectName: string) => { +79: loadProjects(); +80: +81: +82: Modal.confirm({ +83: title: "Open Project?", +84: content: `Would you like to open "${projectName}" now?`, +85: okText: "Open Project", +86: cancelText: "Later", +87: onOk: async () => { +88: try { +89: const hasProject = await invoke("has_open_project"); +90: if (hasProject) { +91: await invoke("open_project", { projectId }); +92: message.info("Opening project in new window..."); +93: } else { +94: await invoke("open_project_in_current_window", { projectId }); +95: message.success("Project opened successfully"); +96: } +97: } catch (error) { +98: message.error("Failed to open project: " + error); +99: } +100: }, +101: }); +102: }; +103: +104: const handleProjectImported = async (projectId: string, projectName: string) => { +105: loadProjects(); +106: message.success(`Project "${projectName}" imported successfully!`); +107: +108: +109: Modal.confirm({ +110: title: "Open Project?", +111: content: `Would you like to open "${projectName}" now?`, +112: okText: "Open Project", +113: cancelText: "Later", +114: onOk: async () => { +115: try { +116: const hasProject = await invoke("has_open_project"); +117: if (hasProject) { +118: await invoke("open_project", { projectId }); +119: message.info("Opening project in new window..."); +120: } else { +121: await invoke("open_project_in_current_window", { projectId }); +122: message.success("Project opened successfully"); +123: } +124: } catch (error) { +125: message.error("Failed to open project: " + error); +126: } +127: }, +128: }); +129: }; +130: +131: +132: const formatDate = (dateString?: string): string => { +133: if (!dateString) return "Never"; +134: const date = new Date(dateString); +135: return date.toLocaleDateString("en-US", { +136: year: "numeric", +137: month: "short", +138: day: "numeric", +139: hour: "2-digit", +140: minute: "2-digit", +141: }); +142: }; +143: +144: const getDisplayPath = (fullPath?: string): string => { +145: if (!fullPath) return "No path"; +146: const parts = fullPath.split(/[/\\]/); +147: if (parts.length >= 2) { +148: return ".../" + parts.slice(-2).join("/"); +149: } +150: return fullPath; +151: }; +152: +153: const getStatusColor = (status: string): "green" | "default" | "red" => { +154: switch (status) { +155: case "active": +156: return "green"; +157: case "archived": +158: return "default"; +159: case "deleted": +160: return "red"; +161: default: +162: return "default"; +163: } +164: }; +165: +166: +167: if (loading) { +168: return ( +169:
+170: +171:
Loading projects...
+172:
+173: ); +174: } +175: +176: return ( +177:
+178: {} +179:
+187:

Projects

+188: +189: +192: +195: +196:
+197: +198: {} +199: {projects.length === 0 ? ( +200: +201: +202: +205: +208: +209: +210: ) : ( +211:
+218: {projects.map((project) => ( +219: } +226: onClick={() => handleOpenProject(project.project_id || project.projectId || "")} +227: style={{ color: "#1890ff", width: "90%" }} +228: > +229: Open +230: , +231: , +239: , +248: ]} +249: > +250: +253: {project.name} +254: +255: {project.status} +256: +257:
+258: } +259: description={ +260:
+261: +262:
+272: {project.description || "No description provided"} +273:
+274:
+275:
+276: +277: +287: +288: {getDisplayPath(project.workspace_path || project.workspacePath)} +289: +290: +291:
+292:
+293: +294: +295: {project.metadata?.session_count || 0} sessions +296: +297: +298: +299: Last opened: {formatDate(project.last_opened_at)} +300: +301:
+302: {project.metadata?.technology_stack?.length > 0 && ( +303:
+304: {project.metadata.technology_stack.slice(0, 4).map((tech, idx) => ( +305: +306: {tech} +307: +308: ))} +309: {project.metadata.technology_stack.length > 4 && ( +310: +{project.metadata.technology_stack.length - 4} +311: )} +312:
+313: )} +314:
+315: } +316: /> +317: +318: ))} +319:
+320: )} +321: +322: {} +323: setShowCreateModal(false)} +326: onSuccess={handleProjectCreated} +327: /> +328: +329: { +332: setShowEditModal(false); +333: setSelectedProject(null); +334: }} +335: onSuccess={loadProjects} +336: project={selectedProject} +337: /> +338: +339: setShowImportModal(false)} +342: onSuccess={handleProjectImported} +343: /> +344:
+345: ); +346: }; +347: +348: export default ProjectsPanel; +``` + +### crates/cowork-gui/src/components/config/AgentConfigForm.tsx (712 lines) + +``` +1: import React, { useState, useEffect } from 'react'; +2: import { +3: Card, +4: List, +5: Button, +6: Space, +7: Typography, +8: Tag, +9: Modal, +10: Form, +11: Input, +12: Select, +13: Switch, +14: Slider, +15: message, +16: Popconfirm, +17: Empty, +18: Drawer, +19: Descriptions, +20: Divider, +21: Tabs, +22: InputNumber, +23: Alert, +24: Tooltip, +25: } from 'antd'; +26: import { +27: PlusOutlined, +28: EditOutlined, +29: DeleteOutlined, +30: ExportOutlined, +31: ImportOutlined, +32: RobotOutlined, +33: ToolOutlined, +34: CodeOutlined, +35: SettingOutlined, +36: InfoCircleOutlined, +37: FolderOpenOutlined, +38: } from '@ant-design/icons'; +39: import { useConfigStore } from '../../stores/configStore'; +40: import type { AgentDefinition, ToolReference, AgentType, ModelConfig, BuiltinInstruction, InstructionType, ToolInfo } from '../../types/config'; +41: import { open } from '@tauri-apps/plugin-dialog'; +42: +43: const { Title, Text, Paragraph } = Typography; +44: const { TextArea } = Input; +45: +46: const AgentConfigForm: React.FC = () => { +47: const { +48: agents, +49: skills, +50: selectedAgent, +51: selectAgent, +52: saveAgent, +53: deleteAgent, +54: validateAgent, +55: exportConfig, +56: importConfig, +57: getBuiltinInstructions, +58: availableTools, +59: loadAvailableTools, +60: getToolsByCategory, +61: } = useConfigStore(); +62: +63: const [editModalVisible, setEditModalVisible] = useState(false); +64: const [editingAgent, setEditingAgent] = useState(null); +65: const [detailDrawerVisible, setDetailDrawerVisible] = useState(false); +66: const [importModalVisible, setImportModalVisible] = useState(false); +67: const [importJson, setImportJson] = useState(''); +68: const [form] = Form.useForm(); +69: +70: +71: const [builtinInstructions, setBuiltinInstructions] = useState([]); +72: const [instructionType, setInstructionType] = useState('builtin'); +73: const [selectedBuiltinId, setSelectedBuiltinId] = useState(''); +74: const [instructionFilePath, setInstructionFilePath] = useState(''); +75: const [instructionInlineContent, setInstructionInlineContent] = useState(''); +76: +77: +78: useEffect(() => { +79: getBuiltinInstructions().then(setBuiltinInstructions); +80: loadAvailableTools(); +81: }, [getBuiltinInstructions, loadAvailableTools]); +82: +83: const handleCreate = () => { +84: setEditingAgent(null); +85: form.resetFields(); +86: form.setFieldsValue({ +87: id: `agent-${Date.now()}`, +88: name: '', +89: description: '', +90: agent_type: 'simple', +91: instruction: '', +92: tools: [], +93: skills: [], +94: model: {}, +95: include_contents: 'none', +96: tags: [], +97: }); +98: +99: setInstructionType('builtin'); +100: setSelectedBuiltinId(''); +101: setInstructionFilePath(''); +102: setInstructionInlineContent(''); +103: setEditModalVisible(true); +104: }; +105: +106: +107: const parseInstruction = (instruction: string): { type: InstructionType; builtinId: string; filePath: string; content: string } => { +108: if (instruction.startsWith('builtin://')) { +109: return { +110: type: 'builtin', +111: builtinId: instruction.substring('builtin://'.length), +112: filePath: '', +113: content: '', +114: }; +115: } else if (instruction.startsWith('file://')) { +116: return { +117: type: 'file', +118: builtinId: '', +119: filePath: instruction.substring('file://'.length), +120: content: '', +121: }; +122: } else if (instruction.startsWith('inline://')) { +123: return { +124: type: 'inline', +125: builtinId: '', +126: filePath: '', +127: content: instruction.substring('inline://'.length), +128: }; +129: } else { +130: +131: +132: const matchingBuiltin = builtinInstructions.find(bi => bi.id === instruction); +133: if (matchingBuiltin) { +134: return { +135: type: 'builtin', +136: builtinId: instruction, +137: filePath: '', +138: content: '', +139: }; +140: } +141: +142: return { +143: type: 'inline', +144: builtinId: '', +145: filePath: '', +146: content: instruction, +147: }; +148: } +149: }; +150: +151: const handleEdit = (agent: AgentDefinition) => { +152: setEditingAgent(agent); +153: +154: const toolIds = agent.tools.map(t => t.tool_id); +155: form.setFieldsValue({ +156: ...agent, +157: agent_type: typeof agent.agent_type === 'string' ? agent.agent_type : 'loop', +158: tools: toolIds, +159: }); +160: +161: +162: const parsed = parseInstruction(agent.instruction); +163: setInstructionType(parsed.type); +164: setSelectedBuiltinId(parsed.builtinId); +165: setInstructionFilePath(parsed.filePath); +166: +167: +168: if (parsed.type === 'inline' && parsed.content) { +169: setInstructionInlineContent(parsed.content); +170: } else if (parsed.type === 'builtin' && parsed.builtinId) { +171: const builtin = builtinInstructions.find(bi => bi.id === parsed.builtinId); +172: setInstructionInlineContent(builtin?.content || ''); +173: } else { +174: setInstructionInlineContent(parsed.content); +175: } +176: +177: setEditModalVisible(true); +178: selectAgent(agent.id); +179: }; +180: +181: const handleView = (agent: AgentDefinition) => { +182: selectAgent(agent.id); +183: setDetailDrawerVisible(true); +184: }; +185: +186: const handleDelete = async (id: string) => { +187: try { +188: await deleteAgent(id); +189: message.success('Agent deleted successfully'); +190: } catch (error) { +191: message.error('Failed to delete agent'); +192: } +193: }; +194: +195: const handleSave = async () => { +196: try { +197: const values = await form.validateFields(); +198: +199: +200: const tools: ToolReference[] = (values.tools || []).map((toolId: string) => ({ +201: tool_id: toolId, +202: })); +203: +204: +205: let instruction = ''; +206: switch (instructionType) { +207: case 'builtin': +208: instruction = `builtin: +209: beak; +210: case 'file': +211: instruction = `file: +212: beak; +213: case 'inline': +214: instruction = `inline: +215: beak; +216: } +217: +218: const agent: AgentDefinition = { +219: ...editingAgent, +220: ...values, +221: instruction, +222: tools, +223: metadata: {}, +224: }; +225: +226: const validation = await validateAgent(agent); +227: if (!validation.valid) { +228: const errors = validation.issues.map(i => i.message).join(', '); +229: message.error(`Validation failed: ${errors}`); +230: return; +231: } +232: +233: await saveAgent(agent); +234: message.success('Agent saved successfully'); +235: setEditModalVisible(false); +236: } catch (error) { +237: message.error('Failed to save agent'); +238: } +239: }; +240: +241: const handleExport = async (id: string) => { +242: try { +243: const json = await exportConfig('agent', id); +244: navigator.clipboard.writeText(json); +245: message.success('Agent exported to clipboard'); +246: } catch (error) { +247: message.error('Failed to export agent'); +248: } +249: }; +250: +251: const handleImport = async () => { +252: try { +253: await importConfig('agent', importJson); +254: message.success('Agent imported successfully'); +255: setImportModalVisible(false); +256: setImportJson(''); +257: } catch (error) { +258: message.error('Failed to import agent'); +259: } +260: }; +261: +262: const selectedAgentData = selectedAgent ? agents[selectedAgent] : null; +263: +264: const getAgentTypeTag = (type: AgentType) => { +265: if (typeof type === 'string') { +266: return {type}; +267: } +268: return loop ({(type as { loop: { max_iterations?: number } }).loop?.max_iterations || 'unlimited'}); +269: }; +270: +271: +272: const handleSelectInstructionFile = async () => { +273: try { +274: const selected = await open({ +275: multiple: false, +276: filters: [{ name: 'Markdown', extensions: ['md', 'txt'] }], +277: }); +278: if (selected && typeof selected === 'string') { +279: setInstructionFilePath(selected); +280: } +281: } catch (error) { +282: console.error('Failed to select file:', error); +283: } +284: }; +285: +286: +287: const handleBuiltinChange = (builtinId: string) => { +288: setSelectedBuiltinId(builtinId); +289: const builtin = builtinInstructions.find(bi => bi.id === builtinId); +290: if (builtin) { +291: setInstructionInlineContent(builtin.content); +292: } +293: }; +294: +295: +296: const handleInstructionTypeChange = (type: InstructionType) => { +297: setInstructionType(type); +298: +299: if (type === 'inline' && selectedBuiltinId) { +300: const builtin = builtinInstructions.find(bi => bi.id === selectedBuiltinId); +301: if (builtin) { +302: setInstructionInlineContent(builtin.content); +303: } +304: } +305: }; +306: +307: const availableSkills = Object.keys(skills); +308: +309: return ( +310:
+311:
+312: Agent Definitions +313: +314: +317: +320: +321:
+322: +323: {Object.keys(agents).length === 0 ? ( +324: +325: ) : ( +326: a.name.localeCompare(b.name))} +329: renderItem={(agent) => ( +330: } +337: onClick={() => handleView(agent)} +338: > +339: View +340: , +341: , +350: , +359: handleDelete(agent.id)} +363: > +364: +367: , +368: ]} +369: > +370: +373: {agent.name} +374: {agent.version && {agent.version}} +375: {getAgentTypeTag(agent.agent_type)} +376: +377: } +378: description={ +379: +380: {agent.description || 'No description'} +381: +382: {agent.tools.slice(0, 5).map((t, i) => ( +383: {t.tool_id} +384: ))} +385: {agent.tools.length > 5 && +{agent.tools.length - 5}} +386: +387: +388: } +389: /> +390: +391: )} +392: /> +393: )} +394: +395: {} +396: setEditModalVisible(false)} +400: onOk={handleSave} +401: width={800} +402: okText="Save" +403: > +404:
+405: +412: +413: +414: +415: +416: +417: +418: +419: