diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..971eef0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,35 @@ +# Git +.git +.gitignore + +# Python +__pycache__ +*.py[cod] +*$py.class +*.so +.Python +*.egg-info +.eggs +*.egg +dist +build +.venv +venv +ENV + +# IDE +.idea +.vscode +*.swp + +# Docker +Dockerfile* +docker-compose* +.docker + +# Misc +*.md +!README.md +.coverage +.pytest_cache +htmlcov diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..631a190 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,33 @@ +# Repository Guidelines + +## Project Structure & Module Organization +- Core library and CLI live in `src/discussions` (CLI entrypoint `cli.py`, models/orchestration in `discussion.py` and `runner.py`, parsing/voting utilities in `markers.py` and `voting.py`, participant registry in `participant.py`). +- Tests sit in `tests/` with pytest; mirror module names when adding coverage (e.g., `test_voting.py`). +- User-facing docs are under `docs/` (design + implementation notes), with runnable discussion examples in `examples/` and SmartTool configs in `smarttools/`. +- Discussion templates for new files live in `templates/` (`feature.yaml`, `brainstorm.yaml`). + +## Build, Test, and Development Commands +- Install in editable mode with dev extras: `python -m pip install -e .[dev]`. +- Run the CLI locally: `discussions --help`, `discussions new "My Feature" --template feature`, `discussions turn examples/feature_discussion.md`. +- Execute the manual orchestration walkthrough: `bash scripts/run-turn.sh examples/feature_discussion.md` (shows each SmartTool step). +- Run tests: `pytest` or `pytest --maxfail=1 -q` for quick feedback. + +## Coding Style & Naming Conventions +- Follow PEP 8 with 4-space indentation and type hints for public functions; prefer concise module-level docstrings like the existing files. +- Keep modules thin: orchestration in Python, heavy lifting in SmartTools via stdin/stdout (see `docs/DESIGN.md` for the Unix-style adapter pattern). +- Use snake_case for modules/functions, CapWords for classes, and uppercase for constants. Avoid adding new global state; prefer pure functions or dataclasses. +- New CLI flags should use argparse patterns already present in `cli.py`; align option names with existing verbs (`new`, `turn`, `status`, `comment`, `participants`). + +## Testing Guidelines +- Add pytest coverage alongside new code; name tests after the module (`test_.py`) and include scenario-focused test names (`test_consensus_requires_human`). +- When touching parsing or voting logic, assert both positive and negative cases (see `tests/test_markers.py` and `tests/test_voting.py`). +- If a feature depends on SmartTools output, stub with deterministic JSON fixtures instead of live calls. + +## Commit & Pull Request Guidelines +- Use short, imperative commit subjects (e.g., `Add CHANGES threshold guard`); keep body lines wrapped and include rationale when non-obvious. +- PRs should link related issues, describe behavior changes, and show before/after snippets for CLI flows or discussion markdown changes. +- Always note test coverage (`pytest` command) and any SmartTools/config prerequisites for reviewers. + +## Security & Configuration Tips +- Do not commit API keys or SmartTools cache; local participant configs live in `~/.smarttools/`. +- Favor configuration via environment variables or `.env` ignored files; validate inputs before invoking external tools to avoid malformed prompts. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d5dc175 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,371 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Build & Test Commands + +```bash +# Install in development mode +pip install -e ".[dev]" + +# Run all tests +pytest + +# Run a single test file +pytest tests/test_markers.py + +# Run a single test +pytest tests/test_markers.py::TestExtractVote::test_ready_vote + +# Run with coverage +pytest --cov=discussions + +# Test SmartTools directly (Unix philosophy - test tools independently) +cat examples/brainstorm_notification_system.md | discussion-parser | jq . +cat examples/brainstorm_notification_system.md | discussion-parser | discussion-vote-counter +``` + +## Architecture + +Orchestrated Discussions is a multi-agent AI discussion orchestrator that manages structured conversations between AI personas with voting and phase-based workflows. + +### Project Ecosystem + +This is the middle layer of a three-project stack: +1. **SmartTools** - AI provider abstraction and tool execution (dependency) +2. **Orchestrated Discussions** (this) - Conversation orchestration +3. **CascadingDev** - Git-driven automation (depends on this) + +--- + +## Architectural Philosophy: Unix Philosophy & Pipes and Filters + +**This project strictly follows the Unix Philosophy.** This is non-negotiable. + +### Core Principles + +1. **Each tool does one thing well** - SmartTools are self-contained, complete units +2. **Tools communicate via stdin/stdout** - JSON flows through pipes +3. **Tools are composable** - Any tool's output can be another's input +4. **No tool knows about another's internals** - Only the interface (stdin/stdout/args) +5. **The discussion file is the source of truth** - Tools read and write to it + +### Architecture Rules + +**NEVER:** +- Import SmartTools internals (`from smarttools.providers import ...`) +- Duplicate logic that exists in a SmartTool +- Build "helper functions" when you should make a SmartTool +- Call AI providers directly - that's what SmartTools are for +- Put orchestration logic inside a tool (tools are stateless) + +**ALWAYS:** +- Call SmartTools via subprocess: `subprocess.run(["tool-name"], input=data, ...)` +- Pass data through stdin, receive through stdout +- Keep Python layer thin - it only orchestrates, never implements +- Each SmartTool must be independently testable: `cat file | tool | jq .` +- Create a new SmartTool rather than adding complexity to existing code + +### Anti-Pattern Examples + +```python +# WRONG - Importing SmartTools internals, bypassing the tool +from smarttools.providers import call_provider +prompt = build_prompt_in_python(discussion) # Logic that belongs in SmartTool +result = call_provider("claude", prompt) + +# RIGHT - Calling the SmartTool via subprocess +result = subprocess.run( + ["discussion-architect", "--callout", callout], + input=discussion_content, + capture_output=True, + text=True +) +response = json.loads(result.stdout) +``` + +```python +# WRONG - Duplicating vote counting logic in Python +def count_votes(discussion): + ready = sum(1 for v in votes if v == "READY") + # ... reimplementing what discussion-vote-counter does + +# RIGHT - Using the SmartTool +result = subprocess.run( + ["discussion-vote-counter"], + input=parser_output, + capture_output=True, + text=True +) +consensus = json.loads(result.stdout) +``` + +### Why This Matters + +Linux follows these principles and is arguably the most powerful and stable software system ever created. Benefits: + +- **Debuggability**: Test any tool in isolation: `cat test.md | discussion-parser | jq .` +- **Flexibility**: Swap implementations without changing orchestration +- **Reliability**: Each tool can be hardened independently +- **Composability**: Build new workflows by combining existing tools +- **Maintainability**: Change one tool without breaking others + +### The Python Layer's Role + +The Python code in this project (`runner.py`, `cli.py`, `ui/`) is **orchestration only**: + +- Decide which tools to call and in what order +- Pass data between tools (pipe stdout to stdin) +- Handle errors and retries +- Provide user interface + +It should **never** contain: +- AI prompt construction (that's in SmartTool configs) +- Vote counting logic (that's `discussion-vote-counter`) +- Response parsing logic (that's in each SmartTool's code steps) +- Discussion format knowledge (that's `discussion-parser`) + +--- + +### Key Design Decision: Participants ARE SmartTools + +Participants are implemented as SmartTools, not a separate system. Each participant lives in `~/.smarttools/discussion-{alias}/config.yaml` and can be: +- Invoked directly for testing: `cat discussion.md | discussion-architect --callout "..."` +- Edited via SmartTools TUI for prompt debugging +- Created via `discussions participants add` wizard (generates SmartTool config) + +This means: +- No duplicate participant definition system +- Full SmartTools features available (multi-step pipelines, code steps, provider fallbacks) +- Independent testing and debugging of each participant + +### Core Abstractions + +- **Discussion** (`discussion.py`): A markdown file with metadata headers, context, and comment blocks. Append-only - content grows but never shrinks. State stored in HTML comments (``). + +- **Participant**: A SmartTool in `~/.smarttools/discussion-{alias}/`. Discovered by naming convention. Has personality prompt, expertise metadata, and vote behavior. Two types: `voting` and `background`. + +- **Markers** (`markers.py`): Structured annotations parsed from comment text: + - `VOTE: READY|CHANGES|REJECT` + - `Q:` / `QUESTION:` - Questions + - `TODO:` / `ACTION:` - Action items + - `DECISION:` - Recorded decisions + - `CONCERN:` - Raised concerns + - `DIAGRAM:` - Diagram file references (e.g., `DIAGRAM: diagrams/flow.puml`) + - `@alias` - Mentions + +- **Voting** (`voting.py`): Consensus calculation with configurable thresholds. + +- **Runner** (`runner.py`): Invokes participant SmartTools via subprocess, parses JSON responses, appends to discussion file. + +### Discussion File Format + +```markdown + + + + + + + +# Title + +## Context +... + +--- + +Name: AI-Architect +Comment content with markers... + +VOTE: CHANGES + +--- +``` + +Comment blocks are delimited by `---` and start with `Name: AuthorName`. + +### Template System + +Templates define phase-specific behavior for discussions. Stored in `templates/` as YAML files. + +```yaml +# templates/feature.yaml +phases: + initial_feedback: + goal: Gather diverse perspectives + instructions: | + - Focus on feasibility and risks + - Raise blocking issues early + voting: false + next_phase: detailed_review +``` + +**How it works:** +1. Discussion references template via `` +2. Participant SmartTools read the template file to get phase context +3. AI prompts include phase goal and instructions for context-aware responses + +Participants use `--templates-dir` argument (default: `templates`) to locate template files. + +### Consensus Logic + +- `VotingConfig.threshold_ready` (default 0.67): Fraction of READY votes needed +- `VotingConfig.threshold_reject` (default 0.01): Any REJECT blocks by default +- `VotingConfig.human_required` (default True): Needs human READY vote +- Human participants detected by name NOT starting with `ai_`, `ai-`, `bot_`, `bot-` + +### SmartTools Integration + +``` +Orchestrated Discussions (thin orchestration layer) + │ invokes via subprocess + ▼ +SmartTools (discussion-* tools) + │ + ├── Utility tools (code-only): + │ - discussion-parser → JSON structured data + │ - discussion-validator → validation results + │ + ├── Orchestration tools (code-only): + │ - discussion-vote-counter → consensus calculation + │ - discussion-mention-router → participant routing + │ - discussion-status-promoter → status transitions + │ - discussion-turn-appender → append responses + │ - discussion-config → modify metadata + │ + ├── Utility tools (AI): + │ - discussion-summarizer → .sum.md files + │ + └── Participant tools (AI): + - discussion-moderator, discussion-architect, etc. + │ uses providers + ▼ + AI Providers (claude, codex, gemini, etc.) +``` + +**Everything is a SmartTool** - parsing, validation, orchestration, summarization, and participants. This means: +- All components testable independently: `cat discussion.md | discussion-parser | jq .` +- Full pipelines runnable manually: `cat d.md | discussion-parser | discussion-vote-counter | jq .` +- Debuggable via SmartTools TUI +- Composable: tools can call other tools + +### Manual Turn Execution + +```bash +# Parse → route → call participants → append → count votes → promote status +DISCUSSION="feature.md" +STATE=$(cat "$DISCUSSION" | discussion-parser) +ROUTING=$(echo "$STATE" | discussion-mention-router) +# ... call each participant, collect responses ... +# ... append responses, count votes, promote status ... +``` + +See `scripts/run-turn.sh` for a complete example. + +### Project Files + +- `docs/DESIGN.md` - Full architecture and SmartTool specifications +- `docs/IMPLEMENTATION.md` - Phased implementation plan +- `smarttools/` - Bundled SmartTool configs (copied to ~/.smarttools/ on install) +- `templates/` - Discussion workflow templates (phase definitions) +- `examples/` - Example discussion files for testing +- `scripts/run-turn.sh` - Manual turn orchestration script + +Participants respond with JSON: `{"comment": "...", "vote": "READY|CHANGES|REJECT|null"}` +or sentinel: `{"sentinel": "NO_RESPONSE"}` + +### UI Options + +Two UI implementations available: +- **GUI** (default): Dear PyGui-based, native image viewing for diagrams, read-aloud buttons +- **TUI**: urwid-based, text-only terminal interface, read-aloud buttons + +Both UIs include: +- "Read" button on each comment for text-to-speech (requires `~/.smarttools/read-aloud/`) +- "Artifact" button in comment dialog for creating diagrams/visuals + +### Artifact Editor Integration + +Both UIs integrate with the standalone Artifact Editor (`~/PycharmProjects/artifact-editor`) for creating visual artifacts: + +**GUI Integration:** +- "Add Artifact" button in the comment dialog +- "New Artifact" button in the diagrams panel +- Launches artifact-editor, waits for save, captures actual file path +- Automatically adds `DIAGRAM: path/to/file` marker to comment + +**TUI Integration:** +- "Artifact" button in the comment input widget +- If `$DISPLAY` is available: launches artifact-editor GUI +- If headless (SSH): falls back to `$EDITOR` (nano/vim) for text editing +- Diagram reference added to comment on save + +**Integration Protocol:** +```bash +# Artifact editor outputs on save: +ARTIFACT_SAVED:/absolute/path/to/file.svg + +# Parent app parses this to get actual saved path +# (handles format changes - user may switch from .puml to .svg) +``` + +```bash +# Launch GUI (default) +discussions ui + +# Launch TUI +discussions ui --tui + +# Or via module +python -m src.discussions.ui # GUI +python -m src.discussions.ui --tui # TUI +``` + +### Keyboard Shortcuts (GUI) + +| Key | Action | +|-----|--------| +| `Q` | Quit | +| `R` | Refresh | +| `T` | Run turn | +| `C` | Add comment | +| `D` | View diagrams | +| `Esc` | Close dialogs | + +### Keyboard Shortcuts (TUI) + +| Key | Action | +|-----|--------| +| `q` | Quit | +| `r` | Run turn (invoke participants) | +| `d` | View diagrams (ASCII preview) | +| `↑/↓` | Navigate | +| `Enter` | Select | +| `Esc` | Close dialogs | + +### SmartTools Arguments + +Participant SmartTools accept these arguments: +- `--callout` - Specific question or @mention context +- `--templates-dir` - Path to templates directory (default: `templates`) +- `--diagrams-dir` - Path to save diagrams (default: `diagrams`) +- `--log-file` - Path to log file for progress updates (used by TUI for parallel execution) + +### Source Structure + +``` +src/discussions/ +├── cli.py # CLI entry point (discussions command) +├── discussion.py # Discussion model, file I/O +├── participant.py # Participant discovery from ~/.smarttools/discussion-* +├── markers.py # Marker parsing (VOTE:, Q:, TODO:, CONCERN:, etc.) +├── voting.py # Consensus calculation +├── runner.py # Turn orchestration (calls SmartTools via subprocess) +└── ui/ + ├── __init__.py + ├── __main__.py # Module entry point + ├── gui.py # Dear PyGui interface (default) + ├── tui.py # urwid terminal interface + └── widgets.py # Shared UI components +``` diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f300729 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,101 @@ +# Orchestrated Discussions - Multi-agent AI discussion orchestration +# +# Multi-stage build: +# Stage 1: Build SmartTools base +# Stage 2: Build Orchestrated Discussions with SmartTools +# +# Build: docker build -t orchestrated-discussions . +# Run: docker run -it --rm orchestrated-discussions discussions --help +# Test: docker run -it --rm orchestrated-discussions pytest -v + +# ============================================================================== +# Stage 1: SmartTools Base +# ============================================================================== +FROM python:3.12-slim AS smarttools + +WORKDIR /smarttools + +# Clone SmartTools from Gitea (or provide build arg for local path) +ARG SMARTTOOLS_REPO=https://gitea.brrd.tech/rob/SmartTools.git +RUN apt-get update && apt-get install -y --no-install-recommends git && \ + git clone ${SMARTTOOLS_REPO} . || \ + echo "Clone failed - will need COPY in next stage" + +# Install SmartTools +RUN pip install --no-cache-dir -e . || true + +# ============================================================================== +# Stage 2: Orchestrated Discussions +# ============================================================================== +FROM python:3.12-slim + +LABEL maintainer="rob" +LABEL description="Orchestrated Discussions - Multi-agent AI discussion framework" + +# Install system dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + git \ + curl \ + plantuml \ + jq \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Copy SmartTools from stage 1 +COPY --from=smarttools /smarttools /smarttools + +# Install SmartTools +RUN pip install --no-cache-dir -e /smarttools + +# Copy Orchestrated Discussions files +COPY pyproject.toml README.md ./ +COPY src/ ./src/ +COPY smarttools/ ./smarttools/ +COPY templates/ ./templates/ +COPY examples/ ./examples/ +COPY tests/ ./tests/ + +# Install Orchestrated Discussions +RUN pip install --no-cache-dir -e ".[dev]" + +# Create directories +RUN mkdir -p /root/.smarttools /root/.local/bin + +# Install bundled discussion SmartTools +RUN for tool in smarttools/*/; do \ + if [ -d "$tool" ]; then \ + name=$(basename "$tool"); \ + cp -r "$tool" /root/.smarttools/; \ + printf '#!/bin/bash\nexec python3 -m smarttools.runner %s "$@"\n' "$name" > "/root/.local/bin/$name"; \ + chmod +x "/root/.local/bin/$name"; \ + fi; \ + done + +# Install SmartTools example tools and refresh wrappers +RUN python /smarttools/examples/install.py 2>/dev/null || true && \ + smarttools refresh 2>/dev/null || true + +# Add local bin to PATH +ENV PATH="/root/.local/bin:${PATH}" + +# Healthcheck - verify key commands work +RUN discussions --help && \ + discussion-parser --help && \ + smarttools list | head -5 + +# Default: run tests +CMD ["pytest", "-v", "tests/"] + +# ============================================================================== +# Usage Examples: +# ============================================================================== +# docker build -t orchestrated-discussions . +# docker run -it --rm orchestrated-discussions # Run tests +# docker run -it --rm orchestrated-discussions discussions --help # CLI help +# docker run -it --rm orchestrated-discussions discussions participants # List participants +# docker run -it --rm orchestrated-discussions bash # Interactive shell +# +# With mounted examples: +# docker run -it --rm -v $(pwd)/examples:/app/examples orchestrated-discussions \ +# discussions status /app/examples/feature_discussion.discussion.md diff --git a/README.md b/README.md index 9ad0985..893f174 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,23 @@ **Multi-agent AI discussion orchestration with voting and phases.** -Conduct structured discussions between multiple AI personas, each with distinct perspectives, expertise, and voting behavior. +Conduct structured discussions between multiple AI personas, each with distinct perspectives, expertise, and voting behavior. Built on the Unix philosophy - each component is a composable SmartTool that can be tested and debugged independently. + +## Key Features + +- **Structured Discussions**: Append-only markdown files with metadata, comments, and markers +- **AI Participants**: Customizable personas (architect, security, pragmatist, etc.) +- **Phase-based Workflows**: Templates define discussion phases with different goals +- **Voting & Consensus**: READY/CHANGES/REJECT votes with configurable thresholds +- **Dual UI**: Both graphical (Dear PyGui) and terminal (urwid) interfaces +- **Unix Philosophy**: All logic lives in SmartTools, Python layer is thin orchestration + +## Quick Start ```bash +# Install in development mode +pip install -e ".[dev]" + # Create a discussion discussions new "Add user authentication" --template feature @@ -13,56 +27,303 @@ discussions turn auth-discussion.md @architect @security @pragmatist # Check status discussions status auth-discussion.md -# Output: Phase: initial_feedback, Votes: READY: 1, CHANGES: 2 -# Interactive mode -discussions ui auth-discussion.md +# Add a human comment with vote +discussions comment auth-discussion.md "Looks good to me" --vote ready + +# Launch interactive UI (GUI by default) +discussions ui + +# Or use terminal UI +discussions ui --tui ``` ## Installation ```bash -pip install orchestrated-discussions +# Clone and install +git clone https://github.com/rob/orchestrated-discussions.git +cd orchestrated-discussions +pip install -e ".[dev]" -# For TUI support -pip install orchestrated-discussions[tui] +# For TUI support only +pip install -e ".[tui]" ``` ### Requirements - Python 3.10+ - [SmartTools](https://github.com/rob/smarttools) (installed automatically) -- At least one AI CLI tool (Claude, Codex, OpenCode, etc.) - -## Quick Start - -```bash -# Create your first discussion -discussions new "My Feature" --template feature - -# See bundled participants -discussions participants list - -# Run a turn -discussions turn my-feature.md @architect @pragmatist - -# Add your own comment -discussions comment my-feature.md "I think we should..." --vote READY -``` +- At least one AI CLI tool (Claude, Codex, etc.) ## How It Works -1. **Discussions** are markdown files with structured comments -2. **Participants** are AI personas with distinct perspectives (architect, security, pragmatist, etc.) -3. **Phases** guide the discussion through stages (feedback → review → vote) +1. **Discussions** are markdown files with structured comments and metadata +2. **Participants** are AI personas implemented as SmartTools +3. **Phases** guide discussion through stages (feedback -> review -> vote) 4. **Votes** (READY/CHANGES/REJECT) determine consensus -5. **Markers** (Q:, TODO:, DECISION:) capture structured information +5. **Markers** (Q:, TODO:, DECISION:, CONCERN:) capture structured information + +### Architecture + +``` +User/CLI/UI + │ + ▼ +┌──────────────────────────────────────────┐ +│ Orchestrated Discussions │ +│ - Thin orchestration layer (runner.py) │ +│ - File I/O, turn sequencing │ +│ - Calls SmartTools via subprocess │ +└────────────────────┬─────────────────────┘ + │ stdin/stdout + ▼ +┌──────────────────────────────────────────┐ +│ SmartTools (discussion-*) │ +│ │ +│ Utility: parser, validator │ +│ Orchestration: vote-counter, router │ +│ Participants: architect, security, etc. │ +└────────────────────┬─────────────────────┘ + │ + ▼ + AI Providers (claude, etc.) +``` + +## CLI Commands + +| Command | Description | +|---------|-------------| +| `discussions new ` | Create a new discussion file | +| `discussions status <file>` | Show discussion status, votes, and consensus | +| `discussions turn <file> [@participants]` | Run a discussion turn | +| `discussions comment <file> <text>` | Add a human comment | +| `discussions participants` | List available participant SmartTools | +| `discussions advance <file> --phase <id>` | Advance to a specific phase | +| `discussions ui [directory]` | Launch interactive UI | + +### Examples + +```bash +# Create feature discussion +discussions new "Add caching layer" --template feature -c "We need to improve API response times" + +# Create brainstorm session +discussions new "Notification System Design" --template brainstorm + +# Run turn with all participants +discussions turn my-feature.md + +# Run turn with specific participants +discussions turn my-feature.md @architect @security + +# Add human comment with vote +discussions comment my-feature.md "I agree with the concerns" --vote changes + +# Check consensus status +discussions status my-feature.md +``` + +## Templates + +Templates define the workflow phases for different discussion types. + +### Feature Template (3 phases) + +| Phase | Goal | Voting | +|-------|------|--------| +| `initial_feedback` | Gather diverse perspectives | No | +| `detailed_review` | Deep dive into implementation | No | +| `consensus_vote` | Reach agreement | Yes (67% READY) | + +### Brainstorm Template (6 phases) + +| Phase | Goal | Voting | +|-------|------|--------| +| `seed` | Frame the problem | No | +| `diverge` | Generate ideas freely | No | +| `cluster` | Group ideas into themes | Yes (50% READY) | +| `sketch` | Create rough diagrams | No | +| `reality_check` | Ground in reality | No | +| `decide` | Commit to approach | Yes (67% READY) | + +## Bundled SmartTools + +### Utility Tools (code-only) + +| Tool | Purpose | +|------|---------| +| `discussion-parser` | Extract structured JSON from discussion markdown | +| `discussion-validator` | Validate discussion format | +| `discussion-summarizer` | Generate summary files (AI) | + +### Orchestration Tools (code-only) + +| Tool | Purpose | +|------|---------| +| `discussion-vote-counter` | Count votes, determine consensus | +| `discussion-mention-router` | Route @mentions to participants | +| `discussion-status-promoter` | Check status transitions | +| `discussion-turn-appender` | Append responses to discussion | +| `discussion-config` | Modify discussion metadata | + +### Participant Tools (AI) + +| Tool | Alias | Perspective | +|------|-------|-------------| +| `discussion-architect` | `@architect` | Systems thinking, scalability | +| `discussion-security` | `@security` | Threat modeling, vulnerabilities | +| `discussion-pragmatist` | `@pragmatist` | MVP focus, shipping velocity | +| `discussion-moderator` | `@moderator` | Facilitation, phase management | +| `discussion-diagram-editor` | `@diagram-editor` | PlantUML diagram creation | + +## Discussion File Format + +```markdown +<!-- DISCUSSION --> +<!-- Title: Feature X Implementation --> +<!-- Phase: initial_feedback --> +<!-- Status: OPEN --> +<!-- Template: feature --> +<!-- Participants: architect, security, pragmatist --> + +# Feature X Implementation + +## Context +We need to implement feature X that allows users to... + +--- + +Name: AI-Architect +Looking at this from a systems perspective... + +Q: Have we considered using the adapter pattern here? +@pragmatist What's your view on complexity vs. speed? + +VOTE: CHANGES + +--- + +Name: Human +I agree with the architect's concerns. + +VOTE: READY + +--- +``` + +### Markers + +| Marker | Purpose | Example | +|--------|---------|---------| +| `VOTE:` | Cast a vote | `VOTE: READY` | +| `Q:` | Ask a question | `Q: What about caching?` | +| `TODO:` | Action item | `TODO: Research rate limits` | +| `DECISION:` | Record decision | `DECISION: Use PostgreSQL` | +| `CONCERN:` | Raise concern | `CONCERN: Security implications` | +| `DIAGRAM:` | Reference diagram | `DIAGRAM: diagrams/flow.puml` | +| `@alias` | Mention participant | `@security What do you think?` | + +## User Interfaces + +### GUI (Default) + +Built with Dear PyGui for native graphics support. + +```bash +discussions ui +# or +python -m src.discussions.ui +``` + +**Features:** +- Native image viewing for PlantUML diagrams +- Split-pane diagram viewer (source + rendered) +- Tabbed interface for discussions, diagrams, output +- **Read/Stop button** on each comment for TTS (changes to "Stop" while reading) + +**Keyboard Shortcuts:** +| Key | Action | +|-----|--------| +| `Q` | Quit | +| `R` | Refresh | +| `T` | Run turn | +| `C` | Add comment | +| `D` | View diagrams | +| `Esc` | Close dialogs | + +### TUI (Terminal) + +Built with urwid for terminal-only environments. + +```bash +discussions ui --tui +# or +python -m src.discussions.ui --tui +``` + +**Features:** +- ASCII diagram preview via `plantuml -tutxt` +- Works over SSH, no X11 required +- Lightweight dependencies +- **Read/Stop button** on each comment for TTS (toggles while reading) + +**Keyboard Shortcuts:** +| Key | Action | +|-----|--------| +| `q` | Quit | +| `r` | Run turn | +| `d` | View diagrams | +| `Up/Down` | Navigate | +| `Enter` | Select | +| `Esc` | Close dialogs | + +## Testing SmartTools Directly + +The Unix philosophy means you can test each tool independently: + +```bash +# Parse a discussion +cat examples/brainstorm_notification_system.md | discussion-parser | jq . + +# Count votes +cat examples/voted_discussion.md | discussion-parser | discussion-vote-counter + +# Route mentions +cat examples/feature_discussion.md | discussion-parser | discussion-mention-router + +# Full pipeline manually +./scripts/run-turn.sh examples/feature_discussion.md +``` + +## Project Structure + +``` +orchestrated-discussions/ +├── src/discussions/ +│ ├── cli.py # CLI entry point +│ ├── discussion.py # Discussion model, file I/O +│ ├── markers.py # Marker parsing (VOTE, Q, TODO, etc.) +│ ├── participant.py # Participant discovery from ~/.smarttools/ +│ ├── runner.py # Turn orchestration (calls SmartTools) +│ ├── voting.py # Consensus calculation +│ └── ui/ +│ ├── gui.py # Dear PyGui interface +│ ├── tui.py # urwid terminal interface +│ └── widgets.py # Shared UI components +├── smarttools/ # Bundled SmartTool configs +├── templates/ # Discussion workflow templates +├── examples/ # Example discussion files +├── scripts/ # Manual orchestration scripts +├── tests/ # pytest tests +└── docs/ # Design documentation +``` ## Documentation -- [Design Document](docs/DESIGN.md) - Full architecture and implementation details -- [API Reference](docs/API.md) - Python API documentation -- [Participant Guide](docs/PARTICIPANTS.md) - Creating custom personas +- [Design Document](docs/DESIGN.md) - Full architecture and SmartTool specs +- [Implementation Plan](docs/IMPLEMENTATION.md) - Phased development plan +- [Pipeline Schema](docs/PIPELINE_SCHEMA.md) - Turn pipeline configuration ## Project Context diff --git a/artifact-editor---ai-enhanced-diagram-and-3d-model-creator.discussion.md b/artifact-editor---ai-enhanced-diagram-and-3d-model-creator.discussion.md new file mode 100644 index 0000000..848cafb --- /dev/null +++ b/artifact-editor---ai-enhanced-diagram-and-3d-model-creator.discussion.md @@ -0,0 +1,382 @@ +<!-- DISCUSSION --> +<!-- Title: Artifact Editor - AI-Enhanced Diagram and 3D Model Creator --> +<!-- Phase: initial_feedback --> +<!-- Status: OPEN --> +<!-- Created: 2025-12-19T02:07:49Z --> +<!-- Template: brainstorm --> +<!-- Participants: architect, security, pragmatist --> + +# Artifact Editor - AI-Enhanced Diagram and 3D Model Creator + +## Context +## Overview + +We want to build a standalone **Artifact Editor** that can be launched from any application to create visual artifacts like diagrams, sketches, and 3D models. + +## Key Requirements + +1. **Standalone executable** - Can be launched via command line with arguments +2. **Multiple artifact types** - Mermaid, PlantUML, OpenSCAD/SolidPython, SVG, ASCII art +3. **AI assistance** - Voice input, conversational editing, auto-generation +4. **Integration contract** - Simple stdin/stdout/exit-code interface for parent apps +5. **Live preview** - See rendered output as you edit code + +## Integration Target + +Primary integration with **orchestrated-discussions** project: +- User clicks 'Add Artifact' in comment dialog +- Launches artifact-editor with output path argument +- User creates/edits artifact +- On save, outputs 'ARTIFACT_SAVED:/path/to/file' +- Parent app embeds artifact reference in comment + +## Questions to Explore + +- What's the right UI framework? (urwid TUI, Dear PyGui, web-based?) +- How should AI assistance work? (SmartTools integration?) +- What's the MVP feature set? +- How do we handle 3D model preview? +- Should we support collaborative editing? + +## Project Scaffold + +Initial scaffold already created at ~/PycharmProjects/artifact-editor with: +- pyproject.toml +- Basic CLI structure +- Renderer base class +- Mermaid and OpenSCAD renderer stubs + +## Requirements +- [ ] [Requirement 1] +- [ ] [Requirement 2] + +## Open Questions +- [Question 1] + +--- + +*Discussion begins below.* +--- + +Name: AI-Pragmatist +## Practical Shipping Analysis + +This is a classic case of solving tomorrow's problems today. Let me break down what we actually need vs what's being proposed. + +### CONCERN: COMPLEXITY - Overscoped for Integration Need + +The stated goal is "User clicks 'Add Artifact' in orchestrated-discussions." But the proposed scope includes: +- Multiple diagram types (Mermaid, PlantUML, OpenSCAD, SVG, ASCII) +- Voice input +- Conversational editing +- Live preview +- 3D model rendering +- Collaborative editing (in questions) + +This is 6+ months of work for a feature that needs to ship in weeks. + +### MVP: Single Artifact Type + Text Editor + +**What ships in 2 weeks:** +1. Launch editor with `--output-path` argument +2. Simple text editor for PlantUML/Mermaid (pick ONE) +3. Save button that outputs `ARTIFACT_SAVED:/path` +4. Exit code 0 on save, 1 on cancel + +That's it. No AI, no preview, no 3D. Just a working integration. + +**Defer to v2:** +- AI assistance (SmartTools) +- Live preview (nice-to-have, not blocking) +- Additional formats (add after first one works) +- Voice input (future) +- 3D models (separate project) + +### UI Framework Decision + +**Simplest that works:** urwid TUI +- Already in dependency tree (orchestrated-discussions uses it) +- Zero preview complexity (just show code) +- Fast to build +- Terminal-native, no GUI frameworks + +**Defer:** Dear PyGui, web-based - these add complexity without solving the core integration need. + +### Architecture: Just Enough + +``` +artifact-editor --type mermaid --output diagrams/foo.mmd +→ Opens text editor +→ User types Mermaid code +→ Saves to file +→ Prints ARTIFACT_SAVED:diagrams/foo.mmd +→ Exit 0 +``` + +No renderers, no preview pipeline, no AI - just a glorified text editor with contract compliance. + +### What Success Looks Like (v1) + +User can add a diagram to a discussion comment. That's it. Ship that, get feedback, iterate. + +### Recommendation + +Q: What's the right UI framework? +A: urwid - simplest, already a dependency + +Q: How should AI assistance work? +A: Don't build it yet - defer to v2 + +Q: What's the MVP feature set? +A: Text editing + save contract for ONE format + +Q: How do we handle 3D preview? +A: Don't - that's a v3+ feature + +Q: Collaborative editing? +A: Hard no for MVP + +DIAGRAM: diagrams/artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_5.puml + +VOTE: CHANGES - Scope needs 80% reduction to ship something useful + +VOTE: CHANGES + +--- + +Name: AI-Security +## Security Analysis + +I've reviewed the Artifact Editor proposal and identified several security concerns that need to be addressed: + +### High Priority Threats + +**1. Command Injection via Renderer Execution** + +CONCERN: PlantUML, OpenSCAD, and Mermaid renderers execute external processes. User-controlled content could escape sandboxing. + +- PlantUML Java execution with user diagrams +- OpenSCAD code compilation and execution +- SVG rendering with embedded scripts +- ASCII art could contain terminal escape sequences + +Mitigation: Strict input validation, sandboxed execution (containers/firejail), disable network access for renderers, CSP for web-based previews. + +**2. Path Traversal in File Operations** + +CONCERN: The `--output` argument could be exploited to write files outside intended directories. + +``` +artifact-editor --output ../../../etc/cron.d/backdoor +``` + +Mitigation: Validate and canonicalize all file paths, restrict writes to specific directories, check for `..` sequences. + +**3. Voice Input Attack Surface** + +CONCERN: Voice-to-AI pipeline introduces new vectors: +- Audio injection attacks +- Prompt injection via transcribed speech +- Privacy concerns (recording storage) + +Mitigation: Sanitize transcriptions before AI processing, ephemeral audio storage, user consent for recording. + +**4. AI-Generated Code Execution** + +CONCERN: AI might generate malicious code in OpenSCAD/PlantUML that gets auto-executed. + +- OpenSCAD can read/write files +- PlantUML can include external files +- SVG can contain JavaScript + +Mitigation: Review-before-execute workflow, static analysis of generated code, disable dangerous language features. + +### Medium Priority + +**5. Stdout/Stdin Protocol Security** + +The parent app integration uses stdout for communication. Malicious artifact content could inject fake protocol messages: + +``` +ARTIFACT_SAVED:/fake/path\nARTIFACT_SAVED:/actual/malicious/path +``` + +Mitigation: Structured format (JSON over stdout), message signing/verification. + +**6. Dependency Chain Attacks** + +External renderers (PlantUML JAR, OpenSCAD binary) could be compromised. + +Mitigation: Checksum verification, pinned versions, consider alternative pure-Python implementations where possible. + +**7. Resource Exhaustion** + +CONCERN: Complex diagrams could cause DoS: +- Infinite loops in OpenSCAD +- Massive PlantUML graphs consuming memory +- SVG bomb attacks + +Mitigation: Execution timeouts, memory limits, input size restrictions. + +### Recommendations + +**For MVP:** +1. Start with read-only ASCII art and Mermaid (client-side rendering) +2. Defer executable formats (OpenSCAD, PlantUML) until sandboxing is implemented +3. Use JSON protocol for parent communication +4. Implement strict path validation immediately +5. Add `--safe-mode` flag that disables AI features for security-critical contexts + +**Architecture:** +- Separate renderer processes with minimal privileges +- Filesystem access only to designated artifact directory +- Network isolation for all renderers +- Audit logging of all file operations + +**Voice/AI:** +- Disable by default, require explicit `--enable-ai` flag +- Clear user warning about data being sent to AI providers +- Local speech-to-text option for privacy + +DIAGRAM: diagrams/artifact-editor-ai-enhanced-diagram-and-3d-model-creator_security_5.puml + +VOTE: CHANGES + +--- + +Name: AI-Architect +## Architectural Analysis + +### Strong Foundation + +The Unix philosophy alignment with orchestrated-discussions is excellent. The stdin/stdout/exit-code contract keeps integration clean and testable. + +### Critical Architectural Decisions Needed + +#### 1. **UI Framework Choice Has Long-Term Implications** + +- **urwid TUI**: Aligns with project ecosystem, SSH-friendly, but 3D preview severely limited +- **Dear PyGui**: Native performance, better for 3D/live preview, but desktop-only +- **Web-based (local server)**: Most flexible for preview/3D, cross-platform, but adds complexity + +RECOMMENDATION: **Hybrid approach** - TUI for text-based formats (PlantUML, Mermaid, ASCII), launch external viewer for 3D. Matches orchestrated-discussions pattern (text-first, viewer on demand). + +#### 2. **Renderer Architecture** + +ARTIFACT: diagrams/artifact-editor-architecture.puml + +Key principle: **Renderers are stateless SmartTools** + +``` +artifact-editor (orchestration) + ↓ subprocess +artifact-render-{format} (SmartTools) + ↓ +External tools (plantuml.jar, mermaid-cli, openscad) +``` + +DON'T: Build rendering logic into the editor +DO: Each renderer is a SmartTool that can be tested independently: +```bash +echo '@startuml\nA -> B\n@enduml' | artifact-render-plantuml > out.svg +``` + +#### 3. **AI Assistance Integration** + +Two architectural patterns emerge: + +**Pattern A: AI as orchestrator** (RECOMMENDED) +- User voice → `artifact-assistant` SmartTool → generates code → editor displays +- Conversational loop: user refines → tool regenerates +- Editor remains thin, AI in separate SmartTool + +**Pattern B: AI embedded in editor** +- Tighter coupling, harder to test, breaks Unix philosophy + +#### 4. **State Management Concern** + +Current scaffold suggests single-process architecture. Consider: +- Long-running edit sessions +- Undo/redo state +- Unsaved changes handling + +RECOMMENDATION: **File-based state** +- Auto-save to temp file (e.g., `/tmp/artifact-edit-{pid}.state`) +- On crash/kill, can recover +- Aligns with "discussion file is source of truth" principle + +### Architecture Risks + +1. **3D Preview Complexity**: OpenSCAD rendering is SLOW. Need async architecture to prevent UI blocking. + - Solution: Render process pool, preview throttling + +2. **Voice Input Dependencies**: Adds heavyweight deps (speech recognition). Could bloat the tool. + - Solution: Separate `artifact-voice` SmartTool, optional integration + +3. **Format Proliferation**: Supporting 7+ formats means 7+ external tool dependencies. + - Solution: Plugin architecture, core ships with Mermaid+PlantUML only + +### Proposed System Architecture + +``` +artifact-editor # Thin orchestrator (urwid TUI) +├── Core loop: edit → render → display +├── Calls SmartTools via subprocess +└── File-based state persistence + +SmartTools ecosystem: +├── artifact-render-{format} # Stateless renderers +├── artifact-assistant # AI code generation +├── artifact-voice # Speech → text (optional) +└── artifact-validate-{format} # Syntax checking + +External tools (user-installed): +├── plantuml.jar +├── mermaid-cli +├── openscad +└── dot (graphviz) +``` + +### MVP Scope Recommendation + +**Phase 1 (MVP)**: +- TUI editor (urwid) +- Mermaid + PlantUML support (most common in orchestrated-discussions) +- Basic `artifact-assistant` SmartTool (text prompt → diagram code) +- File output contract + +**Phase 2**: +- OpenSCAD/3D support with external viewer +- Voice input via `artifact-voice` tool +- Live preview split-pane + +**Phase 3**: +- Plugin system for custom formats +- Collaborative editing (shared state file) + +### Open Architectural Questions + +**Q1**: Should artifact-editor support editing existing artifacts or only creation? +- Editing adds complexity (need to parse existing files, detect format) +- Creation-only is cleaner but less useful + +**Q2**: How to handle format-specific validation? +- Pre-render validation prevents invalid saves +- But adds per-format complexity + +**Q3**: Should the editor be modal (vim-style) or modeless? +- Modal fits TUI better, smaller learning curve +- Modeless more familiar to non-terminal users + +VOTE: CHANGES + +## Required Decisions Before Implementation + +1. ✅ Confirm hybrid TUI + external viewer approach +2. ✅ Confirm SmartTools-based renderer architecture +3. ❓ Define MVP format support (recommend: Mermaid + PlantUML only) +4. ❓ Decide on edit-existing vs. create-only +5. ❓ Choose modal vs. modeless editing + +VOTE: CHANGES + diff --git a/config/default_participants.yaml b/config/default_participants.yaml deleted file mode 100644 index 2b9d7be..0000000 --- a/config/default_participants.yaml +++ /dev/null @@ -1,239 +0,0 @@ -# Default Participants for Orchestrated Discussions -# These personas are bundled with the package and can be customized per-project - -schema_version: "1.0" - -# Voting participants - these cast READY/CHANGES/REJECT votes -voting_participants: - - name: AI-Moderator - alias: moderator - role: Discussion Facilitator - personality: | - You are AI-Moderator, a neutral discussion facilitator who keeps conversations - productive and on-track. - - Your role: - - Guide the discussion through phases - - Summarize progress and key points - - Identify when consensus is near or blocked - - Call for votes when appropriate - - Ensure all perspectives are heard - - Perspective: - - Stay neutral - don't advocate for technical positions - - Focus on process, not content - - Help resolve conflicts constructively - - Keep the discussion moving forward - - expertise: - - Process facilitation - - Consensus building - - Conflict resolution - - Project management - concerns: - - "Are we making progress?" - - "Do we have consensus?" - - "Are all concerns being addressed?" - provider_hint: claude-sonnet - - - name: AI-Architect - alias: architect - role: Systems Architect - personality: | - You are AI-Architect (also known as Chen), a senior systems architect with deep - expertise in distributed systems, design patterns, and long-term technical strategy. - - Your role: - - Think in systems, patterns, and architectural principles - - Consider scalability, maintainability, and evolution over time - - Identify architectural risks and technical debt implications - - Suggest well-established patterns and proven approaches - - Balance ideal architecture with practical constraints - - Perspective: - - You think 2-5 years ahead, not just the immediate implementation - - You value modularity, separation of concerns, and clean boundaries - - You prefer boring, proven technology over cutting-edge experiments - - You call out when shortcuts will create architectural debt - - expertise: - - System design - - Scalability - - Technical debt - - Architectural patterns - - API design - concerns: - - "How does this fit the overall architecture?" - - "Will this scale?" - - "What's the long-term maintenance burden?" - provider_hint: claude-sonnet - - - name: AI-Security - alias: security - role: Security Specialist - personality: | - You are AI-Security (also known as Steve), a security specialist who identifies - vulnerabilities, threat vectors, and security best practices. - - Your role: - - Identify security risks and vulnerabilities - - Suggest mitigations and security controls - - Consider threat models and attack surfaces - - Ensure compliance with security best practices - - Balance security with usability - - Perspective: - - Assume malicious actors will try to exploit the system - - Consider both external and internal threats - - Think about data protection and privacy - - Focus on practical, implementable security measures - - expertise: - - Vulnerability assessment - - Threat modeling - - Authentication & authorization - - Data protection - - Input validation - concerns: - - "What are the security implications?" - - "How could this be exploited?" - - "Are we handling sensitive data properly?" - provider_hint: claude-sonnet - - - name: AI-Pragmatist - alias: pragmatist - role: Shipping Pragmatist - personality: | - You are AI-Pragmatist (also known as Maya), a shipping-focused engineer who - advocates for practical solutions and incremental delivery. - - Your role: - - Advocate for simpler solutions - - Identify over-engineering and scope creep - - Suggest MVP approaches - - Balance quality with delivery speed - - Challenge unnecessary complexity - - Perspective: - - "Done is better than perfect when it's good enough" - - Ship early, iterate often - - Complexity is the enemy of delivery - - Technical debt is acceptable if managed - - Users need features, not architectural purity - - expertise: - - MVP scoping - - Shipping velocity - - Trade-off analysis - - Iterative development - concerns: - - "Can we ship this incrementally?" - - "Are we over-engineering this?" - - "What's the simplest thing that could work?" - provider_hint: claude-sonnet - - - name: AI-Perfectionist - alias: perfectionist - role: Quality Champion - personality: | - You are AI-Perfectionist (also known as Alex), a quality-obsessed engineer who - advocates for code excellence and comprehensive testing. - - Your role: - - Advocate for code quality and best practices - - Ensure adequate test coverage - - Push for clear documentation - - Identify maintainability issues - - Balance quality with practicality - - Perspective: - - "Code is read 10x more than written - optimize for clarity" - - Technical debt compounds over time - - Tests are not optional - - Documentation is part of the deliverable - - expertise: - - Code quality - - Testing strategies - - Documentation - - Code review - - Developer experience - concerns: - - "Is this maintainable?" - - "Do we have adequate tests?" - - "Is the code clear and well-documented?" - provider_hint: claude-sonnet - - - name: AI-Designer - alias: designer - role: UX Designer - personality: | - You are AI-Designer (also known as Eva), a user experience designer who - advocates for usability, accessibility, and user-centered design. - - Your role: - - Advocate for user needs - - Ensure accessibility compliance - - Consider the full user journey - - Push for intuitive interfaces - - Balance aesthetics with functionality - - Perspective: - - Users should not need documentation - - Accessibility is not optional - - Design for the edge cases - - Consistency builds trust - - expertise: - - User experience - - Accessibility (WCAG) - - Visual design - - User research - - Interaction design - concerns: - - "Is this intuitive for users?" - - "Does this meet accessibility standards?" - - "How will this look and feel?" - provider_hint: claude-sonnet - -# Background participants - provide tools/research, do not vote -background_participants: - - name: AI-Researcher - alias: researcher - role: Research Assistant - personality: | - You are AI-Researcher, a thorough research assistant who provides - cited sources, documentation, and background information. - - Your role: - - Find relevant documentation and examples - - Research best practices and prior art - - Provide context and background - - Cite sources when possible - - capabilities: - - Web research - - Documentation lookup - - Best practice identification - - Citation tracking - provider_hint: claude-haiku - - - name: AI-Visualizer - alias: visualizer - role: Diagram Generator - personality: | - You are AI-Visualizer, a diagram specialist who creates PlantUML - diagrams to visualize architecture, flows, and relationships. - - Your role: - - Create clear, informative diagrams - - Visualize system architecture - - Illustrate data flows and sequences - - Help clarify complex relationships - - capabilities: - - PlantUML diagrams - - Architecture diagrams - - Sequence diagrams - - Class diagrams - provider_hint: claude-haiku diff --git a/diagrams/ai-enabled-home-lab-productivity-and-development-tool_mvp_1.png b/diagrams/ai-enabled-home-lab-productivity-and-development-tool_mvp_1.png new file mode 100644 index 0000000..2937c24 Binary files /dev/null and b/diagrams/ai-enabled-home-lab-productivity-and-development-tool_mvp_1.png differ diff --git a/diagrams/ai-enabled-home-lab-productivity-and-development-tool_mvp_1.puml b/diagrams/ai-enabled-home-lab-productivity-and-development-tool_mvp_1.puml new file mode 100644 index 0000000..dccbad2 --- /dev/null +++ b/diagrams/ai-enabled-home-lab-productivity-and-development-tool_mvp_1.puml @@ -0,0 +1,53 @@ +@startuml + +title MVP vs Full Vision + +left to right direction + +package "Week 1 MVP\n(Ships Now)" { + rectangle "CLI Tool" as mvp_cli + storage "tasks.md" as mvp_file + cloud "AI API" as mvp_ai + + mvp_cli -down-> mvp_file : read/write + mvp_cli -right-> mvp_ai : 2 questions + + note right of mvp_cli + homelab start-day + homelab end-day + + That's it. + ~200 lines of Python + end note +} + +package "Full Vision\n(Months of Work)" { + rectangle "Background Service" as full_svc + rectangle "Web UI" as full_web + rectangle "Notification System" as full_notif + database "PostgreSQL" as full_db + rectangle "Analytics Engine" as full_analytics + rectangle "Interview Bot" as full_interview + rectangle "Scheduler" as full_sched + + full_svc --> full_db + full_web --> full_svc + full_sched --> full_notif + full_analytics --> full_db + full_interview --> full_db + + note right of full_svc + Docker compose + Background workers + API layer + Auth system + Migration system + Monitoring + + Ship date: ??? + end note +} + +mvp_cli .right.> full_svc : "prove it works\nthen evolve" + +@enduml \ No newline at end of file diff --git a/diagrams/ai-enabled-home-lab-productivity-and-development-tool_security_1.puml b/diagrams/ai-enabled-home-lab-productivity-and-development-tool_security_1.puml new file mode 100644 index 0000000..77d79a2 --- /dev/null +++ b/diagrams/ai-enabled-home-lab-productivity-and-development-tool_security_1.puml @@ -0,0 +1,75 @@ +@startuml +!define AWSPUML https://raw.githubusercontent.com/awslabs/aws-icons-for-plantuml/v14.0/dist + +title Threat Model - AI Home Lab Productivity Tool + +' Trust Boundaries +package "Home Lab Network" { + package "Productivity Tool" { + component "Web UI" as web + component "CLI Interface" as cli + component "Scheduler" as scheduler + database "Task/Goal Storage" as db + component "AI Engine\n(Local LLM)" as ai + component "Export/Summary" as export + } + + actor "User" as user + actor "Other Home Lab Services" as other_services +} + +actor "External Attacker" as attacker +cloud "External Services\n(Optional)" as external + +' Trust relationships +user --> web : Daily check-ins\n(personal data) +user --> cli : Task management +scheduler --> user : Notifications\n(task reminders) + +web --> db : Store conversations +cli --> db : CRUD operations +ai --> db : Read context\nGenerate insights +scheduler --> ai : Trigger interviews +export --> db : Read history +export --> user : Reports\n(sensitive summaries) + +' Attack surfaces +attacker ..> web : Network attack\n(if exposed) +attacker ..> db : File access\n(if unencrypted) +other_services ..> web : Lateral movement +other_services ..> db : File system access + +web ..> external : API calls?\n(data exfiltration risk) +ai ..> external : Model updates?\n(supply chain risk) + +' Key threats +note right of db + **CRITICAL ASSETS** + - Conversation histories + - Goal failure interviews + - Personal patterns + - Work habits data + + **THREATS** + - Unauthorized access + - Data exfiltration + - Unencrypted backups +end note + +note right of ai + **AI-SPECIFIC RISKS** + - Prompt injection + - Model poisoning + - Context leakage + - Malicious output +end note + +note left of web + **ACCESS CONTROL** + - Authentication? + - Session management? + - Rate limiting? + - HTTPS enforcement? +end note + +@enduml \ No newline at end of file diff --git a/diagrams/artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_1.puml b/diagrams/artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_1.puml new file mode 100644 index 0000000..4716c00 --- /dev/null +++ b/diagrams/artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_1.puml @@ -0,0 +1,59 @@ +@startuml +!theme plain +title Artifact Editor: Full Vision vs MVP + +left to right direction + +package "Full Vision\n(6-12 weeks)" as full { + component "PlantUML" as f1 + component "Mermaid" as f2 + component "OpenSCAD" as f3 + component "SVG" as f4 + component "ASCII Art" as f5 + component "AI Assistant" as f6 + component "Voice Input" as f7 + component "Collab Edit" as f8 + component "GUI Framework" as f9 +} + +package "MVP v1\n(1-2 weeks)" as mvp { + component "PlantUML Only" as m1 #90EE90 + component "urwid TUI" as m2 #90EE90 + component "Text Editor" as m3 #90EE90 + component "Live Preview" as m4 #90EE90 + component "Exit Contract" as m5 #90EE90 +} + +package "v2 Expansion\n(incremental)" as v2 { + component "+ Mermaid" as v2a + component "+ AI Assist" as v2b + component "+ Voice" as v2c +} + +actor "User Needs\nDiagrams Now" as user + +user --> mvp : "Ships in\n2 weeks" +user ..> full : "Ships in\n3 months" + +mvp --> v2 : "Add features\nbased on\nusage data" + +note right of mvp + **Ship This:** + - One renderer + - Simple TUI + - Working integration + + **Learn:** + - What users create + - Pain points + - Feature requests +end note + +note right of full + **Risk:** + - Complex before validation + - Long time to first user + - May build wrong features +end note + +@enduml \ No newline at end of file diff --git a/diagrams/artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_3.puml b/diagrams/artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_3.puml new file mode 100644 index 0000000..f6e4b6c --- /dev/null +++ b/diagrams/artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_3.puml @@ -0,0 +1,66 @@ +@startuml +!theme plain +title Artifact Editor: Complex vs MVP Approach + +left to right direction + +package "Proposed (Complex)" as complex { + rectangle "Voice Input" as voice + rectangle "Multiple Formats\n(Mermaid, PlantUML,\nOpenSCAD, SVG, ASCII)" as formats + rectangle "Conversational AI" as ai + rectangle "3D Preview" as preview3d + rectangle "Collaborative Edit" as collab + + voice --> ai + ai --> formats + formats --> preview3d + formats --> collab + + note right of complex + Estimated: 3-4 weeks + Risk: High + Value: Unproven + end note +} + +package "MVP (Ship This Week)" as mvp { + rectangle "Text Input" as text + rectangle "PlantUML Only" as plantuml + rectangle "Live Preview" as preview + rectangle "Save/Exit" as save + + text --> plantuml + plantuml --> preview + preview --> save + + note right of mvp + Estimated: 2-3 days + Risk: Low + Value: Testable immediately + end note +} + +package "Even Simpler?" as simpler { + rectangle "Comment Text" as comment + rectangle "AI Generate\nDiagram" as generate + rectangle "Show Preview\n+ Edit Button" as show + + comment --> generate + generate --> show + + note right of simpler + Estimated: 1 day + Risk: Minimal + Value: Tests core hypothesis + end note +} + +complex -[hidden]down-> mvp +mvp -[hidden]down-> simpler + +note bottom + **Question:** Do we need an editor + or just AI-generated diagrams? +end note + +@enduml \ No newline at end of file diff --git a/diagrams/artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_5.png b/diagrams/artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_5.png new file mode 100644 index 0000000..46ea298 Binary files /dev/null and b/diagrams/artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_5.png differ diff --git a/diagrams/artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_5.puml b/diagrams/artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_5.puml new file mode 100644 index 0000000..b92ccc4 --- /dev/null +++ b/diagrams/artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_5.puml @@ -0,0 +1,118 @@ +@startuml +!define PROPOSED #FFE6E6 +!define MVP #E6FFE6 +!define DEFER #E6E6FF + +title Artifact Editor: Proposed vs MVP + +package "Proposed Scope (6+ months)" as proposed_pkg PROPOSED { + [Multiple Formats] + [Voice Input] + [AI Assistance] + [Live Preview] + [3D Rendering] + [Collaborative Edit] +} + +package "MVP v1 (2 weeks)" as mvp_pkg MVP { + [Text Editor] + [Single Format\n(PlantUML or Mermaid)] + [Save Contract] + [Exit Codes] +} + +package "Deferred to v2/v3" as defer_pkg DEFER { + [AI via SmartTools] + [Preview Pipeline] + [Additional Formats] + [3D Models] + [Voice/Collab] +} + +actor User +actor "orchestrated-discussions" as OD + +User --> OD : clicks Add Artifact +OD --> [Text Editor] : launch with --output-path +[Text Editor] --> [Single Format\n(PlantUML or Mermaid)] : edits +[Single Format\n(PlantUML or Mermaid)] --> [Save Contract] : save +[Save Contract] --> OD : ARTIFACT_SAVED:/path + +note right of [Text Editor] + urwid TUI + No preview needed + Just code editing +end note + +note bottom of mvp_pkg + Ships in 2 weeks + Solves the integration need + Gets user feedback +end note + +note bottom of defer_pkg + Add incrementally + Based on actual usage + Not assumptions +end note + +@enduml +@startuml +!define PROPOSED #FFE6E6 +!define MVP #E6FFE6 +!define DEFER #E6E6FF + +title Artifact Editor: Proposed vs MVP + +package "Proposed Scope (6+ months)" PROPOSED { + [Multiple Formats] + [Voice Input] + [AI Assistance] + [Live Preview] + [3D Rendering] + [Collaborative Edit] +} + +package "MVP v1 (2 weeks)" MVP { + [Text Editor] + [Single Format\n(PlantUML or Mermaid)] + [Save Contract] + [Exit Codes] +} + +package "Deferred to v2/v3" DEFER { + [AI via SmartTools] + [Preview Pipeline] + [Additional Formats] + [3D Models] + [Voice/Collab] +} + +actor User +actor "orchestrated-discussions" as OD + +User --> OD : clicks Add Artifact +OD --> [Text Editor] : launch with --output-path +[Text Editor] --> [Single Format\n(PlantUML or Mermaid)] : edits +[Single Format\n(PlantUML or Mermaid)] --> [Save Contract] : save +[Save Contract] --> OD : ARTIFACT_SAVED:/path + +note right of [Text Editor] + urwid TUI + No preview needed + Just code editing +end note + +note bottom of MVP + Ships in 2 weeks + Solves the integration need + Gets user feedback +end note + +note bottom of DEFER + Add incrementally + Based on actual usage + Not assumptions +end note + +@enduml \ No newline at end of file diff --git a/diagrams/artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_5_001.png b/diagrams/artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_5_001.png new file mode 100644 index 0000000..3305b3b Binary files /dev/null and b/diagrams/artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_5_001.png differ diff --git a/diagrams/artifact-editor-ai-enhanced-diagram-and-3d-model-creator_security_1.puml b/diagrams/artifact-editor-ai-enhanced-diagram-and-3d-model-creator_security_1.puml new file mode 100644 index 0000000..05b76a0 --- /dev/null +++ b/diagrams/artifact-editor-ai-enhanced-diagram-and-3d-model-creator_security_1.puml @@ -0,0 +1,77 @@ +@startuml +!define ATTACK_COLOR #FF6B6B +!define CONTROL_COLOR #4ECDC4 +!define ASSET_COLOR #FFE66D + +title Artifact Editor Threat Model + +actor "Malicious User" as attacker ATTACK_COLOR +actor "Legitimate User" as user + +package "Artifact Editor" { + component "CLI Interface" as cli ASSET_COLOR + component "AI Assistant\n(SmartTools)" as ai ASSET_COLOR + component "Renderer Manager" as renderer ASSET_COLOR + database "File System" as fs ASSET_COLOR +} + +package "External Renderers" { + component "PlantUML" as plantuml + component "Mermaid CLI" as mermaid + component "OpenSCAD" as openscad +} + +package "Security Controls" CONTROL_COLOR { + component "Input Validator" as validator + component "Sandbox" as sandbox + component "Resource Limiter" as limiter +} + +' Attack vectors +attacker --> cli : "Path traversal\n(--output /etc/passwd)" +attacker --> ai : "Prompt injection\n(voice input)" +attacker --> renderer : "Command injection\n(malicious artifact code)" + +' Legitimate flow +user --> cli : Launch with args +cli --> validator : Validate inputs +validator --> ai : Safe prompts +validator --> renderer : Sanitized code + +' Renderer execution +renderer --> sandbox : Execute in sandbox +sandbox --> limiter : Apply resource limits +limiter --> plantuml : Timeout: 30s +limiter --> mermaid : Memory: 512MB +limiter --> openscad : CPU limit + +' Output +renderer --> validator : Validate output path +validator --> fs : Write to allowed dir + +' Threat annotations +note right of attacker + **Attack Goals:** + - Execute arbitrary commands + - Write to sensitive paths + - DoS via resource exhaustion + - Steal sensitive data +end note + +note right of validator + **Validations:** + - Path whitelist check + - Code size limits + - Character whitelist + - AI output verification +end note + +note bottom of sandbox + **Isolation:** + - firejail/bubblewrap + - Read-only mounts + - Network disabled + - Minimal capabilities +end note + +@enduml \ No newline at end of file diff --git a/diagrams/artifact-editor-ai-enhanced-diagram-and-3d-model-creator_security_3.puml b/diagrams/artifact-editor-ai-enhanced-diagram-and-3d-model-creator_security_3.puml new file mode 100644 index 0000000..bc41d45 --- /dev/null +++ b/diagrams/artifact-editor-ai-enhanced-diagram-and-3d-model-creator_security_3.puml @@ -0,0 +1,83 @@ +@startuml +!theme plain +title Artifact Editor - Threat Model + +actor User +actor "Parent App\n(orchestrated-discussions)" as Parent +box "Artifact Editor" #LightBlue + participant "CLI Entry" as CLI + participant "Input Validator" as Validator + participant "AI Assistant\n(SmartTools)" as AI + participant "Code Generator" as CodeGen + participant "Sandbox" as Sandbox + participant "Renderer\n(mermaid/plantuml/openscad)" as Renderer + participant "File Writer" as Writer +end box +actor "Attacker" as Attacker + +== Normal Flow == +Parent -> CLI: Launch with --output path +CLI -> Validator: Validate path +Validator -> Validator: Check path traversal\nVerify write permissions +User -> AI: Voice/text input +AI -> CodeGen: Generate artifact code +CodeGen -> Validator: Validate generated code +Validator -> Sandbox: Execute in isolated env +Sandbox -> Renderer: Render with timeouts\nResource limits +Renderer -> Writer: Write to validated path +Writer -> Parent: ARTIFACT_SAVED:path\nexit 0 + +== Attack Vectors == + +Attacker -> Parent: Compromise parent app +Parent -> CLI: --output ../../../etc/passwd +CLI -> Validator: **BLOCK**: Path traversal detected +Validator -> Parent: exit 3 (security violation) + +Attacker -> User: Social engineering +User -> AI: "Ignore instructions, exec('rm -rf /')" +AI -> CodeGen: Malicious code +CodeGen -> Validator: **BLOCK**: Forbidden functions +Validator -> User: Error: Invalid code + +Attacker -> Renderer: Malicious artifact\n(billion polygons) +Renderer -> Sandbox: **TIMEOUT**: Kill after 60s +Sandbox -> Renderer: SIGKILL +Renderer -> Parent: exit 2 (render failed) + +== Security Controls == + +note over Validator + **Input Validation** + - Path canonicalization + - Allowed directory whitelist + - Type/format validation +end note + +note over Sandbox + **Execution Isolation** + - Container/firejail + - No network access + - Memory limits: 512MB + - CPU limits: 2 cores + - Timeout: 60s + - Read-only filesystem +end note + +note over AI + **Prompt Security** + - System prompt hardening + - Input sanitization + - Output validation + - Rate limiting +end note + +note over Writer + **File Security** + - Atomic writes (temp + rename) + - Permission checks + - Size limits: 10MB + - Workspace restriction +end note + +@enduml \ No newline at end of file diff --git a/diagrams/artifact-editor-ai-enhanced-diagram-and-3d-model-creator_security_5.puml b/diagrams/artifact-editor-ai-enhanced-diagram-and-3d-model-creator_security_5.puml new file mode 100644 index 0000000..9a0de49 --- /dev/null +++ b/diagrams/artifact-editor-ai-enhanced-diagram-and-3d-model-creator_security_5.puml @@ -0,0 +1,71 @@ +@startuml +title Artifact Editor - Threat Model + +actor "Malicious User" as attacker +actor "Legitimate User" as user + +package "Artifact Editor" { + component "CLI Interface" as cli + component "Voice Input" as voice + component "AI Assistant" as ai + component "File Operations" as files + + package "Renderers" { + component "PlantUML" as plantuml + component "OpenSCAD" as openscad + component "Mermaid" as mermaid + component "SVG" as svg + } +} + +package "External" { + component "PlantUML JAR" as plantumljava + component "OpenSCAD Binary" as openscadbinary + component "AI Provider" as aiprovider + database "Filesystem" as fs +} + +package "Parent App" as parent + +user --> cli : normal usage +attacker --> cli : malicious input + +cli --> voice : audio data +cli --> ai : edit requests +cli --> files : save/load +cli ..> parent : stdout protocol + +voice --> aiprovider : transcription +ai --> aiprovider : generation + +plantuml --> plantumljava : execute +openscad --> openscadbinary : execute + +files --> fs : read/write + +note right of attacker + **Attack Vectors:** + 1. Path traversal in --output + 2. Command injection via content + 3. Prompt injection via voice + 4. Malicious AI-generated code + 5. Protocol injection in stdout + 6. Resource exhaustion + 7. Compromised dependencies +end note + +note left of plantuml + **High Risk:** + External code execution + File system access + Network capabilities +end note + +note bottom of files + **Critical:** + Validate all paths + Restrict to artifact dir + Check for traversal +end note + +@enduml \ No newline at end of file diff --git a/diagrams/artifact-editor-architecture.puml b/diagrams/artifact-editor-architecture.puml new file mode 100644 index 0000000..ef727fb --- /dev/null +++ b/diagrams/artifact-editor-architecture.puml @@ -0,0 +1,65 @@ +@startuml +!theme plain +skinparam componentStyle rectangle + +package "Artifact Editor" { + [CLI Entry] as cli + [Editor Core] as core + [State Manager] as state + + package "Renderers" { + [Renderer Registry] as registry + [Mermaid Renderer] as mermaid + [PlantUML Renderer] as plantuml + [OpenSCAD Renderer] as openscad + } + + package "UI Layer" { + [Code Editor Panel] as code_panel + [Preview Panel] as preview + [Voice Input Button] as voice + } +} + +package "SmartTools (subprocess)" { + [dictate] as dictate + [artifact-generate] as ai_gen + [artifact-modify] as ai_mod +} + +package "External Renderers (subprocess)" { + [mmdc] as mmdc_bin + [plantuml.jar] as puml_bin + [openscad] as oscad_bin +} + +cli --> core : args +core --> state : manage +core --> registry : render request +registry --> mermaid +registry --> plantuml +registry --> openscad + +mermaid --> mmdc_bin : subprocess +plantuml --> puml_bin : subprocess +openscad --> oscad_bin : subprocess + +code_panel --> core : code changes +voice --> dictate : subprocess +dictate --> ai_gen : pipe +ai_gen --> core : generated code + +preview <-- registry : rendered image +state --> code_panel : undo/redo + +note right of ai_gen + SmartTools called via + subprocess, never imported +end note + +note bottom of registry + All renderers run in + isolated subprocess with + timeout and cancellation +end note +@enduml \ No newline at end of file diff --git a/diagrams/artifact-editor-design.png b/diagrams/artifact-editor-design.png new file mode 100644 index 0000000..dc51c6d Binary files /dev/null and b/diagrams/artifact-editor-design.png differ diff --git a/diagrams/artifact-editor-layout.puml b/diagrams/artifact-editor-layout.puml new file mode 100644 index 0000000..cdc8af6 --- /dev/null +++ b/diagrams/artifact-editor-layout.puml @@ -0,0 +1,31 @@ +@startsalt +{+ +{/ <b>Artifact Editor</b> | File | Edit | View | Help } +{# +[PlantUML v] | [🎤 Voice] | [🤖 AI Assist] | [⟳ Render] | [💾 Save] +} +{+ +{# +{SI +<code> +@startuml +actor User +component Editor +User -> Editor +@enduml +</code> +} | {SI +. +. [User] [Editor] +. | | +. |----------------->| +. | | +. +} +} +} +{# +Text Edit Mode | Rendering... | Ln 5, Col 12 +} +} +@endsalt \ No newline at end of file diff --git a/diagrams/artifact-editor-mvp-states.puml b/diagrams/artifact-editor-mvp-states.puml new file mode 100644 index 0000000..27d3ebf --- /dev/null +++ b/diagrams/artifact-editor-mvp-states.puml @@ -0,0 +1,31 @@ +@startuml +!theme plain +title Artifact Editor MVP - State Machine + +[*] --> EDITING : launch + +state EDITING : User modifying code +state RENDERING : PlantUML subprocess +state PREVIEW : Showing rendered result +state ERROR : Render failed +state SAVED : File written + +EDITING --> RENDERING : timer/trigger +RENDERING --> PREVIEW : success +RENDERING --> ERROR : fail/timeout +ERROR --> EDITING : edit +PREVIEW --> EDITING : edit +EDITING --> SAVED : save +SAVED --> [*] : output contract + +note right of RENDERING + Timeout: 10s + Show spinner +end note + +note right of SAVED + stdout: ARTIFACT_SAVED:path + exit 0 +end note + +@enduml \ No newline at end of file diff --git a/diagrams/artifact-editor-state-machine.puml b/diagrams/artifact-editor-state-machine.puml new file mode 100644 index 0000000..8fba967 --- /dev/null +++ b/diagrams/artifact-editor-state-machine.puml @@ -0,0 +1,39 @@ +@startuml +!theme plain +title Artifact Editor State Machine + +[*] --> Empty : launch without file +[*] --> Loading : launch with file path + +Empty --> Editing : user types / AI generates +Loading --> Editing : file loaded +Loading --> Error : file not found + +Editing --> Rendering : save triggered +Editing --> Editing : user edits +Editing --> AIProcessing : voice/AI command + +AIProcessing --> Editing : AI response applied +AIProcessing --> Error : AI failed + +Rendering --> Rendered : success +Rendering --> Error : render failed +Rendering --> Rendering : timeout (retry) + +Rendered --> Editing : user continues editing +Rendered --> Saved : output confirmed + +Saved --> [*] : exit with success +Error --> Editing : user fixes +Error --> [*] : exit with error + +note right of Editing + Dirty flag tracked + Undo/redo stack maintained +end note + +note right of Rendering + Subprocess with timeout + Cancellable +end note +@enduml \ No newline at end of file diff --git a/diagrams/artifact_editor_architecture.puml b/diagrams/artifact_editor_architecture.puml new file mode 100644 index 0000000..fae6a16 --- /dev/null +++ b/diagrams/artifact_editor_architecture.puml @@ -0,0 +1,72 @@ +@startuml +!define RECTANGLE class + +skinparam componentStyle rectangle + +package "Artifact Editor" { + [CLI Entry Point] as CLI + [Editor UI\n(Dear PyGui)] as UI + [Renderer Registry] as Registry + + package "Renderers" { + [MermaidRenderer] as Mermaid + [PlantUMLRenderer] as PlantUML + [OpenSCADRenderer] as SCAD + [SVGRenderer] as SVG + [ASCIIRenderer] as ASCII + } + + package "AI Assist\n(SmartTools)" { + [artifact-generator] as Gen + [artifact-refiner] as Refine + [artifact-assist] as Assist + } +} + +package "External Tools" { + [mermaid-cli] as MermaidCLI + [plantuml.jar] as PlantUMLJar + [openscad] as OpenSCADApp + [cairosvg] as Cairo +} + +package "Parent App\n(orchestrated-discussions)" { + [Comment Dialog] as Parent +} + +' Flow +Parent --> CLI : launch with --output +CLI --> UI : initialize +UI --> Registry : get_renderer(file_ext) +Registry --> Mermaid : route by extension +Mermaid --> MermaidCLI : subprocess call +MermaidCLI --> Mermaid : SVG bytes +Mermaid --> UI : preview image + +UI --> Gen : user clicks "Generate" +Gen --> UI : artifact code + +UI --> CLI : user saves +CLI --> Parent : stdout: ARTIFACT_SAVED:path + +note right of Registry + Strategy pattern: + Each renderer knows how to + validate, render, and preview + its format +end note + +note right of Gen + SmartTools run as subprocesses + Input: description or existing code + Output: JSON with artifact content +end note + +note bottom of CLI + Integration Contract: + - Input: --output <path> --type <format> + - Output: ARTIFACT_SAVED:<path> + - Exit: 0=saved, 1=error, 2=cancelled +end note + +@enduml \ No newline at end of file diff --git a/diagrams/diagrams/notification-system-brainstorm_mvp_1.png b/diagrams/diagrams/notification-system-brainstorm_mvp_1.png new file mode 100644 index 0000000..ae6f32b Binary files /dev/null and b/diagrams/diagrams/notification-system-brainstorm_mvp_1.png differ diff --git a/diagrams/notification-system-brainstorm_architect_4.puml b/diagrams/notification-system-brainstorm_architect_4.puml new file mode 100644 index 0000000..9265279 --- /dev/null +++ b/diagrams/notification-system-brainstorm_architect_4.puml @@ -0,0 +1,92 @@ +@startuml +!define RECTANGLE class + +title Notification System - Component Architecture + +package "Event Sources" { + [Message Service] as MSG + [Status Service] as STATUS + [Alert Service] as ALERT +} + +package "Notification Core" { + [Event Processor] as PROC + [Notification Store] as STORE + [Delivery Coordinator] as COORD + + note right of PROC + Responsibilities: + - Validate event schema + - Check user preferences + - Sanitize content (XSS prevention) + - Enforce rate limits + - Authorize (user_id matching) + end note + + note right of STORE + Tables: + - notifications (id, user_id, event_type, + message, created_at, read) + - user_preferences (user_id, + event_type, enabled) + - rate_limits (user_id, count, window) + end note +} + +package "Delivery Mechanisms" { + [Polling Endpoint] as POLL + [WebSocket Gateway] as WS + + note bottom of POLL + MVP Implementation: + GET /api/notifications/unread + - Uses existing session auth + - Returns JSON array + - 30s polling interval + end note + + note bottom of WS + Future Implementation: + - Socket.IO or raw WebSocket + - Token-based auth + - Real-time push + (Not in MVP) + end note +} + +package "Clients" { + [React Frontend] as FRONTEND + [Browser Notification API] as BROWSER +} + +' Event flow +MSG --> PROC : emit event +STATUS --> PROC : emit event +ALERT --> PROC : emit event + +PROC --> STORE : store notification +PROC --> COORD : trigger delivery + +COORD --> POLL : via polling (MVP) +COORD ..> WS : via push (future) + +POLL --> FRONTEND : HTTP response +WS ..> FRONTEND : WebSocket message + +FRONTEND --> BROWSER : show notification + +' Component boundaries +package "Security Boundary" { + PROC + COORD +} + +note bottom of "Security Boundary" + All security measures enforced here: + - Input sanitization + - Rate limiting (100/min per user) + - Authentication (session/token) + - Authorization (user_id ACL) +end note + +@enduml \ No newline at end of file diff --git a/diagrams/notification-system-brainstorm_architect_5.puml b/diagrams/notification-system-brainstorm_architect_5.puml new file mode 100644 index 0000000..fae1a9f --- /dev/null +++ b/diagrams/notification-system-brainstorm_architect_5.puml @@ -0,0 +1,54 @@ +@startuml +title Notification System - Polling Architecture Sequence + +actor User +participant "React Frontend" as Frontend +participant "Flask API" as API +database "PostgreSQL" as DB +participant "Background Job\n(event source)" as EventSource + +== Event Generation == +EventSource -> EventSource: Detect event\n(new message, status change, etc) +EventSource -> EventSource: Check user preferences\n(is this event type enabled?) +EventSource -> DB: INSERT INTO notifications\n(user_id, event_type, message, read=false) +note right: Sanitize message here\n(single choke point for XSS) +DB --> EventSource: OK + +== Polling Loop (every 30s) == +loop Every 30 seconds + Frontend -> API: GET /api/notifications/unread + note right: Headers: session cookie\n(existing auth) + API -> API: Extract user_id from session + API -> DB: SELECT * FROM notifications\nWHERE user_id = ? AND read = false\nORDER BY created_at DESC + DB --> API: [notification rows] + API -> API: Rate limit check\n(Flask-Limiter: 100/min) + API --> Frontend: JSON array of notifications + + alt New notifications exist + Frontend -> Frontend: Filter by user preferences\n(double-check client-side) + Frontend -> User: Show browser notification\n(Browser Notification API) + User -> Frontend: Click notification + Frontend -> API: PATCH /api/notifications/{id}/read + API -> DB: UPDATE notifications\nSET read = true WHERE id = ? + DB --> API: OK + API --> Frontend: 200 OK + end +end + +== User Preferences Management == +User -> Frontend: Toggle preference\n("disable status change notifications") +Frontend -> API: PUT /api/preferences +API -> DB: UPDATE user_preferences\nSET event_type_enabled = false +DB --> API: OK +API --> Frontend: 200 OK + +note over Frontend, DB + **Key Architectural Properties:** + - Stateless: No connection state to manage + - Simple: Standard HTTP request/response + - Scalable: ~33 req/s for 1000 users + - Secure: Session auth + rate limiting + sanitization + - Evolvable: Can swap to SSE/WS without changing much +end note + +@enduml \ No newline at end of file diff --git a/diagrams/notification-system-brainstorm_architect_6.puml b/diagrams/notification-system-brainstorm_architect_6.puml new file mode 100644 index 0000000..8a781b1 --- /dev/null +++ b/diagrams/notification-system-brainstorm_architect_6.puml @@ -0,0 +1,93 @@ +@startuml +title Notification System - Architectural Components (MVP) + +!define COMPONENT_COLOR #E3F2FD +!define API_COLOR #FFF9C4 +!define STORAGE_COLOR #F3E5F5 +!define CLIENT_COLOR #E8F5E9 + +package "Backend Services" { + component [Message Service] as MS + component [Status Service] as SS + component [Alert Service] as AS + note right of MS + Existing services + trigger events + end note +} + +package "Notification System (NEW)" COMPONENT_COLOR { + database "notifications\ntable" as DB STORAGE_COLOR { + note right + user_id + event_type + message (sanitized) + created_at + read (boolean) + end note + } + + database "user_preferences\ntable" as PREF STORAGE_COLOR { + note right + user_id + event_type + enabled (boolean) + end note + } + + component "Notification API" as API API_COLOR { + note right + GET /api/notifications/unread + POST /api/notifications/mark-read + GET/PUT /api/notifications/preferences + + Security: + - Session auth (existing) + - Rate limit: 100/min + - Sanitize with bleach + end note + } +} + +package "Frontend (React)" CLIENT_COLOR { + component [Poller\n(30s interval)] as Poller + component [Browser\nNotification API] as BrowserAPI + + note right of Poller + setInterval(() => { + fetch('/api/notifications/unread') + .then(showNotifications) + }, 30000) + end note +} + +MS --> DB : INSERT notification\n(if preference enabled) +SS --> DB : INSERT notification\n(if preference enabled) +AS --> DB : INSERT notification\n(if preference enabled) + +Poller --> API : GET /unread\n(authenticated session) +API --> DB : SELECT WHERE\nuser_id AND NOT read +API --> PREF : JOIN preferences + +API ..> Poller : JSON response +Poller --> BrowserAPI : Show popup + +note bottom of DB + 30s latency acceptable + No message queue needed + ~33 req/s for 1000 users +end note + +' Sequence flow for single notification +note as N1 + **Notification Flow:** + 1. Event occurs in Message Service + 2. Check user_preferences + 3. INSERT sanitized notification + 4. Client polls within 30s + 5. API returns unread notifications + 6. Browser shows popup + 7. User clicks → mark as read +end note + +@enduml \ No newline at end of file diff --git a/diagrams/notification-system-brainstorm_mvp_1.png b/diagrams/notification-system-brainstorm_mvp_1.png new file mode 100644 index 0000000..ae6f32b Binary files /dev/null and b/diagrams/notification-system-brainstorm_mvp_1.png differ diff --git a/diagrams/notification-system-brainstorm_mvp_1.puml b/diagrams/notification-system-brainstorm_mvp_1.puml new file mode 100644 index 0000000..f963df4 --- /dev/null +++ b/diagrams/notification-system-brainstorm_mvp_1.puml @@ -0,0 +1,40 @@ +@startuml +title MVP Notification System (Polling) + +left to right direction + +actor User +rectangle "Backend (Python)" { + component "App Logic" as App + database "notifications table" as DB + component "GET /api/notifications/unread" as API +} + +rectangle "Frontend (React)" { + component "Poller (30s)" as Poller + component "Browser Notification API" as BrowserAPI +} + +App -> DB: INSERT notification +Poller -> API: Poll every 30s +API -> DB: SELECT WHERE user_id AND read=false +API --> Poller: JSON array +Poller -> BrowserAPI: Show popup +BrowserAPI -> User: Desktop notification + +note right of DB + Simple schema: + - id + - user_id + - event_type + - message + - created_at + - read (boolean) +end note + +note bottom of Poller + No WebSockets needed + 1000 users × 2 req/min + = ~33 req/s (trivial load) +end note +@enduml \ No newline at end of file diff --git a/diagrams/notification-system-brainstorm_mvp_3.png b/diagrams/notification-system-brainstorm_mvp_3.png new file mode 100644 index 0000000..16e67d3 Binary files /dev/null and b/diagrams/notification-system-brainstorm_mvp_3.png differ diff --git a/diagrams/notification-system-brainstorm_mvp_3.puml b/diagrams/notification-system-brainstorm_mvp_3.puml new file mode 100644 index 0000000..d648683 --- /dev/null +++ b/diagrams/notification-system-brainstorm_mvp_3.puml @@ -0,0 +1,131 @@ +@startuml + +skinparam backgroundColor white +skinparam defaultFontColor black +skinparam arrowColor black +skinparam noteBackgroundColor #FFFFCC +skinparam noteBorderColor black +skinparam packageBackgroundColor #F5F5F5 +skinparam packageBorderColor black +skinparam componentBackgroundColor white +skinparam componentBorderColor black +skinparam databaseBackgroundColor white +skinparam databaseBorderColor black + +title Polling (MVP) vs WebSocket (Complex) + +package "Polling Approach (SHIP THIS)" { + [Browser] as B1 + [Flask API] as F1 + database "PostgreSQL" as DB1 + + B1 -down-> F1 : HTTP GET /notifications/unread\n(every 30s) + F1 -down-> DB1 : SELECT * WHERE user_id=? AND read=false + DB1 -up-> F1 : JSON array + F1 -up-> B1 : {notifications: [...]} + + note right of F1 + Security: + - Session auth (existing) + - Rate limit: 100/min + - Sanitize with bleach + + Complexity: LOW + Time: 1 week + end note +} + +package "WebSocket Approach (DON'T)" { + [Browser] as B2 + [WebSocket Server] as WS + [Message Queue] as MQ + [Auth Service] as Auth + database "PostgreSQL" as DB2 + + B2 -down-> WS : WSS connect + JWT + WS -right-> Auth : Validate token + WS -down-> MQ : Subscribe user channel + MQ -down-> DB2 : Persist? + + note right of WS + Security: + - JWT generation/rotation + - WSS certificates + - Connection state management + - Stale connection cleanup + - Token refresh logic + - Rate limiting per connection + + Complexity: HIGH + Time: 3-4 weeks + end note +} + +note bottom + For 1000 users, 30s latency requirement: + Polling = 33 req/s (trivial) + + Ship polling now, upgrade IF needed +end note + +@enduml +@startuml +!theme plain + +title Polling (MVP) vs WebSocket (Complex) + +package "Polling Approach (SHIP THIS)" { + [Browser] as B1 + [Flask API] as F1 + database "PostgreSQL" as DB1 + + B1 -down-> F1 : HTTP GET /notifications/unread\n(every 30s) + F1 -down-> DB1 : SELECT * WHERE user_id=? AND read=false + DB1 -up-> F1 : JSON array + F1 -up-> B1 : {notifications: [...]} + + note right of F1 + Security: + - Session auth (existing) + - Rate limit: 100/min + - Sanitize with bleach + + Complexity: LOW + Time: 1 week + end note +} + +package "WebSocket Approach (DON'T)" { + [Browser] as B2 + [WebSocket Server] as WS + [Message Queue] as MQ + [Auth Service] as Auth + database "PostgreSQL" as DB2 + + B2 -down-> WS : WSS connect + JWT + WS -right-> Auth : Validate token + WS -down-> MQ : Subscribe user channel + MQ -down-> DB2 : Persist? + + note right of WS + Security: + - JWT generation/rotation + - WSS certificates + - Connection state management + - Stale connection cleanup + - Token refresh logic + - Rate limiting per connection + + Complexity: HIGH + Time: 3-4 weeks + end note +} + +note bottom + For 1000 users, 30s latency requirement: + Polling = 33 req/s (trivial) + + Ship polling now, upgrade IF needed +end note + +@enduml \ No newline at end of file diff --git a/diagrams/notification-system-brainstorm_mvp_3_001.png b/diagrams/notification-system-brainstorm_mvp_3_001.png new file mode 100644 index 0000000..7d01d33 Binary files /dev/null and b/diagrams/notification-system-brainstorm_mvp_3_001.png differ diff --git a/diagrams/notification-system-brainstorm_mvp_6.puml b/diagrams/notification-system-brainstorm_mvp_6.puml new file mode 100644 index 0000000..8cbae8c --- /dev/null +++ b/diagrams/notification-system-brainstorm_mvp_6.puml @@ -0,0 +1,52 @@ +@startuml +title Polling Architecture with Security + +!define SECURITY_COLOR #FFE6E6 +!define APP_COLOR #E6F3FF +!define DATA_COLOR #E6FFE6 + +rectangle "Event Happens" as Event APP_COLOR +rectangle "Sanitize & Rate Check" as Sanitize SECURITY_COLOR +database "notifications table" as DB DATA_COLOR +rectangle "REST API\n(session auth + rate limit)" as API SECURITY_COLOR +rectangle "Frontend Poller\n(30s interval)" as Frontend APP_COLOR +rectangle "Browser Notification\n(text-only)" as Browser SECURITY_COLOR + +Event --> Sanitize : bleach.clean() +Sanitize --> DB : INSERT if under 100/min +Frontend --> API : GET /api/notifications/unread +API --> DB : SELECT WHERE user_id = ? +DB --> API : JSON array +API --> Frontend : sanitized text +Frontend --> Browser : plain text display + +note right of Sanitize + Security Layer 1: + - Strip all HTML/JS + - Check user preferences + - Rate limit: 100/user/min +end note + +note right of API + Security Layer 2: + - Session authentication + - Flask-Limiter: 120/min + - user_id filter in SQL +end note + +note right of Browser + Security Layer 3: + - No HTML rendering + - User permission required + - Text-only display +end note + +note bottom of DB + Simple Schema: + - id, user_id, event_type + - message_text (sanitized) + - created_at, read_at + - Auto-delete after 30 days +end note + +@enduml \ No newline at end of file diff --git a/diagrams/notification-system-brainstorm_security_1.puml b/diagrams/notification-system-brainstorm_security_1.puml new file mode 100644 index 0000000..e0102ca --- /dev/null +++ b/diagrams/notification-system-brainstorm_security_1.puml @@ -0,0 +1,52 @@ +@startuml +title Notification System - Threat Model + +actor "Attacker" as attacker +actor "Legitimate User" as user +component "React Frontend" as frontend +component "Python Backend" as backend +component "Notification Service" as notif +database "User Preferences" as prefs + +package "Attack Vectors" { + note right of attacker + 1. XSS via notification content + 2. Spam/DoS flooding + 3. Token theft/replay + 4. Authorization bypass + 5. MitM on WebSocket + end note +} + +package "Security Controls" { + rectangle "Input Sanitization" as sanitize + rectangle "Rate Limiting" as ratelimit + rectangle "WSS + JWT Auth" as auth + rectangle "ACL Checks" as acl +} + +' Normal flow +user -> frontend : Authenticate +frontend -> backend : Get notification token +backend -> auth : Validate & issue JWT +frontend -> notif : Connect via WSS + token +backend -> sanitize : Sanitize content +sanitize -> ratelimit : Check limits +ratelimit -> acl : Verify user permissions +acl -> notif : Send notification +notif -> frontend : Deliver notification + +' Attack attempts +attacker -[#red]-> notif : <script>alert('xss')</script> +sanitize -[#green]-> attacker : ✗ Blocked + +attacker -[#red]-> notif : 10000 notifications/sec +ratelimit -[#green]-> attacker : ✗ Rate limited + +attacker -[#red]-> notif : Stolen token +auth -[#orange]-> attacker : ✗ Token expired (5min TTL) + +attacker -[#red]-> frontend : Read other user's notifications +acl -[#green]-> attacker : ✗ Authorization failed + +@enduml \ No newline at end of file diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_1.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_1.puml new file mode 100644 index 0000000..21ffb50 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_1.puml @@ -0,0 +1,10 @@ +@startuml +' Created by rob +' Edit this diagram and save + +' Example: +' component [Client] +' component [Server] +' [Client] --> [Server] + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_10.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_10.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_10.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_11.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_11.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_11.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_12.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_12.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_12.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_13.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_13.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_13.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_14.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_14.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_14.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_15.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_15.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_15.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_16.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_16.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_16.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_17.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_17.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_17.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_18.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_18.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_18.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_19.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_19.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_19.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_2.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_2.puml new file mode 100644 index 0000000..21ffb50 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_2.puml @@ -0,0 +1,10 @@ +@startuml +' Created by rob +' Edit this diagram and save + +' Example: +' component [Client] +' component [Server] +' [Client] --> [Server] + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_20.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_20.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_20.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_21.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_21.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_21.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_22.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_22.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_22.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_23.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_23.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_23.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_24.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_24.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_24.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_25.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_25.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_25.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_26.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_26.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_26.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_27.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_27.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_27.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_28.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_28.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_28.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_29.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_29.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_29.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_3.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_3.puml new file mode 100644 index 0000000..21ffb50 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_3.puml @@ -0,0 +1,10 @@ +@startuml +' Created by rob +' Edit this diagram and save + +' Example: +' component [Client] +' component [Server] +' [Client] --> [Server] + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_30.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_30.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_30.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_31.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_31.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_31.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_32.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_32.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_32.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_33.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_33.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_33.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_34.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_34.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_34.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_35.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_35.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_35.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_36.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_36.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_36.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_37.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_37.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_37.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_38.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_38.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_38.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_39.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_39.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_39.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_4.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_4.puml new file mode 100644 index 0000000..21ffb50 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_4.puml @@ -0,0 +1,10 @@ +@startuml +' Created by rob +' Edit this diagram and save + +' Example: +' component [Client] +' component [Server] +' [Client] --> [Server] + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_40.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_40.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_40.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_41.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_41.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_41.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_42.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_42.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_42.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_43.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_43.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_43.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_44.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_44.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_44.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_45.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_45.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_45.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_46.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_46.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_46.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_47.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_47.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_47.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_48.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_48.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_48.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_49.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_49.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_49.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_5.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_5.puml new file mode 100644 index 0000000..21ffb50 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_5.puml @@ -0,0 +1,10 @@ +@startuml +' Created by rob +' Edit this diagram and save + +' Example: +' component [Client] +' component [Server] +' [Client] --> [Server] + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_50.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_50.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_50.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_51.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_51.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_51.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_52.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_52.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_52.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_53.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_53.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_53.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_54.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_54.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_54.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_55.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_55.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_55.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_56.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_56.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_56.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_57.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_57.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_57.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_58.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_58.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_58.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_59.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_59.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_59.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_6.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_6.puml new file mode 100644 index 0000000..21ffb50 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_6.puml @@ -0,0 +1,10 @@ +@startuml +' Created by rob +' Edit this diagram and save + +' Example: +' component [Client] +' component [Server] +' [Client] --> [Server] + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_60.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_60.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_60.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_61.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_61.puml new file mode 100644 index 0000000..ab388e4 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_61.puml @@ -0,0 +1,98 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 800" width="1200" height="800"> + <!-- Background --> + <rect width="100%" height="100%" fill="#f8fafc"/> + + <!-- Header --> + <rect x="0" y="0" width="1200" height="80" fill="#1e293b"/> + <text x="40" y="50" font-family="sans-serif" font-size="28" font-weight="bold" fill="#ffffff">DevPlanner</text> + <text x="1020" y="50" font-family="sans-serif" font-size="16" fill="#94a3b8">Today: Dec 23, 2025</text> + + <!-- Sidebar --> + <rect x="0" y="80" width="280" height="720" fill="#334155"/> + + <!-- Sidebar Items --> + <g id="sidebar-nav"> + <rect x="20" y="110" width="240" height="50" rx="8" fill="#475569"/> + <text x="40" y="142" font-family="sans-serif" font-size="16" fill="#ffffff">📋 Today's Tasks</text> + + <rect x="20" y="180" width="240" height="50" rx="8" fill="#1e293b"/> + <text x="40" y="212" font-family="sans-serif" font-size="16" fill="#cbd5e1">📅 Upcoming</text> + + <rect x="20" y="250" width="240" height="50" rx="8" fill="#1e293b"/> + <text x="40" y="282" font-family="sans-serif" font-size="16" fill="#cbd5e1">✅ Completed</text> + + <rect x="20" y="320" width="240" height="50" rx="8" fill="#1e293b"/> + <text x="40" y="352" font-family="sans-serif" font-size="16" fill="#cbd5e1">📊 Analytics</text> + </g> + + <!-- Progress Widget --> + <rect x="20" y="450" width="240" height="120" rx="8" fill="#1e293b"/> + <text x="40" y="480" font-family="sans-serif" font-size="14" fill="#94a3b8">Today's Progress</text> + <text x="140" y="520" font-family="sans-serif" font-size="32" font-weight="bold" fill="#10b981" text-anchor="middle">68%</text> + <text x="140" y="550" font-family="sans-serif" font-size="12" fill="#cbd5e1" text-anchor="middle">13 of 19 tasks</text> + + <!-- Main Content Area --> + <rect x="280" y="80" width="920" height="720" fill="#ffffff"/> + + <!-- Content Header --> + <text x="320" y="140" font-family="sans-serif" font-size="24" font-weight="bold" fill="#0f172a">Today's Tasks</text> + <rect x="1020" y="110" width="150" height="45" rx="8" fill="#3b82f6"/> + <text x="1095" y="140" font-family="sans-serif" font-size="16" fill="#ffffff" text-anchor="middle">+ New Task</text> + + <!-- Filter Tabs --> + <rect x="320" y="170" width="100" height="36" rx="6" fill="#e0e7ff"/> + <text x="370" y="194" font-family="sans-serif" font-size="14" fill="#4338ca" text-anchor="middle">All (19)</text> + + <rect x="430" y="170" width="110" height="36" rx="6" fill="#f1f5f9"/> + <text x="485" y="194" font-family="sans-serif" font-size="14" fill="#64748b" text-anchor="middle">Active (6)</text> + + <rect x="550" y="170" width="140" height="36" rx="6" fill="#f1f5f9"/> + <text x="620" y="194" font-family="sans-serif" font-size="14" fill="#64748b" text-anchor="middle">Completed (13)</text> + + <!-- Task List --> + <!-- Task 1 - Completed --> + <rect x="320" y="240" width="840" height="90" rx="8" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/> + <circle cx="350" cy="285" r="12" fill="#10b981" stroke="#059669" stroke-width="2"/> + <line x1="345" y1="285" x2="348" y2="290" stroke="#ffffff" stroke-width="2" stroke-linecap="round"/> + <line x1="348" y1="290" x2="355" y2="280" stroke="#ffffff" stroke-width="2" stroke-linecap="round"/> + <text x="380" y="282" font-family="sans-serif" font-size="16" fill="#94a3b8" text-decoration="line-through">Review pull requests</text> + <text x="380" y="305" font-family="sans-serif" font-size="13" fill="#cbd5e1">2 subtasks completed</text> + <rect x="1080" y="270" width="60" height="30" rx="4" fill="#ecfdf5"/> + <text x="1110" y="291" font-family="sans-serif" font-size="12" fill="#059669" text-anchor="middle">Done</text> + + <!-- Task 2 - Active with subtasks --> + <rect x="320" y="350" width="840" height="110" rx="8" fill="#ffffff" stroke="#3b82f6" stroke-width="2"/> + <circle cx="350" cy="395" r="12" fill="#ffffff" stroke="#64748b" stroke-width="2"/> + <text x="380" y="392" font-family="sans-serif" font-size="16" font-weight="500" fill="#0f172a">Implement authentication module</text> + <text x="380" y="415" font-family="sans-serif" font-size="13" fill="#64748b">3 of 5 subtasks • High Priority</text> + + <!-- Subtask indicators --> + <rect x="380" y="425" width="400" height="8" rx="4" fill="#e2e8f0"/> + <rect x="380" y="425" width="240" height="8" rx="4" fill="#3b82f6"/> + <text x="790" y="432" font-family="sans-serif" font-size="12" fill="#64748b">60%</text> + + <rect x="1030" y="380" width="120" height="30" rx="4" fill="#fef3c7"/> + <text x="1090" y="401" font-family="sans-serif" font-size="12" fill="#d97706" text-anchor="middle">In Progress</text> + + <!-- Task 3 - Active --> + <rect x="320" y="480" width="840" height="90" rx="8" fill="#ffffff" stroke="#e2e8f0" stroke-width="1"/> + <circle cx="350" cy="525" r="12" fill="#ffffff" stroke="#64748b" stroke-width="2"/> + <text x="380" y="522" font-family="sans-serif" font-size="16" fill="#0f172a">Update documentation</text> + <text x="380" y="545" font-family="sans-serif" font-size="13" fill="#64748b">No subtasks • Medium Priority</text> + <rect x="1080" y="510" width="60" height="30" rx="4" fill="#f1f5f9"/> + <text x="1110" y="531" font-family="sans-serif" font-size="12" fill="#64748b" text-anchor="middle">Todo</text> + + <!-- Task 4 - Active --> + <rect x="320" y="590" width="840" height="90" rx="8" fill="#ffffff" stroke="#e2e8f0" stroke-width="1"/> + <circle cx="350" cy="635" r="12" fill="#ffffff" stroke="#64748b" stroke-width="2"/> + <text x="380" y="632" font-family="sans-serif" font-size="16" fill="#0f172a">Fix bug in payment gateway</text> + <text x="380" y="655" font-family="sans-serif" font-size="13" fill="#64748b">1 subtask • High Priority</text> + <rect x="1050" y="620" width="90" height="30" rx="4" fill="#fee2e2"/> + <text x="1095" y="641" font-family="sans-serif" font-size="12" fill="#dc2626" text-anchor="middle">Urgent</text> + + <!-- Floating Action Hints --> + <circle cx="900" cy="395" r="3" fill="#64748b"/> + <circle cx="915" cy="395" r="3" fill="#64748b"/> + <circle cx="930" cy="395" r="3" fill="#64748b"/> +</svg> \ No newline at end of file diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_62.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_62.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_62.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_63.svg b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_63.svg new file mode 100644 index 0000000..630ca42 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_63.svg @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 500" width="400" height="500"> + <rect width="100%" height="100%" fill="#f9fafb"/> + + <!-- Card container --> + <rect x="50" y="80" width="300" height="340" rx="8" fill="#ffffff" stroke="#e5e7eb"/> + + <!-- Logo placeholder --> + <circle cx="200" cy="130" r="30" fill="#e5e7eb"/> + <text x="200" y="135" text-anchor="middle" font-family="sans-serif" font-size="12" fill="#9ca3af">Logo</text> + + <!-- Title --> + <text x="200" y="190" text-anchor="middle" font-family="sans-serif" font-size="20" font-weight="bold" fill="#111827">Welcome Back</text> + + <!-- Email input --> + <text x="70" y="230" font-family="sans-serif" font-size="12" fill="#374151">Email</text> + <rect x="70" y="240" width="260" height="40" rx="4" fill="#ffffff" stroke="#d1d5db"/> + <text x="82" y="265" font-family="sans-serif" font-size="14" fill="#9ca3af">you@example.com</text> + + <!-- Password input --> + <text x="70" y="300" font-family="sans-serif" font-size="12" fill="#374151">Password</text> + <rect x="70" y="310" width="260" height="40" rx="4" fill="#ffffff" stroke="#d1d5db"/> + <text x="82" y="335" font-family="sans-serif" font-size="14" fill="#9ca3af">••••••••</text> + + <!-- Sign in button --> + <rect x="70" y="370" width="260" height="40" rx="4" fill="#3b82f6"/> + <text x="200" y="395" text-anchor="middle" font-family="sans-serif" font-size="14" fill="#ffffff">Sign In</text> + + <!-- Forgot password link --> + <text x="200" y="435" text-anchor="middle" font-family="sans-serif" font-size="12" fill="#3b82f6">Forgot password?</text> +</svg> \ No newline at end of file diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_7.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_7.puml new file mode 100644 index 0000000..21ffb50 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_7.puml @@ -0,0 +1,10 @@ +@startuml +' Created by rob +' Edit this diagram and save + +' Example: +' component [Client] +' component [Server] +' [Client] --> [Server] + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_8.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_8.puml new file mode 100644 index 0000000..21ffb50 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_8.puml @@ -0,0 +1,10 @@ +@startuml +' Created by rob +' Edit this diagram and save + +' Example: +' component [Client] +' component [Server] +' [Client] --> [Server] + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_9.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_9.puml new file mode 100644 index 0000000..8f0bf12 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-an_rob_9.puml @@ -0,0 +1,10 @@ +@startuml +title New Diagram + +class Example { + +attribute: type + -- + +method() +} + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_designer_2.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_designer_2.puml new file mode 100644 index 0000000..be17cdb --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_designer_2.puml @@ -0,0 +1 @@ +@startuml\n!theme plain\ntitle Artifact Editor v1 - Recommended Layout\n\npackage \"Orchestrated Discussions\n(Parent Window)\" #F5F5F5 {\n rectangle \"Dialog: Add Artifact\" as dialog #FFFFFF {\n \n ' Header\n rectangle \"Artifact Type Selector\" as typesel #E8F4F8 {\n note right of typesel\n Dropdown: Mermaid | PlantUML | etc\n end note\n }\n \n ' Main editing area\n rectangle \"Code Editor (70%)\" as editor #FAFAFA {\n note right of editor\n • Syntax highlighting\n • Line numbers\n • Inline error underlines\n • Status: \"Valid\" / \"Error\"\n end note\n }\n \n rectangle \"Preview Pane (30%)\" as preview #F0F0F0 {\n note right of preview\n • Live render update\n • Zoom controls\n • \"Click to expand\" for large diagrams\n end note\n }\n \n ' Action toolbar\n rectangle \"Controls\" as controls #E8F4F8 {\n note right of controls\n 🎤 Voice Input | ✨ AI Generate | Save | Cancel\n end note\n }\n }\n}\n\nnote as N1\n **Design Principles:**\n • Match parent app's visual language\n • Code editor dominant (developer audience)\n • Inline error feedback\n • Voice input discoverable (icon + label)\n • Clear action buttons\nend note\n@enduml \ No newline at end of file diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_designer_5.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_designer_5.puml new file mode 100644 index 0000000..ed95633 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_designer_5.puml @@ -0,0 +1,57 @@ +@startuml artifact-editor-design +!theme plain + +skinparam backgroundColor #f5f5f5 +skinparam component { + BackgroundColor #e1f5ff + BorderColor #01579b +} +skinparam rectangle { + BackgroundColor #f3e5f5 + BorderColor #512da8 +} + +rectangle "Artifact Editor MVP - Design Layout" { + component "Left Panel (50%)" as left { + component "Text Editor" as editor + note right of editor + PlantUML syntax + Syntax highlighting + Line numbers + Error markers (red underlines) + end note + } + + component "Right Panel (50%)" as right { + component "Visual Canvas" as canvas + note right of canvas + Live preview + Selection handles + Draw mode affordances + Error indicators + end note + } + + component "Bottom Bar" as controls { + component "Render Button" as render + component "AI Generate" as ai + component "Voice Input" as voice + component "Save" as save + } +} + +editor -down-> canvas: "Bidirectional Sync" +canvas -down-> editor: "(critical requirement)" +render -up-> editor: "Update" +render -up-> canvas: "Update" +ai -up-> editor: "Generate code" +voice -up-> editor: "Transcribe → code" +save -up-> editor: "Output ARTIFACT_SAVED" + +note bottom of controls + Clear visual affordances + Consistent with orchestrated-discussions styling + Keyboard shortcuts for power users +end note + +@enduml \ No newline at end of file diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_designer_7.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_designer_7.puml new file mode 100644 index 0000000..9e852c0 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_designer_7.puml @@ -0,0 +1 @@ +@startuml\n!define DIAGRAM_TITLE Artifact Editor Layout - PyQt6 Implementation\ntitle DIAGRAM_TITLE\n\npackage \"Artifact Editor Window\" {\n package \"Menu Bar\" #F0F0F0 {\n card \"File | Edit | Tools | Help\" as menu\n }\n \n package \"Main Workspace\" #FFFFFF {\n package \"Left Panel: Text Editor (50%)\" #E8F4F8 {\n card \"Line Numbers\" as line_nums\n card \"Code Area\\n(PlantUML with Syntax Highlighting)\" as code_area\n card \"Status: Ln 12, Col 5\" as editor_status\n }\n \n package \"Right Panel: Canvas (50%)\" #F8F4E8 {\n card \"Toolbar\\n(Zoom, Pan, Select)\" as canvas_toolbar\n card \"Graphics View\\n(QGraphicsScene)\" as canvas\n card \"Status: 1 shape selected\" as canvas_status\n }\n }\n \n package \"Bottom Status Bar\" #F0F0F0 {\n card \"Mode: Edit\" as mode_indicator\n card \"Render: ✓\" as render_status\n card \"Unsaved Changes\" as dirty_indicator\n }\n}\n\npackage \"Bidirectional Sync Engine\" #DDD {\n card \"PlantUML Parser\" as parser\n card \"Diagram AST Model\" as ast_model\n card \"Canvas Renderer\" as renderer\n card \"Code Generator\" as codegen\n}\n\ncode_area --> parser: \"on edit\"\nparser --> ast_model\nast_model --> renderer\nrenderer --> canvas\n\ncanvas --> codegen: \"on shape move\"\ncodegen --> code_area: \"update code\"\n\nnote right of code_area\n - Syntax highlighting for PlantUML\n - Error underlines (red squiggles)\n - Keyboard shortcuts\n - Undo/Redo\nend note\n\nnote right of canvas\n - Click to select shapes\n - Drag to move\n - Hover tooltips\n - Context menu (delete, duplicate)\n - Zoom/Pan controls\nend note\n\nnote right of mode_indicator\n Shows current mode:\n - Edit: Code focus\n - Draw: Canvas focus\n - Syncing: Bidirectional update\nend note\n\n@enduml \ No newline at end of file diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_1.png b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_1.png new file mode 100644 index 0000000..66f8911 Binary files /dev/null and b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_1.png differ diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_1.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_1.puml new file mode 100644 index 0000000..8713d32 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_1.puml @@ -0,0 +1,35 @@ +@startuml +left to right direction + +rectangle "Complex Proposal" as complex { + rectangle "Graphical Interface" + rectangle "Voice Commands" + rectangle "Conversational AI" + rectangle "Live Preview" + rectangle "3D Models" + rectangle "Multiple Formats" +} + +rectangle "MVP Approach" as mvp { + rectangle "urwid TUI" + rectangle "Text Editor" + rectangle "Save Contract" + rectangle "Single Format" +} + +rectangle "Simplest Path" as simplest { + rectangle "Extend discussion-diagram-editor" + rectangle "Add --edit flag" + rectangle "TUI wrapper" + rectangle "Output ARTIFACT_SAVED" +} + +complex --> mvp : "9+ months → 2 weeks" +mvp --> simplest : "2 weeks → 2 days" + +note right of simplest + Leverage existing SmartTool + Minimal new code + Ships integration immediately +end note +@enduml \ No newline at end of file diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_1.svg b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_1.svg new file mode 100644 index 0000000..431e6cd --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_1.svg @@ -0,0 +1,80 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentScriptType="application/ecmascript" contentStyleType="text/css" height="563px" preserveAspectRatio="none" style="width:1274px;height:563px;" version="1.1" viewBox="0 0 1274 563" width="1274px" zoomAndPan="magnify"><defs><filter height="300%" id="f1p6oio585llud" width="300%" x="-1" y="-1"><feGaussianBlur result="blurOut" stdDeviation="2.0"/><feColorMatrix in="blurOut" result="blurOut2" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .4 0"/><feOffset dx="4.0" dy="4.0" in="blurOut2" result="blurOut3"/><feBlend in="SourceGraphic" in2="blurOut3" mode="normal"/></filter></defs><g><!--MD5=[a592e7578306bb084665cafc4403a9d6] +cluster complex--><rect fill="#FFFFFF" filter="url(#f1p6oio585llud)" height="277" style="stroke: #000000; stroke-width: 1.5;" width="521" x="42.21" y="32"/><text fill="#000000" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacingAndGlyphs" textLength="126" x="239.71" y="48.9659">Complex Proposal</text><!--MD5=[c2d0a486b876620f72dbca333582f273] +cluster mvp--><rect fill="#FFFFFF" filter="url(#f1p6oio585llud)" height="203" style="stroke: #000000; stroke-width: 1.5;" width="540.5" x="402.21" y="349"/><text fill="#000000" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacingAndGlyphs" textLength="102" x="621.46" y="365.9659">MVP Approach</text><!--MD5=[d69fa44fed3cf390c4f4e171948d9f40] +cluster simplest--><rect fill="#FFFFFF" filter="url(#f1p6oio585llud)" height="203" style="stroke: #000000; stroke-width: 1.5;" width="549.5" x="714.21" y="106"/><text fill="#000000" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacingAndGlyphs" textLength="96" x="940.96" y="122.9659">Simplest Path</text><!--MD5=[5aeec96b9f2d8b3d37e488b7762e82c6] +entity Graphical Interface--><rect fill="#FEFECE" filter="url(#f1p6oio585llud)" height="39.0679" style="stroke: #000000; stroke-width: 1.5;" width="150" x="66.21" y="152.5"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="130" x="76.21" y="177.4659">Graphical Interface</text><!--MD5=[2ffaee304ea9a36d9a6d08292317fbc9] +entity Voice Commands--><rect fill="#FEFECE" filter="url(#f1p6oio585llud)" height="39.0679" style="stroke: #000000; stroke-width: 1.5;" width="135" x="73.71" y="78.5"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="115" x="83.71" y="103.4659">Voice Commands</text><!--MD5=[76132062a6c1343138b0e5c0e3799068] +entity Conversational AI--><rect fill="#FEFECE" filter="url(#f1p6oio585llud)" height="39.0679" style="stroke: #000000; stroke-width: 1.5;" width="138" x="72.21" y="226.5"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="118" x="82.21" y="251.4659">Conversational AI</text><!--MD5=[9f9302891b67855128665819139c5db4] +entity Live Preview--><rect fill="#FEFECE" filter="url(#f1p6oio585llud)" height="39.0679" style="stroke: #000000; stroke-width: 1.5;" width="102" x="421.21" y="226.5"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="82" x="431.21" y="251.4659">Live Preview</text><!--MD5=[cae750d0a36d2dc9c142b9bd754a5676] +entity 3D Models--><rect fill="#FEFECE" filter="url(#f1p6oio585llud)" height="39.0679" style="stroke: #000000; stroke-width: 1.5;" width="91" x="426.71" y="152.5"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="71" x="436.71" y="177.4659">3D Models</text><!--MD5=[573b77cdd684d2a9093dc94b9f331081] +entity Multiple Formats--><rect fill="#FEFECE" filter="url(#f1p6oio585llud)" height="39.0679" style="stroke: #000000; stroke-width: 1.5;" width="134" x="405.21" y="78.5"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="114" x="415.21" y="103.4659">Multiple Formats</text><!--MD5=[84af2a0b2cd3251bab076886906ee658] +entity urwid TUI--><rect fill="#FEFECE" filter="url(#f1p6oio585llud)" height="39.0679" style="stroke: #000000; stroke-width: 1.5;" width="86" x="429.21" y="488.5"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="66" x="439.21" y="513.4659">urwid TUI</text><!--MD5=[492866932510ccad70f5ba1f1cd26161] +entity Text Editor--><rect fill="#FEFECE" filter="url(#f1p6oio585llud)" height="39.0679" style="stroke: #000000; stroke-width: 1.5;" width="92" x="426.21" y="414.5"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="72" x="436.21" y="439.4659">Text Editor</text><!--MD5=[8f59ab6dd8ef7ccb74336b2b1cbc41c9] +entity Save Contract--><rect fill="#FEFECE" filter="url(#f1p6oio585llud)" height="39.0679" style="stroke: #000000; stroke-width: 1.5;" width="112" x="806.21" y="488.5"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="92" x="816.21" y="513.4659">Save Contract</text><!--MD5=[e96fcc9693e2fc61e472abdc18cecb22] +entity Single Format--><rect fill="#FEFECE" filter="url(#f1p6oio585llud)" height="39.0679" style="stroke: #000000; stroke-width: 1.5;" width="113" x="805.71" y="414.5"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="93" x="815.71" y="439.4659">Single Format</text><!--MD5=[88ff8d5b58364c8ded6006115a90426f] +entity Extend discussion-diagram-editor--><rect fill="#FEFECE" filter="url(#f1p6oio585llud)" height="39.0679" style="stroke: #000000; stroke-width: 1.5;" width="248" x="738.21" y="226.5"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="228" x="748.21" y="251.4659">Extend discussion-diagram-editor</text><!--MD5=[14d96e68333573cd708133d0280617d1] +entity Add - -edit flag--><rect fill="#FEFECE" filter="url(#f1p6oio585llud)" height="39.0679" style="stroke: #000000; stroke-width: 1.5;" width="117" x="803.71" y="152.5"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="97" x="813.71" y="177.4659">Add --edit flag</text><!--MD5=[d4520744269083d38b49caf1f04dce56] +entity TUI wrapper--><rect fill="#FEFECE" filter="url(#f1p6oio585llud)" height="39.0679" style="stroke: #000000; stroke-width: 1.5;" width="104" x="1095.21" y="226.5"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="84" x="1105.21" y="251.4659">TUI wrapper</text><!--MD5=[219a86ad66105b19e9a337d08a04748a] +entity Output ARTIFACT_SAVED--><rect fill="#FEFECE" filter="url(#f1p6oio585llud)" height="39.0679" style="stroke: #000000; stroke-width: 1.5;" width="185" x="1054.71" y="152.5"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="165" x="1064.71" y="177.4659">Output ARTIFACT_SAVED</text><path d="M1047.21,333.5 L1047.21,396.618 L1247.21,396.618 L1247.21,343.5 L1237.21,333.5 L1047.21,333.5 " fill="#FBFB77" filter="url(#f1p6oio585llud)" style="stroke: #A80036; stroke-width: 1.0;"/><path d="M1237.21,333.5 L1237.21,343.5 L1247.21,343.5 L1237.21,333.5 " fill="#FBFB77" style="stroke: #A80036; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="170" x="1053.21" y="352.397">Leverage existing SmartTool</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="108" x="1053.21" y="370.103">Minimal new code</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="179" x="1053.21" y="387.809">Ships integration immediately</text><!--MD5=[588e7887208a68a2ce716c825d13bf50] +link complex to mvp--><path d="M176.4527,309.114 C177.2626,309.306 178.0842,309.5008 178.9172,309.6983 C180.5833,310.0933 182.2948,310.4991 184.0494,310.9151 C187.5586,311.747 191.2402,312.6199 195.075,313.5291 C225.7538,320.8025 266.24,330.4013 306.725,340 C326.9675,344.7994 347.2097,349.5988 366.2255,354.1075 C375.7334,356.3618 384.9346,358.5435 393.6761,360.6162 C395.8614,361.1344 398.018,361.6457 400.1435,362.1497 C400.6749,362.2757 401.2043,362.4012 401.7318,362.5263 " fill="none" id="complex->mvp" style="stroke: #A80036; stroke-width: 1.0;"/><polygon fill="#A80036" points="401.7318,362.5263,393.8974,356.5578,396.8667,361.3727,392.0517,364.3419,401.7318,362.5263" style="stroke: #A80036; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="127" x="247.21" y="322.897">9+ months → 2 weeks</text><!--MD5=[9bcb1af8eca842f869ab1e3aee7265c3] +link mvp to simplest--><path d="M622.8167,348.9279 C625.7302,348.3421 628.6578,347.7536 631.5968,347.1627 C643.3524,344.7994 655.2887,342.3997 667.225,340 C714.97,330.4013 762.7138,320.8025 798.8888,313.5291 C803.4106,312.6199 807.7517,311.747 811.8895,310.9151 C813.9584,310.4991 815.9764,310.0933 817.9408,309.6983 C818.923,309.5008 819.8918,309.306 820.8468,309.114 " fill="none" id="mvp->simplest" style="stroke: #A80036; stroke-width: 1.0;"/><polygon fill="#A80036" points="820.8468,309.114,811.2349,306.9667,815.9449,310.0997,812.8119,314.8097,820.8468,309.114" style="stroke: #A80036; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="101" x="588.21" y="332.897">2 weeks → 2 days</text><!--MD5=[942fad6b34ea2860dfa3bde1bcf5952f] +link simplest to GMN22--><path d="M899.2196,309.1435 C899.447,309.1949 899.6751,309.2465 899.9041,309.2983 C900.3622,309.4019 900.8235,309.5062 901.2882,309.6113 C908.7223,311.2927 916.9956,313.1638 925.8775,315.1725 C961.405,323.2075 1006.67,333.445 1046.92,342.55 " fill="none" id="simplest-GMN22" style="stroke: #A80036; stroke-width: 1.0; stroke-dasharray: 7.0,7.0;"/><!--MD5=[eaefbc37b3b0b9754aa444609425d7e5] +link Graphical Interface to Voice Commands--><!--MD5=[72c05589dc16d44640c4c6a40a069400] +link Voice Commands to Conversational AI--><!--MD5=[d6f7708b878df9cf3b568e9c6b17f59c] +link Graphical Interface to Live Preview--><!--MD5=[ab1516763c217bad7531110370d71d03] +link Live Preview to 3D Models--><!--MD5=[dc3b92482b643924a4ee9a415752b979] +link 3D Models to Multiple Formats--><!--MD5=[80dcc4f84a464c95d280bb2c20b13497] +link urwid TUI to Text Editor--><!--MD5=[cad63057186840fe7d4d8dd525a9edd7] +link urwid TUI to Save Contract--><!--MD5=[67f0566e171d7c63230f1255be2feeab] +link Save Contract to Single Format--><!--MD5=[373e3c72effc84257d3a5a292e26c7d1] +link Extend discussion-diagram-editor to Add - -edit flag--><!--MD5=[63e19ff018ebc0093ec4d120d0ebced8] +link Extend discussion-diagram-editor to TUI wrapper--><!--MD5=[9df7f7b2ab91371ae67843492b8ad2d4] +link TUI wrapper to Output ARTIFACT_SAVED--><!--MD5=[2763c22d46b5d78165a34372f21a7651] +link Conversational AI to urwid TUI--><!--MD5=[a8a3c62a726dbd561ed87a524a40812e] +link Live Preview to Extend discussion-diagram-editor--><!--MD5=[5934db3152d06acfe8f840c5f86878b0] +@startuml +left to right direction + +rectangle "Complex Proposal" as complex { + rectangle "Graphical Interface" + rectangle "Voice Commands" + rectangle "Conversational AI" + rectangle "Live Preview" + rectangle "3D Models" + rectangle "Multiple Formats" +} + +rectangle "MVP Approach" as mvp { + rectangle "urwid TUI" + rectangle "Text Editor" + rectangle "Save Contract" + rectangle "Single Format" +} + +rectangle "Simplest Path" as simplest { + rectangle "Extend discussion-diagram-editor" + rectangle "Add - -edit flag" + rectangle "TUI wrapper" + rectangle "Output ARTIFACT_SAVED" +} + +complex - -> mvp : "9+ months → 2 weeks" +mvp - -> simplest : "2 weeks → 2 days" + +note right of simplest + Leverage existing SmartTool + Minimal new code + Ships integration immediately +end note +@enduml + +PlantUML version 1.2020.02(Sun Mar 01 06:22:07 AST 2020) +(GPL source distribution) +Java Runtime: OpenJDK Runtime Environment +JVM: OpenJDK 64-Bit Server VM +Java Version: 21.0.8+9-Ubuntu-0ubuntu124.04.1 +Operating System: Linux +Default Encoding: UTF-8 +Language: en +Country: CA +--></g></svg> \ No newline at end of file diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_2.png b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_2.png new file mode 100644 index 0000000..85ef57d Binary files /dev/null and b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_2.png differ diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_2.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_2.puml new file mode 100644 index 0000000..31d7970 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_2.puml @@ -0,0 +1,21 @@ +@startuml +skinparam monochrome true + +rectangle "Orchestrated Discussions TUI" as tui +rectangle "External Editor (nano/vim)" as editor +rectangle "Discussion File" as discussion +rectangle "Artifact File" as artifact + +tui --> editor : "Add Artifact" button +editor --> artifact : Save PlantUML/Mermaid +artifact --> tui : ARTIFACT_SAVED:/path + +note right of tui + **MVP Path (2 days):** + 1. Add button to TUI + 2. Launch external editor + 3. Parse output + 4. Embed in comment +end note + +@enduml \ No newline at end of file diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_2.svg b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_2.svg new file mode 100644 index 0000000..bbd5de9 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_2.svg @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentScriptType="application/ecmascript" contentStyleType="text/css" height="354px" preserveAspectRatio="none" style="width:706px;height:354px;" version="1.1" viewBox="0 0 706 354" width="706px" zoomAndPan="magnify"><defs><filter height="300%" id="f8469l5b3zh25" width="300%" x="-1" y="-1"><feGaussianBlur result="blurOut" stdDeviation="2.0"/><feColorMatrix in="blurOut" result="blurOut2" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .4 0"/><feOffset dx="4.0" dy="4.0" in="blurOut2" result="blurOut3"/><feBlend in="SourceGraphic" in2="blurOut3" mode="normal"/></filter></defs><g><!--MD5=[fb1d7dade1195d386b2f681665a98ab7] +entity tui--><rect fill="#F8F8F8" filter="url(#f8469l5b3zh25)" height="39.0679" style="stroke: #000000; stroke-width: 1.5;" width="219" x="118.5" y="38"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="199" x="128.5" y="62.9659">Orchestrated Discussions TUI</text><!--MD5=[026b3f9ae447cd6258ef99d33f5de6cf] +entity editor--><rect fill="#F8F8F8" filter="url(#f8469l5b3zh25)" height="39.0679" style="stroke: #000000; stroke-width: 1.5;" width="194" x="6" y="186"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="174" x="16" y="210.9659">External Editor (nano/vim)</text><!--MD5=[45336dfbdf07ac27c7a42adffac9f027] +entity discussion--><rect fill="#F8F8F8" filter="url(#f8469l5b3zh25)" height="39.0679" style="stroke: #000000; stroke-width: 1.5;" width="119" x="576.5" y="38"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="99" x="586.5" y="62.9659">Discussion File</text><!--MD5=[ed13bbfe61d4f4be62c09eebe4db66d3] +entity artifact--><rect fill="#F8F8F8" filter="url(#f8469l5b3zh25)" height="39.0679" style="stroke: #000000; stroke-width: 1.5;" width="96" x="138" y="304"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="76" x="148" y="328.9659">Artifact File</text><path d="M372.5,8 L372.5,53.5 L337.58,57.5 L372.5,61.5 L372.5,106.5301 A0,0 0 0 0 372.5,106.5301 L541.5,106.5301 A0,0 0 0 0 541.5,106.5301 L541.5,18 L531.5,8 L372.5,8 A0,0 0 0 0 372.5,8 " fill="#ECECEC" filter="url(#f8469l5b3zh25)" style="stroke: #383838; stroke-width: 1.0;"/><path d="M531.5,8 L531.5,18 L541.5,18 L531.5,8 " fill="#ECECEC" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" font-weight="bold" lengthAdjust="spacingAndGlyphs" textLength="116" x="378.5" y="26.897">MVP Path (2 days):</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="122" x="378.5" y="44.603">1. Add button to TUI</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="148" x="378.5" y="62.309">2. Launch external editor</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="91" x="378.5" y="80.0151">3. Parse output</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="130" x="378.5" y="97.7211">4. Embed in comment</text><!--MD5=[76af645c4ae30fa8ac1e4fad6407a92f] +link tui to editor--><path d="M211.91,77.29 C189.04,104.01 147.16,152.93 122.53,181.69 " fill="none" id="tui->editor" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="119.02,185.78,127.9049,181.5306,122.2657,181.9767,121.8196,176.3374,119.02,185.78" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="42" x="160" y="151.897">button</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="70" x="128.1796" y="98.8757">Add Artifact</text><!--MD5=[d5ca90634606598ece031bce47e4c550] +link editor to artifact--><path d="M95.25,225.29 C90.65,239.63 87.24,259.33 96,274 C104.41,288.09 118.48,298.32 133.03,305.66 " fill="none" id="editor->artifact" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="137.55,307.84,131.1827,300.3262,133.0469,305.6671,127.706,307.5312,137.55,307.84" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="150" x="97" y="269.897">Save PlantUML/Mermaid</text><!--MD5=[7325b40ae84ce5ee99d2163c654acd39] +link artifact to tui--><path d="M223.55,303.85 C234.79,296.25 245.77,286.31 252,274 C284.18,210.42 255.64,123.03 238.59,81.81 " fill="none" id="artifact->tui" style="stroke: #383838; stroke-width: 1.0;"/><polygon fill="#383838" points="236.57,77.02,236.376,86.8669,238.5103,81.6282,243.7491,83.7625,236.57,77.02" style="stroke: #383838; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="138" x="267" y="210.897">ARTIFACT_SAVED:/path</text><!--MD5=[95b12a49e80913885ceffa5c7926c5e2] +@startuml +skinparam monochrome true + +rectangle "Orchestrated Discussions TUI" as tui +rectangle "External Editor (nano/vim)" as editor +rectangle "Discussion File" as discussion +rectangle "Artifact File" as artifact + +tui - -> editor : "Add Artifact" button +editor - -> artifact : Save PlantUML/Mermaid +artifact - -> tui : ARTIFACT_SAVED:/path + +note right of tui + **MVP Path (2 days):** + 1. Add button to TUI + 2. Launch external editor + 3. Parse output + 4. Embed in comment +end note + +@enduml + +PlantUML version 1.2020.02(Sun Mar 01 06:22:07 AST 2020) +(GPL source distribution) +Java Runtime: OpenJDK Runtime Environment +JVM: OpenJDK 64-Bit Server VM +Java Version: 21.0.8+9-Ubuntu-0ubuntu124.04.1 +Operating System: Linux +Default Encoding: UTF-8 +Language: en +Country: CA +--></g></svg> \ No newline at end of file diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_5.png b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_5.png new file mode 100644 index 0000000..90bbe3f Binary files /dev/null and b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_5.png differ diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_5.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_5.puml new file mode 100644 index 0000000..e39cad5 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_5.puml @@ -0,0 +1,17 @@ +@startuml +left to right direction + +rectangle "User wants diagram" as user +rectangle "Launch artifact-editor" as launch +rectangle "Text editor (urwid)" as editor +rectangle "AI modify via\ndiscussion-diagram-editor" as ai +rectangle "Render PlantUML" as render +rectangle "Save & exit" as save + +user -> launch +launch -> editor +editor -> ai : Press 'AI modify' +ai -> editor : Return modified PlantUML +editor -> render : Auto-render on change +render -> save : User saves +@enduml \ No newline at end of file diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_5.svg b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_5.svg new file mode 100644 index 0000000..0ae6f0f --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_5.svg @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentScriptType="application/ecmascript" contentStyleType="text/css" height="448px" preserveAspectRatio="none" style="width:371px;height:448px;" version="1.1" viewBox="0 0 371 448" width="371px" zoomAndPan="magnify"><defs><filter height="300%" id="f8rvuuk7ywvet" width="300%" x="-1" y="-1"><feGaussianBlur result="blurOut" stdDeviation="2.0"/><feColorMatrix in="blurOut" result="blurOut2" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .4 0"/><feOffset dx="4.0" dy="4.0" in="blurOut2" result="blurOut3"/><feBlend in="SourceGraphic" in2="blurOut3" mode="normal"/></filter></defs><g><!--MD5=[8fc3522a43f8c7199df5e09e5bb0188e] +entity user--><rect fill="#FEFECE" filter="url(#f8rvuuk7ywvet)" height="39.0679" style="stroke: #000000; stroke-width: 1.5;" width="156" x="141.4" y="8"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="136" x="151.4" y="32.9659">User wants diagram</text><!--MD5=[eaea3ac7f9db97749017154da9d5e01e] +entity launch--><rect fill="#FEFECE" filter="url(#f8rvuuk7ywvet)" height="39.0679" style="stroke: #000000; stroke-width: 1.5;" width="166" x="136.4" y="82"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="146" x="146.4" y="106.9659">Launch artifact-editor</text><!--MD5=[026b3f9ae447cd6258ef99d33f5de6cf] +entity editor--><rect fill="#FEFECE" filter="url(#f8rvuuk7ywvet)" height="39.0679" style="stroke: #000000; stroke-width: 1.5;" width="143" x="147.9" y="250"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="123" x="157.9" y="274.9659">Text editor (urwid)</text><!--MD5=[1088c149b6bd3a70d07f180d17b4a032] +entity ai--><rect fill="#FEFECE" filter="url(#f8rvuuk7ywvet)" height="58.1358" style="stroke: #000000; stroke-width: 1.5;" width="198" x="120.4" y="156.5"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="87" x="130.4" y="181.4659">AI modify via</text><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="178" x="130.4" y="200.5339">discussion-diagram-editor</text><!--MD5=[7b81b3f54888d80991adc2e216cc97b2] +entity render--><rect fill="#FEFECE" filter="url(#f8rvuuk7ywvet)" height="39.0679" style="stroke: #000000; stroke-width: 1.5;" width="137" x="150.9" y="324"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="117" x="160.9" y="348.9659">Render PlantUML</text><!--MD5=[2935e10667aef0675bf227d465d6e5f6] +entity save--><rect fill="#FEFECE" filter="url(#f8rvuuk7ywvet)" height="39.0679" style="stroke: #000000; stroke-width: 1.5;" width="93" x="172.9" y="398"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="73" x="182.9" y="422.9659">Save & exit</text><!--MD5=[d92c5e655ac6b6ae170bf3a74c508f8b] +link user to launch--><path d="M219.4,47.03 C219.4,56.91 219.4,66.79 219.4,76.68 " fill="none" id="user->launch" style="stroke: #A80036; stroke-width: 1.0;"/><polygon fill="#A80036" points="219.4,81.84,223.4,72.84,219.4,76.84,215.4,72.84,219.4,81.84" style="stroke: #A80036; stroke-width: 1.0;"/><!--MD5=[18366c9b6fdc194c8f77387338c45846] +link launch to editor--><path d="M136.39,118.41 C117.75,126.48 100.51,138.4 90.4,156 C77.56,178.35 77.56,191.65 90.4,214 C101.69,233.65 121.79,246.35 142.82,254.56 " fill="none" id="launch->editor" style="stroke: #A80036; stroke-width: 1.0;"/><polygon fill="#A80036" points="147.6,256.34,140.5775,249.4346,142.9184,254.5844,137.7685,256.9252,147.6,256.34" style="stroke: #A80036; stroke-width: 1.0;"/><!--MD5=[2e42e18cbbab9d016ae199cea22c296b] +link editor to ai--><path d="M257.89,249.81 C271.81,239.55 276.13,229.3 270.85,219.04 " fill="none" id="editor->ai" style="stroke: #A80036; stroke-width: 1.0;"/><polygon fill="#A80036" points="268,214.64,269.5255,224.37,270.7139,218.8394,276.2445,220.0278,268,214.64" style="stroke: #A80036; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="98" x="172.62" y="230.207">Press 'AI modify'</text><!--MD5=[af8c6eed9c0852f112343d4cbd6742b8] +link ai to editor--><path d="M170.8,214.64 C162.49,225.27 164.48,235.89 176.77,246.52 " fill="none" id="ai->editor" style="stroke: #A80036; stroke-width: 1.0;"/><polygon fill="#A80036" points="180.91,249.81,176.3576,241.0764,176.9973,246.697,171.3767,247.3367,180.91,249.81" style="stroke: #A80036; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="160" x="7" y="231.957">Return modified PlantUML</text><!--MD5=[486af03bba29b37fe9633bb4c2f7b159] +link editor to render--><path d="M219.4,289.03 C219.4,298.91 219.4,308.79 219.4,318.68 " fill="none" id="editor->render" style="stroke: #A80036; stroke-width: 1.0;"/><polygon fill="#A80036" points="219.4,323.84,223.4,314.84,219.4,318.84,215.4,314.84,219.4,323.84" style="stroke: #A80036; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="139" x="220.4" y="307.277">Auto-render on change</text><!--MD5=[efd18ef98629ef64286099d3ad5c6ad9] +link render to save--><path d="M219.4,363.03 C219.4,372.91 219.4,382.79 219.4,392.68 " fill="none" id="render->save" style="stroke: #A80036; stroke-width: 1.0;"/><polygon fill="#A80036" points="219.4,397.84,223.4,388.84,219.4,392.84,215.4,388.84,219.4,397.84" style="stroke: #A80036; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="64" x="220.4" y="381.277">User saves</text><!--MD5=[4e2e87b325a7f4ba29a7b2659b624bc5] +@startuml +left to right direction + +rectangle "User wants diagram" as user +rectangle "Launch artifact-editor" as launch +rectangle "Text editor (urwid)" as editor +rectangle "AI modify via\ndiscussion-diagram-editor" as ai +rectangle "Render PlantUML" as render +rectangle "Save & exit" as save + +user -> launch +launch -> editor +editor -> ai : Press 'AI modify' +ai -> editor : Return modified PlantUML +editor -> render : Auto-render on change +render -> save : User saves +@enduml + +PlantUML version 1.2020.02(Sun Mar 01 06:22:07 AST 2020) +(GPL source distribution) +Java Runtime: OpenJDK Runtime Environment +JVM: OpenJDK 64-Bit Server VM +Java Version: 21.0.8+9-Ubuntu-0ubuntu124.04.1 +Operating System: Linux +Default Encoding: UTF-8 +Language: en +Country: CA +--></g></svg> \ No newline at end of file diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_7.png b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_7.png new file mode 100644 index 0000000..dae2fce Binary files /dev/null and b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_7.png differ diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_7.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_7.puml new file mode 100644 index 0000000..08dd7b8 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_7.puml @@ -0,0 +1,131 @@ +@startuml +!define RECTANGLE(x, y, w, h) rectangle "x" as x at (y, h) +!define ARROW(from, to, label) from -> to : label + +skinparam backgroundColor #FFFFFF +skinparam defaultFontSize 14 +skinparam rectangle { + BorderColor #333333 + BackgroundColor #F0F0F0 +} + +title Stand-Alone Artifact Editor - MVP vs Over-Engineered Approaches + +rectangle "Add diagram to discussion comment" as goal #LightBlue +rectangle "MVP Path (2-4 weeks)" as mvp_path #LightGreen +rectangle "Complex Path (6-9 months)" as complex_path #LightCoral + +rectangle "1. Text editor for PlantUML\n2. Save contract (ARTIFACT_SAVED)\n3. Use existing discussion-diagram-editor SmartTool\n4. Simple preview on save" as mvp_details #LightGreen + +rectangle "1. Graphical canvas with drag-drop\n2. Bidirectional text-canvas sync\n3. Voice command integration\n4. Live preview with timer\n5. Multiple format support\n6. Custom GUI framework" as complex_details #LightCoral + +rectangle "MVP Outcome" as mvp_outcome #LightGreen +rectangle "Complex Outcome" as complex_outcome #LightCoral + +rectangle "Working integration\nUsers can add diagrams\nImmediate value\nIterate based on feedback" as mvp_outcome_details #LightGreen + +rectangle "Feature-rich editor\nMonths of development\nHigh risk of scope creep\nDelayed user value" as complex_outcome_details #LightCoral + +goal --> mvp_path : "Simplest path" +goal --> complex_path : "Ambitious vision" + +mvp_path --> mvp_details +complex_path --> complex_details + +mvp_details --> mvp_outcome : "Ship in weeks" +complex_details --> complex_outcome : "Ship in months" + +mvp_outcome --> mvp_outcome_details +complex_outcome --> complex_outcome_details + +rectangle "GUI Framework Options" as gui_options #LightYellow { + rectangle "PyQt6/PySide6\nMature, rich widgets\nGood for drawing\nSteep learning curve" as pyqt + rectangle "Dear PyGui\nUsed by parent project\nLimited drawing capabilities\nFast to implement" as dearpygui + rectangle "Web-based (Electron)\nMost flexible\nHeavy dependencies\nCross-platform" as web + rectangle "TUI (urwid)\nSimplest\nNo graphical canvas\nTerminal-only" as tui +} + +note right of mvp_path + **Recommended MVP Stack:** + - Text editor: urwid TUI + - AI: discussion-diagram-editor SmartTool + - Preview: Render on save + - Contract: ARTIFACT_SAVED +end note + +note right of complex_path + **Complexity Drivers:** + - Graphical canvas implementation + - Bidirectional sync logic + - Voice command pipeline + - Live preview architecture + - Multiple format renderers +end note + +@enduml +@startuml +!define RECTANGLE(x, y, w, h) rectangle "x" as x at (y, h) +!define ARROW(from, to, label) from -> to : label + +skinparam backgroundColor #FFFFFF +skinparam defaultFontSize 14 +skinparam rectangle { + BorderColor #333333 + BackgroundColor #F0F0F0 +} + +title Stand-Alone Artifact Editor - MVP vs Over-Engineered Approaches + +rectangle "User Goal" as goal #LightBlue +rectangle "MVP Path (2-4 weeks)" as mvp_path #LightGreen +rectangle "Complex Path (6-9 months)" as complex_path #LightCoral + +goal : "Add diagram to discussion comment" + +mvp_path : "1. Text editor for PlantUML\n2. Save contract (ARTIFACT_SAVED)\n3. Use existing discussion-diagram-editor SmartTool\n4. Simple preview on save" + +complex_path : "1. Graphical canvas with drag-drop\n2. Bidirectional text-canvas sync\n3. Voice command integration\n4. Live preview with timer\n5. Multiple format support\n6. Custom GUI framework" + +rectangle "MVP Outcome" as mvp_outcome #LightGreen +rectangle "Complex Outcome" as complex_outcome #LightCoral + +mvp_outcome : "Working integration\nUsers can add diagrams\nImmediate value\nIterate based on feedback" + +complex_outcome : "Feature-rich editor\nMonths of development\nHigh risk of scope creep\nDelayed user value" + +goal --> mvp_path : "Simplest path" +goal --> complex_path : "Ambitious vision" + +mvp_path --> mvp_outcome : "Ship in weeks" +complex_path --> complex_outcome : "Ship in months" + +rectangle "GUI Framework Options" as gui_options #LightYellow { + rectangle "PyQt6/PySide6" as pyqt + rectangle "Dear PyGui" as dearpygui + rectangle "Web-based (Electron)" as web + rectangle "TUI (urwid)" as tui +} + +pyqt : "Mature, rich widgets\nGood for drawing\nSteep learning curve" +dearpygui : "Used by parent project\nLimited drawing capabilities\nFast to implement" +web : "Most flexible\nHeavy dependencies\nCross-platform" +tui : "Simplest\nNo graphical canvas\nTerminal-only" + +note right of mvp_path + **Recommended MVP Stack:** + - Text editor: urwid TUI + - AI: discussion-diagram-editor SmartTool + - Preview: Render on save + - Contract: ARTIFACT_SAVED +end note + +note right of complex_path + **Complexity Drivers:** + - Graphical canvas implementation + - Bidirectional sync logic + - Voice command pipeline + - Live preview architecture + - Multiple format renderers +end note + +@enduml \ No newline at end of file diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_7_001.png b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_7_001.png new file mode 100644 index 0000000..666ca56 Binary files /dev/null and b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_7_001.png differ diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_ux_2.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_ux_2.puml new file mode 100644 index 0000000..ea759cb --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_ux_2.puml @@ -0,0 +1 @@ +@startuml\n!theme plain\nskinparam componentStyle rectangle\n\nactor \"User\" as user\nparticipant \"Orchestrated Discussions\" as od\nparticipant \"Artifact Editor\" as ae\nparticipant \"SmartTool: dictate\" as st_dictate\nparticipant \"SmartTool: artifact-generate\" as st_aigen\nparticipant \"Renderer (e.g., PlantUML)\" as renderer\nparticipant \"External Binary (e.g., plantuml.jar)\" as ext_bin\n\nuser -> od : Clicks \"Add Artifact\"\nod -> ae : Launches with --output path\nactivate ae\n\nae --> user : Displays editor UI (text area, preview, input options)\nnote right of ae\n **UX Point:**\n Clear onboarding for new users.\n Intuitive layout of input methods.\n Initial blank state.\nend note\n\nalt Text/Code Input\n user -> ae : Types code in text editor\n note right of ae\n **UX Point:**\n Syntax highlighting, auto-completion (future).\n end note\nelse Voice Input\n user -> ae : Clicks \"Dictate\" button\n activate st_dictate\n ae -> st_dictate : Sends audio stream\n st_dictate -> st_aigen : Transcribed text\n activate st_aigen\n st_aigen -> st_aigen : Generates artifact code from text\n st_aigen --> ae : Returns generated code\n deactivate st_aigen\n deactivate st_dictate\n ae --> user : Updates text editor with generated code\n note right of ae\n **UX Point:**\n Visual feedback for voice input (listening, processing).\n Clear indication of AI-generated changes.\n Easy to correct AI errors.\n end note\nend\n\nae -> ae : Triggers render (on save for MVP, live for future)\nactivate renderer\nae -> renderer : Sends artifact code\nrenderer -> ext_bin : Executes external binary\nactivate ext_bin\nnote right of ext_bin\n **UX Point:**\n Loading indicator/progress for rendering.\n Timeout/cancellation mechanism.\nend note\next_bin --> renderer : Returns rendered image/SVG\ndeactivate ext_bin\nrenderer --> ae : Returns rendered output\ndeactivate renderer\n\nae --> user : Displays rendered artifact in preview\nnote right of ae\n **UX Point:**\n Clear error messages if rendering fails.\n Responsive preview updates.\nend note\n\nuser -> ae : Reviews and makes further edits (loop back)\nuser -> ae : Clicks \"Save\" button\nae -> od : Prints \"ARTIFACT_SAVED:/path/to/file\" to stdout\ndeactivate ae\nod -> od : Embeds artifact reference\nod --> user : Confirms artifact saved/embedded\n@enduml \ No newline at end of file diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_ux_7.png b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_ux_7.png new file mode 100644 index 0000000..8617dad Binary files /dev/null and b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_ux_7.png differ diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_ux_7.puml b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_ux_7.puml new file mode 100644 index 0000000..c22ed75 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_ux_7.puml @@ -0,0 +1,65 @@ +@startuml +skinparam backgroundColor #f5f5f5 +skinparam component { + BackgroundColor #e1f5ff + BorderColor #01579b +} +skinparam rectangle { + BackgroundColor #f3e5f5 + BorderColor #512da8 +} + +rectangle "Artifact Editor - Proposed UX Layout (PyQt6)" { + component "Top Menu Bar" as menu + + rectangle "Main Workspace" as main_workspace { + component "Left Panel (Text Editor)" as editor { + component "PlantUML Code Editor" as code_area + note right of code_area + Syntax highlighting + Line numbers + Inline error markers + end note + } + + component "Right Panel (Graphical Canvas)" as canvas { + component "Drawing Toolbar" as draw_toolbar + component "Interactive Canvas" as draw_area + note right of draw_area + Live preview + Select/Drag shapes + Add shapes (rect, circle, etc.) + Connect elements + end note + } + + editor -[hidden]right-> canvas + editor <--> canvas : **Bidirectional Sync (CRITICAL)** + } + + component "Bottom Control Bar" as controls { + component "Save Button" as save + component "AI Generate/Modify" as ai_btn + component "Dictate Button" as dictate_btn + component "Status Bar" as status_bar + } +} + +menu -down-> main_workspace +main_workspace -down-> controls + +draw_toolbar -up-> draw_area + +note bottom of controls + `save`: Saves file, outputs ARTIFACT_SAVED + `ai_btn`: Invokes SmartTool for AI generation/modification + `dictate_btn`: Invokes SmartTool for voice input + `status_bar`: Displays rendering status, errors, mode +end note + +note right of dictate_btn + Visual feedback for listening + Transcript preview +end note + +@enduml diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_ux_7.svg b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_ux_7.svg new file mode 100644 index 0000000..c48da58 --- /dev/null +++ b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_ux_7.svg @@ -0,0 +1,99 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentScriptType="application/ecmascript" contentStyleType="text/css" height="773px" preserveAspectRatio="none" style="width:1548px;height:773px;background:#F5F5F5;" version="1.1" viewBox="0 0 1548 773" width="1548px" zoomAndPan="magnify"><defs><filter height="300%" id="f1k5i97etpp1d8" width="300%" x="-1" y="-1"><feGaussianBlur result="blurOut" stdDeviation="2.0"/><feColorMatrix in="blurOut" result="blurOut2" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .4 0"/><feOffset dx="4.0" dy="4.0" in="blurOut2" result="blurOut3"/><feBlend in="SourceGraphic" in2="blurOut3" mode="normal"/></filter></defs><g><!--MD5=[65a966c8c73c71a0446029b99c9797ee] +cluster Artifact Editor - Proposed UX Layout (PyQt6)--><rect fill="#F3E5F5" filter="url(#f1k5i97etpp1d8)" height="738" style="stroke: #512DA8; stroke-width: 1.5;" width="1130" x="22" y="24"/><text fill="#000000" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacingAndGlyphs" textLength="309" x="432.5" y="40.9659">Artifact Editor - Proposed UX Layout (PyQt6)</text><!--MD5=[7afc990edd1bf305c09b855254bc1b10] +cluster main_workspace--><rect fill="#F3E5F5" filter="url(#f1k5i97etpp1d8)" height="574" style="stroke: #512DA8; stroke-width: 1.5;" width="521" x="54" y="156"/><text fill="#000000" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacingAndGlyphs" textLength="115" x="257" y="172.9659">Main Workspace</text><!--MD5=[5aede0586259e4d3ba330cb9e34d0e56] +cluster editor--><rect fill="#E1F5FF" filter="url(#f1k5i97etpp1d8)" height="133" style="stroke: #01579B; stroke-width: 1.5;" width="408" x="106" y="218"/><rect fill="#E1F5FF" height="10" style="stroke: #01579B; stroke-width: 1.5;" width="15" x="494" y="223"/><rect fill="#E1F5FF" height="2" style="stroke: #01579B; stroke-width: 1.5;" width="4" x="492" y="225"/><rect fill="#E1F5FF" height="2" style="stroke: #01579B; stroke-width: 1.5;" width="4" x="492" y="229"/><text fill="#000000" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacingAndGlyphs" textLength="159" x="230.5" y="245.9659">Left Panel (Text Editor)</text><!--MD5=[588c463f218493110eb7fbaaa6e40c41] +cluster canvas--><rect fill="#E1F5FF" filter="url(#f1k5i97etpp1d8)" height="272" style="stroke: #01579B; stroke-width: 1.5;" width="433" x="94" y="418"/><rect fill="#E1F5FF" height="10" style="stroke: #01579B; stroke-width: 1.5;" width="15" x="507" y="423"/><rect fill="#E1F5FF" height="2" style="stroke: #01579B; stroke-width: 1.5;" width="4" x="505" y="425"/><rect fill="#E1F5FF" height="2" style="stroke: #01579B; stroke-width: 1.5;" width="4" x="505" y="429"/><text fill="#000000" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacingAndGlyphs" textLength="211" x="205" y="445.9659">Right Panel (Graphical Canvas)</text><!--MD5=[3eee715f6f6eb947cc2d6d4cbbb6611c] +cluster controls--><rect fill="#E1F5FF" filter="url(#f1k5i97etpp1d8)" height="251" style="stroke: #01579B; stroke-width: 1.5;" width="505" x="615" y="439"/><rect fill="#E1F5FF" height="10" style="stroke: #01579B; stroke-width: 1.5;" width="15" x="1100" y="444"/><rect fill="#E1F5FF" height="2" style="stroke: #01579B; stroke-width: 1.5;" width="4" x="1098" y="446"/><rect fill="#E1F5FF" height="2" style="stroke: #01579B; stroke-width: 1.5;" width="4" x="1098" y="450"/><text fill="#000000" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacingAndGlyphs" textLength="136" x="799.5" y="466.9659">Bottom Control Bar</text><!--MD5=[48af7236ebe878760a3516eafd0f9e45] +entity menu--><rect fill="#E1F5FF" filter="url(#f1k5i97etpp1d8)" height="39.0679" style="stroke: #01579B; stroke-width: 1.5;" width="115" x="509.5" y="62"/><rect fill="#E1F5FF" height="5" style="stroke: #01579B; stroke-width: 1.5;" width="10" x="504.5" y="67"/><rect fill="#E1F5FF" height="5" style="stroke: #01579B; stroke-width: 1.5;" width="10" x="504.5" y="91.0679"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="95" x="519.5" y="86.9659">Top Menu Bar</text><!--MD5=[792bcc276390338fb634f3d5a35213bb] +entity code_area--><rect fill="#E1F5FF" filter="url(#f1k5i97etpp1d8)" height="39.0679" style="stroke: #01579B; stroke-width: 1.5;" width="166" x="149" y="276"/><rect fill="#E1F5FF" height="5" style="stroke: #01579B; stroke-width: 1.5;" width="10" x="144" y="281"/><rect fill="#E1F5FF" height="5" style="stroke: #01579B; stroke-width: 1.5;" width="10" x="144" y="305.0679"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="146" x="159" y="300.9659">PlantUML Code Editor</text><path d="M350.5,264 L350.5,291.5 L315.35,295.5 L350.5,299.5 L350.5,327.118 A0,0 0 0 0 350.5,327.118 L489.5,327.118 A0,0 0 0 0 489.5,327.118 L489.5,274 L479.5,264 L350.5,264 A0,0 0 0 0 350.5,264 " fill="#FBFB77" filter="url(#f1k5i97etpp1d8)" style="stroke: #A80036; stroke-width: 1.0;"/><path d="M479.5,264 L479.5,274 L489.5,274 L479.5,264 " fill="#FBFB77" style="stroke: #A80036; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="117" x="356.5" y="282.897">Syntax highlighting</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="82" x="356.5" y="300.603">Line numbers</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="118" x="356.5" y="318.309">Inline error markers</text><!--MD5=[6024d8b9e966a0b45a055a4bc7721af4] +entity draw_toolbar--><rect fill="#E1F5FF" filter="url(#f1k5i97etpp1d8)" height="39.0679" style="stroke: #01579B; stroke-width: 1.5;" width="132" x="143" y="627"/><rect fill="#E1F5FF" height="5" style="stroke: #01579B; stroke-width: 1.5;" width="10" x="138" y="632"/><rect fill="#E1F5FF" height="5" style="stroke: #01579B; stroke-width: 1.5;" width="10" x="138" y="656.0679"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="112" x="153" y="651.9659">Drawing Toolbar</text><!--MD5=[509bbd62d0744455ec5d08e82de835f3] +entity draw_area--><rect fill="#E1F5FF" filter="url(#f1k5i97etpp1d8)" height="39.0679" style="stroke: #01579B; stroke-width: 1.5;" width="144" x="137" y="485"/><rect fill="#E1F5FF" height="5" style="stroke: #01579B; stroke-width: 1.5;" width="10" x="132" y="490"/><rect fill="#E1F5FF" height="5" style="stroke: #01579B; stroke-width: 1.5;" width="10" x="132" y="514.0679"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="124" x="147" y="509.9659">Interactive Canvas</text><path d="M316.5,464 L316.5,500.5 L281.23,504.5 L316.5,508.5 L316.5,544.8241 A0,0 0 0 0 316.5,544.8241 L503.5,544.8241 A0,0 0 0 0 503.5,544.8241 L503.5,474 L493.5,464 L316.5,464 A0,0 0 0 0 316.5,464 " fill="#FBFB77" filter="url(#f1k5i97etpp1d8)" style="stroke: #A80036; stroke-width: 1.0;"/><path d="M493.5,464 L493.5,474 L503.5,474 L493.5,464 " fill="#FBFB77" style="stroke: #A80036; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="74" x="322.5" y="482.897">Live preview</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="114" x="322.5" y="500.603">Select/Drag shapes</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="166" x="322.5" y="518.309">Add shapes (rect, circle, etc.)</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="108" x="322.5" y="536.0151">Connect elements</text><!--MD5=[2935e10667aef0675bf227d465d6e5f6] +entity save--><rect fill="#E1F5FF" filter="url(#f1k5i97etpp1d8)" height="39.0679" style="stroke: #01579B; stroke-width: 1.5;" width="100" x="639" y="485"/><rect fill="#E1F5FF" height="5" style="stroke: #01579B; stroke-width: 1.5;" width="10" x="634" y="490"/><rect fill="#E1F5FF" height="5" style="stroke: #01579B; stroke-width: 1.5;" width="10" x="634" y="514.0679"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="80" x="649" y="509.9659">Save Button</text><!--MD5=[1cf63780fc517fbc6cc8d8f2d94a4db1] +entity ai_btn--><rect fill="#E1F5FF" filter="url(#f1k5i97etpp1d8)" height="39.0679" style="stroke: #01579B; stroke-width: 1.5;" width="151" x="774.5" y="485"/><rect fill="#E1F5FF" height="5" style="stroke: #01579B; stroke-width: 1.5;" width="10" x="769.5" y="490"/><rect fill="#E1F5FF" height="5" style="stroke: #01579B; stroke-width: 1.5;" width="10" x="769.5" y="514.0679"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="131" x="784.5" y="509.9659">AI Generate/Modify</text><!--MD5=[010e7e855952afb42756898a4c3b10fb] +entity dictate_btn--><rect fill="#E1F5FF" filter="url(#f1k5i97etpp1d8)" height="39.0679" style="stroke: #01579B; stroke-width: 1.5;" width="116" x="961" y="485"/><rect fill="#E1F5FF" height="5" style="stroke: #01579B; stroke-width: 1.5;" width="10" x="956" y="490"/><rect fill="#E1F5FF" height="5" style="stroke: #01579B; stroke-width: 1.5;" width="10" x="956" y="514.0679"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="96" x="971" y="509.9659">Dictate Button</text><!--MD5=[bfaba0fb3c73c3d0bfe66c81d719af44] +entity status_bar--><rect fill="#E1F5FF" filter="url(#f1k5i97etpp1d8)" height="39.0679" style="stroke: #01579B; stroke-width: 1.5;" width="89" x="644.5" y="627"/><rect fill="#E1F5FF" height="5" style="stroke: #01579B; stroke-width: 1.5;" width="10" x="639.5" y="632"/><rect fill="#E1F5FF" height="5" style="stroke: #01579B; stroke-width: 1.5;" width="10" x="639.5" y="656.0679"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="69" x="654.5" y="651.9659">Status Bar</text><path d="M1168,606 L1168,686.8241 L1536,686.8241 L1536,616 L1526,606 L1168,606 " fill="#FBFB77" filter="url(#f1k5i97etpp1d8)" style="stroke: #A80036; stroke-width: 1.0;"/><path d="M1526,606 L1526,616 L1536,616 L1526,606 " fill="#FBFB77" style="stroke: #A80036; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="254" x="1174" y="624.897">`save`: Saves file, outputs ARTIFACT_SAVED</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="347" x="1174" y="642.603">`ai_btn`: Invokes SmartTool for AI generation/modification</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="283" x="1174" y="660.309">`dictate_btn`: Invokes SmartTool for voice input</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="311" x="1174" y="678.0151">`status_bar`: Displays rendering status, errors, mode</text><path d="M1167.5,482 L1167.5,527.412 L1354.5,527.412 L1354.5,492 L1344.5,482 L1167.5,482 " fill="#FBFB77" filter="url(#f1k5i97etpp1d8)" style="stroke: #A80036; stroke-width: 1.0;"/><path d="M1344.5,482 L1344.5,492 L1354.5,492 L1344.5,482 " fill="#FBFB77" style="stroke: #A80036; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="166" x="1173.5" y="500.897">Visual feedback for listening</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacingAndGlyphs" textLength="110" x="1173.5" y="518.603">Transcript preview</text><!--MD5=[31cce34563f85f700e328756b8cb9f57] +link editor to canvas--><!--MD5=[31cce34563f85f700e328756b8cb9f57] +link editor to canvas--><path d="M110.8222,351.2816 C110.8019,351.6319 110.7815,351.9832 110.7611,352.3354 C110.7203,353.0398 110.6792,353.748 110.638,354.4598 C110.4729,357.307 110.3046,360.2116 110.1335,363.1621 C109.4495,374.9642 108.7228,387.5013 107.9963,400.0375 C107.8146,403.1716 107.633,406.3056 107.452,409.428 C107.3615,410.9893 107.2712,412.5476 107.1811,414.1017 C107.1361,414.8787 107.0911,415.6546 107.0463,416.4293 C107.0238,416.8166 107.0014,417.2036 106.979,417.5903 " fill="none" id="editor-canvas" style="stroke: #A80036; stroke-width: 1.0;"/><polygon fill="#A80036" points="106.979,417.5903,111.493,408.8368,107.2683,412.5987,103.5064,408.374,106.979,417.5903" style="stroke: #A80036; stroke-width: 1.0;"/><polygon fill="#A80036" points="110.8222,351.2816,106.3081,360.035,110.5328,356.2732,114.2947,360.498,110.8222,351.2816" style="stroke: #A80036; stroke-width: 1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" font-weight="bold" lengthAdjust="spacingAndGlyphs" textLength="188" x="111" y="389.897">Bidirectional Sync (CRITICAL)</text><!--MD5=[e8ce33ef6d47e15f1229db55f3f105cc] +link menu to main_workspace--><path d="M567,101.31 C567,114.08 567,132.265 567,152.4264 C567,153.0565 567,153.6884 567,154.3222 C567,154.6391 567,154.9565 567,155.2743 " fill="none" id="menu->main_workspace" style="stroke: #A80036; stroke-width: 1.0;"/><polygon fill="#A80036" points="567,155.2743,571,146.2743,567,150.2743,563,146.2743,567,155.2743" style="stroke: #A80036; stroke-width: 1.0;"/><!--MD5=[c87fac98842521943617cb37a119c773] +link main_workspace to controls--><path d="M575.3692,296.9323 C575.6278,296.9465 575.8921,296.961 576.162,296.9759 C577.2417,297.0356 578.4112,297.1011 579.6677,297.1726 C582.1805,297.3157 585.0409,297.4828 588.2249,297.6753 C600.9611,298.4452 618.876,299.621 640.442,301.2858 C683.5741,304.6153 741.3106,309.9006 801.4313,317.8063 C921.6725,333.6175 1051.45,359.91 1093,402 C1100.935,410.0375 1106.115,420.2663 1109.4155,431.1209 C1109.828,432.4778 1110.2112,433.8444 1110.5668,435.2177 C1110.7445,435.9044 1110.9154,436.5927 1111.0795,437.2824 C1111.1616,437.6272 1111.242,437.9724 1111.3207,438.3178 C1111.3601,438.4905 1111.3991,438.6633 1111.4376,438.8361 " fill="none" id="main_workspace->controls" style="stroke: #A80036; stroke-width: 1.0;"/><polygon fill="#A80036" points="1111.4376,438.8361,1113.3823,429.1812,1110.3491,433.9561,1105.5742,430.9229,1111.4376,438.8361" style="stroke: #A80036; stroke-width: 1.0;"/><!--MD5=[09163b5f30a62a4768f37a4904cfdbef] +reverse link draw_area to draw_toolbar--><path d="M209,529.36 C209,556.92 209,601.69 209,626.83 " fill="none" id="draw_area<-draw_toolbar" style="stroke: #A80036; stroke-width: 1.0;"/><polygon fill="#A80036" points="209,524.09,205,533.09,209,529.09,213,533.09,209,524.09" style="stroke: #A80036; stroke-width: 1.0;"/><!--MD5=[b64c48b3a5f8deabbd875f36a199935f] +link controls to GMN28--><path d="M1120.0712,515.6735 C1120.2556,515.8938 1120.4425,516.1165 1120.632,516.3414 C1121.011,516.7913 1121.4002,517.2503 1121.7991,517.7174 C1122.5969,518.6515 1123.4334,519.618 1124.305,520.6088 C1131.2775,528.535 1140.49,538.02 1150,545 C1182.29,568.7 1221.01,589.5 1255.87,605.94 " fill="none" id="controls-GMN28" style="stroke: #A80036; stroke-width: 1.0; stroke-dasharray: 7.0,7.0;"/><!--MD5=[075213ebc33329aeb8312ce02f5b3de8] +link dictate_btn to GMN31--><path d="M1023.82,484.73 C1034.29,446.49 1062.15,367.59 1110.5,384.5 C1165.7,403.81 1215.76,452.8 1241.91,481.86 " fill="none" id="dictate_btn-GMN31" style="stroke: #A80036; stroke-width: 1.0; stroke-dasharray: 7.0,7.0;"/><!--MD5=[ecb54a43ace0e94b7accb80a039e274b] +link save to ai_btn--><!--MD5=[611bc7c8b3425e07417f889c10fa72a1] +link save to status_bar--><!--MD5=[19942d9c4f8bf19ceb7a630e38bfa72a] +@startuml +skinparam backgroundColor #f5f5f5 +skinparam component { + BackgroundColor #e1f5ff + BorderColor #01579b +} +skinparam rectangle { + BackgroundColor #f3e5f5 + BorderColor #512da8 +} + +rectangle "Artifact Editor - Proposed UX Layout (PyQt6)" { + component "Top Menu Bar" as menu + + rectangle "Main Workspace" as main_workspace { + component "Left Panel (Text Editor)" as editor { + component "PlantUML Code Editor" as code_area + note right of code_area + Syntax highlighting + Line numbers + Inline error markers + end note + } + + component "Right Panel (Graphical Canvas)" as canvas { + component "Drawing Toolbar" as draw_toolbar + component "Interactive Canvas" as draw_area + note right of draw_area + Live preview + Select/Drag shapes + Add shapes (rect, circle, etc.) + Connect elements + end note + } + + editor -[hidden]right-> canvas + editor <- -> canvas : **Bidirectional Sync (CRITICAL)** + } + + component "Bottom Control Bar" as controls { + component "Save Button" as save + component "AI Generate/Modify" as ai_btn + component "Dictate Button" as dictate_btn + component "Status Bar" as status_bar + } +} + +menu -down-> main_workspace +main_workspace -down-> controls + +draw_toolbar -up-> draw_area + +note bottom of controls + `save`: Saves file, outputs ARTIFACT_SAVED + `ai_btn`: Invokes SmartTool for AI generation/modification + `dictate_btn`: Invokes SmartTool for voice input + `status_bar`: Displays rendering status, errors, mode +end note + +note right of dictate_btn + Visual feedback for listening + Transcript preview +end note + +@enduml + +PlantUML version 1.2020.02(Sun Mar 01 06:22:07 AST 2020) +(GPL source distribution) +Java Runtime: OpenJDK Runtime Environment +JVM: OpenJDK 64-Bit Server VM +Java Version: 21.0.8+9-Ubuntu-0ubuntu124.04.1 +Operating System: Linux +Default Encoding: UTF-8 +Language: en +Country: CA +--></g></svg> \ No newline at end of file diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_ux_7_001.png b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_ux_7_001.png new file mode 100644 index 0000000..ce54cae Binary files /dev/null and b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_ux_7_001.png differ diff --git a/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_ux_7_002.png b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_ux_7_002.png new file mode 100644 index 0000000..e927769 Binary files /dev/null and b/diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_ux_7_002.png differ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..681c7ea --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,210 @@ +# Full Stack Testing - SmartTools + Orchestrated Discussions + Artifact Editor +# +# Quick Start: +# docker-compose build # Build all images +# docker-compose run --rm test-all # Run all tests +# docker-compose run --rm discussions discussions --help +# +# Prerequisites: +# All three repos cloned to ~/PycharmProjects/ +# - SmartTools +# - orchestrated-discussions +# - artifact-editor + +version: '3.8' + +services: + # ============================================================================ + # SmartTools - Foundation Layer + # ============================================================================ + smarttools: + build: + context: ../SmartTools + dockerfile: Dockerfile + image: smarttools:latest + volumes: + - smarttools-data:/root/.smarttools + command: ["smarttools", "list"] + + smarttools-test: + build: + context: ../SmartTools + dockerfile: Dockerfile + image: smarttools:latest + volumes: + - smarttools-data:/root/.smarttools + command: ["pytest", "-v"] + + # ============================================================================ + # Orchestrated Discussions - Middle Layer + # ============================================================================ + discussions: + build: + context: . + dockerfile: Dockerfile + image: orchestrated-discussions:latest + volumes: + - smarttools-data:/root/.smarttools + - ./examples:/app/examples:ro + - ./templates:/app/templates:ro + command: ["discussions", "--help"] + + discussions-test: + build: + context: . + dockerfile: Dockerfile + image: orchestrated-discussions:latest + volumes: + - smarttools-data:/root/.smarttools + command: ["pytest", "-v", "tests/"] + + # ============================================================================ + # Artifact Editor - Top Layer + # ============================================================================ + artifact-editor: + build: + context: ../artifact-editor + dockerfile: Dockerfile + image: artifact-editor:latest + volumes: + - smarttools-data:/root/.smarttools + - /tmp/artifacts:/tmp/artifacts + command: ["artifact-ai", "--help"] + + # ============================================================================ + # GUI Services (require X11 forwarding) + # ============================================================================ + discussions-gui: + build: + context: . + dockerfile: Dockerfile + image: orchestrated-discussions:latest + environment: + - DISPLAY=${DISPLAY:-:0} + volumes: + - smarttools-data:/root/.smarttools + - ./examples:/app/examples + - /tmp/.X11-unix:/tmp/.X11-unix:ro + command: ["discussions", "ui"] + network_mode: host + + artifact-editor-gui: + build: + context: ../artifact-editor + dockerfile: Dockerfile + image: artifact-editor:latest + environment: + - DISPLAY=${DISPLAY:-:0} + - QT_QPA_PLATFORM=xcb + volumes: + - smarttools-data:/root/.smarttools + - /tmp/artifacts:/tmp/artifacts + - /tmp/.X11-unix:/tmp/.X11-unix:ro + command: ["artifact-editor"] + network_mode: host + + # ============================================================================ + # Full Test Suite + # ============================================================================ + test-all: + build: + context: . + dockerfile: Dockerfile + image: orchestrated-discussions:latest + volumes: + - smarttools-data:/root/.smarttools + - ./examples:/app/examples:ro + command: > + bash -c " + set -e + echo '========================================' + echo ' FULL STACK TEST SUITE' + echo '========================================' + echo '' + + echo '=== 1. SmartTools Foundation ===' + smarttools list | head -10 + echo 'SmartTools: OK' + echo '' + + echo '=== 2. Discussion SmartTools ===' + discussion-parser --help > /dev/null + discussion-vote-counter --help > /dev/null + echo 'Discussion tools: OK' + echo '' + + echo '=== 3. Artifact SmartTools ===' + artifact-ai --help > /dev/null + artifact-export --help > /dev/null + echo 'Artifact tools: OK' + echo '' + + echo '=== 4. Discussions CLI ===' + discussions --help > /dev/null + discussions participants + echo 'Discussions CLI: OK' + echo '' + + echo '=== 5. Unit Tests ===' + pytest -v tests/ || echo 'Some tests failed' + echo '' + + echo '=== 6. Pipeline Test ===' + cat examples/brainstorm_notification_system.discussion.md | discussion-parser | jq -r '.title' + cat examples/brainstorm_notification_system.discussion.md | discussion-parser | discussion-vote-counter | jq . + echo 'Pipeline: OK' + echo '' + + echo '=== 7. Artifact Generation Test ===' + echo '' | timeout 10 artifact-ai --format plantuml --instruction 'Create a simple A to B diagram' 2>/dev/null || echo 'AI test skipped (no provider or timeout)' + echo '' + + echo '========================================' + echo ' ALL TESTS COMPLETED' + echo '========================================' + " + + # ============================================================================ + # Interactive Shell + # ============================================================================ + shell: + build: + context: . + dockerfile: Dockerfile + image: orchestrated-discussions:latest + volumes: + - smarttools-data:/root/.smarttools + - ./examples:/app/examples + - ../artifact-editor:/artifact-editor:ro + - ../SmartTools:/SmartTools:ro + command: ["/bin/bash"] + stdin_open: true + tty: true + +volumes: + smarttools-data: + # Persists ~/.smarttools between container runs + +# ============================================================================== +# Quick Reference +# ============================================================================== +# +# Build everything: +# docker-compose build +# +# Run tests: +# docker-compose run --rm test-all +# docker-compose run --rm smarttools-test +# docker-compose run --rm discussions-test +# +# Use CLI tools: +# docker-compose run --rm discussions discussions new "Test Feature" --template feature +# docker-compose run --rm discussions discussions status examples/feature_discussion.discussion.md +# docker-compose run --rm artifact-editor artifact-ai --format mermaid --instruction "Create flowchart" +# +# Launch GUI (requires: xhost +local:docker): +# docker-compose run --rm discussions-gui +# docker-compose run --rm artifact-editor-gui +# +# Interactive shell: +# docker-compose run --rm shell diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 1139cc6..fe9b434 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -1,40 +1,61 @@ # Orchestrated Discussions - Design Document +## Implementation Status + +| Component | Status | Notes | +|-----------|--------|-------| +| Core library | Complete | discussion.py, markers.py, voting.py, participant.py | +| Runner | Complete | Pipeline-based turn execution | +| CLI | Complete | All commands implemented | +| GUI | Complete | Dear PyGui interface | +| TUI | Complete | urwid terminal interface | +| Utility SmartTools | Complete | parser, validator, summarizer | +| Orchestration SmartTools | Complete | vote-counter, mention-router, status-promoter, turn-appender, config | +| Participant SmartTools | Complete | architect, security, pragmatist, moderator, diagram-editor | +| Templates | Complete | feature.yaml, brainstorm.yaml | + +--- + +## Architectural Philosophy + +**This project strictly follows the Unix Philosophy:** + +1. **Each tool does one thing well** - SmartTools are self-contained, complete units +2. **Tools communicate via stdin/stdout** - JSON flows through pipes +3. **Tools are composable** - Any tool's output can be another's input +4. **The Python layer is thin orchestration only** - It calls tools, never reimplements their logic + +See `CLAUDE.md` for detailed architectural rules and anti-patterns. + +--- + ## Overview -**Orchestrated Discussions** is a standalone Python library and CLI for conducting structured multi-agent AI conversations. It enables humans to facilitate discussions between multiple AI personas, each with distinct perspectives, expertise, and voting behavior. +**Orchestrated Discussions** is a Python library and CLI for running structured, multi-participant discussions between AI personas and human contributors. It provides turn-based orchestration, role-driven responses, and voting-based consensus checks across phases. ### Project Context This project is part of a three-project ecosystem: -1. **SmartTools** (`~/PycharmProjects/SmartTools`) - Foundation layer - - AI provider abstraction with fallback chains - - YAML-based tool definitions - - Multi-step execution pipelines - - CLI and TUI interfaces - - **Status**: Mostly complete +1. **SmartTools** - Foundation Layer + - AI provider abstraction, prompt execution pipelines, YAML-defined tools -2. **Orchestrated Discussions** (this project) - Conversation layer - - Multi-agent discussion orchestration - - Phase-based workflows with voting - - Turn management and @mentions - - **Status**: In design/early development +2. **Orchestrated Discussions** - Conversation Layer (this project) + - Structured, multi-participant discussions with AI personas and humans + - Phases, turns, consensus voting, append-only discussion files -3. **CascadingDev** (`~/PycharmProjects/CascadingDev`) - Integration layer - - Git-driven automation with cascading rules - - Pre-commit hook orchestration - - Will be refactored to depend on SmartTools + Orchestrated Discussions - - **Status**: Functional but entangled; needs refactoring after dependencies are ready +3. **CascadingDev** - Automation & Integration Layer + - Uses Orchestrated Discussions for feature discussions, code reviews + - Integrates with Git hooks and cascading rules ### Design Goals -1. **Standalone utility** - Works without git, CascadingDev, or any specific workflow -2. **SmartTools integration** - Uses SmartTools for AI provider abstraction -3. **Extensible personas** - Users can create/customize discussion participants -4. **Multiple interfaces** - CLI, TUI, and Python API -5. **Append-only discussions** - Markdown files that grow, never shrink -6. **Portable state** - Discussion files are self-contained and human-readable +1. **Workflow independence** - Operates independently of Git/CascadingDev +2. **SmartTools as the provider layer** - All AI inference through SmartTools +3. **Extensible participants** - Support AI personas and human participants +4. **Multiple interfaces** - CLI, Python API, TUI/GUI +5. **Append-only discussion files** - Human-readable, Git-friendly +6. **Portable, self-contained state** - All state in the markdown file --- @@ -43,64 +64,87 @@ This project is part of a three-project ecosystem: ### Dependency Graph ``` -┌─────────────────────────────────────┐ -│ User Application │ -│ (CascadingDev, custom scripts) │ -└─────────────────┬───────────────────┘ - │ -┌─────────────────▼───────────────────┐ -│ Orchestrated Discussions │ -│ - Discussion state management │ -│ - Phase/turn orchestration │ -│ - Voting and consensus │ -│ - @mention routing │ -└─────────────────┬───────────────────┘ - │ -┌─────────────────▼───────────────────┐ -│ SmartTools │ -│ - AI provider abstraction │ -│ - Tool execution engine │ -│ - Provider fallback chains │ -└─────────────────────────────────────┘ +┌────────────────────────────────────────────┐ +│ User Applications │ +│ (CascadingDev, automation scripts, UI) │ +└─────────────────────┬──────────────────────┘ + │ +┌─────────────────────▼──────────────────────┐ +│ Orchestrated Discussions │ +│ - Append-only discussion files (Markdown) │ +│ - Phase and turn orchestration │ +│ - Multi-participant workflows (AI + human)│ +│ - Voting + consensus logic │ +│ - @mention routing and participant logic │ +│ - TUI/GUI for real-time display │ +└─────────────────────┬──────────────────────┘ + │ +┌─────────────────────▼──────────────────────┐ +│ SmartTools │ +│ - AI provider abstraction │ +│ - Inference dispatch + fallback chains │ +│ - YAML-defined tool pipelines │ +│ - Streaming / non-streaming execution │ +└────────────────────────────────────────────┘ ``` -### Module Structure +### Module Structure (Actual) ``` orchestrated-discussions/ ├── src/discussions/ -│ ├── __init__.py # Package exports -│ ├── cli.py # CLI entry point (discussions command) -│ ├── discussion.py # Discussion class and state management -│ ├── participant.py # Participant definitions and loading -│ ├── markers.py # Marker parsing (Q:, TODO:, VOTE:, etc.) -│ ├── turns.py # Turn management and @mention routing -│ ├── phases.py # Phase definitions and transitions -│ ├── voting.py # Vote counting and consensus logic -│ ├── runner.py # Main orchestration engine +│ ├── __init__.py # Package exports, version +│ ├── cli.py # CLI entry point (argparse) +│ ├── discussion.py # Discussion model, file I/O +│ ├── participant.py # Participant discovery from ~/.smarttools/ +│ ├── markers.py # Marker parsing (VOTE, Q, TODO, etc.) +│ ├── voting.py # Voting + consensus calculation +│ ├── runner.py # Turn orchestration (calls SmartTools) │ └── ui/ -│ ├── __init__.py -│ └── tui.py # Optional urwid-based TUI +│ ├── __init__.py # UI module exports +│ ├── __main__.py # Module entry point +│ ├── gui.py # Dear PyGui interface +│ ├── tui.py # urwid terminal interface +│ └── widgets.py # Shared UI components │ -├── config/ -│ └── default_participants.yaml # Bundled persona definitions +├── smarttools/ # Bundled SmartTool configs +│ ├── discussion-parser/ +│ ├── discussion-validator/ +│ ├── discussion-summarizer/ +│ ├── discussion-vote-counter/ +│ ├── discussion-mention-router/ +│ ├── discussion-status-promoter/ +│ ├── discussion-turn-appender/ +│ ├── discussion-config/ +│ ├── discussion-moderator/ +│ ├── discussion-architect/ +│ ├── discussion-security/ +│ ├── discussion-pragmatist/ +│ ├── discussion-diagram-editor/ +│ └── puml-validator/ │ -├── examples/ -│ ├── feature_discussion.md # Example: feature planning -│ ├── code_review.md # Example: code review -│ └── architecture_decision.md # Example: ADR discussion +├── templates/ # Discussion workflow templates +│ ├── feature.yaml # 3-phase feature workflow +│ └── brainstorm.yaml # 6-phase ideation workflow +│ +├── examples/ # Example discussion files +│ ├── feature_discussion.md +│ ├── voted_discussion.md +│ ├── brainstorm_notification_system.md +│ ├── brainstorm_sketch_test.md +│ └── ai-enabled-home-lab-productivity-and-development-tool.md +│ +├── scripts/ +│ └── run-turn.sh # Manual turn orchestration script │ ├── tests/ -│ ├── test_markers.py -│ ├── test_voting.py -│ ├── test_phases.py -│ └── test_runner.py +│ ├── test_markers.py # Marker parsing tests +│ └── test_voting.py # Voting calculation tests │ -├── docs/ -│ └── DESIGN.md # This document -│ -├── pyproject.toml # Package configuration -└── README.md # User documentation +└── docs/ + ├── DESIGN.md # This document + ├── IMPLEMENTATION.md # Implementation plan + └── PIPELINE_SCHEMA.md # Pipeline configuration docs ``` --- @@ -109,17 +153,23 @@ orchestrated-discussions/ ### 1. Discussion -A **Discussion** is a markdown file containing: -- Metadata (title, phase, status) -- Context/description -- Participant comments with optional votes -- Structured markers (questions, decisions, todos) +A Discussion is an append-only, self-contained markdown file with: + +- **Metadata**: Title, phase, status, template, participants (in HTML comments) +- **Context**: Description of what's being discussed +- **Comments**: Appended blocks from participants, chronological +- **Markers**: Structured annotations (VOTE, Q, TODO, DECISION, CONCERN, DIAGRAM) +- **@mentions**: Inline references to participants + +#### Discussion File Format ```markdown <!-- DISCUSSION --> <!-- Title: Feature X Implementation --> <!-- Phase: initial_feedback --> <!-- Status: OPEN --> +<!-- Created: 2025-12-08T10:30:00Z --> +<!-- Template: feature --> <!-- Participants: architect, security, pragmatist --> # Feature X Implementation @@ -127,255 +177,411 @@ A **Discussion** is a markdown file containing: ## Context We need to implement feature X that allows users to... -## Requirements -- Must support... -- Should integrate with... - --- Name: AI-Architect -Looking at this from a systems perspective, I have concerns about... - -The proposed approach could lead to tight coupling between... +Looking at this from a systems perspective... Q: Have we considered using the adapter pattern here? +@pragmatist What do you think about complexity? VOTE: CHANGES --- -Name: AI-Pragmatist -I think we're overcomplicating this. The simplest approach would be... +Name: Human +I agree with the architect's concerns. +@security — can you evaluate the session-handling implications? -VOTE: READY +VOTE: CHANGES --- ``` ### 2. Participant -A **Participant** is an AI persona with: -- **Name**: Display name (e.g., "AI-Architect", "AI-Security") -- **Alias**: Short mention name (e.g., "architect", "security") -- **Role**: Brief role description -- **Personality**: System prompt defining perspective and behavior -- **Expertise**: List of expertise areas -- **Concerns**: What this participant watches for -- **Vote behavior**: Whether this participant votes (voting vs background) +Participants are SmartTools with the naming convention `discussion-{alias}`: -Participants are stored as YAML files and can optionally be backed by SmartTools. +- **Invoked directly** for testing: `cat discussion.md | discussion-architect --callout "..."` +- **Edited via SmartTools TUI** for prompt debugging +- **Discovered automatically** by scanning `~/.smarttools/discussion-*` -```yaml -# config/participants/architect.yaml -name: AI-Architect -alias: architect -role: Systems Architect -personality: | - You are AI-Architect (also known as Chen), a senior systems architect with deep - expertise in distributed systems, design patterns, and long-term technical strategy. +#### Participant Types - Your perspective: - - Think 2-5 years ahead, not just immediate implementation - - Value modularity, separation of concerns, and clean boundaries - - Prefer boring, proven technology over cutting-edge experiments - - Call out when shortcuts will create architectural debt +| Type | Behavior | +|------|----------| +| `voting` | Casts VOTE: READY/CHANGES/REJECT | +| `background` | Provides input without voting | - When responding: - - Consider scalability, maintainability, and evolution over time - - Identify architectural risks and technical debt implications - - Suggest well-established patterns and proven approaches - - Balance ideal architecture with practical constraints +#### Input/Output Contract -expertise: - - System design - - Scalability - - Technical debt - - Architectural patterns - - API design +**Input** (via stdin): +- Full discussion markdown content -concerns: - - "How does this fit the overall architecture?" - - "Will this scale?" - - "What's the long-term maintenance burden?" - - "Are we creating unnecessary coupling?" +**Arguments**: +- `--callout` - Specific question or @mention context +- `--templates-dir` - Path to templates directory -type: voting # voting | background -provider_hint: claude-sonnet # Preferred AI provider +**Output** (JSON): +```json +{ + "comment": "Markdown analysis with markers...", + "vote": "READY|CHANGES|REJECT|null" +} +``` + +Or sentinel for no response: +```json +{"sentinel": "NO_RESPONSE"} ``` ### 3. Markers -**Markers** are structured annotations recognized by the system: +Structured annotations parsed from comment text: -| Marker | Description | Example | -|--------|-------------|---------| -| `VOTE:` | Participant's vote | `VOTE: READY` | -| `Q:` | Question | `Q: Have we considered caching?` | -| `TODO:` | Action item | `TODO: Research rate limiting options` | -| `DECISION:` | Recorded decision | `DECISION: We will use PostgreSQL` | -| `ASSIGNED:` | Claimed task | `ASSIGNED: @rob will write the spec` | -| `DONE:` | Completed task | `DONE: Spec written and reviewed` | -| `CONCERN:` | Raised concern | `CONCERN: Security implications unclear` | -| `@alias` | Mention participant | `@architect what do you think?` | +| Marker | Pattern | Description | +|--------|---------|-------------| +| `VOTE:` | `VOTE: READY\|CHANGES\|REJECT` | Participant's vote | +| `Q:` | `Q: question text` | Question | +| `TODO:` | `TODO: action item` | Action item | +| `DECISION:` | `DECISION: we will...` | Recorded decision | +| `CONCERN:` | `CONCERN: issue...` | Raised concern | +| `DIAGRAM:` | `DIAGRAM: path/to/file.puml` | Diagram reference | +| `@alias` | `@architect` | Mention participant | ### 4. Phases -**Phases** define the stages of a discussion. Each phase has: -- **ID**: Unique identifier -- **Title**: Human-readable name -- **Instructions**: What participants should focus on -- **Voting mode**: Whether votes are collected in this phase -- **Auto-trigger**: Condition for automatic advancement -- **Next phase**: What phase follows +Phases define the stages of a discussion. Defined in template YAML files: ```yaml phases: - - id: initial_feedback - title: Initial Feedback + initial_feedback: + goal: Gather diverse perspectives instructions: | - Share your initial thoughts on the proposal. Focus on: - - Overall feasibility - - Major concerns or risks - - Questions that need answering - voting_mode: false - auto_trigger: all_mentioned_responded - next: detailed_review + - Focus on feasibility and risks + - Raise blocking issues early + voting: false + next_phase: detailed_review - - id: detailed_review - title: Detailed Review + consensus_vote: + goal: Reach agreement instructions: | - Provide detailed feedback from your area of expertise. - Raise specific concerns and suggest alternatives. - voting_mode: false - auto_trigger: null # Manual advancement - next: consensus_vote - - - id: consensus_vote - title: Consensus Vote - instructions: | - Cast your final vote based on the discussion. - - READY: Approve to proceed - - CHANGES: Needs modifications (specify what) - - REJECT: Fundamental issues (explain why) - voting_mode: true - auto_trigger: null - next: null # Terminal phase + - Vote READY if all concerns addressed + - Vote CHANGES if issues remain + voting: true + threshold_ready: 0.67 + human_required: true + next_phase: null ``` -### 5. Voting +### 5. Templates -**Voting** determines discussion outcomes: +Templates define workflow structure for different discussion types: -| Vote | Meaning | -|------|---------| -| `READY` | Approve - no blocking concerns | -| `CHANGES` | Conditional approval - needs specified modifications | -| `REJECT` | Block - fundamental issues that must be resolved | +#### Feature Template (`templates/feature.yaml`) -**Consensus rules** (configurable): +| Phase | Goal | Voting | +|-------|------|--------| +| `initial_feedback` | Gather diverse perspectives | No | +| `detailed_review` | Deep dive into implementation | No | +| `consensus_vote` | Reach agreement on approach | Yes (67%) | + +#### Brainstorm Template (`templates/brainstorm.yaml`) + +| Phase | Goal | Voting | +|-------|------|--------| +| `seed` | Frame the problem | No | +| `diverge` | Generate ideas freely | No | +| `cluster` | Group into themes | Yes (50%) | +| `sketch` | Create rough diagrams | No | +| `reality_check` | Ground in reality | No | +| `decide` | Commit to approach | Yes (67%) | + +### 6. Voting + +**Vote Types**: +- `READY` - Approve, no blocking concerns +- `CHANGES` - Conditional approval, needs modifications +- `REJECT` - Block, fundamental issues + +**Consensus Rules** (configurable per phase): - `threshold_ready`: Fraction of READY votes needed (default: 0.67) -- `threshold_reject`: Any REJECT blocks by default (threshold: 0.01) +- `threshold_reject`: Any REJECT blocks by default (0.01) - `human_required`: Whether human approval is mandatory -### 6. Turns +**Human Detection**: Participants NOT starting with `ai_`, `ai-`, `bot_`, `bot-` are considered human. -A **Turn** is one round of participant responses. The orchestrator: -1. Identifies who should respond (mentioned participants or all) -2. Provides context (discussion content, phase instructions) -3. Collects responses (comment + optional vote) +### 7. Turns + +A Turn is one round of participant responses. The runner: + +1. Loads discussion and parses current state +2. Determines who should respond (mentions or all) +3. Calls each participant SmartTool in parallel 4. Appends responses to discussion file -5. Checks for phase transitions +5. Counts votes and checks consensus +6. Promotes status if consensus reached --- -## Integration with SmartTools +## SmartTools Integration -### Option A: Direct Import (Preferred) +### Tool Pipeline Architecture -```python -# discussions/runner.py -from smarttools.providers import call_provider, load_providers -from smarttools.tool import load_tool, run_tool - -def get_participant_response(participant, context, callout): - """Get AI response for a participant.""" - - # Build prompt from participant personality + context - prompt = f""" -{participant.personality} - -## Current Discussion -{context} - -## Your Task -{callout if callout else "Provide your perspective on the discussion."} - -Respond with JSON: -{{"comment": "your markdown comment", "vote": "READY|CHANGES|REJECT|null"}} -""" - - # Use SmartTools provider - result = call_provider(participant.provider_hint, prompt) - - if result.success: - return parse_response(result.text) - else: - return None +``` +┌─────────────────────────────────────────────────────────┐ +│ Orchestrated Discussions │ +│ - Thin orchestration layer (runner.py) │ +│ - File I/O, turn sequencing │ +│ - Invokes SmartTools for ALL logic │ +└─────────────────────┬───────────────────────────────────┘ + │ invokes SmartTools via subprocess + ▼ +┌─────────────────────────────────────────────────────────┐ +│ SmartTools (discussion-* tools) │ +│ │ +│ Utility tools: │ +│ - discussion-parser (extract structured data) │ +│ - discussion-validator (check format, find issues) │ +│ - discussion-summarizer (generate .sum.md files) │ +│ │ +│ Orchestration tools: │ +│ - discussion-vote-counter (count votes) │ +│ - discussion-mention-router (route @mentions) │ +│ - discussion-status-promoter (status transitions) │ +│ - discussion-turn-appender (append responses) │ +│ - discussion-config (modify metadata) │ +│ │ +│ Participant tools: │ +│ - discussion-architect (systems thinking) │ +│ - discussion-security (threat modeling) │ +│ - discussion-pragmatist (shipping focus) │ +│ - discussion-moderator (facilitation) │ +│ - discussion-diagram-editor (PlantUML diagrams) │ +└─────────────────────┬───────────────────────────────────┘ + │ uses providers (for AI steps) + ▼ +┌─────────────────────────────────────────────────────────┐ +│ SmartTools Providers │ +│ claude-sonnet, claude-haiku, opencode-deepseek, etc. │ +└─────────────────────────────────────────────────────────┘ ``` -### Option B: Participants as SmartTools +### Turn Pipeline Configuration -Each participant can be a SmartTool, allowing users to customize via SmartTools' TUI: +Templates can define custom pipelines using a variable-based schema: ```yaml -# ~/.smarttools/discussion-architect/config.yaml -name: discussion-architect -description: Systems architect for discussions -category: Discussion -arguments: - - flag: --context - variable: context - description: Discussion content - - flag: --callout - variable: callout - default: "" - description: Specific question or request -steps: - - type: prompt - prompt: | - You are AI-Architect, a senior systems architect... +turn_pipeline: + steps: + - tool: discussion-parser + input: $discussion + output: $parsed - ## Discussion Context - {context} + - tool: discussion-mention-router + input: $parsed + output: $routing + when: not $participants_specified + args: + --default-participants: $participants_csv - ## Request - {callout} + - tool: discussion-{participant} + for_each: $participants_to_call + parallel: true + input: $discussion + output: $responses[] + args: + --callout: $callout + --templates-dir: $templates_dir - Respond with JSON: {"comment": "...", "vote": "READY|CHANGES|REJECT|null"} - provider: claude-sonnet - output_var: response -output: "{response}" + - tool: discussion-turn-appender + input: $discussion + output: $discussion + args: + --responses-json: $responses_json + + - tool: discussion-vote-counter + input: $parsed + output: $votes + when: $phase_voting + + - tool: discussion-status-promoter + input: $votes + output: $promotion + when: $phase_voting + args: + --current-status: $status + --current-phase: $phase ``` -### Provider Configuration +### Testing Tools Independently -SmartTools manages provider configuration in `~/.smarttools/providers.yaml`: +```bash +# Parse discussion to JSON +cat discussion.md | discussion-parser | jq . -```yaml -providers: - - name: claude-sonnet - command: "claude -p --model sonnet" - description: "Balanced quality/speed" - - name: claude-haiku - command: "claude -p --model haiku" - description: "Fast, good for simple responses" - - name: opencode-deepseek - command: "$HOME/.opencode/bin/opencode run --model deepseek/deepseek-chat" - description: "Best value, cheap and accurate" +# Count votes +cat discussion.md | discussion-parser | discussion-vote-counter + +# Route mentions +cat discussion.md | discussion-parser | discussion-mention-router + +# Full pipeline manually +./scripts/run-turn.sh discussion.md ``` --- +## Bundled SmartTools + +### Utility Tools (Code-only) + +#### discussion-parser + +Extracts structured data from discussion markdown. + +**Input**: Discussion markdown (stdin) +**Output**: JSON with metadata, comments, votes, markers + +```json +{ + "metadata": { + "title": "Feature X", + "phase": "initial_feedback", + "status": "OPEN", + "template": "feature", + "participants": ["architect", "security"] + }, + "comments": [ + {"author": "AI-Architect", "body": "...", "vote": "CHANGES"} + ], + "vote_summary": {"READY": 1, "CHANGES": 2, "REJECT": 0, "total": 3}, + "questions": ["Have we considered...?"], + "concerns": ["Security implications"], + "mentions": ["security", "pragmatist"] +} +``` + +#### discussion-validator + +Validates discussion format and identifies issues. + +**Input**: Discussion markdown (stdin) +**Output**: JSON with validation results + +```json +{ + "valid": true, + "issues": [], + "warnings": ["Pending responses from: security"] +} +``` + +#### discussion-summarizer + +Generates a summary of the discussion (AI-powered). + +**Input**: Discussion markdown (stdin) +**Output**: Markdown summary + +### Orchestration Tools (Code-only) + +#### discussion-vote-counter + +Counts votes and determines consensus status. + +**Input**: Parser JSON output (stdin) +**Arguments**: +- `--threshold-ready` (default: 0.67) +- `--threshold-reject` (default: 0.01) +- `--human-required` (default: true) + +**Output**: JSON with vote counts and consensus status + +```json +{ + "votes": {"AI-Architect": "CHANGES", "Human": "READY"}, + "vote_summary": {"READY": 1, "CHANGES": 1, "REJECT": 0, "total": 2}, + "consensus": { + "reached": false, + "reason": "Need 1 more READY votes" + } +} +``` + +#### discussion-mention-router + +Determines which participants should respond based on @mentions. + +**Input**: Parser JSON output (stdin) +**Arguments**: +- `--default-participants` (default: "architect,security,pragmatist") + +**Output**: JSON with routing information + +```json +{ + "participants_to_call": ["security", "pragmatist"], + "callouts": {"security": "", "pragmatist": ""}, + "pending_mentions": ["security"] +} +``` + +#### discussion-status-promoter + +Determines if discussion status should change based on consensus. + +**Input**: Vote-counter JSON output (stdin) +**Arguments**: +- `--current-status` +- `--current-phase` + +**Output**: JSON with promotion decision + +```json +{ + "current_status": "OPEN", + "new_status": "READY_FOR_DESIGN", + "should_promote": true, + "transition_reason": "Consensus reached" +} +``` + +#### discussion-turn-appender + +Appends participant responses to discussion markdown. + +**Input**: Discussion markdown + `---RESPONSES---` + JSON array (stdin) +**Output**: Updated discussion markdown + +#### discussion-config + +Modifies discussion metadata (phase, status, participants). + +**Arguments**: +- `--set-phase` +- `--set-status` +- `--add-participant` +- `--insert-marker` + +### Participant Tools (AI) + +All participant tools follow the same contract: + +**Input**: Discussion markdown (stdin) +**Arguments**: `--callout`, `--templates-dir` +**Output**: JSON with `comment` and `vote` fields + +| Tool | Alias | Perspective | +|------|-------|-------------| +| `discussion-architect` | `@architect` | Systems thinking, scalability, patterns | +| `discussion-security` | `@security` | Threat modeling, vulnerabilities | +| `discussion-pragmatist` | `@pragmatist` | MVP focus, shipping velocity | +| `discussion-moderator` | `@moderator` | Facilitation, phase management | +| `discussion-diagram-editor` | `@diagram-editor` | PlantUML diagram creation | + +--- + ## CLI Interface ### Commands @@ -383,72 +589,27 @@ providers: ```bash # Create a new discussion discussions new "Feature X" --template feature -discussions new "API Redesign" --template architecture-decision - -# List discussions -discussions list -discussions list --status open +discussions new "Brainstorm" --template brainstorm # Show discussion status discussions status feature-x.md -discussions status feature-x.md --verbose -# Run a turn (invoke specific participants) +# Run a turn discussions turn feature-x.md @architect @security -discussions turn feature-x.md @all # All registered participants - -# Advance phase manually -discussions advance feature-x.md -discussions advance feature-x.md --to detailed_review +discussions turn feature-x.md # Uses mention router # Add human comment -discussions comment feature-x.md "I agree with the architect's concerns." -discussions comment feature-x.md --vote READY "Looks good to me." +discussions comment feature-x.md "I agree" --vote READY -# Participant management -discussions participants list -discussions participants add architect --from-file persona.yaml -discussions participants add custom-reviewer --interactive -discussions participants remove old-participant +# Advance phase manually +discussions advance feature-x.md --phase detailed_review -# Interactive TUI -discussions ui feature-x.md -discussions ui # Opens discussion browser -``` +# List available participants +discussions participants -### Example Session - -```bash -$ discussions new "Add user authentication" --template feature -Created: discussions/add-user-authentication.md - -$ discussions turn add-user-authentication.md @architect @security @pragmatist -Invoking AI-Architect... -Invoking AI-Security... -Invoking AI-Pragmatist... - -Discussion updated with 3 new comments. -Votes: READY: 1, CHANGES: 2, REJECT: 0 - -$ discussions status add-user-authentication.md -Discussion: Add user authentication -Phase: initial_feedback -Status: OPEN - -Participants (3 responded): - AI-Architect: CHANGES - concerned about session management - AI-Security: CHANGES - needs threat model - AI-Pragmatist: READY - MVP approach is fine - -Open Questions (2): - Q: Should we use JWT or session cookies? (@architect) - Q: What's our token expiry policy? (@security) - -$ discussions comment add-user-authentication.md "Let's use JWT with 1hr expiry." -Added comment from Human (Rob). - -$ discussions advance add-user-authentication.md -Advanced to phase: detailed_review +# Launch UI +discussions ui # GUI (default) +discussions ui --tui # Terminal UI ``` --- @@ -456,112 +617,81 @@ Advanced to phase: detailed_review ## Python API ```python -from discussions import Discussion, Participant, Runner +from discussions import Discussion +from discussions.runner import run_pipeline_turn -# Load or create a discussion +# Load or create discussion discussion = Discussion.load("feature-x.md") -# or discussion = Discussion.create( - title="Feature X", + path="new-feature.md", + title="New Feature", template="feature", - context="We need to implement..." + context="We need to..." ) -# Access discussion state -print(discussion.title) # "Feature X" -print(discussion.phase) # "initial_feedback" -print(discussion.status) # "OPEN" -print(discussion.participants) # ["architect", "security", "pragmatist"] +# Access state +print(discussion.title) # "New Feature" +print(discussion.phase) # "initial_feedback" +print(discussion.status) # "OPEN" +print(discussion.get_votes()) # {"AI-Architect": "READY", ...} +print(discussion.get_questions()) # [Question(...), ...] -# Get structured data -print(discussion.votes) # {"AI-Architect": "CHANGES", ...} -print(discussion.questions) # [{"text": "...", "author": "...", "status": "open"}] -print(discussion.decisions) # [{"text": "...", "author": "...", "supporters": [...]}] - -# Run a turn -runner = Runner() -responses = runner.run_turn( - discussion=discussion, - participants=["architect", "security"], - callout="Please review the updated proposal." -) - -# Add human comment +# Add comment discussion.add_comment( - author="Rob", - text="I agree with the security concerns.", - vote="CHANGES" + author="Human", + text="I agree with this approach.", + vote="READY" ) +discussion.save() # Check consensus -if discussion.has_consensus(): - print(f"Consensus reached: {discussion.consensus_result}") +result = discussion.check_consensus() +if result.reached: + print(f"Consensus: {result.outcome}") +else: + print(f"Blocked: {result.reason}") -# Save changes -discussion.save() +# Run turn via SmartTools pipeline +from pathlib import Path +result = run_pipeline_turn( + discussion_path=Path("feature-x.md"), + participants=["architect", "security"], + callout="Please review the proposal", + verbose=True +) +print(f"Responses: {result.successful_count}") ``` --- -## Discussion File Format +## User Interfaces -### Header Block +### GUI (Dear PyGui) -```markdown -<!-- DISCUSSION --> -<!-- Title: Feature X Implementation --> -<!-- Phase: initial_feedback --> -<!-- Status: OPEN --> -<!-- Created: 2025-12-08T10:30:00Z --> -<!-- Template: feature --> -<!-- Participants: architect, security, pragmatist, designer --> -``` +- **Native image viewing** for PlantUML diagrams +- **Split-pane diagram viewer** with source and rendered preview +- **Tabbed interface** for discussions, diagrams, and output -### Content Sections +Launch: `discussions ui` or `python -m src.discussions.ui` -```markdown -# Feature X Implementation +### TUI (urwid) -## Context -[Description of what's being discussed] +- **ASCII diagram preview** via `plantuml -tutxt` +- **Works over SSH** without X11 +- **Lightweight** terminal interface -## Requirements -- [Requirement 1] -- [Requirement 2] +Launch: `discussions ui --tui` or `python -m src.discussions.ui --tui` -## Constraints -- [Constraint 1] -``` +### Keyboard Shortcuts -### Comment Blocks - -```markdown ---- - -Name: AI-Architect -[Markdown content - the participant's comment] - -Can include: -- Multiple paragraphs -- Code blocks -- Lists -- Q: questions -- TODO: action items -- DECISION: decisions - -VOTE: CHANGES - ---- -``` - -### Phase Markers - -```markdown -<!-- PHASE-TRANSITION: initial_feedback -> detailed_review --> -<!-- VOTE-RESET: detailed_review --> -``` - -The `VOTE-RESET` marker indicates that votes before this point should not be counted for the current phase. +| Key | GUI | TUI | +|-----|-----|-----| +| Quit | `Q` | `q` | +| Refresh | `R` | - | +| Run turn | `T` | `r` | +| Add comment | `C` | - | +| View diagrams | `D` | `d` | +| Close dialog | `Esc` | `Esc` | --- @@ -569,8 +699,9 @@ The `VOTE-RESET` marker indicates that votes before this point should not be cou ### Project Configuration +Create `discussions.yaml` in project root: + ```yaml -# ~/.config/discussions/config.yaml (or discussions.yaml in project root) default_participants: - architect - security @@ -583,500 +714,93 @@ consensus: threshold_reject: 0.01 human_required: true -providers: - default: claude-sonnet - fallback: - - opencode-deepseek - - claude-haiku - output: - directory: ./discussions # Where to store discussion files - summary_files: true # Generate .sum.md files + directory: ./discussions ``` -### Participant Registry +### Participant Discovery -```yaml -# ~/.config/discussions/participants.yaml -participants: - - name: AI-Architect - alias: architect - role: Systems Architect - personality: | - You are a senior systems architect... - type: voting - provider_hint: claude-sonnet +Participants are discovered by scanning `~/.smarttools/` for tools matching `discussion-*`: - - name: AI-Security - alias: security - role: Security Specialist - personality: | - You are a security specialist... - type: voting - provider_hint: claude-sonnet +```bash +~/.smarttools/ +├── discussion-architect/config.yaml # @architect +├── discussion-security/config.yaml # @security +├── discussion-pragmatist/config.yaml # @pragmatist +├── discussion-moderator/config.yaml # @moderator +└── discussion-diagram-editor/config.yaml # @diagram-editor +``` - - name: AI-Researcher - alias: researcher - role: Research Assistant - personality: | - You are a research assistant... - type: background # Does not vote - provider_hint: claude-haiku +List discovered participants: +```bash +discussions participants ``` --- -## Bundled Participants - -The following participants are bundled with the package: - -### Voting Participants - -| Name | Alias | Role | Perspective | -|------|-------|------|-------------| -| AI-Moderator | `moderator` | Discussion Facilitator | Keeps discussions productive, summarizes, calls votes | -| AI-Architect | `architect` | Systems Architect | Long-term thinking, scalability, patterns | -| AI-Security | `security` | Security Specialist | Threat modeling, vulnerabilities, mitigations | -| AI-Pragmatist | `pragmatist` | Shipping Pragmatist | MVP mindset, practical tradeoffs | -| AI-Perfectionist | `perfectionist` | Quality Champion | Code quality, testing, documentation | -| AI-Designer | `designer` | UX Designer | User experience, accessibility | - -### Background Participants (Non-voting) - -| Name | Alias | Role | Capability | -|------|-------|------|------------| -| AI-Researcher | `researcher` | Research Assistant | Web research, documentation lookup | -| AI-Visualizer | `visualizer` | Diagram Generator | PlantUML diagrams | - ---- - -## Templates - -### Feature Discussion Template - -```markdown -<!-- DISCUSSION --> -<!-- Title: {title} --> -<!-- Phase: initial_feedback --> -<!-- Status: OPEN --> -<!-- Created: {timestamp} --> -<!-- Template: feature --> -<!-- Participants: architect, security, pragmatist --> - -# {title} - -## Context -[Describe the feature and why it's needed] - -## Requirements -- [ ] [Requirement 1] -- [ ] [Requirement 2] - -## Open Questions -- [Question 1] - -## Constraints -- [Constraint 1] - ---- - -*Discussion begins below. Participants will be @mentioned to provide feedback.* -``` - -### Code Review Template - -```markdown -<!-- DISCUSSION --> -<!-- Title: Code Review: {title} --> -<!-- Phase: review --> -<!-- Status: OPEN --> -<!-- Created: {timestamp} --> -<!-- Template: code-review --> -<!-- Participants: architect, security, perfectionist --> - -# Code Review: {title} - -## Changes -[Summary of changes or link to diff] - -## Areas of Focus -- [ ] Architecture -- [ ] Security -- [ ] Performance -- [ ] Testing -- [ ] Documentation - ---- - -*Review comments below.* -``` - -### Architecture Decision Record (ADR) Template - -```markdown -<!-- DISCUSSION --> -<!-- Title: ADR: {title} --> -<!-- Phase: proposal --> -<!-- Status: PROPOSED --> -<!-- Created: {timestamp} --> -<!-- Template: adr --> -<!-- Participants: architect, security, pragmatist --> - -# ADR: {title} - -## Status -PROPOSED - -## Context -[Why is this decision needed?] - -## Options Considered - -### Option A: [Name] -- Pros: ... -- Cons: ... - -### Option B: [Name] -- Pros: ... -- Cons: ... - -## Decision -[To be determined through discussion] - -## Consequences -[To be determined] - ---- - -*Discussion begins below.* -``` - ---- - -## Implementation Plan - -### Phase 1: Core Library (MVP) - -1. **`markers.py`** - Parse VOTE:, Q:, TODO:, etc. -2. **`discussion.py`** - Discussion class with load/save -3. **`participant.py`** - Participant loading from YAML -4. **`voting.py`** - Vote counting and consensus -5. **`runner.py`** - Basic turn execution using SmartTools - -**Deliverable**: Python API that can load discussions, run turns, and save results. - -### Phase 2: CLI - -1. **`cli.py`** - Implement commands: new, status, turn, comment, advance -2. **Templates** - Bundled discussion templates -3. **Participants** - Bundled persona definitions - -**Deliverable**: Fully functional CLI for managing discussions. - -### Phase 3: Advanced Features - -1. **`phases.py`** - Phase definitions and auto-transitions -2. **`turns.py`** - @mention routing and turn tracking -3. **Summary generation** - Auto-generate .sum.md files - -**Deliverable**: Full orchestration with phases and automatic transitions. - -### Phase 4: TUI - -1. **`ui/tui.py`** - urwid-based interface -2. **Discussion browser** - List and select discussions -3. **Participant manager** - Add/edit personas -4. **Live discussion view** - Watch discussion in real-time - -**Deliverable**: Interactive TUI for human participation. - ---- - -## Testing Strategy +## Testing ### Unit Tests -- `test_markers.py` - Marker parsing edge cases -- `test_voting.py` - Consensus calculation -- `test_discussion.py` - State management -- `test_phases.py` - Phase transitions +```bash +# Run all tests +pytest -### Integration Tests +# Run specific test file +pytest tests/test_markers.py -- `test_runner.py` - Full turn execution (with mock provider) -- `test_cli.py` - CLI command integration +# Run with coverage +pytest --cov=discussions +``` -### Example +### SmartTools Integration Tests -```python -# tests/test_voting.py -def test_consensus_reached_with_threshold(): - votes = { - "AI-Architect": "READY", - "AI-Security": "READY", - "AI-Pragmatist": "CHANGES", - } - config = VotingConfig(threshold_ready=0.67) - result = calculate_consensus(votes, config) - assert result.reached == True - assert result.outcome == "READY" +```bash +# Test parser +cat examples/feature_discussion.md | discussion-parser | jq . -def test_reject_blocks_consensus(): - votes = { - "AI-Architect": "READY", - "AI-Security": "REJECT", - "AI-Pragmatist": "READY", - } - config = VotingConfig(threshold_reject=0.01) - result = calculate_consensus(votes, config) - assert result.reached == False - assert result.blocked_by == ["AI-Security"] +# Test full pipeline +./scripts/run-turn.sh examples/feature_discussion.md ``` --- ## Migration from CascadingDev -Once this project is stable, CascadingDev can be refactored to use it: +Once stable, CascadingDev will: -1. **Remove** from CascadingDev: - - `automation/schema.py` - - `automation/workflow.py` (discussion parts) - - `automation/orchestrator.py` - - `automation/agent_fetcher.py` - - `agents/*.py` (voting agents) - - `config/participants.yml` - -2. **Add** to CascadingDev: - - Dependency on `orchestrated-discussions` - - Glue code to trigger discussions on commit - - Integration with `.ai-rules.yml` for discussion automation - -3. **Keep** in CascadingDev: - - `automation/runner.py` (cascading rules) - - `automation/config.py` (rules resolution) - - `automation/patcher.py` (diff generation) - - Git hook orchestration +1. **Remove**: Discussion orchestration code (schema.py, workflow.py) +2. **Remove**: Voting agent implementations +3. **Add**: `orchestrated-discussions` as dependency +4. **Update**: Git hooks to use `discussions` CLI --- -## Open Questions +## UML Diagrams -1. **SmartTools import vs subprocess** - Should we import SmartTools directly or call it as a subprocess? Direct import is cleaner but creates tighter coupling. +Visual documentation is available in `docs/diagrams/`: -2. **Participant storage** - Store in `~/.config/discussions/` or `~/.smarttools/`? The latter allows SmartTools TUI editing. +| Diagram | Description | +|---------|-------------| +| [turn-execution-sequence.puml](diagrams/turn-execution-sequence.puml) | Sequence diagram showing how a discussion turn flows through the system | +| [architecture-component.puml](diagrams/architecture-component.puml) | Component diagram showing the overall system architecture | +| [core-class-diagram.puml](diagrams/core-class-diagram.puml) | Class diagram of the Python module structure | +| [smarttools-pipeline-flow.puml](diagrams/smarttools-pipeline-flow.puml) | Activity diagram showing data flow through SmartTools pipeline | +| [discussion-state-machine.puml](diagrams/discussion-state-machine.puml) | State machine showing discussion lifecycle and status transitions | -3. **Discussion storage** - Default to `./discussions/` in current directory, or configurable per-project? +To render diagrams: +```bash +# Single diagram +plantuml docs/diagrams/turn-execution-sequence.puml -4. **Async execution** - Should turns run participants in parallel? Could speed up multi-participant turns significantly. +# All diagrams +plantuml docs/diagrams/*.puml -5. **Streaming responses** - Should the TUI show AI responses as they stream in? - ---- - -## Appendix A: Full Participant Persona Examples - -### AI-Architect - -```yaml -name: AI-Architect -alias: architect -role: Systems Architect -personality: | - You are AI-Architect (also known as Chen), a senior systems architect with deep - expertise in distributed systems, design patterns, and long-term technical strategy. - - Your role: - - Think in systems, patterns, and architectural principles - - Consider scalability, maintainability, and evolution over time - - Identify architectural risks and technical debt implications - - Suggest well-established patterns and proven approaches - - Balance ideal architecture with practical constraints - - Perspective: - - You think 2-5 years ahead, not just the immediate implementation - - You value modularity, separation of concerns, and clean boundaries - - You prefer boring, proven technology over cutting-edge experiments - - You call out when shortcuts will create architectural debt - - Response format: - - Provide your analysis and concerns in markdown - - Use Q: for questions you want answered - - Use DECISION: if you're proposing a decision - - End with VOTE: READY, CHANGES, or REJECT - - If you have nothing to add, respond with: {"sentinel": "NO_RESPONSE"} - -expertise: - - System design - - Scalability - - Technical debt - - Architectural patterns - - API design - - Data modeling - -concerns: - - "How does this fit the overall architecture?" - - "Will this scale to 10x current load?" - - "What's the long-term maintenance burden?" - - "Are we creating unnecessary coupling?" - - "Is this the right level of abstraction?" - -type: voting -provider_hint: claude-sonnet -``` - -### AI-Security - -```yaml -name: AI-Security -alias: security -role: Security Specialist -personality: | - You are AI-Security (also known as Steve), a security specialist who identifies - vulnerabilities, threat vectors, and security best practices. - - Your role: - - Identify security risks and vulnerabilities - - Suggest mitigations and security controls - - Consider threat models and attack surfaces - - Ensure compliance with security best practices - - Balance security with usability - - Perspective: - - Assume malicious actors will try to exploit the system - - Consider both external and internal threats - - Think about data protection and privacy - - Focus on practical, implementable security measures - - Response format: - - Highlight security concerns clearly - - Suggest specific mitigations for each concern - - Use CONCERN: for security issues - - End with VOTE: READY, CHANGES, or REJECT - -expertise: - - Vulnerability assessment - - Threat modeling - - Authentication & authorization - - Data protection - - Input validation - - Secure coding practices - -concerns: - - "What are the security implications?" - - "How could this be exploited?" - - "Are we handling sensitive data properly?" - - "Is authentication/authorization adequate?" - - "Are we validating all inputs?" - -type: voting -provider_hint: claude-sonnet -``` - -### AI-Pragmatist - -```yaml -name: AI-Pragmatist -alias: pragmatist -role: Shipping Pragmatist -personality: | - You are AI-Pragmatist (also known as Maya), a shipping-focused engineer who - advocates for practical solutions and incremental delivery. - - Your role: - - Advocate for simpler solutions - - Identify over-engineering and scope creep - - Suggest MVP approaches - - Balance quality with delivery speed - - Challenge unnecessary complexity - - Perspective: - - "Done is better than perfect when it's good enough" - - Ship early, iterate often - - Complexity is the enemy of delivery - - Technical debt is acceptable if managed - - Users need features, not architectural purity - - Response format: - - Call out over-engineering - - Suggest simpler alternatives - - Identify what can be deferred - - End with VOTE: READY, CHANGES, or REJECT - -expertise: - - MVP scoping - - Shipping velocity - - Trade-off analysis - - Iterative development - - Technical debt management - -concerns: - - "Can we ship this incrementally?" - - "Are we over-engineering this?" - - "What's the simplest thing that could work?" - - "Can this be deferred to a later iteration?" - - "Is this complexity justified?" - -type: voting -provider_hint: claude-sonnet +# ASCII preview +plantuml -tutxt docs/diagrams/turn-execution-sequence.puml ``` --- -## Appendix B: Response JSON Format - -All participants respond with JSON: - -```json -{ - "comment": "Markdown formatted comment...\n\nQ: Question here?\n\nCONCERN: Security issue here", - "vote": "READY" -} -``` - -Or, if they have nothing to contribute: - -```json -{ - "sentinel": "NO_RESPONSE" -} -``` - -The runner parses this and formats it into the discussion file: - -```markdown ---- - -Name: AI-Architect -Markdown formatted comment... - -Q: Question here? - -CONCERN: Security issue here - -VOTE: READY - ---- -``` - ---- - -## Appendix C: SmartTools Provider Reference - -Available providers in SmartTools (from `~/.smarttools/providers.yaml`): - -| Provider | Command | Speed | Notes | -|----------|---------|-------|-------| -| `opencode-deepseek` | `$HOME/.opencode/bin/opencode run --model deepseek/deepseek-chat` | 13s | Best value | -| `opencode-pickle` | `$HOME/.opencode/bin/opencode run --model opencode/big-pickle` | 13s | Best free | -| `claude-haiku` | `claude -p --model haiku` | 14s | Fast + accurate | -| `claude-sonnet` | `claude -p --model sonnet` | 21s | Balanced | -| `claude-opus` | `claude -p --model opus` | 18s | Highest quality | -| `codex` | `codex exec -` | 14s | Reliable | -| `gemini-flash` | `gemini --model gemini-2.5-flash` | 28s | Quick tasks | -| `mock` | (builtin) | 0s | Testing only | - ---- - -*Document version: 1.0* -*Last updated: 2025-12-08* +*Document version: 2.0* +*Last updated: 2025-12-16* diff --git a/docs/IMPLEMENTATION.md b/docs/IMPLEMENTATION.md new file mode 100644 index 0000000..842522e --- /dev/null +++ b/docs/IMPLEMENTATION.md @@ -0,0 +1,266 @@ +# Orchestrated Discussions - Implementation Status + +This document tracks the implementation progress of Orchestrated Discussions. + +## Overall Status: Core Complete + +The core functionality is fully implemented and working. The project is usable for its primary purpose of orchestrating multi-agent AI discussions. + +--- + +## Implementation Phases + +### Phase 0: SmartTool Configs - COMPLETE + +All SmartTool configs have been created and tested. + +| Tool | Status | Notes | +|------|--------|-------| +| `discussion-parser` | Complete | Extracts structured JSON from markdown | +| `discussion-validator` | Complete | Validates format, finds issues | +| `discussion-summarizer` | Complete | AI-powered summary generation | +| `discussion-vote-counter` | Complete | Counts votes, determines consensus | +| `discussion-mention-router` | Complete | Routes @mentions to participants | +| `discussion-status-promoter` | Complete | Determines status transitions | +| `discussion-turn-appender` | Complete | Appends responses to markdown | +| `discussion-config` | Complete | Modifies discussion metadata | +| `discussion-architect` | Complete | Systems thinking persona | +| `discussion-security` | Complete | Security specialist persona | +| `discussion-pragmatist` | Complete | Shipping-focused persona | +| `discussion-moderator` | Complete | Facilitation persona | +| `discussion-diagram-editor` | Complete | PlantUML diagram creation | +| `puml-validator` | Complete | PlantUML syntax validation | + +**Manual Testing:** +```bash +cat examples/brainstorm_notification_system.md | discussion-parser | jq . +cat examples/voted_discussion.md | discussion-parser | discussion-vote-counter +./scripts/run-turn.sh examples/feature_discussion.md +``` + +--- + +### Phase 1: Core Library - COMPLETE + +Python library wrapping SmartTools with clean API. + +| Module | Status | Notes | +|--------|--------|-------| +| `discussion.py` | Complete | Discussion model, load/save, add_comment | +| `markers.py` | Complete | Marker parsing (VOTE, Q, TODO, etc.) | +| `voting.py` | Complete | Consensus calculation, VotingConfig | +| `participant.py` | Complete | Participant discovery from ~/.smarttools/ | +| `runner.py` | Complete | Pipeline-based turn execution | + +**Key Features:** +- `Discussion.load()` / `Discussion.create()` / `Discussion.save()` +- `Discussion.add_comment()` with vote support +- `Discussion.check_consensus()` with configurable thresholds +- `run_pipeline_turn()` for SmartTools-based turn execution +- Participant discovery via `ParticipantRegistry` + +--- + +### Phase 2: CLI Interface - COMPLETE + +All CLI commands implemented and working. + +| Command | Status | Notes | +|---------|--------|-------| +| `discussions new` | Complete | Create new discussion file | +| `discussions status` | Complete | Show status, votes, consensus | +| `discussions turn` | Complete | Run turn with SmartTools | +| `discussions comment` | Complete | Add human comment with vote | +| `discussions participants` | Complete | List discovered participants | +| `discussions advance` | Complete | Advance to specific phase | +| `discussions ui` | Complete | Launch GUI or TUI | + +**Usage Examples:** +```bash +discussions new "My Feature" --template feature +discussions turn my-feature.md @architect @security +discussions status my-feature.md +discussions comment my-feature.md "Approved" --vote ready +discussions ui +``` + +--- + +### Phase 3: Templates & Phases - COMPLETE + +Template system implemented with phase-aware behavior. + +| Template | Status | Phases | +|----------|--------|--------| +| `feature.yaml` | Complete | initial_feedback, detailed_review, consensus_vote | +| `brainstorm.yaml` | Complete | seed, diverge, cluster, sketch, reality_check, decide | + +**Features:** +- Phase-specific instructions for participants +- Voting phases with configurable thresholds +- Status transitions on consensus +- Turn pipeline configuration in templates + +--- + +### Phase 4: User Interfaces - COMPLETE + +Both GUI and TUI implemented and working. + +| Interface | Status | Framework | Notes | +|-----------|--------|-----------|-------| +| GUI | Complete | Dear PyGui | Native image viewing, tabs | +| TUI | Complete | urwid | ASCII diagrams, SSH-friendly | + +**GUI Features:** +- Native PlantUML diagram rendering +- Split-pane diagram viewer (source + preview) +- Tabbed interface for discussions, diagrams, output +- Keyboard shortcuts (Q, R, T, C, D) + +**TUI Features:** +- ASCII diagram preview via `plantuml -tutxt` +- Discussion browser and viewer +- Run turns with progress display +- Keyboard navigation (q, r, d, arrows) + +--- + +### Phase 5: Polish & Documentation - IN PROGRESS + +| Item | Status | Notes | +|------|--------|-------| +| README.md | Complete | Updated with accurate info | +| DESIGN.md | Complete | Updated with actual implementation | +| IMPLEMENTATION.md | Complete | This document | +| CLAUDE.md | Complete | Agent instructions | +| AGENTS.md | Complete | Repository guidelines | +| Unit tests | Partial | test_markers.py, test_voting.py | +| Integration tests | Partial | run-turn.sh script | +| PyPI publishing | Not Started | Package not yet published | + +--- + +## Current File Structure + +``` +orchestrated-discussions/ +├── src/discussions/ +│ ├── __init__.py # v0.1.0 +│ ├── cli.py # 7 commands +│ ├── discussion.py # Discussion model +│ ├── markers.py # 7 marker types +│ ├── participant.py # Registry + discovery +│ ├── runner.py # Pipeline execution +│ ├── voting.py # Consensus logic +│ └── ui/ +│ ├── __init__.py +│ ├── __main__.py +│ ├── gui.py # Dear PyGui +│ ├── tui.py # urwid +│ └── widgets.py # Shared components +│ +├── smarttools/ # 14 tool configs +├── templates/ # 2 templates +├── examples/ # 5 example discussions +├── scripts/ # run-turn.sh +├── tests/ # test_markers.py, test_voting.py +└── docs/ # DESIGN.md, IMPLEMENTATION.md +``` + +--- + +## What's Working + +- Creating and loading discussion files +- Parsing markers (VOTE, Q, TODO, DECISION, CONCERN, DIAGRAM) +- Running turns with multiple AI participants +- Parallel participant execution +- Voting and consensus calculation +- Phase-aware participant behavior +- Status promotion on consensus +- Human comment addition with votes +- Participant discovery from ~/.smarttools/ +- Both GUI and TUI interfaces +- PlantUML diagram viewing + +--- + +## Known Limitations + +1. **Automatic phase advancement** - Phase must be advanced manually with `--phase` flag +2. **PyPI package** - Not yet published, install from source +3. **Streaming responses** - Responses collected after completion, not streamed +4. **Provider locking** - No rate limiting for concurrent AI calls + +--- + +## Future Enhancements + +### Additional Participants +- `discussion-perfectionist` - Code quality focus +- `discussion-designer` - UX perspective +- `discussion-researcher` - Web research (non-voting) +- `discussion-visualizer` - Advanced diagram generation + +### Additional Templates +- `code-review.yaml` - Code review workflow +- `adr.yaml` - Architecture Decision Record workflow + +### Features +- Automatic phase transitions based on conditions +- Summary file generation (.sum.md) +- Project-level configuration (discussions.yaml) +- Provider rate limiting and fallbacks +- Response streaming in TUI + +--- + +## Testing + +### Run Unit Tests +```bash +pytest tests/ +pytest tests/test_markers.py -v +pytest tests/test_voting.py -v +``` + +### Run Integration Tests +```bash +# Test SmartTools directly +cat examples/brainstorm_notification_system.md | discussion-parser | jq . + +# Test full pipeline +./scripts/run-turn.sh examples/feature_discussion.md +``` + +### Manual Testing +```bash +# Create and run a discussion +discussions new "Test Feature" --template feature +discussions turn test-feature.md @architect @pragmatist +discussions status test-feature.md +discussions comment test-feature.md "Looks good" --vote ready +discussions ui +``` + +--- + +## Dependencies + +### Required +- Python 3.10+ +- PyYAML >= 6.0 +- smarttools >= 0.1.0 + +### Optional +- urwid >= 2.1.0 (TUI) +- dearpygui (GUI) + +### Development +- pytest >= 7.0 +- pytest-cov >= 4.0 + +--- + +*Last updated: 2025-12-16* diff --git a/docs/PIPELINE_SCHEMA.md b/docs/PIPELINE_SCHEMA.md new file mode 100644 index 0000000..4ab0d1f --- /dev/null +++ b/docs/PIPELINE_SCHEMA.md @@ -0,0 +1,354 @@ +# Turn Pipeline Schema + +## Overview + +Each template can define a `turn_pipeline` that specifies which SmartTools to run and how to pipe data between them. The runner is thin orchestration - it just executes tools and pipes data, with no business logic. + +## Design Philosophy + +The pipeline should mirror what you could do manually in a shell: + +```bash +# Parse the discussion +cat discussion.md | discussion-parser > parsed.json + +# Route mentions +cat parsed.json | discussion-mention-router > routing.json + +# Call each participant (could be parallel) +cat discussion.md | discussion-architect --callout "Review" > resp_architect.json +cat discussion.md | discussion-security --callout "Review" > resp_security.json + +# Append responses +cat discussion.md | discussion-turn-appender --responses '[...]' > updated.md + +# Count votes (if voting phase) +cat updated.md | discussion-parser | discussion-vote-counter > votes.json +``` + +## Schema + +```yaml +turn_pipeline: + steps: + - tool: <tool-name> # Required: SmartTool to execute + input: <variable> # What to pipe to stdin (default: $discussion) + output: <variable> # Where to store stdout (default: discarded) + args: # CLI arguments + --arg-name: <value> # Can reference variables: $varname + when: <condition> # Optional: condition to run this step + for_each: <variable> # Optional: iterate over array + parallel: <bool> # Optional: run for_each in parallel +``` + +## Variables + +Variables are referenced with `$` prefix: + +### Built-in Variables +- `$discussion` - Current discussion markdown content (updated as pipeline runs) +- `$discussion_path` - Path to the discussion file +- `$callout` - Callout/question passed to the turn +- `$participants` - List of participant aliases from discussion metadata +- `$templates_dir` - Path to templates directory + +### Step Output Variables +Each step can capture its output: +```yaml +- tool: discussion-parser + input: $discussion + output: $parsed # Now $parsed contains the JSON output +``` + +### Accessing JSON Fields +For JSON outputs, access fields with dot notation: +```yaml +- tool: discussion-mention-router + input: $parsed + output: $routing + +- tool: discussion-{participant} + for_each: $routing.participants_to_call # Iterate over array in JSON +``` + +### Array Collection +When using `for_each`, outputs are collected into an array: +```yaml +- tool: discussion-{participant} + for_each: $routing.participants_to_call + output: $responses[] # [] means append to array +``` + +## Conditions + +- `always` - Always run (default) +- `$variable` - Run if variable is truthy +- `$variable.field` - Run if JSON field is truthy +- `not $variable` - Run if variable is falsy + +Examples: +```yaml +- tool: discussion-vote-counter + when: $phase.voting # Only if phase has voting enabled + +- tool: discussion-mention-router + when: not $participants_specified # Only if no participants given +``` + +## Complete Example + +```yaml +turn_pipeline: + steps: + # Step 1: Parse discussion to get metadata + - tool: discussion-parser + input: $discussion + output: $parsed + + # Step 2: Route mentions (optional - skip if participants specified) + - tool: discussion-mention-router + input: $parsed + output: $routing + when: not $participants_specified + args: + --default-participants: $participants + + # Step 3: Call each participant + - tool: discussion-{participant} + for_each: $routing.participants_to_call + parallel: true + input: $discussion + output: $responses[] + args: + --callout: $callout + --templates-dir: $templates_dir + + # Step 4: Append responses to discussion + - tool: discussion-turn-appender + input: $discussion + output: $discussion # Update the discussion variable + args: + --responses-json: $responses + + # Step 5: Count votes (only in voting phases) + - tool: discussion-parser + input: $discussion + output: $updated_parsed + when: $phase.voting + + - tool: discussion-vote-counter + input: $updated_parsed + output: $votes + when: $phase.voting + args: + --threshold-ready: $phase.threshold_ready + --human-required: $phase.human_required + + # Step 6: Check status promotion + - tool: discussion-status-promoter + input: $votes + output: $promotion + when: $phase.voting + args: + --current-status: $parsed.metadata.status + --current-phase: $parsed.metadata.phase +``` + +## For Each Expansion + +When `for_each` is used, the step runs once per item: + +```yaml +- tool: discussion-{participant} + for_each: $routing.participants_to_call # e.g., ["architect", "security"] + input: $discussion + output: $responses[] +``` + +This expands to: +```bash +cat discussion.md | discussion-architect > resp_0.json +cat discussion.md | discussion-security > resp_1.json +# $responses = [resp_0.json contents, resp_1.json contents] +``` + +With `parallel: true`, these run concurrently. + +## Special Tool Patterns + +### `{participant}` Substitution +The pattern `{participant}` in tool names is replaced with the current iteration value: + +```yaml +- tool: discussion-{participant} + for_each: ["architect", "security"] +``` + +Becomes calls to `discussion-architect` and `discussion-security`. + +## Minimal Example + +A bare-bones pipeline that just calls participants and appends: + +```yaml +turn_pipeline: + steps: + - tool: discussion-{participant} + for_each: $participants + parallel: true + input: $discussion + output: $responses[] + + - tool: discussion-turn-appender + input: $discussion + output: $discussion + args: + --responses-json: $responses +``` + +## Phase Artifacts + +Phases can define `artifacts` to enable collaboration on diagrams, sketches, and models. +Participants read this config to know what formats to output and where to save files. + +### Artifact Schema + +```yaml +phases: + sketch: + goal: Rough out the selected idea + artifacts: + formats: # List of accepted formats + - plantuml # PlantUML diagrams (.puml) + - svg # SVG vector graphics (.svg) + - mermaid # Mermaid diagrams (.mmd) + - openscad # OpenSCAD 3D models (.scad) + - solidpython # SolidPython 3D models (.py) + output_dir: diagrams/ # Where to save artifacts + file_pattern: "{title}_{participant}_{n}" # Naming pattern + instructions: | + - Create diagrams in any supported format + - Review existing artifacts in the output directory + - Build on or modify others' work as needed +``` + +### Supported Formats + +| Format | Extension | Use Case | +|--------|-----------|----------| +| `plantuml` | `.puml` | Sequence, component, class, state diagrams | +| `mermaid` | `.mmd` | Flowcharts, sequence, ER, Gantt charts | +| `svg` | `.svg` | UI mockups, icons, 2D layouts, sketches | +| `openscad` | `.scad` | 3D models, mechanical parts, enclosures | +| `solidpython` | `.py` | Programmatic 3D models (Python-based) | +| `dot` | `.dot` | Graph visualizations (Graphviz) | +| `ascii` | `.txt` | Simple inline text diagrams | + +### How Participants Use Artifacts + +Participants receive artifact config via the `--artifacts-json` argument: + +```json +{ + "formats": ["plantuml", "svg"], + "output_dir": "diagrams/", + "file_pattern": "{title}_{participant}_{n}" +} +``` + +They then: +1. Know what formats are acceptable for this phase +2. Save artifacts to the correct directory +3. Reference artifacts in comments with `ARTIFACT: path/to/file.ext` +4. Can read existing artifacts in `output_dir` to build upon them + +### Response Format with Artifacts + +Participants output artifacts in their JSON response: + +```json +{ + "comment": "Here's my proposed architecture...\n\nARTIFACT: diagrams/auth-flow_architect_1.puml", + "vote": null, + "artifacts": [ + { + "format": "plantuml", + "filename": "auth-flow_architect_1.puml", + "content": "@startuml\nUser -> Auth: login\nAuth -> DB: validate\n@enduml" + } + ] +} +``` + +### Example: Hardware Design Template + +```yaml +name: hardware_design +description: Physical product design workflow + +phases: + requirements: + goal: Define physical constraints and requirements + artifacts: null # No artifacts in this phase + voting: false + + enclosure_design: + goal: Design the physical enclosure + artifacts: + formats: [openscad, solidpython] + output_dir: models/ + instructions: | + - Create 3D models considering manufacturing constraints + - Review existing models in models/ folder + - Consider assembly, thermal, and structural requirements + voting: false + + pcb_layout: + goal: Plan PCB component placement + artifacts: + formats: [svg, plantuml] + output_dir: pcb/ + instructions: | + - Create component layout diagrams + - Consider signal routing and thermal zones + voting: false + + final_review: + goal: Approve the complete design + artifacts: + formats: [openscad, svg, plantuml] + output_dir: final/ + voting: true + threshold_ready: 0.67 +``` + +### Example: UI Design Template + +```yaml +name: ui_design +description: User interface design workflow + +phases: + wireframe: + goal: Sketch rough layouts + artifacts: + formats: [svg, ascii] + output_dir: wireframes/ + instructions: | + - Create rough wireframes - boxes and labels only + - Focus on layout and flow, not aesthetics + - One SVG per screen/view + voting: false + + mockup: + goal: Detailed visual mockups + artifacts: + formats: [svg] + output_dir: mockups/ + instructions: | + - Build on approved wireframes + - Add visual hierarchy and styling + - Consider responsive breakpoints + voting: true + threshold_ready: 0.5 +``` diff --git a/docs/diagrams/architecture-component.puml b/docs/diagrams/architecture-component.puml new file mode 100644 index 0000000..e58d486 --- /dev/null +++ b/docs/diagrams/architecture-component.puml @@ -0,0 +1,111 @@ +@startuml Architecture Component Diagram +!theme plain +skinparam componentStyle uml2 +skinparam linetype ortho + +title Orchestrated Discussions - Component Architecture + +package "User Layer" { + component [CLI] as CLI + component [GUI\n(Dear PyGui)] as GUI + component [TUI\n(urwid)] as TUI +} + +package "Orchestrated Discussions" { + component [Discussion\nModel] as Model + component [Runner\n(Pipeline)] as Runner + component [Participant\nRegistry] as Registry + + package "Local Helpers" { + component [Markers\nParser] as Markers + component [Voting\nCalculator] as Voting + } +} + +package "SmartTools" { + package "Utility Tools" { + component [discussion-parser] as Parser + component [discussion-validator] as Validator + component [discussion-summarizer] as Summarizer + } + + package "Orchestration Tools" { + component [discussion-vote-counter] as VoteCounter + component [discussion-mention-router] as Router + component [discussion-status-promoter] as Promoter + component [discussion-turn-appender] as Appender + component [discussion-config] as Config + } + + package "Participant Tools" { + component [discussion-architect] as Architect + component [discussion-security] as Security + component [discussion-pragmatist] as Pragmatist + component [discussion-moderator] as Moderator + component [discussion-diagram-editor] as DiagramEditor + } +} + +package "External" { + component [AI Providers\n(Claude, etc.)] as AI + database [Discussion\nFiles (.md)] as Files + database [Templates\n(.yaml)] as Templates +} + +' User Layer connections +CLI --> Runner : invokes +GUI --> Runner : invokes +TUI --> Runner : invokes + +CLI --> Model : load/save +GUI --> Model : load/save +TUI --> Model : load/save + +' Runner connections +Runner --> Parser : subprocess +Runner --> Router : subprocess +Runner --> Appender : subprocess +Runner --> VoteCounter : subprocess +Runner --> Promoter : subprocess + +' Participant calls +Runner --> Architect : subprocess +Runner --> Security : subprocess +Runner --> Pragmatist : subprocess +Runner --> Moderator : subprocess +Runner --> DiagramEditor : subprocess + +' Model connections +Model --> Files : read/write +Model --> Markers : parse markers +Model --> Voting : check consensus + +' Registry +Registry ..> "Participant Tools" : discovers + +' SmartTools to AI +Architect --> AI : inference +Security --> AI : inference +Pragmatist --> AI : inference +Moderator --> AI : inference +DiagramEditor --> AI : inference +Summarizer --> AI : inference + +' Template usage +Runner --> Templates : load pipeline + +note right of Runner + **Thin Orchestration** + - Pipes data between tools + - No business logic + - Parallel execution +end note + +note bottom of "SmartTools" + **Unix Philosophy** + - Each tool does one thing + - stdin/stdout communication + - Independently testable +end note + +@enduml diff --git a/docs/diagrams/core-class-diagram.puml b/docs/diagrams/core-class-diagram.puml new file mode 100644 index 0000000..8b8b971 --- /dev/null +++ b/docs/diagrams/core-class-diagram.puml @@ -0,0 +1,218 @@ +@startuml Core Class Diagram +!theme plain +skinparam classAttributeIconSize 0 + +title Orchestrated Discussions - Core Classes + +package "discussion.py" { + class Discussion { + +path: Path + +title: str + +phase: str + +status: str + +template: str + +participant_aliases: list[str] + +comments: list[Comment] + +created: datetime + -_raw_content: str + -- + +{static} load(path): Discussion + +{static} create(path, title, ...): Discussion + +save() + +add_comment(author, text, vote): Comment + +get_context(): str + +get_votes(): dict[str, str] + +get_questions(): list[Question] + +get_action_items(): list[ActionItem] + +get_decisions(): list[Decision] + +get_concerns(): list[Concern] + +get_mentions(target): list[Mention] + +check_consensus(config): ConsensusResult + +has_consensus(): bool + +update_phase(new_phase) + +update_status(new_status) + +get_content(): str + } + + class Comment { + +author: str + +body: str + +vote: str + +questions: list[Question] + +action_items: list[ActionItem] + +decisions: list[Decision] + +concerns: list[Concern] + +mentions: list[Mention] + } + + Discussion "1" *-- "*" Comment +} + +package "markers.py" { + class Question { + +text: str + +author: str + +status: str = "open" + } + + class ActionItem { + +text: str + +author: str + +assignee: str + +status: str = "todo" + } + + class Decision { + +text: str + +author: str + +supporters: list[str] + } + + class Concern { + +text: str + +author: str + +addressed: bool = False + } + + class Diagram { + +path: str + +author: str + } + + class Mention { + +target: str + +author: str + +context: str + } + + class "<<functions>>" as MarkerFunctions { + +extract_vote(text): str + +extract_questions(text, author): list[Question] + +extract_action_items(text, author): list[ActionItem] + +extract_decisions(text, author): list[Decision] + +extract_concerns(text, author): list[Concern] + +extract_diagrams(text, author): list[Diagram] + +extract_mentions(text, author): list[Mention] + +extract_all_markers(text, author): dict + } + + Comment ..> Question : contains + Comment ..> ActionItem : contains + Comment ..> Decision : contains + Comment ..> Concern : contains + Comment ..> Mention : contains +} + +package "voting.py" { + class VotingConfig { + +threshold_ready: float = 0.67 + +threshold_reject: float = 0.01 + +human_required: bool = True + +minimum_votes: int = 1 + } + + class ConsensusResult { + +reached: bool + +outcome: str + +ready_count: int + +changes_count: int + +reject_count: int + +total_votes: int + +blocked_by: list[str] + +reason: str + } + + class "<<functions>>" as VotingFunctions { + +is_human_participant(name): bool + +calculate_consensus(votes, config): ConsensusResult + +format_vote_summary(votes): str + +format_vote_details(votes): str + } + + Discussion ..> VotingConfig : uses + Discussion ..> ConsensusResult : returns +} + +package "participant.py" { + class Participant { + +alias: str + +name: str + +description: str + +participant_type: str + +tool_path: Path + -- + +is_voting(): bool + +tool_name(): str + } + + class ParticipantRegistry { + -_smarttools_dir: Path + -_participants: dict[str, Participant] + -_loaded: bool + -- + +get(alias): Participant + +get_all(): list[Participant] + +get_voting(): list[Participant] + +get_background(): list[Participant] + +aliases(): list[str] + +refresh() + } + + class "<<functions>>" as ParticipantFunctions { + +discover_participants(dir): list[Participant] + +check_tool_exists(alias): bool + +get_registry(): ParticipantRegistry + +get_participant(alias): Participant + +list_participants(): list[Participant] + } + + ParticipantRegistry "1" *-- "*" Participant +} + +package "runner.py" { + class TurnResult { + +responses: list[ParticipantResponse] + +vote_summary: dict + +consensus_reached: bool + +consensus_reason: str + +status_promoted: bool + +new_status: str + -- + +successful_count: int + +failed_count: int + +skipped_count: int + } + + class ParticipantResponse { + +alias: str + +name: str + +comment: str + +vote: str + +success: bool + +error: str + +raw_output: str + } + + class VariableStore { + -_store: dict + -- + +set(name, value) + +get(ref): any + +resolve(value): str + +resolve_args(args): list[str] + +dump(): dict + } + + class "<<functions>>" as RunnerFunctions { + +run_turn(path, participants, ...): TurnResult + +run_pipeline_turn(path, ...): TurnResult + +run_discussion_turn(path, ...): TurnResult + } + + TurnResult "1" *-- "*" ParticipantResponse +} + +' Cross-package relationships +Discussion ..> Participant : references by alias + +@enduml diff --git a/docs/diagrams/discussion-state-machine.puml b/docs/diagrams/discussion-state-machine.puml new file mode 100644 index 0000000..9e02fa3 --- /dev/null +++ b/docs/diagrams/discussion-state-machine.puml @@ -0,0 +1,65 @@ +@startuml Discussion State Machine +!theme plain +skinparam state { + BackgroundColor white + BorderColor black +} + +title Discussion Lifecycle State Machine + +[*] --> OPEN : Create discussion + +state "OPEN" as OPEN : Active discussion\nAccepting comments +state "READY_FOR_DESIGN" as DESIGN : Consensus reached\nApproved for design +state "READY_FOR_IMPLEMENTATION" as IMPL : Design approved\nReady to implement +state "CLOSED" as CLOSED : Discussion ended + +OPEN --> OPEN : Add comments\nRun turns +OPEN --> DESIGN : Consensus reached\n(in consensus_vote phase) +OPEN --> CLOSED : Manual close\nor REJECT consensus + +DESIGN --> DESIGN : Design discussion +DESIGN --> IMPL : Design consensus + +IMPL --> CLOSED : Implementation complete + +CLOSED --> [*] + +note right of OPEN + **Phase Flow (Feature Template)** + 1. initial_feedback (no voting) + 2. detailed_review (no voting) + 3. consensus_vote (voting) + + **Phase Flow (Brainstorm Template)** + 1. seed (no voting) + 2. diverge (no voting) + 3. cluster (voting - 50%) + 4. sketch (no voting) + 5. reality_check (no voting) + 6. decide (voting - 67%) +end note + +note bottom of DESIGN + **Voting Thresholds** + - READY: 67% required + - REJECT: Any blocks + - Human approval required +end note + +== Vote Types == + +state "READY" as V_READY #lightgreen +state "CHANGES" as V_CHANGES #lightyellow +state "REJECT" as V_REJECT #lightcoral + +note "Approve - no concerns" as N_READY +V_READY .. N_READY + +note "Conditional approval" as N_CHANGES +V_CHANGES .. N_CHANGES + +note "Blocks progression" as N_REJECT +V_REJECT .. N_REJECT + +@enduml diff --git a/docs/diagrams/smarttools-pipeline-flow.puml b/docs/diagrams/smarttools-pipeline-flow.puml new file mode 100644 index 0000000..20e8ca5 --- /dev/null +++ b/docs/diagrams/smarttools-pipeline-flow.puml @@ -0,0 +1,130 @@ +@startuml SmartTools Pipeline Flow +!theme plain +skinparam activityBackgroundColor white + +title SmartTools Pipeline Data Flow + +start + +:Read discussion.md; +note right + **Input** + Discussion markdown + with metadata headers + and comment blocks +end note + +partition "Step 1: Parse" { + :discussion-parser; + note right + **Output: $parsed** + { + "metadata": {...}, + "comments": [...], + "vote_summary": {...} + } + end note +} + +if (Participants specified?) then (no) + partition "Step 2: Route" { + :discussion-mention-router; + note right + **Output: $routing** + { + "participants_to_call": [...], + "callouts": {...} + } + end note + } +else (yes) + :Use provided\nparticipants; +endif + +partition "Step 3: Call Participants" { + fork + :discussion-architect; + note right + stdin: $discussion + args: --callout, --templates-dir + end note + fork again + :discussion-security; + fork again + :discussion-pragmatist; + end fork + + :Collect $responses[]; + note right + Each response: + {"comment": "...", "vote": "..."} + or {"sentinel": "NO_RESPONSE"} + end note +} + +partition "Step 4: Append" { + :discussion-turn-appender; + note right + **Input** + $discussion + ---RESPONSES--- + $responses + + **Output** + Updated markdown with + new comment blocks + end note +} + +if (Phase has voting?) then (yes) + partition "Step 5: Count Votes" { + :Re-parse updated discussion; + :discussion-vote-counter; + note right + **Output: $votes** + { + "vote_summary": { + "READY": 2, + "CHANGES": 1, + "REJECT": 0 + }, + "consensus": { + "reached": false, + "reason": "..." + } + } + end note + } + + partition "Step 6: Promote Status" { + :discussion-status-promoter; + note right + **Output: $promotion** + { + "should_promote": true, + "new_status": "READY_FOR_DESIGN" + } + end note + } + + if (Should promote?) then (yes) + :Update status in\ndiscussion.md; + endif +endif + +:Return TurnResult; +note right + - responses[] + - vote_summary + - consensus_reached + - status_promoted +end note + +stop + +legend right + | Symbol | Meaning | + | Rectangle | SmartTool | + | Diamond | Condition | + | Fork | Parallel execution | +endlegend + +@enduml diff --git a/docs/diagrams/turn-execution-sequence.puml b/docs/diagrams/turn-execution-sequence.puml new file mode 100644 index 0000000..4e147ea --- /dev/null +++ b/docs/diagrams/turn-execution-sequence.puml @@ -0,0 +1,100 @@ +@startuml Turn Execution Sequence +!theme plain +skinparam sequenceMessageAlign center +skinparam responseMessageBelowArrow true + +title Discussion Turn Execution + +actor User +participant "CLI" as CLI +participant "Runner" as Runner +participant "discussion-parser" as Parser +participant "discussion-mention-router" as Router +participant "discussion-{participant}" as Participant +participant "discussion-turn-appender" as Appender +participant "discussion-vote-counter" as VoteCounter +participant "discussion-status-promoter" as Promoter +database "discussion.md" as File + +User -> CLI: discussions turn file.md @architect @security +activate CLI + +CLI -> Runner: run_pipeline_turn(path, participants) +activate Runner + +Runner -> File: read content +File --> Runner: markdown content + +== Step 1: Parse Discussion == + +Runner -> Parser: stdin: markdown content +activate Parser +Parser --> Runner: JSON (metadata, comments, votes) +deactivate Parser + +== Step 2: Route Mentions (if no participants specified) == + +alt participants not specified + Runner -> Router: stdin: parser JSON + activate Router + Router --> Runner: JSON (participants_to_call) + deactivate Router +end + +== Step 3: Call Participants (parallel) == + +par for each participant + Runner -> Participant: stdin: markdown, --callout + activate Participant + note right of Participant + AI generates response + based on discussion + and phase context + end note + Participant --> Runner: JSON {comment, vote} + deactivate Participant +end + +== Step 4: Append Responses == + +Runner -> Appender: stdin: markdown + responses JSON +activate Appender +Appender --> Runner: updated markdown +deactivate Appender + +Runner -> File: write updated content + +== Step 5: Count Votes (if voting phase) == + +alt phase has voting enabled + Runner -> Parser: stdin: updated markdown + activate Parser + Parser --> Runner: JSON + deactivate Parser + + Runner -> VoteCounter: stdin: parser JSON + activate VoteCounter + VoteCounter --> Runner: JSON (vote_summary, consensus) + deactivate VoteCounter +end + +== Step 6: Check Status Promotion == + +alt voting phase with consensus + Runner -> Promoter: stdin: vote-counter JSON + activate Promoter + Promoter --> Runner: JSON (should_promote, new_status) + deactivate Promoter + + opt should_promote = true + Runner -> File: update status header + end +end + +Runner --> CLI: TurnResult +deactivate Runner + +CLI --> User: Summary (responses, votes, consensus) +deactivate CLI + +@enduml diff --git a/examples/ai-enabled-home-lab-productivity-and-development-tool.discussion.md b/examples/ai-enabled-home-lab-productivity-and-development-tool.discussion.md new file mode 100644 index 0000000..4387437 --- /dev/null +++ b/examples/ai-enabled-home-lab-productivity-and-development-tool.discussion.md @@ -0,0 +1,776 @@ +<!-- DISCUSSION --> +<!-- Title: AI enabled home lab productivity and development tool --> +<!-- Phase: initial_feedback --> +<!-- Status: OPEN --> +<!-- Created: 2025-12-16T06:07:59Z --> +<!-- Template: brainstorm --> +<!-- Participants: architect, security, pragmatist --> + +# AI enabled home lab productivity and development tool + +## Context +- AI generated checkable todo lists with timelines from conversations +- periodic productivity auditing and sumaries +- periodic task re-evaluating and planning +- beggining of day conversations and task deligation as well as end of day check in and summarizing +- periodic non intrusive ai lead interviews to establish why goals were not met and use for input in future planning + +## Requirements + +- [ ] Must run locally in home lab (no cloud dependency) +- [ ] Conversation interface for daily standups and check-ins +- [ ] Persistent storage for tasks, goals, and progress history +- [ ] Scheduling system for periodic check-ins (daily, weekly) +- [ ] Non-intrusive notification system for reminders +- [ ] Export/summary generation (daily, weekly reports) + +## Open Questions + +- What AI model/provider should power the conversations? (local LLM vs API) +- How should tasks be stored? (markdown files, database, calendar integration) +- What platforms need support? (CLI, web UI, mobile notifications) +- How to handle multiple projects/contexts? +- What level of automation vs user prompting for check-ins? +- How to measure "productivity" - what metrics matter? + + +--- + +*Discussion begins below.* + +--- + +Name: AI-Architect +## Architectural Analysis\n\n### System Boundaries & Concerns\n\nThis proposal bundles several distinct capabilities that should be architecturally separated:\n\n1. **Task Management System** - CRUD operations, hierarchies, dependencies\n2. **Conversation Agent** - AI-powered standups and interviews\n3. **Analytics/Reporting Engine** - Summaries, productivity metrics\n4. **Scheduling/Notification Service** - Periodic triggers and reminders\n5. **Storage Layer** - Persistent state management\n\nARTIFACT: diagrams/homelab-arch.puml\n\n### Critical Architectural Decisions Needed\n\n**1. Data Model & Storage Strategy**\n\nThe proposal lacks clarity on how data flows and persists:\n- What's the schema for tasks vs goals vs journal entries vs metrics?\n- How do you handle task history/versioning (user might want undo/audit trail)?\n- How do conversations map to structured task updates?\n- Where's the single source of truth when AI suggests changes?\n\nRecommendation: Adopt **event sourcing** pattern. Store all interactions as events (task created, goal updated, standup completed), derive current state. This gives you audit trail, undo, and analytics for free.\n\n**2. AI Integration Architecture**\n\nLocal LLM vs API is a false dichotomy - you need abstraction:\n```\nConversation Interface\n ↓\nIntent Parser (deterministic)\n ↓\nAI Provider Abstraction Layer\n ↓ (strategy pattern)\n[Local LLM] [API Provider] [Hybrid]\n```\n\nStart with API (faster iteration), add local LLM support later. Don't let AI provider choice dictate your architecture.\n\n**3. Temporal Concerns**\n\nThis system is fundamentally about time-series data:\n- Goals set at T0 may be irrelevant at T30\n- \"Why goals weren't met\" requires comparing past intent with current reality\n- Productivity trends need historical context\n\nRecommendation: Model time explicitly. Each entity should be temporal (valid-from/valid-to). This prevents \"updating\" goals and losing historical context.\n\n**4. Integration Strategy**\n\nDon't build a walled garden. This should be a **productivity hub**, not a replacement for existing tools:\n- Import from/export to standard formats (iCal, Markdown, JSON)\n- Webhook/API support for external tools (GitHub, GitLab, Jira)\n- File-based storage enables git versioning and external processing\n\nRecommendation: Follow **Unix philosophy** - do one thing well, compose with other tools.\n\n### Architectural Risks\n\n**RISK: Scope Creep**\nThis description contains 6+ distinct products. Without clear boundaries, you'll build a monolith that does everything poorly.\n\n**RISK: AI Over-reliance**\nIf core functionality (task CRUD, scheduling) depends on AI availability, system becomes fragile. AI should augment, not replace, deterministic logic.\n\n**RISK: Local-First Constraints**\nLocal-only means:\n- No mobile push notifications (requires cloud relay)\n- No multi-device sync (without building sync infrastructure)\n- Harder integration with cloud tools (GitHub, etc.)\n\nConsider hybrid: core logic local, optional sync/notification services.\n\n**RISK: Privacy/Security Model Undefined**\nPersonal productivity data is sensitive. Needs:\n- Encryption at rest if using local LLM\n- Clear data retention policies\n- Audit logging for AI access to personal data\n\n### Recommended Architecture\n\n**Phase 1 Foundation:**\n1. File-based event store (append-only markdown or JSONLines)\n2. CLI for manual task management (prove data model works)\n3. Simple scheduler for periodic prompts (cron + notification)\n4. Markdown export (human-readable, git-friendly)\n\n**Phase 2 AI Layer:**\n1. Conversation parser (maps natural language → structured events)\n2. AI provider abstraction (start with API, add local later)\n3. Summarization service (reads event store, generates reports)\n\n**Phase 3 Intelligence:**\n1. Analytics engine (productivity metrics from events)\n2. Adaptive scheduling (learns optimal check-in times)\n3. Retrospective interviewer (compares goals vs outcomes)\n\n**Key Patterns to Apply:**\n- **Event Sourcing**: All changes as events, state is derived\n- **CQRS**: Separate write (AI conversations) from read (reports/queries)\n- **Strategy Pattern**: Pluggable AI providers, notification channels\n- **Repository Pattern**: Abstract storage (easy migration from files → DB later)\n\n### Open Questions for Next Phase\n\n1. **Who is the user?** Solo developer or team? Multi-tenant requirements?\n2. **What's the MVP?** Which of these 6 features is the actual核心 (core)?\n3. **What existing tools do you use?** This should integrate, not replace\n4. **What's your deployment model?** Docker, systemd service, cron jobs?\n5. **How do you handle schema evolution?** Data format will change - migration strategy?\n\n### Verdict\n\nThe concept is sound but architecturally under-specified. Before writing code:\n1. Define bounded contexts (separate task mgmt from analytics from AI)\n2. Design the data model (entity relationships, temporal aspects)\n3. Choose file format (Markdown? YAML? JSONLines?)\n4. Sketch the MVP (1-2 features max)\n\nThis prevents building a big ball of mud. +VOTE: CHANGES + +--- + +Name: AI-Security +## Security Analysis - AI-Enabled Home Lab Tool + +I've reviewed the proposal from a security perspective. Here are my findings: + +### Critical Security Considerations + +**CONCERN: Sensitive data exposure** - Daily conversations, goals, task histories, and "why goals weren't met" interviews will contain highly personal and potentially sensitive information about work patterns, mental state, and personal life. + +**CONCERN: Local LLM model security** - If using local models, need to verify model provenance and ensure no data exfiltration capabilities are embedded. + +**CONCERN: Authentication and access control** - No mention of how the system authenticates users or restricts access to personal productivity data. + +### Threat Model Overview + +I've created a threat model diagram showing the primary attack surfaces and trust boundaries. + +DIAGRAM: diagrams/ai-enabled-home-lab-productivity-and-development-tool_security_1.puml + +### Key Security Requirements + +**Q: How will conversation data be encrypted at rest?** - Given the personal nature of daily check-ins and retrospectives, encryption is essential. + +**Q: What authentication mechanism will protect the web UI (if implemented)?** - Home lab doesn't mean no authentication needed. + +**Q: How will the notification system be secured?** - Push notifications or external integrations could leak task/goal information. + +**TODO: Define data retention policy** - How long to keep conversation histories, and secure deletion procedures. + +**TODO: Implement audit logging** - Track who accessed what data and when, especially for sensitive retrospective interviews. + +**TODO: Establish backup security** - Encrypted backups of personal productivity data with secure key management. + +### Recommended Security Controls + +1. **Data Protection** + - Encrypt SQLite/database at rest (e.g., SQLCipher) + - If using markdown files, consider encrypted filesystem or file-level encryption + - Secure deletion for removed tasks/conversations + +2. **Access Control** + - Authentication even for local-only deployments (protect against other home lab services/users) + - Consider hardware token support (YubiKey) for high-value productivity data + - Session management with timeouts + +3. **Network Security** + - If web UI: HTTPS only (even localhost), strong CSP headers + - Rate limiting on API endpoints to prevent brute force + - No external network calls without explicit user consent and audit trail + +4. **Input Validation** + - Sanitize all user inputs to prevent injection attacks + - Validate task/goal formats before storage + - Prevent path traversal in export/summary generation + +5. **AI-Specific Risks** + - Prompt injection protection - users might unintentionally include malicious patterns + - Model output validation - ensure AI can't suggest executing arbitrary commands + - Context isolation between projects to prevent data leakage + +### Privacy Considerations + +**CONCERN: Interview data sensitivity** - "AI-led interviews to establish why goals weren't met" is psychologically sensitive data. This needs the highest protection level. + +Recommendation: Implement granular privacy controls allowing users to mark certain conversations as "extra sensitive" with enhanced encryption or opt-out from persistent storage. + +### Lower Priority Items + +- Dependency scanning for local LLM libraries +- Regular security updates for web framework (if used) +- Sandboxing for AI model execution +- Secure random number generation for session tokens + +### Verdict + +The concept is sound for a home lab project, but given the highly personal nature of productivity/goal data, security must be built in from the start, not added later. + +VOTE: CHANGES + +Required changes before proceeding: +1. Define authentication mechanism +2. Specify encryption approach for data at rest +3. Establish data retention and deletion policy +4. Address the "sensitive interview data" storage concern + +DIAGRAM: diagrams/ai-enabled-home-lab-productivity-and-development-tool_security_1.puml +VOTE: CHANGES + +--- + +Name: AI-Pragmatist +This proposal has classic over-engineering warning signs. Let me break down what we can ship NOW vs what we're dreaming about. + +## CONCERN: COMPLEXITY - Scope Creep Alert + +This isn't one project, it's 3-4 projects mashed together: +1. Task management system +2. AI conversation bot +3. Productivity analytics engine +4. Interview/retrospective system + +## MVP That Ships This Week + +Start with the SMALLEST thing that adds value: + +**Week 1 MVP: Daily Check-in Bot** +- Single markdown file for tasks (`tasks.md`) +- Two CLI commands: `homelab start-day` and `homelab end-day` +- AI reads tasks.md, asks 2-3 questions, appends today's plan +- End of day: quick review, mark done items, note blockers +- That's it. No scheduling, no analytics, no interviews. + +## What To Defer (Maybe Forever) + +**"Periodic productivity auditing"** - Who asked for this? Start with manual `homelab summary` command you run when YOU want it. + +**"Timelines from conversations"** - Just write tasks with optional due dates. Don't build a project management system. + +**"Non-intrusive AI lead interviews"** - This is session 10 material. You don't even know if daily check-ins work yet. + +**"Periodic task re-evaluating"** - Manual for now. Add automation after 2 weeks of actual usage. + +## Open Questions - Pragmatic Answers + +**Q: What AI model?** +A: Whatever you already have API keys for. Don't block on this. OpenAI API or Anthropic works fine. Local LLM setup is a 2-week rabbit hole. + +**Q: Storage?** +A: Markdown files in `~/.homelab/`. Grep-able, git-friendly, zero setup. Database is premature. + +**Q: Platform support?** +A: CLI only. Web UI is month 2. Mobile is month 6 if you're still using this. + +**Q: Multiple projects?** +A: One context. Add tags later if needed (#work, #personal). Don't build project management. + +**Q: Automation level?** +A: Zero automation. You type commands. Add cron jobs after proving you use it daily for a week. + +**Q: Productivity metrics?** +A: Count completed tasks. That's it. Fancy metrics come after you have 30 days of data. + +## Shipping Strategy + +1. **Day 1-2:** `homelab start-day` that reads tasks.md and asks "What's your focus today?" +2. **Day 3-4:** `homelab end-day` that reviews what you said you'd do +3. **Day 5:** Use it yourself for 3 days +4. **Week 2:** Add `homelab add-task` command +5. **Week 3:** Add `homelab summary --week` + +If you're not using it daily by week 2, the fancy features won't save it. + +## Reality Check + +The requirements list says "must run locally" but also implies complex scheduling and notifications. Pick one: +- **Simple:** CLI tool you run manually = ships fast +- **Complex:** Background service with notifications = 3x the work + +Start simple. Add complexity only when manual gets annoying. + +DIAGRAM: diagrams/ai-enabled-home-lab-productivity-and-development-tool_mvp_1.puml + +VOTE: CHANGES + +Simplify to daily check-in CLI tool. Defer analytics, automation, and interviews until you prove the core loop works. + +DIAGRAM: diagrams/ai-enabled-home-lab-productivity-and-development-tool_mvp_1.puml +VOTE: CHANGES + +--- + +Name: rob +Thanks for the thorough feedback. Here are my thoughts: + + ## Scope Decision + + Pragmatist is right - I'm describing 4 projects. Let me narrow to the core value proposition: + + **MVP Focus: Daily AI Check-in CLI** + - `homelab start-day` - Review yesterday, plan today + - `homelab end-day` - Quick reflection, capture blockers + - Simple markdown file storage + + I'll defer: analytics, automated scheduling, interviews, web UI, notifications. + + ## Addressing Architect's Concerns + + 1. **Data Model**: Starting with append-only markdown files. Each day gets a section. Git-friendly, grep-able, zero infrastructure. + + 2. **AI Provider**: Will use Claude API initially (already have access). Abstract behind a simple interface so I can swap later. + + 3. **Single user**: Just me, no multi-tenant complexity. + + ## Addressing Security's Concerns + + 1. **Encryption**: Home directory permissions + encrypted drive is sufficient for MVP. Not storing secrets, just task notes. + + 2. **Authentication**: CLI tool runs as my user. No web UI = no auth needed initially. + + 3. **Sensitive data**: Fair point about interview data. For MVP, I control what I type - will be mindful. + + ## Revised MVP Requirements + + - [ ] CLI tool with start-day and end-day commands + - [ ] Markdown file storage in ~/.homelab/ + - [ ] Claude API integration for conversation + - [ ] No scheduling, no automation, no web UI + + ## Open Questions Answered + + - **Platform**: CLI only + - **Storage**: Markdown files + - **AI**: Claude API + - **Metrics**: Count completed tasks (manual) + + Ready to move to sketching the implementation? +VOTE: READY + +--- + +Name: AI-Architect +## Architectural Review - Revised MVP + +### Excellent Scope Reduction + +The pivot to a focused CLI tool is architecturally sound. You've eliminated 80% of complexity while keeping the core value proposition. This is the right move. + +### Architecture Validation + +**Data Model: Markdown Files ✓** + +Append-only markdown in `~/.homelab/` is perfect for MVP: +- Git-friendly (automatic versioning and audit trail) +- Grep-able (ad-hoc queries without building a query engine) +- Human-readable (can edit manually if needed) +- Zero infrastructure overhead + +Suggested file structure: +``` +~/.homelab/ + ├── journal.md # Daily entries (append-only) + ├── tasks.md # Current active tasks + └── archive/ # Completed tasks (optional) + └── 2025-12.md +``` + +This gives you separation of concerns: journal is temporal, tasks.md is current state. + +**AI Provider Abstraction ✓** + +Starting with Claude API behind an interface is the right call. Minimal abstraction needed: +```python +# Simple provider interface +class ConversationProvider: + def chat(self, messages: list) -> str: + pass + +class ClaudeProvider(ConversationProvider): + def chat(self, messages: list) -> str: + # API call here + pass +``` + +Don't over-engineer this. Single method is enough for MVP. + +**Security Model: Sufficient ✓** + +For single-user CLI: +- Filesystem permissions (0600 on journal files) +- Encrypted drive (already have) +- No network exposure + +This is adequate. Don't add complexity you don't need. + +### Architectural Recommendations + +**1. Command Structure** + +Keep commands simple and composable: +```bash +homelab start-day # Interactive check-in +homelab end-day # Interactive reflection +homelab add-task # Quick task capture (optional) +homelab show # View current tasks +``` + +Follow CLI best practices: +- `--help` on all commands +- Exit codes (0 = success, non-zero = error) +- JSON output option (`--json`) for future scripting + +**2. Conversation Flow Architecture** + +``` +User runs command + ↓ +Read current state (tasks.md, journal.md) + ↓ +Build context for AI (recent entries + current tasks) + ↓ +AI conversation (2-3 exchanges max) + ↓ +Parse AI response for structured data + ↓ +Append to journal.md +Update tasks.md +``` + +Key: **Parse AI output for structure**. Don't just append freeform text. Extract: +- Tasks mentioned (parse "- [ ] task" format) +- Blockers (parse "BLOCKED: reason") +- Priorities (parse "PRIORITY: high/med/low") + +This makes future analytics trivial (just grep for patterns). + +**3. State Management** + +For MVP, two files hold all state: +- `journal.md`: **append-only**, timestamped entries +- `tasks.md`: **current state**, updated in-place + +This separation prevents journal pollution while keeping tasks easily editable. + +**4. Configuration** + +Simple YAML config in `~/.homelab/config.yaml`: +```yaml +ai: + provider: claude + api_key_env: ANTHROPIC_API_KEY # Don't store keys in config + model: claude-3-5-sonnet-20241022 + +paths: + journal: ~/.homelab/journal.md + tasks: ~/.homelab/tasks.md +``` + +Read from env vars for secrets. Never commit API keys. + +### Critical Design Decisions + +**DECISION: Journal Entry Format** + +Standardize on a format NOW to avoid migration pain: +```markdown +## 2025-12-16 Monday + +### Morning Check-in (08:30) +Focus: MVP for homelab tool +Planned: +- [ ] Design CLI architecture +- [ ] Set up project structure + +### Evening Reflection (18:00) +Completed: +- [x] Design CLI architecture +- [ ] Set up project structure (blocked: need to finalize storage format) + +Blockers: Deciding between JSON and markdown +Notes: Made good progress on design +``` + +Consistent structure = easy parsing later. + +**DECISION: AI Context Window Management** + +Don't send entire journal history to AI every time. Limit context: +- Last 3 days of journal entries +- Current tasks.md (all tasks) +- System prompt with personality/instructions + +This keeps tokens low and responses focused. + +**DECISION: Error Handling Strategy** + +For MVP, fail gracefully: +- API down? Degrade to manual entry (prompt user for text) +- Malformed markdown? Show warning but don't block +- Missing config? Use sensible defaults + +Robustness > perfection for a personal tool. + +### Implementation Phases + +**Phase 1: Scaffolding (Day 1)** +- CLI framework (argparse or click) +- Config loading +- File I/O (read/write markdown) +- Basic start-day command (no AI, just prompts) + +**Phase 2: AI Integration (Day 2-3)** +- Claude API client +- Conversation loop +- Response parsing +- Append to journal + +**Phase 3: Polish (Day 4-5)** +- end-day command +- Task extraction from conversations +- Help text and error messages +- Self-dogfood for 3 days + +### Risks & Mitigations + +**RISK: API Costs** +Mitigation: Set token limits, cache responses, use cheaper model (Haiku) for simple prompts. + +**RISK: Journal Size Growth** +Mitigation: Archive old entries monthly. Keep last 90 days in main journal. + +**RISK: AI Hallucinating Tasks** +Mitigation: Always show proposed changes before writing. Require user confirmation. + +### Testing Strategy + +For MVP: +- Manual testing (use it yourself daily) +- Unit tests for markdown parsing/writing +- Integration test with mocked AI responses + +No need for comprehensive test suite yet. Add tests when you find bugs. + +### Future Architecture Considerations + +**Later additions (if you actually use it daily):** +1. `homelab summary --week` - Read journal, generate summary +2. `homelab trends` - Parse task completion patterns +3. Local LLM support - Swap provider implementation +4. Export formats (PDF, HTML) - Read markdown, convert + +All of these are easy to add because of clean separation: +- Data layer (markdown files) +- AI layer (provider interface) +- CLI layer (commands) + +No refactoring needed, just new commands. + +### Verdict + +The revised MVP is **architecturally sound and shippable**. Key strengths: + +✓ Clear scope boundary (CLI only, single user) +✓ Simple data model (markdown files) +✓ Minimal abstractions (just enough for future flexibility) +✓ No premature optimization +✓ Fails gracefully (manual fallback if AI unavailable) + +One recommendation: **Add a `homelab init` command** that creates the directory structure and config template. This makes onboarding trivial. + +VOTE: READY +VOTE: READY + +--- + +Name: AI-Security +## Security Assessment - Revised MVP + +Good news: The scope reduction dramatically shrinks the attack surface. Here's my security analysis of the revised CLI-only MVP. + +### Security Posture: Much Improved + +The pivot to CLI-only with local markdown storage eliminates most of my previous concerns: +- ✅ No web UI = no CSRF, XSS, or session management issues +- ✅ No background service = no daemon security hardening needed +- ✅ No notifications = no external communication channels to secure +- ✅ Single user = no authorization complexity + +### Remaining Security Considerations + +**1. API Key Security** + +CONCERN: Claude API key storage - This is now the primary security risk. + +**TODO: Implement secure credential storage** +- Use system keyring (e.g., `keyring` Python library) rather than plaintext config file +- Never log API keys or include in markdown output +- Document key rotation procedure + +Example: Don't store in `~/.homelab/config.yaml`, use OS keychain. + +**2. Data Protection** + +The markdown files will contain: +- Daily work patterns +- Task priorities +- Blockers and frustrations +- Performance reflection + +Recommendation (not blocking): +- Ensure `~/.homelab/` has 0700 permissions (user-only) +- Consider adding `.homelab/` to backup encryption scope +- Document that git commits of this data should use private repos + +**3. AI Prompt Injection (Low Risk)** + +If you accidentally paste malicious content during check-ins, Claude's output could theoretically include harmful suggestions. + +Mitigation: Don't execute AI suggestions automatically. Since this is conversational CLI, you're the human in the loop - low risk. + +**4. Dependency Security** + +TODO: Pin dependencies and use virtual environment +- `requirements.txt` with exact versions +- Regular updates for the Anthropic SDK +- Consider `pip-audit` in your dev workflow + +### Low Priority / Future Considerations + +- Rate limiting Claude API calls (cost control, not security) +- Input sanitization if you add shell command execution features +- Audit logging if you later add web UI or sharing features + +### Verdict + +The revised MVP has a reasonable security posture for a personal CLI tool. The main risk is API key exposure. + +**Required for READY:** +- Confirm you'll use secure credential storage (keyring/keychain, not plaintext config) +- Set proper file permissions on ~/.homelab/ + +Once you confirm the API key handling approach, this is READY from a security perspective. +VOTE: CHANGES + +--- + +Name: AI-Pragmatist +Parse error +VOTE: NONE + +--- + +Name: AI-Architect +## Architectural Review - API Key Security + +Security raises a valid concern about credential management, but I want to provide architectural context on the tradeoffs. + +### Keyring vs Environment Variables vs Config File + +**Option 1: System Keyring** (Security's recommendation) +- Pros: OS-level encryption, no plaintext storage +- Cons: Adds dependency (`keyring` library), complicates deployment, harder to debug +- Complexity: Medium + +**Option 2: Environment Variable** (My recommendation for MVP) +- Pros: Standard practice, works everywhere, zero dependencies +- Cons: Visible in process list, shell history risk +- Complexity: Zero + +**Option 3: Config file with 0600 permissions** +- Pros: Simple, explicit, easy to manage +- Cons: Plaintext on disk +- Complexity: Minimal + +### Architectural Recommendation + +For a personal CLI tool running on an encrypted drive with filesystem permissions, **environment variables are sufficient**: + +```bash +# In ~/.bashrc or ~/.zshrc +export ANTHROPIC_API_KEY="sk-ant-..." +``` + +Config reads from env: +```python +import os +api_key = os.environ.get('ANTHROPIC_API_KEY') +if not api_key: + print("Error: Set ANTHROPIC_API_KEY environment variable") + sys.exit(1) +``` + +**Why this is architecturally sound:** +1. **Zero dependencies** - No keyring library, no OS-specific code +2. **Standard practice** - How AWS CLI, GitHub CLI, and most dev tools work +3. **Portable** - Works on Linux, macOS, Windows (WSL) +4. **Debuggable** - Clear error message if not set +5. **Secure enough** - On encrypted drive with user-only shell config + +### Security vs Pragmatism Balance + +Security's keyring recommendation is **architecturally over-engineered for this threat model**: + +- **Threat**: Someone with filesystem access reads API key +- **Reality**: If attacker has filesystem access to your home directory, they can: + - Read your SSH keys + - Access your browser session tokens + - Read the journal.md (which is the actual sensitive data) + +Defending the API key with keyring while leaving journal.md as plaintext markdown is **security theater**. + +### Proper Threat Modeling + +The real risks, in order: +1. **Journal data exposure** - Contains personal productivity patterns +2. **API key exposure** - Could run up charges or access Claude +3. **Dependency vulnerabilities** - Outdated packages + +**Mitigation strategy:** +1. Filesystem permissions (0700 on ~/.homelab/) +2. Encrypted drive (already have) +3. Environment variable for API key +4. Dependabot/pip-audit for dependencies + +This provides defense-in-depth without unnecessary complexity. + +### Alternative: Hybrid Approach + +If you want to satisfy Security's concern without adding dependencies: + +```python +# Try env var first, fall back to config with warning +api_key = os.environ.get('ANTHROPIC_API_KEY') +if not api_key: + # Read from config but warn + api_key = config.get('ai', {}).get('api_key') + if api_key: + print("WARNING: API key in config file. Use ANTHROPIC_API_KEY env var instead.") +``` + +This educates users toward better practice without blocking functionality. + +### Decision: Defer to Implementation Phase + +This is an implementation detail, not an architectural blocker. Both approaches work: + +- **MVP (Week 1)**: Environment variable (ships fastest) +- **Hardening (Week 4)**: Add keyring support as optional enhancement + +Don't block on this. The architecture supports either approach via the provider interface. + +### File Permissions - Agreed + +Security's recommendation for 0700 permissions is correct and trivial: + +```python +import os +homelab_dir = os.path.expanduser('~/.homelab') +os.makedirs(homelab_dir, mode=0o700, exist_ok=True) +``` + +This should be in the `homelab init` command I previously recommended. + +### Verdict + +The credential storage approach doesn't affect the core architecture. Start with environment variables (standard practice), add keyring later if needed. + +The file permissions requirement is valid and easy to implement. + +**DECISION: Use environment variable for API key in MVP, document keyring migration path.** + +VOTE: READY - This is an implementation detail, not an architectural concern. The provider abstraction supports any credential source. +VOTE: READY + +--- + +Name: AI-Security +## Security Status: Waiting on Credential Storage Confirmation\n\nStill awaiting confirmation from rob on the API key storage approach.\n\n### Blocking Issue\n\nThe current proposal mentions storing API keys in environment variables (`ANTHROPIC_API_KEY`), which is better than plaintext config files, but I need explicit confirmation of the implementation approach:\n\n**Q: Will you use OS keychain/keyring for API key storage, or rely solely on environment variables?**\n\nEnvironment variables are acceptable for MVP if:\n- Set in shell profile (not committed to git)\n- Protected with proper file permissions (0600)\n- Never logged or echoed\n\nPreferred approach:\n- Use Python `keyring` library to store in OS keychain\n- Prompt for API key on first run: `homelab init --api-key`\n- Retrieve from keychain on subsequent runs\n\n### Additional Security Requirement\n\nTODO: Document in README that `~/.homelab/` should be excluded from backups to cloud services, or ensure backups are encrypted.\n\n### File Permissions\n\nThe `homelab init` command (as Architect suggested) should automatically:\n```bash\nmkdir -p ~/.homelab\nchmod 700 ~/.homelab\ntouch ~/.homelab/journal.md ~/.homelab/tasks.md\nchmod 600 ~/.homelab/*.md\n```\n\nThis ensures no other users on the system can read your productivity data.\n\n### Verdict\n\nOnce you confirm the API key handling approach (environment variables with proper precautions OR keyring), I'll vote READY. The rest of the security posture is acceptable for a personal CLI tool.\n\nCONCERN: API key storage method not yet confirmed +VOTE: CHANGES + +--- + +Name: AI-Pragmatist +## Security Response - API Key Storage + +Good question on the API key. Here's the pragmatic answer: + +**MVP Approach: Environment Variable** + +For shipping this week: +```bash +export ANTHROPIC_API_KEY="sk-..." +homelab start-day +``` + +Add to your `.bashrc` or `.zshrc`. Done. Ships today. + +**Why Not OS Keychain?** + +Security suggested `keyring` library. That's adding: +- New dependency +- Platform-specific code (macOS Keychain vs Linux Secret Service vs Windows Credential Locker) +- Setup complexity for a single secret +- Debugging pain when keyring daemon isn't running + +For a personal CLI tool, env vars are fine. You already protect your shell config. + +**File Permissions: Already Handled** + +If you're on Linux/macOS, `~/.homelab/` gets your user's default permissions (typically 755 or 700). Explicitly setting 0700 is one line: + +```python +import os +config_dir = os.path.expanduser('~/.homelab') +os.makedirs(config_dir, mode=0o700, exist_ok=True) +``` + +Add this to `homelab init`. Takes 30 seconds. + +**Dependency Pinning: Yes, But Later** + +```bash +pip freeze > requirements.txt +``` + +Run this after you get it working. Don't block on it now. + +**Reality Check** + +This tool accesses: +- Your markdown task notes (already on your encrypted drive) +- Claude API (HTTPS, already secure) +- No external services, no network exposure, no user data + +The threat model is "someone with access to your laptop reads your todo list." If that happens, you have bigger problems than this tool. + +## Verdict + +Security's concerns are valid but don't block shipping: +- ✅ Use env var for API key (add to shell config) +- ✅ Set 0700 on ~/.homelab/ in init command +- ✅ Pin dependencies after first working version + +This is a personal productivity tool, not a bank. Ship it, use it for a week, then harden if you're still using it. + +VOTE: READY + +The MVP is simple, scoped, and secure enough for personal use. Let's build it. +VOTE: READY diff --git a/examples/brainstorm_notification_system.discussion.md b/examples/brainstorm_notification_system.discussion.md new file mode 100644 index 0000000..340ff62 --- /dev/null +++ b/examples/brainstorm_notification_system.discussion.md @@ -0,0 +1,703 @@ +<!-- DISCUSSION --> +<!-- Title: Notification System Brainstorm --> +<!-- Phase: decide --> +<!-- Status: READY_FOR_DESIGN --> +<!-- Template: brainstorm --> +<!-- Created: 2025-12-10T10:00:00Z --> +<!-- Participants: architect, security, pragmatist, moderator --> + +# Notification System Brainstorm + +## Problem Statement + +Our application needs a way to notify users about important events (new messages, status changes, alerts). Currently we have no notification system at all. + +## Known Constraints + +- Small team (2 developers) +- Need something working within 2 weeks +- Must work on web and mobile +- Budget: minimal (prefer open source) +- Current stack: Python backend, React frontend + +## What Success Looks Like + +- Users get timely notifications about events they care about +- Users can control what notifications they receive +- System is reliable enough that important notifications aren't lost + +--- + +*Discussion begins below. We're in the seed phase - clarifying questions only, no solutions yet.* +--- + +Name: AI-Architect +As an architect, I see the problem statement is clear, but we need to sharpen understanding before diverging into solutions. + +**Problem Frame:** You're building a notification system where users receive timely alerts about events they care about, with control over preferences. This is solid. + +**Critical Clarifications Needed:** + +1. **Event Sources** - What systems/services generate these events? Are they: + - All from your Python backend, or distributed across multiple services? + - Real-time (WebSocket-like latency), or can they batch (minute/hour scale)? + - Sporadic or high-volume (100/sec, 10k/sec, 1M/sec)? + +2. **Notification Delivery Priority** - Which matters most: + - Speed (sub-second latency)? + - Guaranteed delivery (never lose a notification)? + - Notification ordering (maintain sequence)? + - Cost efficiency? + +3. **User Control Granularity** - What level of preference control: + - Simple mute all/all-on, or per-event-type? + - Do mobile/web notifications differ? + - Time-based (quiet hours)? + - Batching (weekly digest vs instant)? + +4. **Mobile Delivery** - Using: + - Push notifications (native iOS/Android)? + - In-app only? + - Email as fallback? + - Which push provider (Firebase, OneSignal, in-house)? + +5. **Data Retention** - How long keep notification history: + - Show last 30 days in UI? + - Permanent archive? + - Just transient (server doesn't store)? + +6. **Team Context** - Two developers suggests: + - Pick existing service (Firebase, Pusher, Ably) over building? + - Or do you need full control/custom logic? + +These answers will shape whether this is a thin orchestration layer, a message queue system, or a full event-driven platform. + +--- + +Name: AI-Security +**Problem Statement (Security Framing):** +A notification system introduces several attack surfaces that need consideration early: +- User event subscriptions and filtering +- Message delivery channels and transport +- User data exposure (who notifies whom) +- System reliability under attack + +**Clarifying Questions for the Seed Phase:** + +1. **Scope of events**: What types of events will trigger notifications? (User actions, system alerts, third-party integrations?) This affects authorization requirements. + +2. **Data sensitivity**: Do notifications contain sensitive data, or just pointers to resources? (e.g., 'New message from Alice' vs full message content) + +3. **Delivery channels**: Beyond web/mobile - are you considering email, SMS, push notifications? Each has different security properties. + +4. **User consent model**: How do users express notification preferences? Can they be manipulated into unwanted notifications? + +5. **Authentication scope**: Can users subscribe others to notifications, or only themselves? Prevents griefing/harassment attacks. + +6. **Reliability guarantees**: Does 'important notifications aren't lost' mean at-least-once delivery? This affects retry logic and duplicate handling. + +7. **Access control**: Who can see the notification audit trail? Notification history could leak sensitive information about user activity. + +8. **Rate limiting**: Will there be protection against notification spam or DoS (millions of events triggering millions of notifications)? + +These questions help frame what security properties are actually required before diving into architecture. + +--- + +Name: AI-Pragmatist +Problem is clear and constraints are realistic. Two weeks is tight but doable with an MVP approach. + +Clarifying questions before we scope this: + +1. **Event volume**: Are we talking 10s of notifications per user per day, or 100s? This affects whether we need async queues vs simple polling. + +2. **Delivery model**: Do users need real-time push (within seconds), or is delayed delivery (minutes) acceptable for MVP? This changes architecture dramatically. + +3. **User preferences**: When you say "control what notifications they receive" - is this simple on/off toggles, or complex filtering rules? Start with simple? + +4. **Persistence requirements**: If notification delivery fails, do we retry forever or is best-effort acceptable? + +5. **Current auth/user model**: Do we already have user accounts and a database? This affects whether we can ship in 2 weeks. + +The 2-week timeline suggests we should be ruthless about MVP scope. Most notification systems start too complex. + +--- + +Name: Human (Rob) +Thanks for the questions. Here are answers to help scope this: + +**Event sources:** All from our Python backend. Single service. Low volume - maybe 50-100 notifications per user per day max. Not real-time critical - within 30 seconds is fine. + +**Delivery priority:** Best-effort is acceptable for MVP. We can add guaranteed delivery later. Ordering doesn't matter. + +**User control:** Simple on/off toggles per event type (messages, status, alerts). No quiet hours or digests for MVP. + +**Mobile:** Web first. Mobile can come in phase 2. No email/SMS for now. + +**Data sensitivity:** Notifications are pointers only ("You have a new message from Alice") - no sensitive content in the notification itself. + +**Persistence:** Keep last 7 days of notification history. Best-effort delivery, no retries for MVP. + +**Existing infrastructure:** Yes, we have users table in Postgres, JWT auth already working. + +**Rate limiting:** Good point - let's cap at 100 notifications per user per hour. + +Let's move to generating ideas. +--- + +Name: AI-Architect +Great scoping, Rob. Now we have concrete constraints - let me think architecturally about what this enables. + +Phase note: I'm diverging here, building on what's been clarified. All ideas welcome. + +**Architectural possibilities:** + +1. **Notification event table + polling worker** + - New events insert into `notifications` table + - Simple background worker polls every 5-10 seconds, pushes to connected clients + - WebSocket for web clients, HTTP polling fallback + - Dead simple to understand and debug + +2. **In-database event queue** + - Each event creates a notification record marked undelivered + - Worker processes batch, marks delivered + - 7-day TTL on records (database cleanup) + - No external message queue needed + +3. **Redis pubsub + in-memory subscriptions** + - Redis channels per user + - Connected clients subscribe + - Events publish directly + - Notification history still lives in Postgres (separate concern) + +4. **Event sourcing light** + - Events table is append-only event log + - Read models (materialized views) project user notification state + - Gives us audit trail + flexibility later + +5. **Preference engine as middleware** + - Event dispatcher checks user preferences before creating notification + - Centralized logic: "should user X get notified about Y?" + - Scales horizontally because it's stateless + +6. **Notification aggregation for bursts** + - If 10 events hit same user in 30 seconds, could batch into one notification + - Single notification instead of spam + - Rate limiting sits here + +7. **Background job framework (Celery/RQ)** + - Overkill for MVP but patterns are cleaner + - Event triggers async job + - Job handles preference check + delivery + - Decouples event source from notification logic + +8. **Hybrid: sync writes + async delivery** + - Event writes notification record synchronously + - Async worker handles preference filtering + client delivery + - If worker dies, notifications still in database + +**Cross-cutting thoughts:** +- All approaches need preference checking (user toggled notifications?) +- All need delivery mechanism (WebSocket, polling, or push) +- All need 7-day cleanup strategy +- User preferences table is critical - schema choice matters +- Test approach: can we mock/fake notification delivery easily? + +**Scalability thinking (2-5 year horizon):** +- Approaches 1-2 (database-centric) hit limits around 1M+ notifications/day +- Approaches 3-5 scale to 10M+ with minor tweaks +- Approach 7 is designed for scale from day one +- But for 50-100 notifications/user/day with small user base? All fine + +Let's diverge - what other patterns am I missing? + +--- + +Name: AI-Security +Security attack surface brainstorm for MVP: + +**Diverge ideas (quantity, no judgment):** + +- IDEA: Notification history as audit log - users can see "what happened to me" timeline (timestamps, event types). Helps detect compromised accounts. + +- IDEA: Subscription tampering - what if attacker subscribes victim to spam event types? Need validation that preferences changes are by the account owner (not another user). + +- IDEA: Notification injection attacks - if backend generates notifications from user input, could craft malicious event payloads. Validate event schema strictly. + +- IDEA: Rate limit bypass - 100/hour per user is good, but what about per-event-type? Could someone spam one event type to drown out others? + +- IDEA: Notification delivery channel poisoning - if web UI polls for notifications, attacker could inject fake ones via CSRF. Validate responses are from trusted backend. + +- IDEA: Historical data leakage - 7-day history exposed in API? Users could enumerate other users' activity patterns. Add granular permissions. + +- IDEA: WebSocket for real-time - simpler than polling, but introduces new auth challenges (JWT refresh on socket?). Worth considering for future phases. + +- IDEA: Event type enumeration - if we expose list of all possible event types, attacker learns business logic. Could keep enum private to frontend. + +- IDEA: Distributed denial of service - if one notification triggers cascading notifications (event loops), could amplify attacks. Prevent self-referential triggers. + +- IDEA: Encryption of notification metadata - pointers are low-sensitivity, but encrypting with user's key prevents admin snooping on who notifies whom. + +No blocking issues yet - just surface area to be aware of. + +--- + +Name: AI-Pragmatist +MVP-first ideas for shipping in 2 weeks: + +**Idea 1: Synchronous Insert + Frontend Polling** +When events happen in backend, insert into notifications table. Frontend polls `/api/notifications` every 5 seconds. Dead simple, ships day 1. +- Pro: No infrastructure, zero latency concerns, easy to debug +- Con: Polling overhead, but at 50-100/day per user it's fine +- DB schema: id, user_id, event_type, content, created_at, read_at +- Rate limiting: Just check created_at in last hour before insert + +**Idea 2: WebSocket for Real-Time + Polling Fallback** +Add websocket connection for live push, but keep polling as fallback for unreliable networks. Only use WebSocket for active users. +- Pro: Feels snappy, mobile-friendly when we add it +- Con: More moving parts, but we get it free if we're already using WebSockets +- Question: Do we have WebSockets already? + +**Idea 3: Simple Event Log + Background Batch Job** +Log all events in a single events table. Nightly cron job processes them into user-specific notifications based on preferences. +- Pro: Decoupled, easy to test, can retry silently +- Con: Not real-time, but 30sec requirement is met if cron runs every 15 seconds + +**Idea 4: Hybrid - Immediate Insert + Smart Fetch** +Insert notification immediately when event fires. On React mount, `/api/notifications?since=X` returns unread. No polling for active users - just load on mount. +- Pro: Works offline, catch-up friendly, minimal network overhead +- Con: Slightly more frontend logic + +**Real Talk:** Ideas 1 or 4 are ship-ready in week 1. Don't start with WebSockets unless you have them already. Polling at 50-100 notifs/day is unnoticeable load. Rate limiting is just a SQL WHERE clause. Preferences table: user_id, event_type, enabled. Done. +--- + +Name: AI-Moderator +## Discussion State Summary + +We've successfully moved through the **seed phase** with rich clarification from all participants: + +### What's Been Established +- **Event sources**: Single Python backend, low volume (50-100 notifications/user/day) +- **Delivery**: Best-effort within 30 seconds (no real-time requirement) +- **User control**: Simple on/off toggles per event type +- **Scope**: Web first, mobile deferred, no email/SMS +- **Data model**: Notification pointers only (no sensitive content) +- **Infrastructure**: Existing Postgres + JWT auth +- **Retention**: 7-day history, rate limit 100/hour per user + +### Ideas Generated + +**Architectural patterns explored:** +Architect proposed 8 approaches (polling worker, in-database queue, Redis pubsub, event sourcing, preference middleware, aggregation, background jobs, sync+async hybrid). All are viable at current scale; database-centric approaches (1-2) are simplest for MVP. + +**Security surface identified:** +Security raised 9 attack vectors (audit logs, subscription tampering, injection attacks, rate limit bypass, CSRF, data leakage, WebSocket auth, event enumeration, cascading events, encryption). No blocking issues - all are architectural considerations. + +**MVP pragmatism:** +Pragmatist outlined 4 implementation paths, with Ideas 1 (sync insert + polling) and 4 (immediate insert + smart fetch) as ship-ready week 1 candidates. Strong emphasis on ruthless scope: preferences table (user_id, event_type, enabled), simple rate limiting via SQL. + +### Cluster Phase Analysis + +The cluster phase asks us to: **Group related ideas into 3-5 themes and name each clearly.** + +I see these natural groupings emerging: + +1. **Delivery Mechanism** (How do notifications reach users?) + - Polling (Ideas 1, 4 from Pragmatist) + - WebSocket + fallback (Idea 2) + - In-database queue (Architect approach 2) + +2. **Data Flow & Persistence** (Where does notification state live?) + - Synchronous table inserts (Ideas 1, 4) + - Background batch processing (Idea 3) + - Event sourcing / append-only logs (Architect approach 4) + +3. **Preference & Rate Limiting** (When should notifications be suppressed?) + - Pre-insertion filtering (Architect approach 5) + - Post-fetch filtering (Frontend responsibility) + - Per-event-type limits (Security concern) + +4. **Security & Reliability** (Protection against attack and data loss?) + - Injection validation, subscription tampering prevention + - Historical audit trails (Security idea 1) + - Self-referential event loop prevention + - Encryption options (Security idea 10) + +5. **Scale & Complexity** (What's the growth path?) + - MVP minimal (Ideas 1, 4) → 1M+ notifications/day + - Infrastructure-ready from day 1 (Approaches 3, 5, 7) + - Decoupled patterns (Idea 3) for future evolution + +### Readiness for Sketch Phase + +**All participants have responded.** The discussion has moved naturally from clarification → divergence → synthesis. We have: +- Concrete constraints and success criteria +- Multiple viable approaches with tradeoffs articulated +- Security surface mapped (not blocking) +- MVP path identified (Ideas 1 or 4 in week 1) + +**Recommendation**: Advance to **sketch phase** to: +1. Choose primary delivery mechanism (consensus on polling vs WebSocket vs batch) +2. Finalize data model (schema for notifications, preferences, rate limits) +3. Sketch API contract (`GET /notifications`, `PUT /preferences`) +4. Map security mitigations to chosen approach +5. Create rough implementation timeline + +The ideas are divergent enough; clustering them now helps the sketch phase focus on concrete design. + + +--- + +Name: Human (Rob) +Great synthesis! For MVP, let's focus on: + +1. **Delivery Mechanism** - Polling approach (Ideas 1/4) - simplest for 2 weeks +3. **Preference & Rate Limiting** - Pre-insertion filtering +4. **Security & Reliability** - Basic protections (no encryption needed for MVP) + +Skip event sourcing and complex WebSocket for now. Let's sketch this out. + +VOTE: READY +--- + +Name: AI-Architect +**Architectural Sketch Complete** + +Created two diagrams capturing the MVP polling approach: + +**Component Architecture** (`diagrams/notification-system-brainstorm_6.puml`): +- Shows Python backend, React frontend, PostgreSQL components +- Highlights 5 key pieces: Notification Service, Preference Checker, Rate Limiter, Frontend Poller, Notifications Table +- Maps relationships and data flows + +**Sequence Flow** (`diagrams/notification-system-brainstorm_7.puml`): +- Details 5 flows: notification creation, frontend polling, user interaction, preference management, cleanup +- Shows decision points and error paths +- Marks unknowns around polling optimization and error handling + +**Key Insights:** +1. Database-centric approach fits 2-week timeline +2. Polling at 5-10s intervals meets 30s requirement +3. Rate limiting via SQL counting is simple but effective +4. 7-day TTL cleanup prevents unbounded growth +5. Preference checking before insert reduces unnecessary notifications + +**Architectural Assessment:** This is a boring, proven approach that will ship in 2 weeks. It's not sexy but it's maintainable by a small team. The patterns (polling, SQL rate limiting, preference filtering) are well-understood and debuggable. + +**VOTE:** READY - The sketch captures the essence of the MVP approach with clear boundaries and growth paths. + +VOTE: READY + +--- + +Name: AI-Security +Security analysis for sketch phase: + +**Key Security Pieces for MVP (3-5 items):** +1. **Authentication boundary** - JWT validation for notification API endpoints +2. **Authorization checks** - Users can only access/modify their own notifications and preferences +3. **Input validation** - Event type whitelist, content sanitization for notification pointers +4. **Rate limiting** - 100/hour per user at application layer (not just SQL) +5. **CSRF protection** - For preference updates (POST/PUT endpoints) + +**Threat Model Unknowns (?):** +- Event loop detection - how to prevent self-referential notifications? +- Admin access controls - who can see notification audit logs? +- Data retention compliance - 7-day cleanup mechanism security + +**Security Architecture Notes:** +- Polling approach reduces WebSocket auth complexity +- Pre-insertion filtering prevents unwanted notifications at source +- Notification pointers (not content) reduces data exposure risk +- Basic protections align with 2-week MVP timeline + +VOTE: READY + +--- + +Name: AI-Pragmatist +Perfect MVP direction. Polling + pre-insertion filtering is exactly what ships in 2 weeks. + +**Key pieces for MVP:** +1. Notifications table (id, user_id, event_type, content, created_at, read_at) +2. Preferences table (user_id, event_type, enabled) +3. API endpoint: GET /api/notifications?since=X +4. Rate limiting: SQL check before insert +5. 7-day cleanup cron job + +**Unknowns marked '?':** +- Frontend polling interval? (Start with 10s) +- How to handle offline users? (Catch-up on mount) +- WebSocket migration path? (Add later if needed) + +CONCERN: COMPLEXITY - Architect's 8 approaches are overkill. Stick to simple polling. + +VOTE: READY +--- + +Name: AI-Architect +**MVP Version (1 week max):** +1. **Core tables**: notifications (id, user_id, event_type, content, created_at, read_at), user_preferences (user_id, event_type, enabled) +2. **Notification creation**: Synchronous insert with preference check + SQL rate limiting +3. **Frontend polling**: GET /api/notifications?since=X every 10s +4. **Cleanup**: Daily cron deleting >7 days +5. **API endpoints**: GET notifications, POST read, PUT preferences + +**Biggest Risk**: Polling scalability if user base grows 10x+ unexpectedly. SQL counting for rate limits could become bottleneck. + +**Prototype First**: Notification creation flow (Event → Preference check → Rate limit → Insert). Test with 1000 simulated events. + +**What to Cut**: WebSocket, notification aggregation, encryption, complex preference rules, mobile push, email/SMS fallback. + +CONCERN: COMPLEXITY - The 8 architectural approaches from earlier are overkill. Stick to simple polling + SQL. + +**Architectural Assessment**: This is a boring, maintainable approach that ships in 2 weeks. The patterns are well-understood and debuggable by a small team. Growth path: add WebSocket layer later if needed. + +VOTE: READY + +--- + +Name: AI-Security +**MVP Security Reality Check** + +**1-week MVP (Security-focused):** +- Basic JWT auth on API endpoints (already exists) +- User-specific authorization (only access own notifications) +- SQL injection prevention via parameterized queries +- Rate limiting at app layer (not just SQL) +- CSRF protection for preference updates + +**Biggest Risk:** Notification injection via event payloads. If events come from user input (e.g., 'message from Alice'), attacker could craft malicious notification content. + +**Prototype First:** Notification creation flow with input validation. Whitelist event types, sanitize content strings, test injection attempts. + +**What to Cut:** Encryption, audit logs, admin controls, WebSocket auth complexity. These are phase 2. + +CONCERN: COMPLEXITY - Architect's diagrams show 5 components but MVP needs just 2: NotificationService (create/read) and PreferenceChecker (filter). Rate limiting can be inline. + +**Security MVP:** Validate event types, sanitize content, enforce user boundaries, rate limit at app layer. That's 2-3 days of work. + +VOTE: READY + +--- + +Name: AI-Pragmatist +Perfect MVP direction. The polling approach is exactly what ships in 2 weeks. + +**MVP Version (1 week max):** +1. Notifications table (id, user_id, event_type, content, created_at, read_at) +2. Preferences table (user_id, event_type, enabled) +3. API endpoint: GET /api/notifications?since=X (poll every 10s) +4. Rate limiting: SQL check before insert +5. 7-day cleanup cron job + +**Biggest Risk:** Polling overhead at scale, but at 50-100 notifications/user/day it's negligible. + +**Prototype First:** Build the notifications table and polling endpoint. Test with 2-3 event types. + +**What We Can Cut:** WebSockets, encryption, complex invalidation, mobile push, email/SMS, quiet hours, digests, event sourcing. + +CONCERN: COMPLEXITY - Architect's 8 approaches are overkill. Stick to simple polling. + +**Shipping Priority:** Week 1: Tables + polling. Week 2: Preferences UI + cleanup job. + +VOTE: READY +--- + +Name: AI-Architect +**Architectural Assessment:** The MVP polling approach is architecturally sound for the 2-week timeline. Database-centric design with synchronous inserts and frontend polling meets all constraints (low volume, 30s latency, simple preferences). + +**Consensus Analysis:** Current votes: Human (Rob) READY, AI-Architect READY, AI-Security READY, AI-Pragmatist READY. All participants agree on the polling approach. No REJECT votes, no CHANGES votes. + +**Template Requirements:** Brainstorm template requires 0.67 threshold (2/3) and human approval. Both conditions are met with 4/4 READY votes and Human (Rob) READY. + +**Architectural Validation:** The approach is boring, proven, and maintainable by a small team. Growth path exists (add WebSocket layer later). Risk: polling scalability at 10x+ user growth, but acceptable for MVP. + +**VOTE:** READY - Consensus reached, approach is architecturally appropriate for constraints. + +VOTE: READY + +--- + +Name: AI-Security +Security assessment complete for the decide phase. + +**Threat Model Analysis:** Created diagram `diagrams/notification-system-brainstorm_security_14.puml` showing MVP security boundaries. + +**Security MVP Scope Assessment:** +✅ **Covered:** JWT auth, user-specific authorization, input validation (whitelist + sanitization), rate limiting (app + SQL layer), CSRF protection. +⚠️ **Deferred (Phase 2):** Encryption at rest, audit logging, admin controls, WebSocket auth, advanced DoS protection. + +**Risk Level:** LOW - MVP scope is security-minimal and well-contained for 2-week timeline. + +**Consensus Status:** All participants (Human, Architect, Security, Pragmatist) have voted READY. + +**Security Vote:** No blocking security issues. MVP polling approach is secure enough for immediate implementation. + +**Final Security Position:** The approach balances security requirements with development constraints. Basic protections are in place, deferred items are appropriate for phase 2. + +VOTE: READY + +--- + +Name: AI-Pragmatist +Perfect MVP approach. The polling + SQL solution ships in 2 weeks with minimal complexity. + +**Shipping Reality Check:** +1. Week 1: Notifications table + polling endpoint (2 days) +2. Week 1: Preference checking + rate limiting (1 day) +3. Week 2: Frontend UI + cleanup job (2 days) +4. Week 2: Testing + deployment (1 day) + +**What We're Cutting:** WebSockets, encryption, audit logs, mobile push, email/SMS, complex invalidation, event sourcing. + +**Simple Wins:** +- Polling at 10s meets 30s requirement +- SQL rate limiting works at 50-100 notifications/day +- 7-day cleanup prevents unbounded growth +- Preference checking before insert reduces noise + +CONCERN: COMPLEXITY - Architect's 5-component diagram is overkill. MVP needs just 2: NotificationService (create/read) and PreferenceChecker (filter). Rate limiting is a SQL WHERE clause. + +VOTE: READY + + +--- + +Name: Human (Rob) +All participants have voted READY. The MVP approach is clear: + +**Approved Design:** +- Polling-based notification delivery (10s interval) +- Two tables: notifications, user_preferences +- Pre-insertion filtering with rate limiting (100/hour/user) +- 7-day history with daily cleanup +- Web first, mobile later + +**Implementation Timeline:** +- Week 1: Schema + API endpoints + polling +- Week 2: Preferences UI + cleanup job + testing + +This is a solid, boring, maintainable approach. Let's build it. + +VOTE: READY + +--- + +Name: rob +Rob +I am happy + +--- + +Name: AI-Architect +**Architectural Perspective: Decide Phase** + +The discussion has reached unanimous consensus (5/5 READY votes including human approval). From an architectural standpoint: + +**What's Been Validated:** +- Database-centric polling architecture is appropriate for the scale (50-100 notifications/user/day) +- Two-table schema (notifications + user_preferences) is minimal and correct +- Pre-insertion filtering with SQL-based rate limiting is simple and maintainable +- 7-day retention with cron cleanup prevents unbounded growth +- Frontend polling at 10s intervals meets 30s latency requirement with margin + +**Architectural Soundness:** +This is a textbook "boring technology" choice - proven patterns, no premature optimization, debuggable by a 2-person team. The approach avoids: +- Infrastructure complexity (no message queues) +- Concurrency challenges (no WebSockets for MVP) +- Operational overhead (no distributed systems) + +**Growth Path Preserved:** +The design doesn't paint us into a corner: +- WebSocket layer can wrap existing polling API later +- Message queue can slot in behind NotificationService +- Event sourcing can replace direct inserts without changing API contract + +**Risk Assessment:** +Single point of concern (polling scalability at 10x+ growth) is acceptable given: +- Current constraints are well-defined +- Migration path exists +- Team can monitor and adapt + +**Architectural Decision:** PROCEED with polling-based MVP. The approach is appropriately scoped, technically sound, and maintainable. + +**Rob's statement "I am happy" confirms human approval.** +VOTE: READY + +--- + +Name: AI-Security +**Security Perspective: Decide Phase - Final Assessment** + +The discussion has reached unanimous consensus with all security concerns appropriately scoped for MVP. + +**Security Posture Validated:** +✅ Authentication/Authorization - JWT validation + user-specific access controls +✅ Input Validation - Event type whitelist + content sanitization +✅ Rate Limiting - Application layer enforcement (100/hour/user) +✅ CSRF Protection - For preference update endpoints +✅ SQL Injection Prevention - Parameterized queries + +**Threat Surface Assessment:** +The polling-based approach actually reduces attack surface compared to WebSocket alternatives: +- No persistent connection state to hijack +- Simpler authentication flow (standard JWT on each request) +- Reduced DoS vectors (no connection flooding) +- Standard HTTP security controls apply + +**Deferred Items (Appropriate for Phase 2):** +- Encryption at rest (low sensitivity data - pointers only) +- Comprehensive audit logging +- Admin access controls +- Advanced DoS protection beyond rate limiting +- Event loop detection (low risk at current scale) + +**Risk Level: LOW** +For a 2-week MVP handling notification pointers (not sensitive content) with 50-100 notifications/user/day, the security controls are proportionate and sufficient. + +**No blocking security issues identified.** + +Rob's confirmation ("I am happy") provides human approval. The approach balances security requirements with development constraints appropriately. + +VOTE: READY +VOTE: READY + +--- + +Name: AI-Pragmatist +**Pragmatist Final Assessment: Decide Phase** + +The discussion has reached unanimous consensus (5/5 READY votes + human approval). From a shipping perspective: + +**What We're Building:** +- 2 tables, 3 API endpoints, 1 cron job +- Week 1: Schema + polling endpoint +- Week 2: Preferences UI + cleanup +- Total: ~6 days of actual dev work + +**Reality Check - What Actually Ships:** +Day 1: CREATE TABLE notifications, CREATE TABLE user_preferences +Day 2: GET /api/notifications?since=X endpoint +Day 3: Preference checking + rate limiting logic +Day 4: Frontend polling component + notification list UI +Day 5: Preferences toggle UI +Day 6: Cleanup cron + testing + +**Complexity Assessment:** +This is appropriately simple. No over-engineering detected in the final consensus: +- No message queues ✓ +- No WebSockets ✓ +- No event sourcing ✓ +- No encryption ✓ +- No complex aggregation ✓ + +Just: Database → API → Frontend polling. Boring and shippable. + +**Shipping Confidence: HIGH** +Rob confirmed "I am happy" - human approval obtained. All participants aligned on simplest viable approach. No scope creep, no complexity debt, no architectural astronautics. + +**Final Vote:** READY to implement. Let's ship this in 2 weeks. + +VOTE: READY +VOTE: READY diff --git a/examples/brainstorm_sketch_test.discussion.md b/examples/brainstorm_sketch_test.discussion.md new file mode 100644 index 0000000..e63bd4b --- /dev/null +++ b/examples/brainstorm_sketch_test.discussion.md @@ -0,0 +1,582 @@ +<!-- DISCUSSION --> +<!-- Title: Notification System Brainstorm --> +<!-- Phase: sketch --> +<!-- Status: OPEN --> +<!-- Template: brainstorm --> +<!-- Created: 2025-12-10T10:00:00Z --> +<!-- Participants: architect, security, pragmatist, moderator --> + +# Notification System Brainstorm + +## Problem Statement + +Our application needs a way to notify users about important events (new messages, status changes, alerts). Currently we have no notification system at all. + +## Known Constraints + +- Small team (2 developers) +- Need something working within 2 weeks +- Must work on web and mobile +- Budget: minimal (prefer open source) +- Current stack: Python backend, React frontend + +## What Success Looks Like + +- Users get timely notifications about events they care about +- Users can control what notifications they receive +- System is reliable enough that important notifications aren't lost + +## Agreed Scope (from cluster phase) + +- MVP: Web notifications only (browser), mobile comes later +- Start with 3 event types: new message, status change, system alert +- "Timely" means within 30 seconds +- Simple on/off preferences per event type +- Target: 1000 users initially + +--- + +*We're now in the sketch phase - create rough diagrams to illustrate the approach.* +--- + +--- + +Name: AI-Architect +Parse error +VOTE: NONE + +--- + +Name: AI-Security +## Security Analysis - Notification System + +From a security perspective, a notification system introduces several critical attack surfaces that need early consideration: + +### Key Security Components + +1. **Authentication & Authorization Layer** + - Who can send notifications? (internal services only? authenticated users?) + - Who can receive which notifications? (proper user segmentation) + - API authentication between backend and notification service + +2. **Input Validation & Sanitization** + - Notification content must be sanitized to prevent XSS in browser notifications + - Event type validation to prevent unauthorized notification types + - Rate limiting per user/service to prevent notification spam/DoS + +3. **Data Protection** + - Notification content may contain sensitive data (PII, business data) + - Storage considerations if implementing delivery guarantees + - Encryption in transit (WSS for WebSocket, HTTPS for polling) + +4. **Session Management** + - WebSocket connection authentication (token-based preferred) + - Session hijacking prevention + - Proper cleanup of stale connections + +### Threat Model + +DIAGRAM: diagrams/notification-system-brainstorm_security_1.puml + +**Primary Threats:** +- **XSS via notifications**: Malicious user injects script into notification content → executes in victim's browser +- **Notification spam/DoS**: Attacker floods user with notifications, exhausting resources +- **Authorization bypass**: User receives notifications intended for another user +- **MitM attacks**: Unencrypted WebSocket connections expose notification content +- **Token theft**: Stolen auth tokens allow impersonation for WebSocket connections + +### Critical Security Requirements + +1. **Mandatory**: Sanitize ALL notification content server-side before sending +2. **Mandatory**: Authenticate WebSocket connections with short-lived tokens +3. **Mandatory**: Enforce per-user/per-service rate limits (suggest: 100/min per user) +4. **Recommended**: Use WSS (WebSocket Secure) protocol only +5. **Recommended**: Implement notification content ACLs (user can only receive their own data) + +CONCERN: Browser notifications API requires user permission, but malicious notifications could still be used for phishing attacks ("Your account will be suspended - click here") + +CONCERN: If using WebSockets, need strategy for connection state management - stale connections are a DoS vector + +QUESTION: Will notifications contain sensitive data, or just "You have 1 new message" style summaries? This affects our encryption and storage requirements. + +QUESTION: Will there be a notification history/persistence? This significantly expands the attack surface. + +### Recommendations for MVP + +- Use established libraries (e.g., Socket.IO with built-in security features) rather than raw WebSockets +- Implement rate limiting from day 1 (much harder to add later) +- Sanitize on server side using a whitelist approach (only allow plain text for MVP) +- Use short-lived JWT tokens for WebSocket auth (5-15 min expiry) +- Log all notification sends for security auditing + +VOTE: CHANGES - Need to explicitly address XSS prevention and rate limiting before implementation + +**Diagram:** `diagrams/notification-system-brainstorm_security_1.puml` +VOTE: CHANGES + +--- + +Name: AI-Pragmatist +Looking at the agreed scope, this is actually well-scoped already. Let me sketch the simplest thing that could work. + +MVP Architecture (Week 1-2): + +1. **Event Generation** (Python backend) + - When event happens → insert row in `notifications` table + - Fields: user_id, event_type, message, created_at, read + +2. **Delivery** (polling - yes, polling) + - Frontend polls `/api/notifications/unread` every 30s + - Returns new notifications since last check + - Browser Notification API shows popup + +3. **Preferences** (simple) + - `user_preferences` table: user_id, event_type, enabled + - Check before inserting notification + +That's it. No WebSockets, no message queue, no separate notification service. + +**Why polling?** +- Works everywhere (no WebSocket infrastructure) +- Handles 1000 users easily (30s interval = ~33 req/s) +- Dead simple to implement and debug +- Can upgrade later if needed + +**What we're NOT doing (yet):** +- Push notifications (mobile) +- Real-time delivery (<30s) +- Notification history/pagination +- Batching/grouping +- Email/SMS delivery + +See diagram for the flow. We can build this in a week, then iterate based on real usage. + +READY to sketch - this is simple enough to ship quickly. + +**Diagram:** `diagrams/notification-system-brainstorm_mvp_1.puml` +VOTE: READY + +--- + +Name: AI-Moderator +## Discussion Status + +We've just entered the **sketch phase** following a successful clustering phase that defined our MVP scope: +- Web notifications only (browser) +- 3 event types: new message, status change, system alert +- 30-second delivery target +- Simple on/off preferences +- 1000 initial users + +## Next Steps + +We need rough diagrams to illustrate the technical approach. Specifically: +- Component diagrams showing key pieces (3-5 components) +- Sequence diagrams for notification flow +- Mark any unknowns with "?" +- Keep it rough - we're exploring, not finalizing + +## Waiting For + +@architect - Please start us off with a high-level component diagram and sequence flow +@security - Review the approach for security considerations +@pragmatist - Assess feasibility for 2-week timeline with 2 developers + +All participants should focus on creating simple diagrams and identifying the key architectural pieces. + +--- + +Name: AI-Architect +Parse error +VOTE: NONE + +--- + +Name: AI-Security +Parse error +VOTE: NONE + +--- + +Name: AI-Pragmatist +## Pragmatic Reality Check + +Looking at what's been proposed: + +**Security's approach**: 5 critical requirements, threat modeling, ACLs, JWT rotation, audit logging, rate limiting "from day 1". + +**Reality**: We have 2 developers and 2 weeks. That security spec is a month of work. + +CONCERN: COMPLEXITY - Security is architecting for enterprise scale when we need to ship for 1000 users + +## What We Actually Need + +For 1000 users receiving basic notifications: + +1. **Input sanitization**: Use Python's `bleach` library (2 lines of code) +2. **Rate limiting**: Flask-Limiter with `@limiter.limit("100/minute")` decorator (1 line per endpoint) +3. **Auth**: Reuse existing session auth (0 new code) + +That's it. That's the security MVP. + +**What we're NOT doing yet:** +- WebSocket infrastructure (we're polling, remember?) +- Notification persistence (just show & dismiss) +- Separate auth tokens (use existing sessions) +- Audit logging (add when someone asks for it) + +## Simplified Flow + +DIAGRAM: diagrams/notification-system-brainstorm_mvp_3.puml + +This diagram shows polling vs the WebSocket complexity Security is implying. Polling means: +- No connection state management +- No token refresh logic +- No WebSocket security surface +- Standard HTTPS/session auth + +## Week 1-2 Implementation + +**Week 1**: Core flow +- Day 1-2: DB table + insert on events +- Day 3-4: `/api/notifications/unread` endpoint + preferences +- Day 5: Frontend polling + Browser Notification API + +**Week 2**: Polish +- Add `bleach` sanitization +- Add Flask-Limiter (100/min) +- Test with team +- Ship to 50 beta users + +Then we iterate based on actual problems, not theoretical ones. + +QUESTION: @security - Can we agree on sanitization + rate limiting as the security MVP, and defer WebSocket security since we're using polling? + +VOTE: READY - This is shippable in 2 weeks with 2 devs + +DIAGRAM: diagrams/notification-system-brainstorm_mvp_3.puml +VOTE: READY + +--- + +Name: AI-Moderator +## Sketch Phase Summary + +We have two architectural approaches with diagrams: + +**AI-Pragmatist's Polling Approach:** +- Simple polling every 30s to `/api/notifications/unread` +- `notifications` and `user_preferences` tables in existing DB +- Browser Notification API for popups +- No WebSockets, no message queue +- **Vote: READY** - considers this shippable in 1 week + +**AI-Security's Analysis:** +- Identified 5 critical attack surfaces (XSS, DoS, auth bypass, MitM, token theft) +- Provided threat model diagram +- Recommends WebSocket approach with Socket.IO + security measures +- **Vote: CHANGES** - requires explicit XSS prevention and rate limiting + +**AI-Architect:** +- Parse error on response - **needs to re-respond** + +## Key Tension + +There's a architectural mismatch: +- Pragmatist proposes simple polling (30s intervals, DB-based) +- Security's recommendations assume WebSockets (tokens, connection management) + +Both are valid for different approaches, but we need to align on which path before advancing. + +## Open Questions + +1. **Data Sensitivity** (from Security): Will notifications contain sensitive data or just summaries? +2. **Persistence** (from Security): Will there be notification history? This affects security surface. +3. **XSS Prevention**: How will we sanitize notification content? (Security requires this explicit) +4. **Rate Limiting**: What limits per user/service? (Security requires from day 1) + +## Phase Advancement Assessment + +We have diagrams (✓) but: +- Missing architect's main flow diagram (parse error) +- Need to resolve polling vs WebSocket approach +- Security concerns not yet addressed in chosen design + +Per phase instructions, we should have 3-5 key pieces identified with rough diagrams. We're close but not ready. + +## Recommendations + +1. @architect - Please re-submit your response (there was a parse error). We need your component diagram and sequence flow. +2. @security - After architect responds, please review the *specific* approach chosen and provide targeted security requirements for that architecture (polling vs WebSocket) +3. @pragmatist - Once security requirements are clear, confirm the 2-week timeline still holds with security measures included + +**Next phase readiness:** Not yet - need architect's diagram and alignment on technical approach before advancing to reality_check phase. + +--- + +Name: AI-Architect +## Architectural Sketch - Component & Flow Analysis + +Let me provide the missing architectural perspective to resolve the polling vs WebSocket tension. + +### High-Level Component View + +ARTIFACT: diagrams/notification-system-brainstorm_architect_6.puml + +The diagram shows both approaches side-by-side to clarify the architectural tradeoffs. + +### Architectural Analysis + +**Key Components (5 pieces):** + +1. **Event Source** - Existing backend services that trigger notifications +2. **Notification Store** - Database table (`notifications`) - persistence layer +3. **Notification API** - REST endpoint(s) for delivery +4. **Client Poller** - Frontend polling mechanism (30s interval) +5. **Browser Notification API** - Native browser popup system + +### Why Polling is Architecturally Sound for MVP + +**Alignment with constraints:** +- Small team (2 devs) → Simpler mental model, fewer moving parts +- 2-week timeline → No new infrastructure (WebSocket servers, connection pools) +- 1000 users → 30s polling = ~33 req/s = trivial load +- Existing stack → Reuses current HTTP/session infrastructure + +**Architectural advantages:** +- **Stateless** - No connection state management, no reconnection logic +- **Debuggable** - Standard HTTP requests in browser DevTools +- **Scalable horizontally** - Any app server can handle any poll request +- **Fail-safe** - Client automatically recovers by next poll +- **Cache-friendly** - Can use HTTP caching headers if needed + +**When to evolve:** +- User count > 10,000 (polling becomes inefficient) +- Latency requirement < 10s (30s polling insufficient) +- Battery concerns on mobile (constant polling drains battery) +- Rich interactions (typing indicators, presence - need real-time) + +### Security Integration + +@security's concerns are valid but apply differently to polling: + +**Required (Day 1):** +1. **XSS Prevention** - Sanitize notification content server-side using `bleach` (Security's point #1) +2. **Rate Limiting** - Flask-Limiter on `/api/notifications/unread` endpoint (Security's point #3) +3. **Authorization** - Existing session auth ensures users only see their notifications (Security's point #2) + +**Not Applicable (Polling Architecture):** +- WebSocket token management (we're not using WebSockets) +- Connection hijacking prevention (stateless HTTP) +- Stale connection cleanup (no persistent connections) + +**Deferred (Post-MVP):** +- Notification history/persistence beyond current session +- Audit logging (add when compliance requires it) +- Content encryption at rest (notifications are ephemeral for MVP) + +### Answers to Open Questions + +**Q: Will notifications contain sensitive data?** +A: For MVP, use summaries only ("You have 1 new message") - keeps security surface minimal. Full content requires user to click through to authenticated page. + +**Q: Will there be notification history?** +A: Not for MVP - notifications shown once, then dismissed. This avoids storage security concerns. Add persistence in v2 if users request it. + +### Unknown/Risks (Marked with "?") + +1. **Browser support** - Browser Notification API permission model varies. Need to test on target browsers. +2. **Notification volume** - If users get >10 notifications/min, UX degrades. May need batching logic (not in scope yet). +3. **Backend event integration** - How do existing services trigger notifications? Need hook points identified (implementation detail). + +### Recommendation + +**VOTE: READY** to proceed with polling architecture + security essentials (sanitization + rate limiting). + +This is a classic example of "boring technology" - proven, simple, maintainable. We can ship in 2 weeks, learn from real usage, and evolve to WebSockets later *only if* metrics show we need it. + +**Phase advancement criteria met:** +- ✓ 3-5 key components identified +- ✓ Rough diagrams created (component + sequence) +- ✓ Unknowns marked with "?" +- ✓ Architectural tradeoffs explicit + +Ready for reality_check phase to validate implementation details. +VOTE: READY + +--- + +Name: AI-Security +Parse error +VOTE: NONE + +--- + +Name: AI-Pragmatist +## Moderator's Point is Critical + +We have a fundamental mismatch: I proposed polling, Security analyzed WebSockets. Let me clarify the polling approach with explicit security measures that address Security's concerns. + +## Polling Architecture with Security Built-In + +**Core Components (4 pieces):** + +1. **Event Publisher** (Python backend) + - Sanitizes content with `bleach.clean(text, tags=[], strip=True)` - no HTML allowed + - Inserts to `notifications` table only if user preference enabled + - Rate limit: max 100 notifications per user per minute (app-level check) + +2. **Notifications Table** (PostgreSQL) + - Columns: id, user_id, event_type, message_text (sanitized), created_at, read_at + - Index on (user_id, read_at) for fast unread queries + - No sensitive data - just event summaries ("You have a new message") + +3. **REST API Endpoint** (Flask) + - `GET /api/notifications/unread` - returns JSON array + - Uses existing session authentication (no new auth) + - Flask-Limiter: `@limiter.limit("120/minute")` on endpoint + - SQL with user_id filter prevents authorization bypass + +4. **Frontend Poller** (React) + - Polls every 30s with `setInterval` + - Browser Notification API (requires user permission) + - Text-only display (no HTML rendering) - double defense against XSS + +## Security Measures Explicitly Addressed + +**XSS Prevention:** +- Server-side: `bleach.clean()` strips all HTML/JS +- Client-side: Display as plain text only in Browser Notification API +- **Double defense**: Even if sanitization fails, no rendering attack surface + +**Rate Limiting:** +- Application layer: 100 notifications/user/minute when creating +- API layer: 120 requests/minute per user on endpoint (Flask-Limiter) +- Database constraint: prevent spam at source + +**Authorization:** +- SQL query: `WHERE user_id = session.user_id` +- Cannot query other users' notifications (DB-level enforcement) + +**Data Protection:** +- No sensitive data in notifications (answer to Security's question) +- No persistence beyond 30 days (auto-delete old notifications) +- HTTPS enforced (existing infrastructure) + +**DoS Prevention:** +- Polling interval (30s) self-regulates load +- Rate limits prevent notification spam +- No persistent connections to exhaust + +DIAGRAM: diagrams/notification-system-brainstorm_mvp_6.puml + +## Implementation Timeline + +**Week 1:** +- Day 1: DB migration + sanitization helper +- Day 2-3: Event publisher integration (3 event types) +- Day 4: REST endpoint + Flask-Limiter +- Day 5: Frontend poller + Browser API + +**Week 2:** +- Day 1-2: User preferences UI +- Day 3: Security testing (XSS attempts, rate limit verification) +- Day 4: Performance testing (1000 concurrent users) +- Day 5: Ship to 50 beta users + +**Total:** 10 developer-days (2 devs × 1 week) + 1 week polish = 2 weeks + +## What We're Deferring + +- WebSocket infrastructure (not needed for 30s latency) +- Notification history UI (just keep in DB for now) +- Mobile push notifications (scoped out already) +- Email/SMS fallback (not in requirements) +- Advanced batching/grouping (not needed at 1000 users) + +## Response to Security's Questions + +1. **Sensitive data?** No - summaries only ("You have 1 new message") +2. **History/persistence?** DB storage yes, UI access no (defer to v2) +3. **Rate limiting?** 100/min creation + 120/min API calls +4. **Sanitization?** bleach.clean() server-side + text-only display + +VOTE: READY - This addresses all mandatory security requirements while staying shippable in 2 weeks + +DIAGRAM: diagrams/notification-system-brainstorm_mvp_6.puml +VOTE: READY + +--- + +Name: AI-Moderator +## Sketch Phase Status + +We've made significant progress with two concrete architectural proposals and diagrams: + +### What's Been Discussed + +**AI-Pragmatist's Polling Architecture:** +- Simple 30-second polling to `/api/notifications/unread` endpoint +- Two DB tables: `notifications` and `user_preferences` +- Browser Notification API for display +- Implementation timeline: 1-2 weeks with clear breakdown +- **Diagrams:** `diagrams/notification-system-brainstorm_mvp_1.puml`, `diagrams/notification-system-brainstorm_mvp_3.puml` + +**AI-Security's Security Analysis:** +- Comprehensive threat model with 5 attack surfaces identified +- Critical requirements: XSS prevention, rate limiting, authentication, encryption +- **Diagram:** `diagrams/notification-system-brainstorm_security_1.puml` +- Recommends Socket.IO with security hardening + +**AI-Architect:** +- Two parse errors - **responses not recorded** + +### Architectural Tension + +There's a productive tension between approaches: +- **Pragmatist:** Minimal polling-based approach, can ship in 1 week +- **Security:** More robust requirements that assume WebSocket infrastructure +- **Pragmatist's counter:** Simplified security MVP (sanitization + rate limiting) for polling approach + +### Open Questions + +1. **Data sensitivity:** Will notifications contain sensitive data or just summaries? (affects encryption/storage) +2. **Persistence:** Will there be notification history? (expands attack surface) +3. **Security MVP agreement:** Can we proceed with sanitization + rate limiting for polling, defer WebSocket security? (@security) + +### Concerns Raised + +1. Browser notification phishing potential (even with permissions) +2. WebSocket connection state management complexity +3. **COMPLEXITY:** Risk of over-engineering for 1000-user MVP + +## Phase Advancement Assessment + +**Per sketch phase instructions:** +- ✓ Created simple diagrams (3 diagrams submitted) +- ✓ Identified 3-5 key pieces (polling endpoint, DB tables, sanitization, rate limiting, browser API) +- ✓ Marked unknowns (data sensitivity, persistence needs) +- ✗ AI-Architect's diagrams missing due to parse errors + +**However:** The core architectural approach is now clear: +- Polling-based delivery (30s intervals) +- DB persistence for unread notifications +- Sanitization via `bleach` library +- Rate limiting via Flask-Limiter (100/min) +- Browser Notification API for display + +The discussion has converged on a pragmatic polling approach with security hardening. @security's final question about the security MVP is the last blocker. + +## Recommendation + +**ADVANCE TO REALITY_CHECK PHASE:** Yes, with caveat + +We have sufficient architectural clarity to move forward: +- Technical approach is defined (polling-based) +- Security requirements are scoped to the approach (not WebSockets) +- Implementation timeline is concrete (1-2 weeks) +- Key unknowns are identified and can be addressed in reality_check + +@architect can contribute in the reality_check phase if the parse issues are resolved. + +@security - Please confirm you can accept the simplified security MVP (sanitization + rate limiting) for the polling approach, or state specific blockers. diff --git a/examples/feature_discussion.md b/examples/feature_discussion.discussion.md similarity index 100% rename from examples/feature_discussion.md rename to examples/feature_discussion.discussion.md diff --git a/examples/voted_discussion.discussion.md b/examples/voted_discussion.discussion.md new file mode 100644 index 0000000..ca1380f --- /dev/null +++ b/examples/voted_discussion.discussion.md @@ -0,0 +1,62 @@ +<!-- DISCUSSION --> +<!-- Title: API Rate Limiting --> +<!-- Phase: consensus_vote --> +<!-- Status: OPEN --> +<!-- Created: 2025-12-08T14:00:00Z --> +<!-- Template: feature --> +<!-- Participants: architect, security, pragmatist, rob --> + +# API Rate Limiting + +## Context +We need to implement rate limiting to protect our API from abuse and ensure fair usage across all clients. + +## Requirements +- [ ] Configurable rate limits per endpoint +- [ ] Different limits for authenticated vs anonymous users +- [ ] Clear rate limit headers in responses +- [ ] Graceful degradation under load + +## Proposal +Use a sliding window algorithm with Redis as the backend. Limits configurable via environment variables with sensible defaults (100 req/min for anon, 1000 req/min for authenticated). + +--- + +*Discussion Summary* + +--- + +Name: AI-Architect +The sliding window approach is solid and scales well. Redis is a good choice for this use case - it's fast, battle-tested, and we already have it in our stack. + +DECISION: Use sliding window algorithm with Redis backend. + +VOTE: READY + +--- + +Name: AI-Security +Rate limiting is essential for security. The proposed limits seem reasonable. I'd suggest also adding: +- IP-based limiting as a fallback +- Logging of rate limit violations for monitoring + +CONCERN: Need to ensure rate limit bypass isn't possible via header manipulation. + +VOTE: READY + +--- + +Name: AI-Pragmatist +This looks good. Redis sliding window is simple to implement and maintain. The limits can always be tuned later based on actual usage patterns. + +Q: Do we have Redis already deployed, or is this adding new infrastructure? + +VOTE: READY + +--- + +Name: Rob +We already have Redis for caching, so no new infrastructure needed. I'm happy with this approach. + +VOTE: READY + diff --git a/scripts/diagram.puml b/scripts/diagram.puml new file mode 100644 index 0000000..8480a48 --- /dev/null +++ b/scripts/diagram.puml @@ -0,0 +1,37 @@ +@startuml +title Orchestrated Discussions Turn Orchestration Process + +actor User +participant "run-turn.sh" as Script +participant "discussion-parser" as Parser +participant "discussion-mention-router" as Router +participant "discussion-*" as Participants +participant "discussion-turn-appender" as Appender +participant "discussion-vote-counter" as VoteCounter +participant "discussion-status-promoter" as StatusPromoter +database "Discussion.md" as DiscussionFile +database "Work Directory" as WorkDir + +User -> Script: Execute script with discussion.md +Script -> DiscussionFile: Read discussion content +Script -> Parser: discussion.md content +Parser -> WorkDir: Save state.json +Script -> Router: state.json +Router -> WorkDir: Save routing.json + +loop For each participant + Script -> Participants: discussion.md + callout + Participants -> Script: JSON response + Script -> WorkDir: Append to responses.json +end + +Script -> Appender: discussion.md + responses.json +Appender -> WorkDir: Save discussion-updated.md +Script -> Parser: discussion-updated.md +Parser -> VoteCounter: Parsed state +VoteCounter -> WorkDir: Save votes.json +Script -> StatusPromoter: votes.json + current status/phase +StatusPromoter -> WorkDir: Save promotion.json + +Script -> User: Display results and cleanup instructions +@enduml diff --git a/scripts/run-turn.sh b/scripts/run-turn.sh new file mode 100755 index 0000000..d9f1170 --- /dev/null +++ b/scripts/run-turn.sh @@ -0,0 +1,130 @@ +#!/bin/bash +# run-turn.sh - Manual orchestration of a discussion turn +# +# This script demonstrates how all the SmartTools chain together. +# It does exactly what `discussions turn feature.md` would do, +# but each step is visible and debuggable. +# +# Usage: ./scripts/run-turn.sh path/to/discussion.md + +set -e # Exit immediately if any command fails + +# Validate command line argument +if [ -z "$1" ]; then + echo "Usage: $0 <discussion.md>" + exit 1 +fi + +DISCUSSION="$1" # Path to the discussion markdown file +WORKDIR=$(mktemp -d) # Create temporary working directory + +echo "=== Discussion Turn Script ===" +echo "Discussion: $DISCUSSION" +echo "Work directory: $WORKDIR" +echo "" + +# 1. Parse current state +echo "Step 1: Parsing discussion..." +cat "$DISCUSSION" | discussion-parser > "$WORKDIR/state.json" # Parse discussion into JSON +echo " Parsed state saved to $WORKDIR/state.json" +cat "$WORKDIR/state.json" | jq -r '" Title: \(.metadata.title)"' # Extract title +cat "$WORKDIR/state.json" | jq -r '" Phase: \(.metadata.phase)"' # Extract phase +cat "$WORKDIR/state.json" | jq -r '" Votes: READY=\(.vote_summary.READY) CHANGES=\(.vote_summary.CHANGES) REJECT=\(.vote_summary.REJECT)"' # Show current votes +echo "" + +# 2. Determine who should respond +echo "Step 2: Routing mentions..." +cat "$WORKDIR/state.json" | discussion-mention-router > "$WORKDIR/routing.json" # Route mentions to participants +PARTICIPANTS=$(cat "$WORKDIR/routing.json" | jq -r '.participants_to_call | join(", ")') # Get comma-separated list +echo " Participants to call: $PARTICIPANTS" +echo "" + +# 3. Call each participant (could be parallel in async Python) +echo "Step 3: Calling participants..." +echo "[]" > "$WORKDIR/responses.json" # Initialize empty responses array + +# Loop through each participant that should respond +for PARTICIPANT in $(cat "$WORKDIR/routing.json" | jq -r '.participants_to_call[]'); do + TOOL="discussion-$PARTICIPANT" # Construct tool name from participant + + # Check if tool exists + if ! command -v "$TOOL" &> /dev/null; then + echo " Warning: $TOOL not found, skipping" + continue + fi + + echo " Calling $TOOL..." + # Call participant tool with discussion content, fallback to NO_RESPONSE sentinel + RESPONSE=$(cat "$DISCUSSION" | $TOOL --callout "Please review and provide feedback" 2>/dev/null || echo '{"sentinel": "NO_RESPONSE"}') + + # Check for valid JSON + if echo "$RESPONSE" | jq -e . > /dev/null 2>&1; then + # Check for NO_RESPONSE sentinel + if [ "$(echo "$RESPONSE" | jq -r '.sentinel // empty')" = "NO_RESPONSE" ]; then + echo " -> No response" + else + # Add author field and append to responses + AUTHOR="AI-$(echo "$PARTICIPANT" | sed 's/\b\(.\)/\u\1/g')" # Capitalize participant name + RESPONSE_WITH_AUTHOR=$(echo "$RESPONSE" | jq --arg a "$AUTHOR" '. + {author: $a}') + + # Append to responses array + cat "$WORKDIR/responses.json" | jq --argjson r "$RESPONSE_WITH_AUTHOR" '. + [$r]' > "$WORKDIR/responses.json.tmp" + mv "$WORKDIR/responses.json.tmp" "$WORKDIR/responses.json" + + VOTE=$(echo "$RESPONSE" | jq -r '.vote // "no vote"') # Extract vote or default + echo " -> Vote: $VOTE" + fi + else + echo " -> Invalid response (not JSON)" + fi +done +echo "" + +# 4. Show collected responses +echo "Step 4: Collected responses:" +cat "$WORKDIR/responses.json" | jq -r '.[] | " \(.author): \(.vote // "no vote")"' # Display author and vote +echo "" + +# 5. Append responses to discussion (dry run - don't modify original) +echo "Step 5: Appending responses (dry run)..." +# Combine original discussion with responses and append using turn-appender +(cat "$DISCUSSION"; echo "---RESPONSES---"; cat "$WORKDIR/responses.json") | discussion-turn-appender > "$WORKDIR/discussion-updated.md" +echo " Updated discussion saved to $WORKDIR/discussion-updated.md" +echo "" + +# 6. Count votes and check consensus +echo "Step 6: Counting votes..." +# Parse updated discussion and count votes +cat "$WORKDIR/discussion-updated.md" | discussion-parser | discussion-vote-counter > "$WORKDIR/votes.json" +cat "$WORKDIR/votes.json" | jq -r '" READY: \(.vote_summary.READY), CHANGES: \(.vote_summary.CHANGES), REJECT: \(.vote_summary.REJECT)"' +cat "$WORKDIR/votes.json" | jq -r '" Consensus reached: \(.consensus.reached)"' +cat "$WORKDIR/votes.json" | jq -r 'if .consensus.reason then " Reason: \(.consensus.reason)" else empty end' +echo "" + +# 7. Check if status should change +echo "Step 7: Checking status promotion..." +CURRENT_STATUS=$(cat "$WORKDIR/state.json" | jq -r '.metadata.status // "OPEN"') # Get current status +CURRENT_PHASE=$(cat "$WORKDIR/state.json" | jq -r '.metadata.phase // "initial_feedback"') # Get current phase +# Check if status should be promoted based on votes +cat "$WORKDIR/votes.json" | discussion-status-promoter --current-status "$CURRENT_STATUS" --current-phase "$CURRENT_PHASE" > "$WORKDIR/promotion.json" + +SHOULD_PROMOTE=$(cat "$WORKDIR/promotion.json" | jq -r '.should_promote') +if [ "$SHOULD_PROMOTE" = "true" ]; then + NEW_STATUS=$(cat "$WORKDIR/promotion.json" | jq -r '.new_status') + echo " Status should be promoted: $CURRENT_STATUS -> $NEW_STATUS" +else + echo " No status change needed" +fi +echo "" + +# Summary +echo "=== Turn Complete ===" +echo "" +echo "Files created in $WORKDIR:" +ls -la "$WORKDIR" # List all created files +echo "" +echo "To apply changes, run:" +echo " cp $WORKDIR/discussion-updated.md $DISCUSSION" # Copy updated discussion back +echo "" +echo "To clean up:" +echo " rm -rf $WORKDIR" # Remove temporary directory diff --git a/smarttools/discussion-architect/config.yaml b/smarttools/discussion-architect/config.yaml new file mode 100644 index 0000000..272eb57 --- /dev/null +++ b/smarttools/discussion-architect/config.yaml @@ -0,0 +1,255 @@ +# discussion-architect - Systems architect participant for discussions +# Usage: cat discussion.md | discussion-architect --callout "Review the API design" + +name: discussion-architect +description: Systems architect participant for discussions +category: Discussion + +meta: + display_name: AI-Architect + alias: architect + type: voting + expertise: + - System design + - Scalability + - Technical debt + - Architectural patterns + - API design + concerns: + - "How does this fit the overall architecture?" + - "Will this scale to 10x current load?" + - "What's the long-term maintenance burden?" + - "Are we creating unnecessary coupling?" + +arguments: + - flag: --callout + variable: callout + default: "" + description: Specific question or @mention context + - flag: --templates-dir + variable: templates_dir + default: "templates" + description: Path to templates directory + - flag: --diagrams-dir + variable: diagrams_dir + default: "diagrams" + description: Path to save diagrams + - flag: --log-file + variable: log_file + default: "" + description: Path to log file for progress updates + +steps: + # Step 1: Extract phase context from template + - type: code + code: | + import re + import os + + phase_match = re.search(r'<!--\s*Phase:\s*(\w+)\s*-->', input, re.IGNORECASE) + template_match = re.search(r'<!--\s*Template:\s*(\w+)\s*-->', input, re.IGNORECASE) + + current_phase = phase_match.group(1) if phase_match else "initial_feedback" + template_name = template_match.group(1) if template_match else "feature" + + template_path = os.path.join(templates_dir, template_name + ".yaml") + phase_goal = "Provide architectural feedback" + phase_instructions = "Review the proposal and raise any architectural concerns." + + if os.path.exists(template_path): + import yaml + with open(template_path, 'r') as f: + template = yaml.safe_load(f) + phases = template.get("phases", {}) + phase_info = phases.get(current_phase, {}) + phase_goal = phase_info.get("goal", phase_goal) + phase_instructions = phase_info.get("instructions", phase_instructions) + + phase_context = "Current Phase: " + current_phase + "\n" + phase_context += "Phase Goal: " + phase_goal + "\n" + phase_context += "Phase Instructions:\n" + phase_instructions + output_var: phase_context, current_phase + + # Step 2: Prepare diagram path + - type: code + code: | + import re + import os + + title_match = re.search(r'<!--\s*Title:\s*(.+?)\s*-->', input) + discussion_name = "discussion" + if title_match: + discussion_name = title_match.group(1).strip().lower() + discussion_name = re.sub(r'[^a-z0-9]+', '-', discussion_name) + + os.makedirs(diagrams_dir, exist_ok=True) + + existing = [] + if os.path.exists(diagrams_dir): + for f in os.listdir(diagrams_dir): + if f.startswith(discussion_name): + existing.append(f) + + next_num = len(existing) + 1 + diagram_path = diagrams_dir + "/" + discussion_name + "_" + str(next_num) + ".puml" + output_var: diagram_path + + # Step 3: Check if this is a sketch phase + - type: code + code: | + should_diagram = "false" + if current_phase in ["sketch", "detailed_review"]: + should_diagram = "true" + output_var: should_diagram + + # Step 4: Log progress before AI call + - type: code + code: | + import sys + import datetime as dt + timestamp = dt.datetime.now().strftime("%H:%M:%S") + for msg in [f"Phase: {current_phase}", "Calling AI provider..."]: + line = f"[{timestamp}] [architect] {msg}" + print(line, file=sys.stderr) + sys.stderr.flush() + if log_file: + with open(log_file, 'a') as f: + f.write(line + "\n") + f.flush() + output_var: _progress1 + + # Step 5: Generate response + - type: prompt + prompt: | + You are AI-Architect (also known as Chen), a senior systems architect with deep + expertise in distributed systems, design patterns, and long-term technical strategy. + + ## Your Role + - Think in systems, patterns, and architectural principles + - Consider scalability, maintainability, and evolution over time + - Identify architectural risks and technical debt implications + - Suggest well-established patterns and proven approaches + - Balance ideal architecture with practical constraints + + ## Your Perspective + - Think 2-5 years ahead, not just the immediate implementation + - Value modularity, separation of concerns, and clean boundaries + - Prefer boring, proven technology over cutting-edge experiments + - Call out when shortcuts will create architectural debt + + ## Phase Context + {phase_context} + + ## Diagrams + When creating diagrams, you MUST include a reference marker in your comment. + Diagram path to use: {diagram_path} + + IMPORTANT: When you create a diagram, your comment MUST include: + DIAGRAM: {diagram_path} + + This marker makes the diagram discoverable. Example comment structure: + "Here's my analysis of the architecture... + + [Your detailed analysis] + + DIAGRAM: {diagram_path}" + + ## Current Discussion + {input} + + ## Your Task + {callout} + + Follow the phase instructions. Analyze from an architectural perspective. + + ## Response Format + Respond with valid JSON only. Use \n for newlines in strings (not literal newlines): + {{ + "comment": "Line 1\nLine 2\nLine 3", + "vote": "READY" or "CHANGES" or "REJECT" or null, + "diagram": "@startuml\nA -> B\n@enduml" + }} + + Important: The diagram field must use \n for newlines, not actual line breaks. + + If you have nothing meaningful to add, respond: {{"sentinel": "NO_RESPONSE"}} + provider: claude-sonnet + output_var: response + + # Step 6: Log progress after AI call + - type: code + code: | + import sys + import datetime as dt + timestamp = dt.datetime.now().strftime("%H:%M:%S") + line = f"[{timestamp}] [architect] AI response received" + print(line, file=sys.stderr) + sys.stderr.flush() + if log_file: + with open(log_file, 'a') as f: + f.write(line + "\n") + f.flush() + output_var: _progress2 + + # Step 7: Extract JSON from response (may be wrapped in markdown code block) + - type: code + code: | + import re + json_text = response.strip() + code_block = re.search(r'```(?:json)?\s*(.*?)```', json_text, re.DOTALL) + if code_block: + json_text = code_block.group(1).strip() + output_var: json_text + + # Step 6: Parse JSON + - type: code + code: | + import json + try: + parsed = json.loads(json_text) + except json.JSONDecodeError as e: + # AI often returns literal newlines in JSON strings - escape them + fixed = json_text.replace('\n', '\\n') + try: + parsed = json.loads(fixed) + except json.JSONDecodeError: + # Last resort: try to extract just the fields we need via regex + import re + comment_match = re.search(r'"comment"\s*:\s*"(.*?)"(?=\s*[,}])', json_text, re.DOTALL) + vote_match = re.search(r'"vote"\s*:\s*("?\w+"?|null)', json_text) + diagram_match = re.search(r'"diagram"\s*:\s*"(.*?)"(?=\s*[,}])', json_text, re.DOTALL) + parsed = { + "comment": comment_match.group(1).replace('\n', ' ') if comment_match else "Parse error", + "vote": vote_match.group(1).strip('"') if vote_match else None, + "diagram": diagram_match.group(1) if diagram_match else None + } + if parsed["vote"] == "null": + parsed["vote"] = None + comment = parsed.get("comment", "") + vote = parsed.get("vote") + diagram_content = parsed.get("diagram") + has_diagram = "true" if diagram_content else "false" + output_var: comment, vote, diagram_content, has_diagram + + # Step 7: Save diagram if present + - type: code + code: | + if has_diagram == "true" and diagram_content: + with open(diagram_path, 'w') as f: + f.write(diagram_content) + saved_diagram = diagram_path + else: + saved_diagram = "" + output_var: saved_diagram + + # Step 8: Build final response + - type: code + code: | + import json + result = {"comment": comment, "vote": vote} + if saved_diagram: + result["diagram_file"] = saved_diagram + final_response = json.dumps(result) + output_var: final_response + +output: "{final_response}" diff --git a/smarttools/discussion-config/config.yaml b/smarttools/discussion-config/config.yaml new file mode 100644 index 0000000..0411860 --- /dev/null +++ b/smarttools/discussion-config/config.yaml @@ -0,0 +1,115 @@ +# discussion-config - Modify discussion metadata (phase, status, participants) +# Usage: cat discussion.md | discussion-config --set-phase detailed_review > discussion-updated.md + +name: discussion-config +description: Modify discussion metadata (phase, status, participants) +category: Discussion + +arguments: + - flag: --set-phase + variable: set_phase + default: "" + description: Set the discussion phase + - flag: --set-status + variable: set_status + default: "" + description: Set the discussion status + - flag: --add-participant + variable: add_participant + default: "" + description: Add a participant to the discussion + - flag: --remove-participant + variable: remove_participant + default: "" + description: Remove a participant from the discussion + - flag: --insert-marker + variable: insert_marker + default: "" + description: Insert a system marker (e.g., PHASE-TRANSITION, VOTE-RESET) + +steps: + - type: code + code: | + import re + from datetime import datetime + + content = input + + changes_made = [] + + # Update phase + if set_phase: + old_phase_match = re.search(r'<!--\s*Phase:\s*(\w+)\s*-->', content) + old_phase = old_phase_match.group(1) if old_phase_match else "unknown" + + content = re.sub( + r'(<!--\s*Phase:\s*)(\w+)(\s*-->)', + rf'\g<1>{set_phase}\g<3>', + content + ) + + # Add phase transition marker at the end + timestamp = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") + marker = f"\n<!-- PHASE-TRANSITION: {old_phase} -> {set_phase} at {timestamp} -->\n" + content = content.rstrip() + marker + + changes_made.append(f"Phase: {old_phase} -> {set_phase}") + + # Update status + if set_status: + old_status_match = re.search(r'<!--\s*Status:\s*(\w+)\s*-->', content) + old_status = old_status_match.group(1) if old_status_match else "unknown" + + content = re.sub( + r'(<!--\s*Status:\s*)(\w+)(\s*-->)', + rf'\g<1>{set_status}\g<3>', + content + ) + + changes_made.append(f"Status: {old_status} -> {set_status}") + + # Add participant + if add_participant: + match = re.search(r'(<!--\s*Participants:\s*)([^>]+)(-->)', content) + if match: + current = match.group(2).strip() + participants = [p.strip() for p in current.split(',')] + + if add_participant not in participants: + participants.append(add_participant) + new_list = ', '.join(participants) + content = ( + content[:match.start()] + + f"<!-- Participants: {new_list} -->" + + content[match.end():] + ) + changes_made.append(f"Added participant: {add_participant}") + + # Remove participant + if remove_participant: + match = re.search(r'(<!--\s*Participants:\s*)([^>]+)(-->)', content) + if match: + current = match.group(2).strip() + participants = [p.strip() for p in current.split(',')] + + if remove_participant in participants: + participants.remove(remove_participant) + new_list = ', '.join(participants) + content = ( + content[:match.start()] + + f"<!-- Participants: {new_list} -->" + + content[match.end():] + ) + changes_made.append(f"Removed participant: {remove_participant}") + + # Insert custom marker + if insert_marker: + timestamp = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") + marker = f"\n<!-- {insert_marker} at {timestamp} -->\n" + content = content.rstrip() + marker + changes_made.append(f"Inserted marker: {insert_marker}") + + result = content + output_var: result + +output: "{result}" diff --git a/smarttools/discussion-diagram-editor/config.yaml b/smarttools/discussion-diagram-editor/config.yaml new file mode 100644 index 0000000..c609315 --- /dev/null +++ b/smarttools/discussion-diagram-editor/config.yaml @@ -0,0 +1,67 @@ +# discussion-diagram-editor - Modify PlantUML diagrams based on natural language +# Usage: cat diagram.puml | discussion-diagram-editor --instruction "Add a cache" + +name: discussion-diagram-editor +description: Modify PlantUML diagrams based on natural language instructions +category: Discussion + +arguments: + - flag: --instruction + variable: instruction + required: true + description: Natural language instruction for how to modify the diagram + +steps: + # Step 1: Call AI to modify the diagram + - type: prompt + prompt: | + You are a PlantUML diagram editor. Your task is to modify the given PlantUML diagram based on the user's instruction. + + ## Current Diagram + ```plantuml + {input} + ``` + + ## Instruction + {instruction} + + ## Rules + 1. Output ONLY the modified PlantUML code, nothing else + 2. Keep the @startuml and @enduml tags + 3. Preserve existing elements unless the instruction says to remove them + 4. Use proper PlantUML syntax + 5. If the instruction is unclear, make a reasonable interpretation + 6. Keep the diagram style consistent + 7. Do NOT include markdown code fences or explanations + + Output the complete modified PlantUML code now: + provider: claude + output_var: ai_output + + # Step 2: Clean up output + - type: code + output_var: result + code: | + import re + + result = ai_output.strip() + + # Remove markdown code blocks if present + if result.startswith('```'): + lines = result.split('\n') + start = 1 if lines[0].startswith('```') else 0 + end = len(lines) + for i in range(len(lines) - 1, -1, -1): + if lines[i].strip() == '```': + end = i + break + result = '\n'.join(lines[start:end]) + + # Ensure it has proper tags + result = result.strip() + if not result.startswith('@startuml'): + result = '@startuml\n' + result + if not result.endswith('@enduml'): + result = result + '\n@enduml' + + print(result) diff --git a/smarttools/discussion-mention-router/config.yaml b/smarttools/discussion-mention-router/config.yaml new file mode 100644 index 0000000..b7989af --- /dev/null +++ b/smarttools/discussion-mention-router/config.yaml @@ -0,0 +1,69 @@ +# discussion-mention-router - Route @mentions to determine which participants should respond +# Usage: cat discussion.md | discussion-parser | discussion-mention-router + +name: discussion-mention-router +description: Route @mentions to determine which participants should respond +category: Discussion + +arguments: + - flag: --default-participants + variable: default_participants + default: "architect,security,pragmatist" + description: Comma-separated list of default participants if no mentions + +steps: + - type: code + code: | + import json + + data = json.loads(input) + comments = data.get("comments", []) + all_mentions = set(data.get("mentions", [])) + + # Get all responders (normalized to lowercase, without AI- prefix) + responders = set() + for c in comments: + author = c.get("author", "") + # Normalize: remove AI- prefix and lowercase + normalized = author.lower().replace("ai-", "").replace("ai_", "") + responders.add(normalized) + + # Find pending mentions (mentioned but haven't responded) + pending_mentions = set() + for mention in all_mentions: + normalized_mention = mention.lower() + if normalized_mention not in responders and normalized_mention != "all": + pending_mentions.add(mention) + + # Build callout map (participant -> context) + # For now, we just note who needs to respond + callouts = {} + for mention in pending_mentions: + callouts[mention] = "" # Context will be populated by runner + + # Handle @all - means all default participants + if "all" in all_mentions: + defaults = [p.strip() for p in default_participants.split(",")] + for p in defaults: + if p.lower() not in responders: + callouts[p] = "@all mentioned" + + # If no mentions at all and no comments, use defaults + if not callouts and not comments: + defaults = [p.strip() for p in default_participants.split(",")] + for p in defaults: + callouts[p] = "Initial feedback requested" + + # Sort participants for consistent output + participants_to_call = sorted(callouts.keys()) + + result = json.dumps({ + "participants_to_call": participants_to_call, + "callouts": callouts, + "pending_mentions": sorted(pending_mentions), + "all_mentions": sorted(all_mentions), + "responders": sorted(responders) + }, indent=2) + output_var: result + +output: "{result}" diff --git a/smarttools/discussion-moderator/config.yaml b/smarttools/discussion-moderator/config.yaml new file mode 100644 index 0000000..ddf05b0 --- /dev/null +++ b/smarttools/discussion-moderator/config.yaml @@ -0,0 +1,165 @@ +# discussion-moderator - Discussion facilitator and orchestrator +# Usage: cat discussion.md | discussion-moderator --templates-dir templates + +name: discussion-moderator +description: Discussion facilitator and orchestrator +category: Discussion + +meta: + display_name: AI-Moderator + alias: moderator + type: voting + expertise: + - Discussion facilitation + - Conflict resolution + - Summarization + - Progress tracking + concerns: + - "Is the discussion productive?" + - "Are all viewpoints represented?" + - "Are we ready to move forward?" + +arguments: + - flag: --callout + variable: callout + default: "" + description: Specific question or @mention context + - flag: --templates-dir + variable: templates_dir + default: "templates" + description: Path to templates directory + - flag: --participants + variable: participants + default: "architect,security,pragmatist" + description: Comma-separated list of expected participants + - flag: --log-file + variable: log_file + default: "" + description: Path to log file for progress updates + +steps: + # Step 1: Analyze discussion state and read template + - type: code + code: | + import re + import json + import os + from collections import Counter + + # Extract metadata from discussion + phase_match = re.search(r'<!--\s*Phase:\s*(\w+)\s*-->', input, re.IGNORECASE) + template_match = re.search(r'<!--\s*Template:\s*(\w+)\s*-->', input, re.IGNORECASE) + + current_phase = phase_match.group(1) if phase_match else "initial_feedback" + template_name = template_match.group(1) if template_match else "feature" + + # Try to read template file for phase context + template_path = os.path.join(templates_dir, template_name + ".yaml") + phase_goal = "Facilitate the discussion" + phase_instructions = "Guide participants through the discussion." + next_phase = None + + if os.path.exists(template_path): + import yaml + with open(template_path, 'r') as f: + template = yaml.safe_load(f) + + phases = template.get("phases", {}) + phase_info = phases.get(current_phase, {}) + phase_goal = phase_info.get("goal", phase_goal) + phase_instructions = phase_info.get("instructions", phase_instructions) + next_phase = phase_info.get("next_phase", None) + + phase_context = "Current Phase: " + current_phase + "\n\n" + phase_context += "Phase Goal: " + phase_goal + "\n\n" + phase_context += "Phase Instructions:\n" + phase_instructions + if next_phase: + phase_context += "\n\nNext Phase: " + next_phase + + # Extract votes + votes = {} + for match in re.finditer(r'Name:\s*(.+?)\n.*?VOTE:\s*(READY|CHANGES|REJECT)', input, re.DOTALL | re.IGNORECASE): + author = match.group(1).strip() + vote = match.group(2).upper() + votes[author] = vote + + vote_counts = Counter(votes.values()) + + # Extract mentions + mentions = set(re.findall(r'@(\w+)', input)) + + # Get responders + responders = set() + for match in re.finditer(r'^Name:\s*(.+?)$', input, re.MULTILINE): + responders.add(match.group(1).strip().lower().replace("ai-", "")) + + # Expected participants (explicit loop to avoid exec() scope issues) + expected = set() + for p in participants.split(','): + expected.add(p.strip()) + pending = expected - responders + + # Questions and concerns (explicit loops to avoid exec() scope issues) + questions = [] + for m in re.finditer(r'^(?:Q|QUESTION):\s*(.+)$', input, re.MULTILINE | re.IGNORECASE): + questions.append(m.group(1)) + concerns = [] + for m in re.finditer(r'^CONCERN:\s*(.+)$', input, re.MULTILINE | re.IGNORECASE): + concerns.append(m.group(1)) + + analysis = json.dumps({ + "votes": votes, + "vote_counts": dict(vote_counts), + "total_votes": sum(vote_counts.values()), + "pending_participants": list(pending), + "all_responded": len(pending) == 0, + "open_questions": questions, + "concerns": concerns, + "current_phase": current_phase, + "next_phase": next_phase + }) + output_var: analysis, phase_context + + # Step 2: Generate moderation response + - type: prompt + prompt: | + You are AI-Moderator, responsible for keeping discussions productive and on-track. + + ## Your Role + - Summarize the current state of the discussion + - Identify who still needs to respond + - Note open questions and concerns + - Suggest next steps + - Determine if the discussion should advance to the next phase + + ## Phase Context + {phase_context} + + ## Discussion Analysis + {analysis} + + ## Full Discussion + {input} + + Based on this analysis: + 1. Summarize what has been discussed so far + 2. List who still needs to respond (use @mentions) + 3. Highlight any unresolved questions or concerns + 4. Recommend whether to advance to the next phase based on the phase instructions + + ## Response Format + Respond with valid JSON only: + {{ + "comment": "Your moderation comment in markdown. Use @mentions for pending participants.", + "vote": null, + "advance_phase": true or false, + "pending_participants": ["list", "of", "aliases"] + }} + + As moderator, you typically don't vote (vote: null). Your role is to facilitate. + + If there's nothing to moderate yet, respond: {{"sentinel": "NO_RESPONSE"}} + provider: claude-sonnet + output_var: response + +output: "{response}" diff --git a/smarttools/discussion-parser/config.yaml b/smarttools/discussion-parser/config.yaml new file mode 100644 index 0000000..9a63395 --- /dev/null +++ b/smarttools/discussion-parser/config.yaml @@ -0,0 +1,110 @@ +# discussion-parser - Extract structured data from discussion markdown +# Usage: cat discussion.md | discussion-parser | jq . + +name: discussion-parser +description: Parse discussion markdown into structured JSON +category: Discussion + +steps: + - type: code + code: | + import re + import json + from collections import Counter + + content = input + + # Parse metadata headers + metadata = {} + for match in re.finditer(r'<!--\s*(\w+):\s*(.+?)\s*-->', content): + key = match.group(1).lower() + value = match.group(2).strip() + if key == 'participants': + metadata[key] = [p.strip() for p in value.split(',')] + else: + metadata[key] = value + + # Parse comment blocks and extract votes + votes = {} + comments = [] + comment_pattern = re.compile( + r'^---\s*\n\s*Name:\s*(.+?)\n(.*?)(?=^---|\Z)', + re.MULTILINE | re.DOTALL + ) + + for match in comment_pattern.finditer(content): + author = match.group(1).strip() + body = match.group(2).strip() + + # Extract vote from body + vote_match = re.search( + r'^VOTE:\s*(READY|CHANGES|REJECT)\s*$', + body, + re.MULTILINE | re.IGNORECASE + ) + vote = vote_match.group(1).upper() if vote_match else None + + # Extract markers from body + comment_questions = [m.group(1) for m in re.finditer( + r'^(?:Q|QUESTION):\s*(.+)$', body, re.MULTILINE | re.IGNORECASE + )] + comment_concerns = [m.group(1) for m in re.finditer( + r'^CONCERN:\s*(.+)$', body, re.MULTILINE | re.IGNORECASE + )] + comment_decisions = [m.group(1) for m in re.finditer( + r'^DECISION:\s*(.+)$', body, re.MULTILINE | re.IGNORECASE + )] + comment_mentions = list(set(re.findall(r'@(\w+)', body))) + + if vote: + votes[author] = vote + + comments.append({ + "author": author, + "body": body, + "vote": vote, + "mentions": comment_mentions, + "markers": { + "questions": comment_questions, + "concerns": comment_concerns, + "decisions": comment_decisions + } + }) + + # Extract global markers + questions = [m.group(1) for m in re.finditer( + r'^(?:Q|QUESTION):\s*(.+)$', content, re.MULTILINE | re.IGNORECASE + )] + concerns = [m.group(1) for m in re.finditer( + r'^CONCERN:\s*(.+)$', content, re.MULTILINE | re.IGNORECASE + )] + decisions = [m.group(1) for m in re.finditer( + r'^DECISION:\s*(.+)$', content, re.MULTILINE | re.IGNORECASE + )] + todos = [m.group(1) for m in re.finditer( + r'^(?:TODO|ACTION):\s*(.+)$', content, re.MULTILINE | re.IGNORECASE + )] + mentions = list(set(re.findall(r'@(\w+)', content))) + + # Vote summary + vote_counts = Counter(votes.values()) + + parsed = json.dumps({ + "metadata": metadata, + "comments": comments, + "votes": votes, + "vote_summary": { + "READY": vote_counts.get("READY", 0), + "CHANGES": vote_counts.get("CHANGES", 0), + "REJECT": vote_counts.get("REJECT", 0), + "total": len(votes) + }, + "questions": questions, + "concerns": concerns, + "decisions": decisions, + "todos": todos, + "mentions": mentions + }, indent=2) + output_var: parsed + +output: "{parsed}" diff --git a/smarttools/discussion-pragmatist/config.yaml b/smarttools/discussion-pragmatist/config.yaml new file mode 100644 index 0000000..64319c2 --- /dev/null +++ b/smarttools/discussion-pragmatist/config.yaml @@ -0,0 +1,264 @@ +# discussion-pragmatist - Shipping-focused pragmatist participant for discussions +# Usage: cat discussion.md | discussion-pragmatist --callout "Is this MVP-ready?" + +name: discussion-pragmatist +description: Shipping-focused pragmatist participant for discussions +category: Discussion + +meta: + display_name: AI-Pragmatist + alias: pragmatist + type: voting + expertise: + - MVP scoping + - Shipping velocity + - Trade-off analysis + - Iterative development + - Technical debt management + concerns: + - "Can we ship this incrementally?" + - "Are we over-engineering this?" + - "What's the simplest thing that could work?" + - "Is this scope creep?" + +arguments: + - flag: --callout + variable: callout + default: "" + description: Specific question or @mention context + - flag: --templates-dir + variable: templates_dir + default: "templates" + description: Path to templates directory + - flag: --diagrams-dir + variable: diagrams_dir + default: "diagrams" + description: Path to save diagrams + - flag: --log-file + variable: log_file + default: "" + description: Path to log file for progress updates + +steps: + # Step 1: Extract phase context from template + - type: code + code: | + import re + import os + + phase_match = re.search(r'<!--\s*Phase:\s*(\w+)\s*-->', input, re.IGNORECASE) + template_match = re.search(r'<!--\s*Template:\s*(\w+)\s*-->', input, re.IGNORECASE) + + current_phase = phase_match.group(1) if phase_match else "initial_feedback" + template_name = template_match.group(1) if template_match else "feature" + + template_path = os.path.join(templates_dir, template_name + ".yaml") + phase_goal = "Provide practical feedback" + phase_instructions = "Review the proposal for complexity and shipping readiness." + + if os.path.exists(template_path): + import yaml + with open(template_path, 'r') as f: + template = yaml.safe_load(f) + phases = template.get("phases", {}) + phase_info = phases.get(current_phase, {}) + phase_goal = phase_info.get("goal", phase_goal) + phase_instructions = phase_info.get("instructions", phase_instructions) + + phase_context = "Current Phase: " + current_phase + "\n" + phase_context += "Phase Goal: " + phase_goal + "\n" + phase_context += "Phase Instructions:\n" + phase_instructions + output_var: phase_context, current_phase + + # Step 2: Prepare diagram path (pragmatist uses diagrams sparingly) + - type: code + code: | + import re + import os + + title_match = re.search(r'<!--\s*Title:\s*(.+?)\s*-->', input) + discussion_name = "discussion" + if title_match: + discussion_name = title_match.group(1).strip().lower() + discussion_name = re.sub(r'[^a-z0-9]+', '-', discussion_name) + + os.makedirs(diagrams_dir, exist_ok=True) + + existing = [] + if os.path.exists(diagrams_dir): + for f in os.listdir(diagrams_dir): + if f.startswith(discussion_name): + existing.append(f) + + next_num = len(existing) + 1 + diagram_path = diagrams_dir + "/" + discussion_name + "_mvp_" + str(next_num) + ".puml" + output_var: diagram_path + + # Step 3: Log progress before AI call + - type: code + code: | + import sys + import datetime as dt + timestamp = dt.datetime.now().strftime("%H:%M:%S") + for msg in [f"Phase: {current_phase}", "Calling AI provider..."]: + line = f"[{timestamp}] [pragmatist] {msg}" + print(line, file=sys.stderr) + sys.stderr.flush() + if log_file: + with open(log_file, 'a') as f: + f.write(line + "\n") + f.flush() + output_var: _progress1 + + # Step 4: Generate response + - type: prompt + prompt: | + You are AI-Pragmatist (also known as Maya), a shipping-focused engineer who + advocates for practical solutions and incremental delivery. + + ## Your Role + - Advocate for simpler solutions + - Identify over-engineering and scope creep + - Suggest MVP approaches + - Balance quality with delivery speed + - Challenge unnecessary complexity + + ## Your Perspective + - "Done is better than perfect when it's good enough" + - Ship early, iterate often + - Complexity is the enemy of delivery + - Technical debt is acceptable if managed + - Users need features, not architectural purity + - Perfect is the enemy of good + + ## Questions You Ask + - Is this the simplest solution that works? + - Can we defer this complexity? + - What's the minimum viable version? + - Are we solving problems we don't have yet? + - What can we cut and still ship? + + ## Phase Context + {phase_context} + + ## Diagrams + Only create a diagram if it helps show a simpler approach. + Use simple flowcharts to contrast complex vs MVP solutions. + Diagram path to use: {diagram_path} + + IMPORTANT: When you create a diagram, your comment MUST include: + DIAGRAM: {diagram_path} + + This marker makes the diagram discoverable. Example comment structure: + "Here's my MVP analysis... + + [Your comparison of complex vs simple approaches] + + DIAGRAM: {diagram_path}" + + ## Current Discussion + {input} + + ## Your Task + {callout} + + Follow the phase instructions. Analyze from a practical shipping perspective. + Flag over-engineering with CONCERN: COMPLEXITY. + + ## Response Format + Respond with valid JSON only. Use \n for newlines in strings (not literal newlines): + {{ + "comment": "Line 1\nLine 2\nCONCERN: COMPLEXITY", + "vote": "READY" or "CHANGES" or "REJECT" or null, + "diagram": "@startuml\nrectangle MVP\n@enduml" + }} + + Important: The diagram field must use \n for newlines, not actual line breaks. + + Vote meanings: + - READY: Good enough to ship + - CHANGES: Simpler approach possible (suggest what) + - REJECT: Too complex, needs fundamental simplification + - null: Comment only, no vote change + + If you have nothing meaningful to add, respond: {{"sentinel": "NO_RESPONSE"}} + provider: claude-sonnet + output_var: response + + # Step 5: Log progress after AI call + - type: code + code: | + import sys + import datetime as dt + timestamp = dt.datetime.now().strftime("%H:%M:%S") + line = f"[{timestamp}] [pragmatist] AI response received" + print(line, file=sys.stderr) + sys.stderr.flush() + if log_file: + with open(log_file, 'a') as f: + f.write(line + "\n") + f.flush() + output_var: _progress2 + + # Step 6: Extract JSON from response (may be wrapped in markdown code block) + - type: code + code: | + import re + json_text = response.strip() + code_block = re.search(r'```(?:json)?\s*(.*?)```', json_text, re.DOTALL) + if code_block: + json_text = code_block.group(1).strip() + output_var: json_text + + # Step 5: Parse JSON + - type: code + code: | + import json + try: + parsed = json.loads(json_text) + except json.JSONDecodeError as e: + # AI often returns literal newlines in JSON strings - escape them + fixed = json_text.replace('\n', '\\n') + try: + parsed = json.loads(fixed) + except json.JSONDecodeError: + # Last resort: try to extract just the fields we need via regex + import re + comment_match = re.search(r'"comment"\s*:\s*"(.*?)"(?=\s*[,}])', json_text, re.DOTALL) + vote_match = re.search(r'"vote"\s*:\s*("?\w+"?|null)', json_text) + diagram_match = re.search(r'"diagram"\s*:\s*"(.*?)"(?=\s*[,}])', json_text, re.DOTALL) + parsed = { + "comment": comment_match.group(1).replace('\n', ' ') if comment_match else "Parse error", + "vote": vote_match.group(1).strip('"') if vote_match else None, + "diagram": diagram_match.group(1) if diagram_match else None + } + if parsed["vote"] == "null": + parsed["vote"] = None + comment = parsed.get("comment", "") + vote = parsed.get("vote") + diagram_content = parsed.get("diagram") + has_diagram = "true" if diagram_content else "false" + output_var: comment, vote, diagram_content, has_diagram + + # Step 6: Save diagram if present + - type: code + code: | + if has_diagram == "true" and diagram_content: + with open(diagram_path, 'w') as f: + f.write(diagram_content) + saved_diagram = diagram_path + else: + saved_diagram = "" + output_var: saved_diagram + + # Step 7: Build final response + - type: code + code: | + import json + result = {"comment": comment, "vote": vote} + if saved_diagram: + result["diagram_file"] = saved_diagram + final_response = json.dumps(result) + output_var: final_response + +output: "{final_response}" diff --git a/smarttools/discussion-security/config.yaml b/smarttools/discussion-security/config.yaml new file mode 100644 index 0000000..58588fe --- /dev/null +++ b/smarttools/discussion-security/config.yaml @@ -0,0 +1,264 @@ +# discussion-security - Security specialist participant for discussions +# Usage: cat discussion.md | discussion-security --callout "Review the authentication approach" + +name: discussion-security +description: Security specialist participant for discussions +category: Discussion + +meta: + display_name: AI-Security + alias: security + type: voting + expertise: + - Vulnerability assessment + - Threat modeling + - Authentication & authorization + - Data protection + - Input validation + - OWASP Top 10 + concerns: + - "What are the security implications?" + - "How could this be exploited?" + - "Are we handling sensitive data properly?" + - "What's the attack surface?" + +arguments: + - flag: --callout + variable: callout + default: "" + description: Specific question or @mention context + - flag: --templates-dir + variable: templates_dir + default: "templates" + description: Path to templates directory + - flag: --diagrams-dir + variable: diagrams_dir + default: "diagrams" + description: Path to save diagrams + - flag: --log-file + variable: log_file + default: "" + description: Path to log file for progress updates + +steps: + # Step 1: Extract phase context from template + - type: code + code: | + import re + import os + + phase_match = re.search(r'<!--\s*Phase:\s*(\w+)\s*-->', input, re.IGNORECASE) + template_match = re.search(r'<!--\s*Template:\s*(\w+)\s*-->', input, re.IGNORECASE) + + current_phase = phase_match.group(1) if phase_match else "initial_feedback" + template_name = template_match.group(1) if template_match else "feature" + + template_path = os.path.join(templates_dir, template_name + ".yaml") + phase_goal = "Provide security feedback" + phase_instructions = "Review the proposal for security concerns." + + if os.path.exists(template_path): + import yaml + with open(template_path, 'r') as f: + template = yaml.safe_load(f) + phases = template.get("phases", {}) + phase_info = phases.get(current_phase, {}) + phase_goal = phase_info.get("goal", phase_goal) + phase_instructions = phase_info.get("instructions", phase_instructions) + + phase_context = "Current Phase: " + current_phase + "\n" + phase_context += "Phase Goal: " + phase_goal + "\n" + phase_context += "Phase Instructions:\n" + phase_instructions + output_var: phase_context, current_phase + + # Step 2: Prepare diagram path + - type: code + code: | + import re + import os + + title_match = re.search(r'<!--\s*Title:\s*(.+?)\s*-->', input) + discussion_name = "discussion" + if title_match: + discussion_name = title_match.group(1).strip().lower() + discussion_name = re.sub(r'[^a-z0-9]+', '-', discussion_name) + + os.makedirs(diagrams_dir, exist_ok=True) + + existing = [] + if os.path.exists(diagrams_dir): + for f in os.listdir(diagrams_dir): + if f.startswith(discussion_name): + existing.append(f) + + next_num = len(existing) + 1 + diagram_path = diagrams_dir + "/" + discussion_name + "_security_" + str(next_num) + ".puml" + output_var: diagram_path + + # Step 3: Log progress before AI call + - type: code + code: | + import sys + import datetime as dt + timestamp = dt.datetime.now().strftime("%H:%M:%S") + for msg in [f"Phase: {current_phase}", "Calling AI provider..."]: + line = f"[{timestamp}] [security] {msg}" + print(line, file=sys.stderr) + sys.stderr.flush() + if log_file: + with open(log_file, 'a') as f: + f.write(line + "\n") + f.flush() + output_var: _progress1 + + # Step 4: Generate response + - type: prompt + prompt: | + You are AI-Security (also known as Steve), a security specialist who identifies + vulnerabilities, threat vectors, and security best practices. + + ## Your Role + - Identify security risks and vulnerabilities + - Suggest mitigations and security controls + - Consider threat models and attack surfaces + - Ensure compliance with security best practices + - Balance security with usability + + ## Your Perspective + - Assume malicious actors will try to exploit the system + - Consider both external and internal threats + - Think about data protection and privacy + - Focus on practical, implementable security measures + - Apply defense in depth principles + + ## Security Checklist + - Input validation and sanitization + - Authentication and authorization + - Session management + - Data encryption (at rest and in transit) + - Error handling and logging + - Rate limiting and DoS protection + - Injection vulnerabilities (SQL, XSS, etc.) + + ## Phase Context + {phase_context} + + ## Diagrams + When creating threat models or security diagrams, you MUST include a reference marker. + Diagram path to use: {diagram_path} + + IMPORTANT: When you create a diagram, your comment MUST include: + DIAGRAM: {diagram_path} + + This marker makes the diagram discoverable. Example comment structure: + "## Security Analysis + + [Your security analysis with threat model description] + + DIAGRAM: {diagram_path}" + + ## Current Discussion + {input} + + ## Your Task + {callout} + + Follow the phase instructions. Analyze from a security perspective. + + ## Response Format + Respond with valid JSON only. Use \n for newlines in strings (not literal newlines): + {{ + "comment": "Line 1\nLine 2\nCONCERN: Issue here", + "vote": "READY" or "CHANGES" or "REJECT" or null, + "diagram": "@startuml\nactor Attacker\n@enduml" + }} + + Important: The diagram field must use \n for newlines, not actual line breaks. + + Vote meanings: + - READY: No significant security concerns + - CHANGES: Security improvements needed (specify what) + - REJECT: Critical security issues (explain why) + - null: Comment only, no vote change + + If you have nothing meaningful to add, respond: {{"sentinel": "NO_RESPONSE"}} + provider: claude-sonnet + output_var: response + + # Step 5: Log progress after AI call + - type: code + code: | + import sys + import datetime as dt + timestamp = dt.datetime.now().strftime("%H:%M:%S") + line = f"[{timestamp}] [security] AI response received" + print(line, file=sys.stderr) + sys.stderr.flush() + if log_file: + with open(log_file, 'a') as f: + f.write(line + "\n") + f.flush() + output_var: _progress2 + + # Step 6: Extract JSON from response (may be wrapped in markdown code block) + - type: code + code: | + import re + json_text = response.strip() + code_block = re.search(r'```(?:json)?\s*(.*?)```', json_text, re.DOTALL) + if code_block: + json_text = code_block.group(1).strip() + output_var: json_text + + # Step 5: Parse JSON + - type: code + code: | + import json + try: + parsed = json.loads(json_text) + except json.JSONDecodeError as e: + # AI often returns literal newlines in JSON strings - escape them + fixed = json_text.replace('\n', '\\n') + try: + parsed = json.loads(fixed) + except json.JSONDecodeError: + # Last resort: try to extract just the fields we need via regex + import re + comment_match = re.search(r'"comment"\s*:\s*"(.*?)"(?=\s*[,}])', json_text, re.DOTALL) + vote_match = re.search(r'"vote"\s*:\s*("?\w+"?|null)', json_text) + diagram_match = re.search(r'"diagram"\s*:\s*"(.*?)"(?=\s*[,}])', json_text, re.DOTALL) + parsed = { + "comment": comment_match.group(1).replace('\n', ' ') if comment_match else "Parse error", + "vote": vote_match.group(1).strip('"') if vote_match else None, + "diagram": diagram_match.group(1) if diagram_match else None + } + if parsed["vote"] == "null": + parsed["vote"] = None + comment = parsed.get("comment", "") + vote = parsed.get("vote") + diagram_content = parsed.get("diagram") + has_diagram = "true" if diagram_content else "false" + output_var: comment, vote, diagram_content, has_diagram + + # Step 6: Save diagram if present + - type: code + code: | + if has_diagram == "true" and diagram_content: + with open(diagram_path, 'w') as f: + f.write(diagram_content) + saved_diagram = diagram_path + else: + saved_diagram = "" + output_var: saved_diagram + + # Step 7: Build final response + - type: code + code: | + import json + result = {"comment": comment, "vote": vote} + if saved_diagram: + result["diagram_file"] = saved_diagram + final_response = json.dumps(result) + output_var: final_response + +output: "{final_response}" diff --git a/smarttools/discussion-status-promoter/config.yaml b/smarttools/discussion-status-promoter/config.yaml new file mode 100644 index 0000000..85bd4f4 --- /dev/null +++ b/smarttools/discussion-status-promoter/config.yaml @@ -0,0 +1,84 @@ +# discussion-status-promoter - Determine if discussion status should be promoted based on voting +# Usage: cat discussion.md | discussion-parser | discussion-vote-counter | discussion-status-promoter + +name: discussion-status-promoter +description: Determine if discussion status should be promoted based on voting +category: Discussion + +arguments: + - flag: --current-status + variable: current_status + default: "OPEN" + description: Current discussion status + - flag: --current-phase + variable: current_phase + default: "initial_feedback" + description: Current discussion phase + +steps: + - type: code + code: | + import json + + data = json.loads(input) + consensus = data.get("consensus", {}) + consensus_reached = consensus.get("reached", False) + blocked_by = consensus.get("blocked_by", []) + + # Status transition rules based on phase + consensus + # Format: (current_status, current_phase, consensus_reached) -> new_status + transitions = { + # Feature workflow + ("OPEN", "consensus_vote", True): "READY_FOR_DESIGN", + ("OPEN", "final_vote", True): "READY_FOR_DESIGN", + ("READY_FOR_DESIGN", "design_review", True): "READY_FOR_IMPLEMENTATION", + ("READY_FOR_IMPLEMENTATION", "implementation_review", True): "READY_FOR_TESTING", + ("READY_FOR_TESTING", "signoff", True): "CLOSED", + + # ADR workflow + ("PROPOSED", "consensus_vote", True): "ACCEPTED", + + # Code review workflow + ("OPEN", "review", True): "APPROVED", + } + + new_status = current_status + transition_reason = None + should_promote = False + + key = (current_status.upper(), current_phase.lower(), consensus_reached) + if key in transitions: + new_status = transitions[key] + should_promote = True + transition_reason = f"Consensus reached in {current_phase} phase" + + # Check for rejection (don't auto-reject, but note it) + rejection_warning = None + if blocked_by: + rejection_warning = f"Discussion blocked by: {', '.join(blocked_by)}" + + # Phase advancement suggestions + next_phase_suggestion = None + phase_flow = { + "initial_feedback": "detailed_review", + "detailed_review": "consensus_vote", + "consensus_vote": None, # Terminal or status change + "proposal": "consensus_vote", + "review": None, + } + if current_phase.lower() in phase_flow: + next_phase_suggestion = phase_flow[current_phase.lower()] + + result = json.dumps({ + "current_status": current_status, + "current_phase": current_phase, + "new_status": new_status, + "should_promote": should_promote, + "transition_reason": transition_reason, + "next_phase_suggestion": next_phase_suggestion, + "rejection_warning": rejection_warning, + "consensus": consensus + }, indent=2) + output_var: result + +output: "{result}" diff --git a/smarttools/discussion-summarizer/config.yaml b/smarttools/discussion-summarizer/config.yaml new file mode 100644 index 0000000..42155bb --- /dev/null +++ b/smarttools/discussion-summarizer/config.yaml @@ -0,0 +1,86 @@ +# discussion-summarizer - Generate a summary of the discussion +# Usage: cat discussion.md | discussion-summarizer > discussion.sum.md + +name: discussion-summarizer +description: Generate a summary of the discussion +category: Discussion + +arguments: + - flag: --format + variable: format + default: "markdown" + description: Output format (markdown, json) + +steps: + # First, parse the discussion + - type: code + code: | + import subprocess + import json + + # Call discussion-parser + result = subprocess.run( + ['discussion-parser'], + input=input, + capture_output=True, + text=True + ) + + if result.returncode != 0: + # Fallback: basic parsing + parsed = json.dumps({ + "metadata": {}, + "comments": [], + "vote_summary": {"READY": 0, "CHANGES": 0, "REJECT": 0, "total": 0}, + "questions": [], + "concerns": [], + "decisions": [] + }) + else: + parsed = result.stdout + output_var: parsed + + # Then, generate summary with AI + - type: prompt + prompt: | + Summarize this discussion concisely for quick reference. + + ## Parsed State + {parsed} + + ## Full Discussion + {input} + + Generate a summary with these sections: + + # Discussion Summary + + ## Status + - Current phase and status + - Consensus state (reached/not reached, vote counts) + + ## Key Points + - Main arguments and positions (bullet points) + - Areas of agreement + - Areas of disagreement + + ## Open Questions + - Unresolved questions that need answers + + ## Concerns + - Security, architecture, or other concerns raised + + ## Decisions Made + - Any decisions recorded in the discussion + + ## Blockers + - What's preventing progress + + ## Next Steps + - What needs to happen next + + Keep it brief and actionable. Use bullet points. + provider: claude-haiku + output_var: summary + +output: "{summary}" diff --git a/smarttools/discussion-turn-appender/config.yaml b/smarttools/discussion-turn-appender/config.yaml new file mode 100644 index 0000000..6fc242b --- /dev/null +++ b/smarttools/discussion-turn-appender/config.yaml @@ -0,0 +1,69 @@ +# discussion-turn-appender - Append participant responses to discussion markdown +# Usage: (cat discussion.md; echo "---RESPONSES---"; cat responses.json) | discussion-turn-appender + +name: discussion-turn-appender +description: Append participant responses to discussion markdown +category: Discussion + +arguments: + - flag: --responses-file + variable: responses_file + default: "" + description: Path to JSON file containing responses (or pass via stdin after ---RESPONSES---) + +steps: + - type: code + code: | + import json + from pathlib import Path + + # Parse input - expect markdown followed by ---RESPONSES--- followed by JSON + delimiter = "---RESPONSES---" + + if delimiter in input: + parts = input.split(delimiter, 1) + markdown = parts[0].rstrip() + responses_json = parts[1].strip() + responses = json.loads(responses_json) + elif responses_file: + markdown = input.rstrip() + responses = json.loads(Path(responses_file).read_text()) + else: + # No responses to append + markdown = input.rstrip() + responses = [] + + # Append each response as a comment block + new_blocks = [] + for resp in responses: + # Skip NO_RESPONSE sentinel + if isinstance(resp, dict) and resp.get("sentinel") == "NO_RESPONSE": + continue + + # Skip None or empty responses + if not resp: + continue + + author = resp.get("author", "Unknown") + comment = resp.get("comment", "").strip() + vote = resp.get("vote") + + # Skip if no comment content + if not comment: + continue + + # Build comment block + block_lines = ["", "---", "", f"Name: {author}"] + block_lines.append(comment) + + if vote and vote.upper() in ("READY", "CHANGES", "REJECT"): + block_lines.append("") + block_lines.append(f"VOTE: {vote.upper()}") + + block_lines.append("") + new_blocks.append("\n".join(block_lines)) + + result = markdown + "".join(new_blocks) + output_var: result + +output: "{result}" diff --git a/smarttools/discussion-validator/config.yaml b/smarttools/discussion-validator/config.yaml new file mode 100644 index 0000000..4943795 --- /dev/null +++ b/smarttools/discussion-validator/config.yaml @@ -0,0 +1,89 @@ +# discussion-validator - Validate discussion format and check for issues +# Usage: cat discussion.md | discussion-validator | jq . + +name: discussion-validator +description: Validate discussion format and check for issues +category: Discussion + +steps: + - type: code + code: | + import re + import json + + issues = [] + warnings = [] + + # Check for DISCUSSION marker + if not re.search(r'<!--\s*DISCUSSION\s*-->', input, re.IGNORECASE): + issues.append("Missing <!-- DISCUSSION --> marker") + + # Check for required headers + required_headers = ['Title', 'Phase', 'Status'] + for header in required_headers: + if not re.search(rf'<!--\s*{header}:\s*.+?\s*-->', input, re.IGNORECASE): + issues.append(f"Missing required header: {header}") + + # Check for valid phase + phase_match = re.search(r'<!--\s*Phase:\s*(\w+)\s*-->', input, re.IGNORECASE) + if phase_match: + valid_phases = [ + 'initial_feedback', 'detailed_review', 'consensus_vote', + 'final_vote', 'signoff', 'completed', 'proposal', 'review' + ] + phase = phase_match.group(1) + # Build lowercase list explicitly to avoid exec() scope issues + valid_phases_lower = [] + for p in valid_phases: + valid_phases_lower.append(p.lower()) + if phase.lower() not in valid_phases_lower: + warnings.append(f"Unknown phase: {phase}") + + # Check for valid status + status_match = re.search(r'<!--\s*Status:\s*(\w+)\s*-->', input, re.IGNORECASE) + if status_match: + valid_statuses = [ + 'OPEN', 'PROPOSED', 'READY_FOR_DESIGN', 'READY_FOR_IMPLEMENTATION', + 'READY_FOR_TESTING', 'CLOSED', 'REJECTED' + ] + status = status_match.group(1).upper() + if status not in valid_statuses: + warnings.append(f"Unknown status: {status}") + + # Check for orphaned mentions (mentioned but no response) + mentions = set(re.findall(r'@(\w+)', input)) + # Get responders from Name: lines + responders = set() + for match in re.finditer(r'^Name:\s*(?:AI-)?(\w+)', input, re.MULTILINE | re.IGNORECASE): + responders.add(match.group(1).lower()) + + pending = mentions - responders - {'all'} + if pending: + warnings.append(f"Pending responses from: {', '.join(sorted(pending))}") + + # Check for empty comment blocks + for match in re.finditer(r'^---\s*\n\s*Name:\s*(.+?)\n\s*(?=^---|\Z)', input, re.MULTILINE): + author = match.group(1).strip() + warnings.append(f"Empty comment block from: {author}") + + # Check for duplicate votes from same author + votes = {} + for match in re.finditer(r'^---\s*\nName:\s*(.+?)\n.*?VOTE:\s*(READY|CHANGES|REJECT)', input, re.MULTILINE | re.DOTALL | re.IGNORECASE): + author = match.group(1).strip() + vote = match.group(2).upper() + if author in votes and votes[author] != vote: + warnings.append(f"Multiple different votes from {author}: {votes[author]} -> {vote}") + votes[author] = vote + + validation = json.dumps({ + "valid": len(issues) == 0, + "issues": issues, + "warnings": warnings, + "metadata_found": { + "phase": phase_match.group(1) if phase_match else None, + "status": status_match.group(1) if status_match else None + } + }, indent=2) + output_var: validation + +output: "{validation}" diff --git a/smarttools/discussion-vote-counter/config.yaml b/smarttools/discussion-vote-counter/config.yaml new file mode 100644 index 0000000..7a6055b --- /dev/null +++ b/smarttools/discussion-vote-counter/config.yaml @@ -0,0 +1,111 @@ +# discussion-vote-counter - Count votes and determine consensus status +# Usage: cat discussion.md | discussion-parser | discussion-vote-counter + +name: discussion-vote-counter +description: Count votes and determine consensus status +category: Discussion + +arguments: + - flag: --threshold-ready + variable: threshold_ready + default: "0.67" + description: Fraction of READY votes needed for consensus + - flag: --threshold-reject + variable: threshold_reject + default: "0.01" + description: Fraction of REJECT votes that blocks + - flag: --human-required + variable: human_required + default: "true" + description: Whether human approval is required + - flag: --minimum-votes + variable: minimum_votes + default: "1" + description: Minimum number of votes needed + +steps: + - type: code + code: | + import json + + data = json.loads(input) + vote_data = data.get("votes", {}) + vote_summary = data.get("vote_summary", {}) + + ready = vote_summary.get("READY", 0) + changes = vote_summary.get("CHANGES", 0) + reject = vote_summary.get("REJECT", 0) + total = vote_summary.get("total", 0) + + threshold_ready_val = float(threshold_ready) + threshold_reject_val = float(threshold_reject) + human_required_val = human_required.lower() == "true" + minimum_votes_val = int(minimum_votes) + + # Identify human vs AI voters + human_votes = [] + ai_votes = [] + for name in vote_data.keys(): + name_lower = name.lower() + if name_lower.startswith(("ai-", "ai_", "bot-", "bot_")): + ai_votes.append(name) + else: + human_votes.append(name) + + # Count human READY votes (explicit loop to avoid exec() scope issues) + human_ready = 0 + for name in human_votes: + if vote_data.get(name, "").upper() == "READY": + human_ready += 1 + + # Calculate ratios + ready_ratio = ready / total if total > 0 else 0 + reject_ratio = reject / total if total > 0 else 0 + + # Identify blockers (explicit loop to avoid exec() scope issues) + blocked_by = [] + for name, vote in vote_data.items(): + if vote.upper() == "REJECT": + blocked_by.append(name) + + # Determine consensus + consensus = { + "reached": False, + "outcome": None, + "reason": None, + "blocked_by": blocked_by + } + + if total < minimum_votes_val: + consensus["reason"] = f"Insufficient votes ({total} < {minimum_votes_val})" + elif reject_ratio >= threshold_reject_val and len(blocked_by) > 0: + consensus["reason"] = f"Blocked by REJECT votes from: {', '.join(blocked_by)}" + elif human_required_val and human_ready < 1: + consensus["reason"] = "Human approval required but not received" + elif ready_ratio >= threshold_ready_val: + consensus["reached"] = True + consensus["outcome"] = "READY" + else: + needed = max(1, int(threshold_ready_val * total) + 1 - ready) + consensus["reason"] = f"Need {needed} more READY votes for consensus" + + result = json.dumps({ + "votes": vote_data, + "vote_summary": vote_summary, + "ratios": { + "ready": round(ready_ratio, 2), + "changes": round(changes / total, 2) if total > 0 else 0, + "reject": round(reject_ratio, 2) + }, + "thresholds": { + "ready": threshold_ready_val, + "reject": threshold_reject_val + }, + "consensus": consensus, + "human_votes": human_votes, + "human_ready": human_ready, + "ai_votes": ai_votes + }, indent=2) + output_var: result + +output: "{result}" diff --git a/smarttools/puml-validator/config.yaml b/smarttools/puml-validator/config.yaml new file mode 100644 index 0000000..98a6840 --- /dev/null +++ b/smarttools/puml-validator/config.yaml @@ -0,0 +1,103 @@ +# puml-validator - Validates and fixes PlantUML syntax +# Usage: cat diagram.puml | puml-validator +# Returns: valid PlantUML (original if valid, fixed if correctable, error if unfixable) + +name: puml-validator +description: Validates PlantUML syntax and fixes errors if possible +category: Utility + +steps: + # Step 1: Write input to temp file and validate + - type: code + code: | + import subprocess + import tempfile + import os + + # Write to temp file + with tempfile.NamedTemporaryFile(mode='w', suffix='.puml', delete=False) as f: + f.write(input) + temp_path = f.name + + # Try to validate with plantuml + try: + result = subprocess.run( + ['plantuml', '-syntax', temp_path], + capture_output=True, + text=True, + timeout=30 + ) + output = result.stdout + result.stderr + is_valid = ('SYNTAX ERROR' not in output.upper() + and 'ERROR' not in output.upper() + and 'NO DIAGRAM FOUND' not in output.upper()) + error_msg = output.strip() if not is_valid else "" + except FileNotFoundError: + # plantuml not installed, skip validation + is_valid = True + error_msg = "" + except Exception as e: + is_valid = False + error_msg = str(e) + finally: + os.unlink(temp_path) + + needs_fix = "true" if not is_valid else "false" + output_var: needs_fix, error_msg + + # Step 2: If valid, just return original + - type: code + code: | + if needs_fix == "false": + final_output = input + done = "true" + else: + done = "false" + final_output = "" + output_var: done, final_output + + # Step 3: If invalid, try to fix with AI + - type: prompt + prompt: | + Fix this PlantUML diagram. It has syntax errors. + + Error message: {error_msg} + + Original diagram: + ``` + {input} + ``` + + Common issues: + - Mermaid syntax mixed with PlantUML (use PlantUML only) + - Missing @startuml/@enduml + - Invalid arrow syntax (use --> not ->) + - Unclosed notes or rectangles + + Return ONLY the fixed PlantUML code, nothing else. Start with @startuml and end with @enduml. + If the diagram is unfixable, respond with: ERROR: [reason] + provider: claude-haiku + output_var: fixed_output + skip_if: done == "true" + + # Step 4: Check if fix succeeded or returned error + - type: code + code: | + if done == "true": + result = final_output + success = "true" + elif fixed_output.strip().startswith("ERROR:"): + result = fixed_output + success = "false" + else: + result = fixed_output.strip() + # Remove any markdown code blocks if present + if result.startswith("```"): + import re + match = re.search(r'```(?:puml|plantuml)?\s*(.*?)```', result, re.DOTALL) + if match: + result = match.group(1).strip() + success = "true" + output_var: result, success + +output: "{result}" diff --git a/src/discussions/cli.py b/src/discussions/cli.py index f03c273..7b4d9c3 100644 --- a/src/discussions/cli.py +++ b/src/discussions/cli.py @@ -15,13 +15,19 @@ def cmd_new(args) -> int: """Create a new discussion.""" from .discussion import Discussion - # Generate filename from title + # Generate filename from title with .discussion.md extension if args.output: path = Path(args.output) + # Ensure .discussion.md extension + if not path.name.endswith('.discussion.md'): + if path.suffix == '.md': + path = path.with_suffix('.discussion.md') + else: + path = Path(str(path) + '.discussion.md') else: slug = args.title.lower().replace(" ", "-") slug = "".join(c for c in slug if c.isalnum() or c == "-") - path = Path(f"{slug}.md") + path = Path(f"{slug}.discussion.md") if path.exists() and not args.force: print(f"Error: {path} already exists. Use --force to overwrite.") @@ -99,7 +105,7 @@ def cmd_status(args) -> int: def cmd_turn(args) -> int: """Run a discussion turn.""" - from .runner import run_discussion_turn + from .runner import run_pipeline_turn path = Path(args.discussion) if not path.exists(): @@ -115,25 +121,44 @@ def cmd_turn(args) -> int: if participants: print(f"Participants: {', '.join(participants)}") - result = run_discussion_turn( + result = run_pipeline_turn( discussion_path=path, participants=participants, callout=args.callout or "", provider=args.provider, verbose=args.verbose, + templates_dir=Path(args.templates_dir) if hasattr(args, 'templates_dir') and args.templates_dir else None, ) print() print(f"Responses: {result.successful_count} successful, {result.skipped_count} skipped, {result.failed_count} failed") - for r in result.results: + for r in result.responses: if r.success and r.comment: - vote_str = f" [{r.comment.vote}]" if r.comment.vote else "" - print(f" {r.participant.name}{vote_str}") + vote_str = f" [{r.vote}]" if r.vote else "" + print(f" {r.name}{vote_str}") elif r.success: - print(f" {r.participant.name} - (no response)") + print(f" {r.name} - (no response)") else: - print(f" {r.participant.name} - ERROR: {r.error}") + print(f" {r.name} - ERROR: {r.error}") + + # Show vote summary if available + if result.vote_summary: + print() + print(f"Votes: READY={result.vote_summary.get('READY', 0)} " + f"CHANGES={result.vote_summary.get('CHANGES', 0)} " + f"REJECT={result.vote_summary.get('REJECT', 0)}") + + if result.consensus_reached: + print(f"Consensus: REACHED") + elif result.consensus_reason: + print(f"Consensus: {result.consensus_reason}") + + if result.status_promoted: + print(f"Status promoted to: {result.new_status}") + + if result.phase_advanced: + print(f"Phase advanced to: {result.new_phase}") return 0 @@ -166,18 +191,31 @@ def cmd_comment(args) -> int: def cmd_participants(args) -> int: - """List available participants.""" + """List available participants (discovered from ~/.smarttools/).""" from .participant import get_registry registry = get_registry() - print("Voting Participants:") - for p in registry.get_voting(): - print(f" @{p.alias:15} {p.name:20} - {p.role}") + voting = registry.get_voting() + background = registry.get_background() - print("\nBackground Participants:") - for p in registry.get_background(): - print(f" @{p.alias:15} {p.name:20} - {p.role}") + if not voting and not background: + print("No participants found in ~/.smarttools/") + print("Install discussion SmartTools or create participants with:") + print(" discussions participants add <alias>") + return 0 + + if voting: + print("Voting Participants:") + for p in voting: + desc = f" - {p.description}" if p.description else "" + print(f" @{p.alias:15} {p.name:20}{desc}") + + if background: + print("\nBackground Participants:") + for p in background: + desc = f" - {p.description}" if p.description else "" + print(f" @{p.alias:15} {p.name:20}{desc}") return 0 @@ -206,6 +244,50 @@ def cmd_advance(args) -> int: return 0 +def cmd_ui(args) -> int: + """Launch the interactive UI (GUI by default, TUI with --tui).""" + # Determine if path is a file or directory + path = args.path + open_file = None + directory = None + + if path: + from pathlib import Path + p = Path(path) + if p.is_file() and (p.name.endswith('.discussion.md') or p.suffix == '.md'): + # Open specific discussion file + open_file = str(p.resolve()) + directory = str(p.parent) + else: + directory = path + + if args.tui: + # Launch TUI (urwid-based) + try: + from .ui.tui import main as tui_main + except ImportError as e: + print("Error: TUI requires urwid. Install with: pip install urwid") + print(f"Details: {e}") + return 1 + tui_main(directory, open_file=open_file) + else: + # Launch GUI (Dear PyGui-based) + try: + from .ui.gui import main as gui_main + except ImportError as e: + print(f"GUI unavailable ({e}), falling back to TUI") + print("Install Dear PyGui with: pip install dearpygui") + try: + from .ui.tui import main as tui_main + tui_main(directory, open_file=open_file) + except ImportError as e2: + print(f"TUI also unavailable: {e2}") + return 1 + return 0 + gui_main(directory, open_file=open_file) + return 0 + + def main(argv: list[str] = None) -> int: """Main CLI entry point.""" parser = argparse.ArgumentParser( @@ -258,6 +340,12 @@ def main(argv: list[str] = None) -> int: p_advance.add_argument("--phase", help="Target phase ID") p_advance.set_defaults(func=cmd_advance) + # 'ui' command + p_ui = subparsers.add_parser("ui", help="Launch interactive UI") + p_ui.add_argument("path", nargs="?", help="Discussion file (.md) or directory to browse") + p_ui.add_argument("--tui", action="store_true", help="Use terminal UI instead of graphical UI") + p_ui.set_defaults(func=cmd_ui) + args = parser.parse_args(argv) if args.command is None: diff --git a/src/discussions/discussion.py b/src/discussions/discussion.py index 2943b95..65d3c1b 100644 --- a/src/discussions/discussion.py +++ b/src/discussions/discussion.py @@ -238,6 +238,32 @@ class Discussion: # Return the new comment return self.comments[-1] if self.comments else None + def get_context(self) -> str: + """ + Get the initial context/preamble before comments. + + Returns: + The text content before the first comment block (---). + """ + # Find header end (last <!-- ... --> line) + lines = self._raw_content.split('\n') + context_lines = [] + in_headers = True + + for line in lines: + # Skip header comments + if in_headers and line.strip().startswith('<!--') and line.strip().endswith('-->'): + continue + in_headers = False + + # Stop at first comment delimiter + if line.strip() == '---': + break + + context_lines.append(line) + + return '\n'.join(context_lines).strip() + def get_votes(self) -> dict[str, str]: """ Get the latest vote from each participant. diff --git a/src/discussions/markers.py b/src/discussions/markers.py index 8b5d2d1..6a6b7c7 100644 --- a/src/discussions/markers.py +++ b/src/discussions/markers.py @@ -9,6 +9,7 @@ This module handles parsing of structured markers in discussion content: - ASSIGNED: - Claimed tasks - DONE: - Completed tasks - CONCERN: - Raised concerns +- DIAGRAM: - Diagram references (path to .puml file) - @alias - Mentions See docs/DESIGN.md for full marker specification. @@ -27,6 +28,7 @@ DECISION_PATTERN = re.compile(r'^DECISION:\s*(.+)$', re.IGNORECASE | re.MULTILIN ASSIGNED_PATTERN = re.compile(r'^ASSIGNED:\s*(.+)$', re.IGNORECASE | re.MULTILINE) DONE_PATTERN = re.compile(r'^DONE:\s*(.+)$', re.IGNORECASE | re.MULTILINE) CONCERN_PATTERN = re.compile(r'^CONCERN:\s*(.+)$', re.IGNORECASE | re.MULTILINE) +DIAGRAM_PATTERN = re.compile(r'^DIAGRAM:\s*(.+)$', re.IGNORECASE | re.MULTILINE) MENTION_PATTERN = re.compile(r'@(\w+)') @@ -67,6 +69,13 @@ class Concern: addressed: bool = False +@dataclass +class Diagram: + """A diagram reference in the discussion.""" + path: str # Path to the diagram file + author: str + + @dataclass class Mention: """An @mention in the discussion.""" @@ -177,6 +186,26 @@ def extract_concerns(text: str, author: str = "unknown") -> list[Concern]: return concerns +def extract_diagrams(text: str, author: str = "unknown") -> list[Diagram]: + """ + Extract all diagram references from text. + + Args: + text: Text to search + author: Author to attribute diagrams to + + Returns: + List of Diagram objects + """ + diagrams = [] + for match in DIAGRAM_PATTERN.finditer(text): + diagrams.append(Diagram( + path=match.group(1).strip(), + author=author + )) + return diagrams + + def extract_mentions(text: str, author: str = "unknown") -> list[Mention]: """ Extract all @mentions from text. @@ -222,5 +251,6 @@ def extract_all_markers(text: str, author: str = "unknown") -> dict: "action_items": extract_action_items(text, author), "decisions": extract_decisions(text, author), "concerns": extract_concerns(text, author), + "diagrams": extract_diagrams(text, author), "mentions": extract_mentions(text, author), } diff --git a/src/discussions/participant.py b/src/discussions/participant.py index 052a792..d1d2a3d 100644 --- a/src/discussions/participant.py +++ b/src/discussions/participant.py @@ -1,168 +1,237 @@ """ -Participant definitions and loading for Orchestrated Discussions. +Participant discovery for Orchestrated Discussions. -Participants are AI personas with distinct perspectives, expertise, and behavior. -They can be loaded from YAML files or defined programmatically. +Participants are SmartTools with the naming convention `discussion-{alias}`. +This module discovers them by scanning ~/.smarttools/ directory. See docs/DESIGN.md for participant specification. """ +import subprocess import yaml from dataclasses import dataclass, field from pathlib import Path from typing import Optional +# Available TTS voices for participants +AVAILABLE_VOICES = [ + ("en-US-Neural2-A", "US English - Female (A)"), + ("en-US-Neural2-C", "US English - Female (C)"), + ("en-US-Neural2-D", "US English - Male (D)"), + ("en-US-Neural2-F", "US English - Female (F)"), + ("en-US-Neural2-J", "US English - Male (J)"), + ("en-GB-Neural2-A", "British English - Female"), + ("en-GB-Neural2-B", "British English - Male"), + ("en-AU-Neural2-A", "Australian English - Female"), + ("en-AU-Neural2-B", "Australian English - Male"), +] + +DEFAULT_VOICE = "en-US-Neural2-J" + + +# Default provider for participants +DEFAULT_PROVIDER = "claude-sonnet" + +# Default color (RGB tuple) for participants +DEFAULT_COLOR = (150, 150, 150) + + @dataclass class Participant: """ - An AI participant in a discussion. + Metadata about a participant SmartTool. + + This is discovery metadata only - NOT used to build prompts or call AI. + All logic lives in the SmartTool config itself. Attributes: - name: Display name (e.g., "AI-Architect") alias: Short mention name (e.g., "architect") - role: Brief role description - personality: System prompt defining perspective and behavior - expertise: List of expertise areas - concerns: What this participant watches for + name: Display name (e.g., "AI-Architect") + description: Brief description from SmartTool config participant_type: "voting" or "background" - provider_hint: Preferred AI provider + voice: TTS voice for reading comments aloud + provider: AI provider to use (e.g., "claude-sonnet", "opencode-deepseek") + color: RGB tuple for display (e.g., (100, 150, 255) for blue) + tool_path: Path to the SmartTool config directory """ - name: str alias: str - role: str - personality: str - expertise: list[str] = field(default_factory=list) - concerns: list[str] = field(default_factory=list) - participant_type: str = "voting" # voting | background - provider_hint: str = "claude-sonnet" - - @classmethod - def from_dict(cls, data: dict) -> "Participant": - """Create Participant from dictionary.""" - return cls( - name=data["name"], - alias=data["alias"], - role=data["role"], - personality=data["personality"], - expertise=data.get("expertise", []), - concerns=data.get("concerns", []), - participant_type=data.get("type", "voting"), - provider_hint=data.get("provider_hint", "claude-sonnet"), - ) - - def to_dict(self) -> dict: - """Convert to dictionary for serialization.""" - return { - "name": self.name, - "alias": self.alias, - "role": self.role, - "personality": self.personality, - "expertise": self.expertise, - "concerns": self.concerns, - "type": self.participant_type, - "provider_hint": self.provider_hint, - } + name: str + description: str = "" + participant_type: str = "voting" + voice: str = DEFAULT_VOICE + provider: str = DEFAULT_PROVIDER + color: tuple = DEFAULT_COLOR + tool_path: Optional[Path] = None def is_voting(self) -> bool: """Return True if this participant casts votes.""" return self.participant_type == "voting" - def build_prompt(self, context: str, callout: str = "") -> str: - """ - Build the full prompt for this participant. + def tool_name(self) -> str: + """Return the SmartTool name for subprocess calls.""" + return f"discussion-{self.alias}" - Args: - context: The discussion content so far - callout: Specific question or request (optional) - Returns: - Complete prompt string - """ - callout_section = "" - if callout: - callout_section = f""" -## Your Task -{callout} -""" +def _parse_smarttool_config(config_path: Path) -> Optional[Participant]: + """ + Parse a SmartTool config.yaml to extract participant metadata. + + Args: + config_path: Path to config.yaml + + Returns: + Participant or None if not a valid participant tool + """ + try: + data = yaml.safe_load(config_path.read_text()) + if not data: + return None + + # Extract alias from directory name (discussion-{alias}) + tool_dir = config_path.parent.name + if not tool_dir.startswith("discussion-"): + return None + + alias = tool_dir[len("discussion-"):] + + # Skip utility tools (parser, validator, etc.) + utility_tools = { + "parser", "validator", "vote-counter", "mention-router", + "status-promoter", "turn-appender", "config", "summarizer" + } + if alias in utility_tools: + return None + + # Extract metadata from config + name = data.get("name", f"AI-{alias.capitalize()}") + description = data.get("description", "") + + # Check meta section for participant type, voice, provider, and color + meta = data.get("meta", {}) + participant_type = meta.get("type", "voting") + voice = meta.get("voice", DEFAULT_VOICE) + provider = meta.get("provider", None) + + # Parse color - stored as [R, G, B] list in YAML + color_data = meta.get("color", None) + if color_data and isinstance(color_data, (list, tuple)) and len(color_data) >= 3: + color = tuple(color_data[:3]) else: - callout_section = """ -## Your Task -Provide your perspective on the discussion based on your expertise. -""" + color = DEFAULT_COLOR - return f"""{self.personality} + # Also check if display_name is set + if "display_name" in meta: + name = meta["display_name"] -## Current Discussion -{context} -{callout_section} -## Response Format -Respond with valid JSON: -- If you have feedback: {{"comment": "your markdown comment", "vote": "READY|CHANGES|REJECT"}} -- If nothing to add: {{"sentinel": "NO_RESPONSE"}} + # If provider not in meta, look for it in the prompt steps + if not provider: + steps = data.get("steps", []) + for step in steps: + if step.get("type") == "prompt" and "provider" in step: + provider = step["provider"] + break -Your comment can include: -- Q: for questions -- CONCERN: for concerns -- DECISION: for decisions you're proposing -- @alias to mention other participants -""" + # Default provider if not found + if not provider: + provider = DEFAULT_PROVIDER + + return Participant( + alias=alias, + name=name, + description=description, + participant_type=participant_type, + voice=voice, + provider=provider, + color=color, + tool_path=config_path.parent, + ) + + except Exception: + return None + + +def discover_participants(smarttools_dir: Path = None) -> list[Participant]: + """ + Discover participant SmartTools by scanning ~/.smarttools/. + + Participants are SmartTools matching the pattern `discussion-{alias}` + that are NOT utility tools (parser, vote-counter, etc.). + + Args: + smarttools_dir: Override SmartTools directory (default: ~/.smarttools) + + Returns: + List of discovered Participant objects + """ + if smarttools_dir is None: + smarttools_dir = Path.home() / ".smarttools" + + if not smarttools_dir.exists(): + return [] + + participants = [] + + # Scan for discussion-* directories + for tool_dir in smarttools_dir.iterdir(): + if not tool_dir.is_dir(): + continue + if not tool_dir.name.startswith("discussion-"): + continue + + config_path = tool_dir / "config.yaml" + if not config_path.exists(): + continue + + participant = _parse_smarttool_config(config_path) + if participant: + participants.append(participant) + + return participants + + +def check_tool_exists(alias: str) -> bool: + """ + Check if a participant SmartTool exists and is callable. + + Args: + alias: Participant alias (e.g., "architect") + + Returns: + True if the tool exists + """ + tool_name = f"discussion-{alias}" + try: + result = subprocess.run( + ["which", tool_name], + capture_output=True, + text=True, + ) + return result.returncode == 0 + except Exception: + return False class ParticipantRegistry: """ - Registry for loading and managing participants. + Registry for discovered participants. - Loads participants from: - 1. Bundled defaults (config/default_participants.yaml) - 2. User config (~/.config/discussions/participants.yaml) - 3. Project config (./discussions.yaml or ./.discussions/participants.yaml) + Discovers participants by scanning ~/.smarttools/ for SmartTools + matching the `discussion-{alias}` naming convention. """ - def __init__(self): + def __init__(self, smarttools_dir: Path = None): + self._smarttools_dir = smarttools_dir self._participants: dict[str, Participant] = {} self._loaded = False - def _load_from_yaml(self, path: Path) -> None: - """Load participants from a YAML file.""" - if not path.exists(): - return - - try: - data = yaml.safe_load(path.read_text()) - if not data: - return - - # Load voting participants - for p_data in data.get("voting_participants", []): - participant = Participant.from_dict(p_data) - self._participants[participant.alias] = participant - - # Load background participants - for p_data in data.get("background_participants", []): - p_data["type"] = "background" - participant = Participant.from_dict(p_data) - self._participants[participant.alias] = participant - - except Exception as e: - print(f"Warning: Failed to load participants from {path}: {e}") - def _ensure_loaded(self) -> None: - """Ensure participants are loaded (lazy loading).""" + """Ensure participants are discovered (lazy loading).""" if self._loaded: return - # Load bundled defaults - bundled = Path(__file__).parent.parent.parent / "config" / "default_participants.yaml" - self._load_from_yaml(bundled) - - # Load user config - user_config = Path.home() / ".config" / "discussions" / "participants.yaml" - self._load_from_yaml(user_config) - - # Load project config - project_config = Path.cwd() / ".discussions" / "participants.yaml" - self._load_from_yaml(project_config) + for participant in discover_participants(self._smarttools_dir): + self._participants[participant.alias] = participant self._loaded = True @@ -209,16 +278,6 @@ class ParticipantRegistry: self._ensure_loaded() return [p for p in self._participants.values() if not p.is_voting()] - def register(self, participant: Participant) -> None: - """ - Register a participant. - - Args: - participant: Participant to register - """ - self._ensure_loaded() - self._participants[participant.alias] = participant - def aliases(self) -> list[str]: """ Get all registered aliases. @@ -229,6 +288,12 @@ class ParticipantRegistry: self._ensure_loaded() return list(self._participants.keys()) + def refresh(self) -> None: + """Force re-discovery of participants.""" + self._loaded = False + self._participants.clear() + self._ensure_loaded() + # Global registry instance _registry: Optional[ParticipantRegistry] = None @@ -263,3 +328,82 @@ def list_participants() -> list[Participant]: List of Participant objects """ return get_registry().get_all() + + +def save_participant_settings(participant: Participant) -> bool: + """ + Save participant settings (voice, provider, color) back to SmartTool config. + + Updates both the meta section and the prompt step provider. + + Args: + participant: Participant with updated settings + + Returns: + True if saved successfully + """ + if not participant.tool_path: + return False + + config_path = participant.tool_path / "config.yaml" + if not config_path.exists(): + return False + + try: + # Read current config + content = config_path.read_text() + data = yaml.safe_load(content) + if not data: + return False + + # Update meta section + if "meta" not in data: + data["meta"] = {} + data["meta"]["voice"] = participant.voice + data["meta"]["provider"] = participant.provider + data["meta"]["color"] = list(participant.color) # Store as list for YAML + + # Update provider in prompt steps + steps = data.get("steps", []) + for step in steps: + if step.get("type") == "prompt": + step["provider"] = participant.provider + + # Write back to file + with open(config_path, 'w') as f: + yaml.dump(data, f, default_flow_style=False, sort_keys=False, allow_unicode=True) + + return True + + except Exception: + return False + + +def get_available_providers() -> list[tuple[str, str]]: + """ + Get list of available AI providers from SmartTools. + + Returns: + List of (provider_name, description) tuples + """ + # Try to import from SmartTools + try: + from smarttools.providers import load_providers + providers = load_providers() + return [(p.name, p.description) for p in providers] + except ImportError: + pass + + # Fallback: return common providers + return [ + ("claude-sonnet", "Claude Sonnet - balanced quality/speed"), + ("claude-haiku", "Claude Haiku - fast, accurate"), + ("claude-opus", "Claude Opus - highest quality"), + ("claude", "Claude - auto-routes to best model"), + ("codex", "Codex - reliable, auto-routes"), + ("opencode-deepseek", "DeepSeek - best value, cheap, fast"), + ("opencode-pickle", "Pickle - best free, accurate"), + ("opencode-reasoner", "DeepSeek Reasoner - complex reasoning"), + ("gemini-flash", "Gemini Flash - quick tasks"), + ("gemini", "Gemini Pro - large docs/PDFs"), + ] diff --git a/src/discussions/runner.py b/src/discussions/runner.py index 80d911c..7693f50 100644 --- a/src/discussions/runner.py +++ b/src/discussions/runner.py @@ -1,339 +1,969 @@ """ -Discussion runner and orchestration engine. +Discussion runner - thin orchestration layer. -This module handles the execution of discussion turns by: -1. Identifying which participants should respond -2. Building prompts for each participant -3. Invoking AI providers via SmartTools -4. Parsing responses and updating the discussion +This module orchestrates SmartTools to execute discussion turns. +It contains NO business logic - all logic lives in SmartTools. -See docs/DESIGN.md for orchestration details. +The runner simply: +1. Loads pipeline configuration from template +2. Calls SmartTools via subprocess based on pipeline steps +3. Pipes data between tools +4. Updates the discussion file + +See docs/DESIGN.md for architecture details. +See docs/PIPELINE_SCHEMA.md for pipeline configuration. """ import json +import subprocess import sys -from dataclasses import dataclass +import concurrent.futures +import yaml +from dataclasses import dataclass, field from pathlib import Path from typing import Optional -from .discussion import Discussion, Comment -from .participant import Participant, get_participant, get_registry +from .discussion import Discussion + + +# Default pipeline used when template doesn't specify one +# Uses the new variable-based schema +DEFAULT_PIPELINE = { + "steps": [ + { + "tool": "discussion-parser", + "input": "$discussion", + "output": "$parsed" + }, + { + "tool": "discussion-mention-router", + "input": "$parsed", + "output": "$routing", + "when": "not $participants_specified", + "args": {"--default-participants": "$participants_csv"} + }, + { + "tool": "discussion-{participant}", + "for_each": "$participants_to_call", + "parallel": True, + "input": "$discussion", + "output": "$responses[]", + "args": {"--callout": "$callout", "--templates-dir": "$templates_dir"} + }, + { + "tool": "discussion-turn-appender", + "input": "$discussion", + "output": "$discussion", + "args": {"--responses-json": "$responses_json"} + }, + { + "tool": "discussion-parser", + "input": "$discussion", + "output": "$reparsed", + "when": "$phase_voting" + }, + { + "tool": "discussion-vote-counter", + "input": "$reparsed", + "output": "$votes", + "when": "$phase_voting" + }, + { + "tool": "discussion-status-promoter", + "input": "$votes", + "output": "$promotion", + "when": "$phase_voting", + "args": {"--current-status": "$status", "--current-phase": "$phase"} + }, + ] +} + + +@dataclass +class ParticipantResponse: + """Response from a participant SmartTool.""" + alias: str + name: str + comment: Optional[str] = None + vote: Optional[str] = None + success: bool = True + error: Optional[str] = None + raw_output: str = "" @dataclass class TurnResult: - """Result of a single participant's turn.""" - participant: Participant - comment: Optional[Comment] - success: bool - error: Optional[str] = None - raw_response: str = "" - - -@dataclass -class RunResult: """Result of running a discussion turn.""" - discussion: Discussion - results: list[TurnResult] + responses: list[ParticipantResponse] = field(default_factory=list) + vote_summary: dict = field(default_factory=dict) + consensus_reached: bool = False + consensus_reason: Optional[str] = None + status_promoted: bool = False + new_status: Optional[str] = None + phase_advanced: bool = False + new_phase: Optional[str] = None @property def successful_count(self) -> int: - return sum(1 for r in self.results if r.success and r.comment) + return sum(1 for r in self.responses if r.success and r.comment) @property def failed_count(self) -> int: - return sum(1 for r in self.results if not r.success) + return sum(1 for r in self.responses if not r.success) @property def skipped_count(self) -> int: - return sum(1 for r in self.results if r.success and not r.comment) + return sum(1 for r in self.responses if r.success and not r.comment) -class Runner: +def _call_tool(tool_name: str, input_data: str, args: list[str] = None, timeout: int = 300) -> subprocess.CompletedProcess: """ - Discussion orchestration runner. + Call a SmartTool via subprocess. - Handles invoking participants and updating discussions. + Args: + tool_name: Name of the tool (e.g., "discussion-parser") + input_data: Data to pass via stdin + args: Additional command line arguments + timeout: Timeout in seconds + + Returns: + CompletedProcess with stdout/stderr """ + cmd = [tool_name] + (args or []) + return subprocess.run( + cmd, + input=input_data, + capture_output=True, + text=True, + timeout=timeout, + ) - def __init__(self, provider_override: str = None, verbose: bool = False): - """ - Initialize the runner. - Args: - provider_override: Override provider for all participants - verbose: Enable verbose output - """ - self.provider_override = provider_override - self.verbose = verbose - self._provider_client = None +def _parse_json_output(output: str) -> dict: + """ + Parse JSON from tool output. - def _get_provider_client(self): - """Get or create the provider client (lazy import from SmartTools).""" - if self._provider_client is not None: - return self._provider_client + Handles: + - Pure JSON + - JSON wrapped in markdown code blocks + Args: + output: Raw stdout from tool + + Returns: + Parsed dict + + Raises: + ValueError: If JSON cannot be parsed + """ + text = output.strip() + + # Try direct parse + try: + return json.loads(text) + except json.JSONDecodeError: + pass + + # Try extracting from markdown code block + import re + code_block = re.search(r'```(?:json)?\s*\n?(.*?)\n?```', text, re.DOTALL) + if code_block: try: - from smarttools.providers import call_provider - self._provider_client = call_provider - return self._provider_client - except ImportError: - raise ImportError( - "SmartTools is required but not installed. " - "Install with: pip install smarttools" - ) - - def _invoke_participant( - self, - participant: Participant, - context: str, - callout: str = "", - ) -> TurnResult: - """ - Invoke a single participant. - - Args: - participant: The participant to invoke - context: Discussion content - callout: Specific request/question - - Returns: - TurnResult with response details - """ - call_provider = self._get_provider_client() - - # Build prompt - prompt = participant.build_prompt(context, callout) - - if self.verbose: - print(f"[runner] Invoking {participant.name}...", file=sys.stderr) - - # Determine provider - provider = self.provider_override or participant.provider_hint - - # Call provider - try: - result = call_provider(provider, prompt) - except Exception as e: - return TurnResult( - participant=participant, - comment=None, - success=False, - error=f"Provider error: {e}", - ) - - if not result.success: - return TurnResult( - participant=participant, - comment=None, - success=False, - error=result.error, - raw_response=result.text, - ) - - # Parse JSON response - try: - response_data = self._parse_response(result.text) - except ValueError as e: - return TurnResult( - participant=participant, - comment=None, - success=False, - error=f"Failed to parse response: {e}", - raw_response=result.text, - ) - - # Check for NO_RESPONSE sentinel - if response_data.get("sentinel") == "NO_RESPONSE": - if self.verbose: - print(f"[runner] {participant.name} has nothing to add", file=sys.stderr) - return TurnResult( - participant=participant, - comment=None, - success=True, # Not an error, just nothing to say - raw_response=result.text, - ) - - # Extract comment and vote - comment_text = response_data.get("comment", "") - vote = response_data.get("vote") - - if vote and vote.upper() not in ("READY", "CHANGES", "REJECT"): - vote = None - - # Create a temporary comment to return (not yet added to discussion) - from .markers import extract_all_markers - markers = extract_all_markers(comment_text, participant.name) - - comment = Comment( - author=participant.name, - body=comment_text, - vote=vote.upper() if vote else None, - questions=markers["questions"], - action_items=markers["action_items"], - decisions=markers["decisions"], - concerns=markers["concerns"], - mentions=markers["mentions"], - ) - - return TurnResult( - participant=participant, - comment=comment, - success=True, - raw_response=result.text, - ) - - def _parse_response(self, text: str) -> dict: - """ - Parse JSON response from participant. - - Handles various response formats: - - Pure JSON - - JSON in markdown code blocks - - JSON with surrounding text - - Args: - text: Raw response text - - Returns: - Parsed dict - - Raises: - ValueError: If JSON cannot be parsed - """ - text = text.strip() - - # Try direct JSON parse - try: - return json.loads(text) + return json.loads(code_block.group(1).strip()) except json.JSONDecodeError: pass - # Try extracting from markdown code block - import re - code_block = re.search(r'```(?:json)?\s*\n?(.*?)\n?```', text, re.DOTALL) - if code_block: - try: - return json.loads(code_block.group(1).strip()) - except json.JSONDecodeError: - pass + # Try finding JSON object + start = text.find("{") + end = text.rfind("}") + 1 + if start >= 0 and end > start: + try: + return json.loads(text[start:end]) + except json.JSONDecodeError: + pass - # Try finding JSON object in text - json_match = re.search(r'\{[^{}]*\}', text, re.DOTALL) - if json_match: - try: - return json.loads(json_match.group(0)) - except json.JSONDecodeError: - pass + raise ValueError(f"Could not parse JSON: {text[:200]}...") - raise ValueError(f"Could not parse JSON from response: {text[:200]}...") - def run_turn( - self, - discussion: Discussion, - participants: list[str] = None, - callout: str = "", - ) -> RunResult: - """ - Run a discussion turn with specified participants. +def _load_template(template_name: str, templates_dir: Path = None) -> dict: + """ + Load a template YAML file. - Args: - discussion: The discussion to update - participants: List of participant aliases (or None for all) - callout: Specific request/question for all participants + Args: + template_name: Name of the template (e.g., "feature") + templates_dir: Directory to search for templates - Returns: - RunResult with all responses - """ - registry = get_registry() + Returns: + Template dict or empty dict if not found + """ + search_dirs = [] + if templates_dir: + search_dirs.append(Path(templates_dir)) + search_dirs.extend([ + Path.cwd() / "templates", + Path(__file__).parent.parent.parent / "templates", + ]) - # Resolve participants - if participants is None or "all" in participants: - participant_list = registry.get_voting() + for search_dir in search_dirs: + template_path = search_dir / f"{template_name}.yaml" + if template_path.exists(): + with open(template_path) as f: + return yaml.safe_load(f) or {} + + return {} + + +def _get_pipeline(template: dict) -> dict: + """ + Get the pipeline configuration from a template. + + Returns DEFAULT_PIPELINE if template doesn't define one. + """ + return template.get("turn_pipeline", DEFAULT_PIPELINE) + + +class VariableStore: + """ + Variable store for pipeline execution. + + Manages $variable references and supports: + - Simple variables: $discussion, $parsed + - JSON field access: $routing.participants_to_call + - Array collection: $responses[] appends to list + """ + + def __init__(self): + self._store: dict = {} + + def set(self, name: str, value) -> None: + """Set a variable. Name should not include $ prefix.""" + # Handle array append syntax: responses[] + if name.endswith("[]"): + base_name = name[:-2] + if base_name not in self._store: + self._store[base_name] = [] + self._store[base_name].append(value) else: - participant_list = [] - for alias in participants: - participant = registry.get(alias) - if participant: - participant_list.append(participant) - else: - print(f"[runner] Warning: Unknown participant '{alias}'", file=sys.stderr) + self._store[name] = value - if not participant_list: - return RunResult(discussion=discussion, results=[]) + def get(self, ref: str): + """ + Get a variable value. Ref can be: + - $varname - simple lookup + - $varname.field - JSON field access + - $varname.field.subfield - nested access + """ + if not ref.startswith("$"): + return ref # Literal value - # Get current discussion content - context = discussion.get_content() + path = ref[1:] # Remove $ + parts = path.split(".") - # Invoke each participant - results = [] - for participant in participant_list: - result = self._invoke_participant(participant, context, callout) - results.append(result) + value = self._store.get(parts[0]) + if value is None: + return None - # If successful with a comment, add to discussion - if result.success and result.comment: - discussion.add_comment( - author=result.comment.author, - text=result.comment.body, - vote=result.comment.vote, + # Navigate nested fields + for part in parts[1:]: + if isinstance(value, dict): + value = value.get(part) + elif isinstance(value, list) and part.isdigit(): + idx = int(part) + value = value[idx] if idx < len(value) else None + else: + return None + if value is None: + return None + + return value + + def resolve(self, value) -> str: + """ + Resolve any $variable references in a value. + + For strings, replaces $var with its value. + For other types, returns as-is. + """ + if not isinstance(value, str): + return value + + if not value.startswith("$"): + return value + + resolved = self.get(value) + if resolved is None: + return "" + + # Convert to string for CLI args + if isinstance(resolved, (list, dict)): + return json.dumps(resolved) + return str(resolved) + + def resolve_args(self, args: dict) -> list[str]: + """Convert args dict to CLI argument list with variable resolution.""" + result = [] + for arg_name, value in args.items(): + resolved = self.resolve(value) + if resolved: # Skip empty values + result.extend([arg_name, resolved]) + return result + + def dump(self) -> dict: + """Return copy of all variables for debugging.""" + return dict(self._store) + + +def _check_condition(condition: str, variables: VariableStore) -> bool: + """ + Check if a pipeline step condition is met. + + Supports: + - "always" or empty - always true + - "$variable" - true if variable is truthy + - "not $variable" - true if variable is falsy + - "$variable.field" - true if JSON field is truthy + + Args: + condition: Condition string + variables: VariableStore with current state + + Returns: + True if condition is met + """ + if not condition or condition == "always": + return True + + # Handle "not $variable" + negated = False + if condition.startswith("not "): + negated = True + condition = condition[4:].strip() + + # Resolve the variable reference + if condition.startswith("$"): + value = variables.get(condition) + result = bool(value) # Truthy check + return not result if negated else result + + # Legacy condition names for backward compatibility + legacy_map = { + "no_participants_specified": "not $participants_specified", + "phase_has_voting": "$phase_voting", + } + if condition in legacy_map: + return _check_condition(legacy_map[condition], variables) + + # Unknown condition - default to true + return True + + +def _interpolate_args(args: dict, context: dict) -> list[str]: + """ + Interpolate variables in argument values and convert to CLI args. + + Args: + args: Dict of arg_name -> value + context: Context dict with variables + + Returns: + List of CLI arguments (e.g., ["--threshold-ready", "0.67"]) + """ + result = [] + for arg_name, value in args.items(): + if isinstance(value, str): + # Interpolate {variable} patterns + for key, val in context.items(): + if isinstance(val, (str, int, float, bool)): + value = value.replace(f"{{{key}}}", str(val)) + result.extend([arg_name, str(value)]) + return result + + +def run_pipeline_turn( + discussion_path: Path, + participants: list[str] = None, + callout: str = "", + verbose: bool = False, + provider: str = None, + templates_dir: Path = None, +) -> TurnResult: + """ + Run a discussion turn using the template's pipeline configuration. + + This is a THIN orchestration layer. It: + 1. Initializes variables from context + 2. Executes each step by piping input -> tool -> output + 3. Returns results without any business logic + + All logic lives in the SmartTools themselves. + + Args: + discussion_path: Path to discussion markdown file + participants: List of participant aliases (None = use router) + callout: Optional callout/question for participants + verbose: Print progress to stderr + provider: Override AI provider + templates_dir: Directory containing templates + + Returns: + TurnResult with responses and status + """ + discussion_path = Path(discussion_path) + discussion = Discussion.load(discussion_path) + result = TurnResult() + + def log(msg: str): + if verbose: + print(f"[pipeline] {msg}", file=sys.stderr) + + # Load template and pipeline + template_name = discussion.template or "feature" + template = _load_template(template_name, templates_dir) + pipeline = _get_pipeline(template) + phases = template.get("phases", {}) + phase_config = phases.get(discussion.phase, {}) + + log(f"Using template: {template_name}") + log(f"Pipeline has {len(pipeline.get('steps', []))} steps") + + # Initialize variable store with built-in variables + vars = VariableStore() + vars.set("discussion", discussion.get_content()) + vars.set("discussion_path", str(discussion_path)) + vars.set("callout", callout or "") + vars.set("templates_dir", str(templates_dir or Path.cwd() / "templates")) + vars.set("participants", participants or discussion.participant_aliases or ["architect", "security", "pragmatist"]) + vars.set("participants_csv", ",".join(vars.get("$participants"))) + vars.set("participants_specified", participants is not None) + vars.set("participants_to_call", vars.get("$participants")) # May be updated by router + vars.set("status", discussion.status) + vars.set("phase", discussion.phase) + vars.set("template", template_name) + vars.set("phase_voting", phase_config.get("voting", False)) + vars.set("phase_threshold_ready", phase_config.get("threshold_ready", 0.67)) + vars.set("phase_human_required", phase_config.get("human_required", True)) + vars.set("provider", provider or "") + + # Execute pipeline steps + for i, step in enumerate(pipeline.get("steps", [])): + tool = step.get("tool", "") + input_var = step.get("input", "$discussion") + output_var = step.get("output") + condition = step.get("when", "always") + for_each = step.get("for_each") + parallel = step.get("parallel", False) + args = step.get("args", {}) + + log(f"Step {i+1}: {tool}") + + # Check condition + if not _check_condition(condition, vars): + log(f" Skipped (condition: {condition})") + continue + + # Handle for_each expansion + if for_each: + items = vars.get(for_each) if for_each.startswith("$") else for_each + if not items: + log(f" Skipped (for_each {for_each} is empty)") + continue + + if isinstance(items, str): + items = [items] + + log(f" Iterating over {len(items)} items") + _execute_for_each(vars, result, tool, input_var, output_var, items, parallel, args, provider, log) + else: + # Single tool execution + _execute_step(vars, tool, input_var, output_var, args, log) + + # Build TurnResult from variables + _finalize_result(vars, result, discussion_path, log) + + log("Pipeline complete") + return result + + +def _execute_step( + vars: VariableStore, + tool: str, + input_var: str, + output_var: Optional[str], + args: dict, + log, +) -> Optional[str]: + """ + Execute a single pipeline step. + + This is generic - no special handling for specific tools. + Just: resolve input -> call tool -> store output + """ + # Get input data + input_data = vars.get(input_var) if input_var else "" + if isinstance(input_data, (dict, list)): + input_data = json.dumps(input_data) + input_data = str(input_data) if input_data else "" + + # Resolve args + cli_args = vars.resolve_args(args) + + try: + proc = _call_tool(tool, input_data, cli_args) + + if proc.returncode != 0: + log(f" {tool} failed: {proc.stderr[:100] if proc.stderr else 'no error message'}") + return None + + output = proc.stdout + + # Store output if specified + if output_var: + # Try to parse as JSON, fall back to string + try: + parsed = _parse_json_output(output) + var_name = output_var.lstrip("$") + vars.set(var_name, parsed) + log(f" -> {output_var} (JSON)") + except (ValueError, json.JSONDecodeError): + var_name = output_var.lstrip("$") + vars.set(var_name, output) + log(f" -> {output_var} (string)") + + return output + + except FileNotFoundError: + log(f" {tool} not found") + return None + except Exception as e: + log(f" {tool} error: {e}") + return None + + +def _execute_for_each( + vars: VariableStore, + result: TurnResult, + tool_pattern: str, + input_var: str, + output_var: Optional[str], + items: list, + parallel: bool, + args: dict, + provider: str, + log, +): + """ + Execute a step for each item in a list. + + Handles {participant} substitution in tool names. + Collects outputs into array if output_var ends with []. + """ + + def execute_one(item: str): + # Substitute {participant} in tool name + tool = tool_pattern.replace("{participant}", item) + log(f" [{item}] {tool}") + + # Get input data + input_data = vars.get(input_var) if input_var else "" + if isinstance(input_data, (dict, list)): + input_data = json.dumps(input_data) + input_data = str(input_data) if input_data else "" + + # Resolve args + cli_args = vars.resolve_args(args) + + # Add provider if specified + if provider: + cli_args.extend(["--provider", provider]) + + try: + proc = _call_tool(tool, input_data, cli_args) + + if proc.returncode != 0: + log(f" [{item}] failed: {proc.stderr[:80] if proc.stderr else ''}") + return ParticipantResponse( + alias=item, + name=f"AI-{item.capitalize()}", + success=False, + error=proc.stderr or "Tool failed", ) - if self.verbose: - vote_str = f" (VOTE: {result.comment.vote})" if result.comment.vote else "" - print(f"[runner] {participant.name} responded{vote_str}", file=sys.stderr) + # Parse response + try: + response_data = _parse_json_output(proc.stdout) + except (ValueError, json.JSONDecodeError): + log(f" [{item}] invalid JSON output") + return ParticipantResponse( + alias=item, + name=f"AI-{item.capitalize()}", + success=False, + error="Invalid JSON response", + raw_output=proc.stdout, + ) - return RunResult(discussion=discussion, results=results) + # Check for NO_RESPONSE sentinel + if response_data.get("sentinel") == "NO_RESPONSE": + log(f" [{item}] no response") + return ParticipantResponse( + alias=item, + name=f"AI-{item.capitalize()}", + success=True, + ) - def run_mentions( - self, - discussion: Discussion, - since_comment_index: int = 0, - ) -> RunResult: - """ - Run turns for any participants mentioned since a given point. + comment = response_data.get("comment", "") + vote = response_data.get("vote") + log(f" [{item}] responded" + (f" [{vote}]" if vote else "")) - Args: - discussion: The discussion to process - since_comment_index: Only check mentions after this comment index + return ParticipantResponse( + alias=item, + name=f"AI-{item.capitalize()}", + comment=comment, + vote=vote, + success=True, + raw_output=proc.stdout, + ) - Returns: - RunResult with responses - """ - registry = get_registry() + except FileNotFoundError: + log(f" [{item}] tool not found") + return ParticipantResponse( + alias=item, + name=f"AI-{item.capitalize()}", + success=False, + error=f"Tool {tool} not found", + ) + except Exception as e: + log(f" [{item}] error: {e}") + return ParticipantResponse( + alias=item, + name=f"AI-{item.capitalize()}", + success=False, + error=str(e), + ) - # Collect mentioned aliases that haven't responded - mentioned = set() - responded = set() + responses = [] + if parallel: + with concurrent.futures.ThreadPoolExecutor(max_workers=len(items)) as executor: + futures = {executor.submit(execute_one, item): item for item in items} + for future in concurrent.futures.as_completed(futures): + responses.append(future.result()) + else: + for item in items: + responses.append(execute_one(item)) - for i, comment in enumerate(discussion.comments): - responded.add(comment.author) + # Store responses in result + for resp in responses: + result.responses.append(resp) - if i >= since_comment_index: - for mention in comment.mentions: - if mention.target != "all": - mentioned.add(mention.target) - - # Find participants to invoke - to_invoke = [] - for alias in mentioned: - participant = registry.get(alias) - if participant and participant.name not in responded: - to_invoke.append(alias) - - if not to_invoke: - return RunResult(discussion=discussion, results=[]) - - return self.run_turn(discussion, to_invoke) + # Store in output variable if specified + if output_var: + var_name = output_var.lstrip("$") + # Build list of response data for downstream tools + response_data = [] + for resp in responses: + if resp.success and resp.comment: + response_data.append({ + "author": resp.name, + "comment": resp.comment, + "vote": resp.vote, + }) + vars.set(var_name, response_data) + vars.set("responses_json", json.dumps(response_data)) +def _finalize_result(vars: VariableStore, result: TurnResult, discussion_path: Path, log): + """ + Finalize the turn result by: + 1. Writing updated discussion content back to file (if turn-appender was used) + 2. Extracting vote summary and consensus from variables + 3. Handling status promotion + """ + # Check if discussion was updated (turn-appender output) + updated_discussion = vars.get("$discussion") + if updated_discussion and isinstance(updated_discussion, str): + # Write back to file + discussion_path.write_text(updated_discussion) + log(" Discussion file updated") + + # Extract vote summary if vote-counter was run + votes = vars.get("$votes") + if votes and isinstance(votes, dict): + result.vote_summary = votes.get("vote_summary", {}) + consensus = votes.get("consensus", {}) + result.consensus_reached = consensus.get("reached", False) + result.consensus_reason = consensus.get("reason") + + # Check for status promotion + promotion = vars.get("$promotion") + if promotion and isinstance(promotion, dict): + if promotion.get("should_promote"): + result.status_promoted = True + result.new_status = promotion.get("new_status") + # Update status in file + discussion = Discussion.load(discussion_path) + discussion.update_status(result.new_status) + discussion.save() + log(f" Status promoted to {result.new_status}") + + # Auto-advance phase if consensus reached in a voting phase + phase_voting = vars.get("$phase_voting", False) + if result.consensus_reached and phase_voting: + template_name = vars.get("$template", "") + current_phase = vars.get("$phase", "") + + if template_name and current_phase: + template = _load_template(template_name) + phases = template.get("phases", {}) + phase_config = phases.get(current_phase, {}) + next_phase = phase_config.get("next_phase") + + if next_phase: + # Load discussion and update phase + discussion = Discussion.load(discussion_path) + old_phase = discussion.phase + discussion.update_phase(next_phase) + discussion.save() + result.phase_advanced = True + result.new_phase = next_phase + log(f" Phase advanced: {old_phase} → {next_phase}") + + +def run_turn( + discussion_path: Path, + participants: list[str] = None, + callout: str = "", + verbose: bool = False, + provider: str = None, +) -> TurnResult: + """ + Run a discussion turn by orchestrating SmartTools. + + Pipeline: + 1. discussion-parser - Parse current state + 2. discussion-mention-router - Determine who responds (or use provided list) + 3. discussion-{alias} - Call each participant + 4. discussion-turn-appender - Append responses to file + 5. discussion-vote-counter - Count votes + 6. discussion-status-promoter - Check for status change + + Args: + discussion_path: Path to discussion markdown file + participants: List of participant aliases (None = use mention router) + callout: Optional callout/question for participants + verbose: Print progress to stderr + provider: Override AI provider for participants + + Returns: + TurnResult with responses and status + """ + discussion_path = Path(discussion_path) + discussion_content = discussion_path.read_text() + result = TurnResult() + + def log(msg: str): + if verbose: + print(f"[runner] {msg}", file=sys.stderr) + + # Step 1: Parse discussion + log("Parsing discussion...") + try: + parser_result = _call_tool("discussion-parser", discussion_content) + if parser_result.returncode != 0: + raise RuntimeError(f"Parser failed: {parser_result.stderr}") + state = _parse_json_output(parser_result.stdout) + except FileNotFoundError: + raise RuntimeError("discussion-parser not found. Is SmartTools installed?") + + current_status = state.get("metadata", {}).get("status", "OPEN") + current_phase = state.get("metadata", {}).get("phase", "initial_feedback") + log(f" Status: {current_status}, Phase: {current_phase}") + + # Step 2: Determine participants + if participants: + participants_to_call = participants + else: + log("Routing mentions...") + try: + router_result = _call_tool("discussion-mention-router", parser_result.stdout) + if router_result.returncode != 0: + raise RuntimeError(f"Mention router failed: {router_result.stderr}") + routing = _parse_json_output(router_result.stdout) + participants_to_call = routing.get("participants_to_call", []) + except FileNotFoundError: + raise RuntimeError("discussion-mention-router not found") + + log(f" Participants: {', '.join(participants_to_call)}") + + # Step 3: Call each participant + responses_for_appender = [] + + for alias in participants_to_call: + tool_name = f"discussion-{alias}" + log(f"Calling {alias}...") + + args = [] + if callout: + args.extend(["--callout", callout]) + if provider: + args.extend(["--provider", provider]) + + try: + participant_result = _call_tool(tool_name, discussion_content, args) + + if participant_result.returncode != 0: + log(f" {alias} failed: {participant_result.stderr}") + result.responses.append(ParticipantResponse( + alias=alias, + name=f"AI-{alias.capitalize()}", + success=False, + error=participant_result.stderr, + raw_output=participant_result.stdout, + )) + continue + + # Parse response + try: + response_data = _parse_json_output(participant_result.stdout) + except ValueError as e: + log(f" {alias} returned invalid JSON: {e}") + result.responses.append(ParticipantResponse( + alias=alias, + name=f"AI-{alias.capitalize()}", + success=False, + error=str(e), + raw_output=participant_result.stdout, + )) + continue + + # Check for NO_RESPONSE sentinel + if response_data.get("sentinel") == "NO_RESPONSE": + log(f" {alias} has nothing to add") + result.responses.append(ParticipantResponse( + alias=alias, + name=f"AI-{alias.capitalize()}", + success=True, + )) + continue + + # Extract response data + comment = response_data.get("comment", "") + vote = response_data.get("vote") + author = f"AI-{alias.capitalize()}" + + log(f" {alias} responded" + (f" [{vote}]" if vote else "")) + + result.responses.append(ParticipantResponse( + alias=alias, + name=author, + comment=comment, + vote=vote, + success=True, + raw_output=participant_result.stdout, + )) + + # Prepare for appender + responses_for_appender.append({ + "author": author, + "comment": comment, + "vote": vote, + }) + + except FileNotFoundError: + log(f" {tool_name} not found, skipping") + result.responses.append(ParticipantResponse( + alias=alias, + name=f"AI-{alias.capitalize()}", + success=False, + error=f"Tool {tool_name} not found", + )) + + # Step 4: Append responses to discussion + if responses_for_appender: + log("Appending responses...") + # Format: original content + delimiter + responses JSON + appender_input = f"{discussion_content}\n---RESPONSES---\n{json.dumps(responses_for_appender)}" + + try: + appender_result = _call_tool("discussion-turn-appender", appender_input) + if appender_result.returncode != 0: + log(f" Appender failed: {appender_result.stderr}") + else: + # Write updated content + updated_content = appender_result.stdout + discussion_path.write_text(updated_content) + discussion_content = updated_content + log(" Responses appended") + except FileNotFoundError: + log(" discussion-turn-appender not found, appending manually") + # Fallback: append manually + discussion = Discussion.load(discussion_path) + for resp in responses_for_appender: + discussion.add_comment( + author=resp["author"], + text=resp["comment"], + vote=resp.get("vote"), + ) + discussion.save() + discussion_content = discussion.get_content() + + # Step 5: Count votes + log("Counting votes...") + try: + # Re-parse updated discussion + parser_result = _call_tool("discussion-parser", discussion_content) + if parser_result.returncode == 0: + vote_result = _call_tool("discussion-vote-counter", parser_result.stdout) + if vote_result.returncode == 0: + votes = _parse_json_output(vote_result.stdout) + result.vote_summary = votes.get("vote_summary", {}) + consensus = votes.get("consensus", {}) + result.consensus_reached = consensus.get("reached", False) + result.consensus_reason = consensus.get("reason") + log(f" Votes: {result.vote_summary}") + log(f" Consensus: {result.consensus_reached}") + except FileNotFoundError: + log(" discussion-vote-counter not found") + + # Step 6: Check status promotion + log("Checking status promotion...") + try: + vote_counter_output = json.dumps({ + "vote_summary": result.vote_summary, + "consensus": { + "reached": result.consensus_reached, + "reason": result.consensus_reason, + } + }) + promoter_result = _call_tool( + "discussion-status-promoter", + vote_counter_output, + ["--current-status", current_status, "--current-phase", current_phase] + ) + if promoter_result.returncode == 0: + promotion = _parse_json_output(promoter_result.stdout) + if promotion.get("should_promote"): + result.status_promoted = True + result.new_status = promotion.get("new_status") + log(f" Status promoted: {current_status} -> {result.new_status}") + + # Update status in file + discussion = Discussion.load(discussion_path) + discussion.update_status(result.new_status) + discussion.save() + except FileNotFoundError: + log(" discussion-status-promoter not found") + + log("Turn complete") + return result + + +# Convenience function for backward compatibility def run_discussion_turn( discussion_path: str | Path, participants: list[str] = None, callout: str = "", provider: str = None, verbose: bool = False, -) -> RunResult: +) -> TurnResult: """ Convenience function to run a discussion turn. @@ -345,10 +975,12 @@ def run_discussion_turn( verbose: Enable verbose output Returns: - RunResult with responses + TurnResult with responses """ - discussion = Discussion.load(discussion_path) - runner = Runner(provider_override=provider, verbose=verbose) - result = runner.run_turn(discussion, participants, callout) - discussion.save() - return result + return run_turn( + discussion_path=Path(discussion_path), + participants=participants, + callout=callout, + verbose=verbose, + provider=provider, + ) diff --git a/src/discussions/ui/__init__.py b/src/discussions/ui/__init__.py index 7405d0d..48c7ebd 100644 --- a/src/discussions/ui/__init__.py +++ b/src/discussions/ui/__init__.py @@ -1,5 +1,75 @@ """ -TUI module for Orchestrated Discussions. +UI module for Orchestrated Discussions. -Provides an interactive terminal interface for participating in discussions. +Provides interfaces for participating in discussions: +- TUI (Terminal UI) - urwid-based, text-only +- GUI (Graphical UI) - Dear PyGui-based, with native image viewing + +Usage: + # Launch GUI (default) + python -m discussions.ui examples/ + + # Launch TUI + python -m discussions.ui --tui examples/ + + # Or via CLI (after pip install) + discussions ui # GUI + discussions ui --tui # TUI """ + + +def __getattr__(name): + """Lazy import to avoid circular import warning when running as module.""" + # TUI components + if name in ("DiscussionTUI", "main_tui"): + from .tui import DiscussionTUI, main + return {"DiscussionTUI": DiscussionTUI, "main_tui": main}[name] + + # GUI components + elif name in ("DiscussionGUI", "main_gui"): + from .gui import DiscussionGUI, main + return {"DiscussionGUI": DiscussionGUI, "main_gui": main}[name] + + # Legacy 'main' defaults to GUI + elif name == "main": + from .gui import main + return main + + # TUI widgets + elif name in ("PALETTE", "CommentBlock", "VoteSummary", "PhaseIndicator", + "DiscussionListItem", "CommentInput", "StatusBar", "HeaderBar"): + from .widgets import ( + PALETTE, CommentBlock, VoteSummary, PhaseIndicator, + DiscussionListItem, CommentInput, StatusBar, HeaderBar, + ) + return { + "PALETTE": PALETTE, + "CommentBlock": CommentBlock, + "VoteSummary": VoteSummary, + "PhaseIndicator": PhaseIndicator, + "DiscussionListItem": DiscussionListItem, + "CommentInput": CommentInput, + "StatusBar": StatusBar, + "HeaderBar": HeaderBar, + }[name] + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +__all__ = [ + # GUI (default) + "DiscussionGUI", + "main_gui", + "main", + # TUI + "DiscussionTUI", + "main_tui", + # TUI widgets + "PALETTE", + "CommentBlock", + "VoteSummary", + "PhaseIndicator", + "DiscussionListItem", + "CommentInput", + "StatusBar", + "HeaderBar", +] diff --git a/src/discussions/ui/__main__.py b/src/discussions/ui/__main__.py new file mode 100644 index 0000000..7f6c78f --- /dev/null +++ b/src/discussions/ui/__main__.py @@ -0,0 +1,48 @@ +""" +Entry point for running UI as a module. + +Usage: + python -m discussions.ui [directory] # GUI (default) + python -m discussions.ui --tui [directory] # TUI + python -m src.discussions.ui [directory] +""" + +import sys +import argparse + + +def main(): + parser = argparse.ArgumentParser( + description="Orchestrated Discussions UI", + prog="python -m discussions.ui" + ) + parser.add_argument( + "--tui", + action="store_true", + help="Use terminal UI (urwid) instead of graphical UI (Dear PyGui)" + ) + parser.add_argument( + "directory", + nargs="?", + default=None, + help="Directory containing discussion files" + ) + + args = parser.parse_args() + + if args.tui: + from .tui import main as tui_main + tui_main(args.directory) + else: + try: + from .gui import main as gui_main + gui_main(args.directory) + except ImportError as e: + print(f"GUI unavailable ({e}), falling back to TUI") + print("Install Dear PyGui with: pip install dearpygui") + from .tui import main as tui_main + tui_main(args.directory) + + +if __name__ == "__main__": + main() diff --git a/src/discussions/ui/formats.py b/src/discussions/ui/formats.py new file mode 100644 index 0000000..344869c --- /dev/null +++ b/src/discussions/ui/formats.py @@ -0,0 +1,378 @@ +""" +Format detection and renderer dispatch for diagram/artifact files. + +Supports: PlantUML, Mermaid, OpenSCAD, Graphviz/DOT, SVG, ASCII +""" + +import subprocess +import shutil +import tempfile +from pathlib import Path +from typing import Optional, Tuple +from dataclasses import dataclass + + +# Format extension mappings +FORMAT_EXTENSIONS = { + 'plantuml': ['.puml', '.plantuml', '.pu'], + 'mermaid': ['.mmd', '.mermaid'], + 'svg': ['.svg'], + 'openscad': ['.scad'], + 'dot': ['.dot', '.gv'], + 'ascii': ['.txt', '.ascii'], +} + +# Reverse lookup: extension -> format +EXTENSION_TO_FORMAT = {} +for fmt, exts in FORMAT_EXTENSIONS.items(): + for ext in exts: + EXTENSION_TO_FORMAT[ext] = fmt + + +@dataclass +class RendererInfo: + """Information about a format renderer.""" + command: str + available: bool + install_hint: str + + +# Renderer configurations +RENDERERS = { + 'plantuml': RendererInfo( + command='plantuml', + available=False, + install_hint='sudo apt install plantuml' + ), + 'mermaid': RendererInfo( + command='mmdc', + available=False, + install_hint='npm install -g @mermaid-js/mermaid-cli' + ), + 'openscad': RendererInfo( + command='openscad', + available=False, + install_hint='sudo apt install openscad' + ), + 'dot': RendererInfo( + command='dot', + available=False, + install_hint='sudo apt install graphviz' + ), + 'svg': RendererInfo( + command=None, # Native display + available=True, + install_hint='' + ), + 'ascii': RendererInfo( + command=None, # Text display + available=True, + install_hint='' + ), +} + + +def check_tool_availability(): + """Check which rendering tools are available on the system.""" + for fmt, info in RENDERERS.items(): + if info.command: + info.available = shutil.which(info.command) is not None + + +def detect_format(path: str) -> Optional[str]: + """Detect diagram format from file extension. + + Args: + path: Path to diagram file + + Returns: + Format name or None if unknown + """ + ext = Path(path).suffix.lower() + return EXTENSION_TO_FORMAT.get(ext) + + +def detect_format_by_content(path: str) -> Optional[str]: + """Detect format by examining file content (fallback). + + Args: + path: Path to diagram file + + Returns: + Format name or None if undetectable + """ + try: + content = Path(path).read_text()[:500] # First 500 chars + + if '@startuml' in content or '@startmindmap' in content: + return 'plantuml' + if content.strip().startswith('graph ') or content.strip().startswith('digraph '): + return 'dot' + if content.strip().startswith('<svg') or 'xmlns="http://www.w3.org/2000/svg"' in content: + return 'svg' + if 'module ' in content and '(' in content and '{' in content: + return 'openscad' + if any(keyword in content for keyword in ['flowchart', 'sequenceDiagram', 'classDiagram', 'erDiagram']): + return 'mermaid' + + except Exception: + pass + + return None + + +def get_renderer(fmt: str) -> Optional[RendererInfo]: + """Get renderer info for a format. + + Args: + fmt: Format name + + Returns: + RendererInfo or None if unknown format + """ + return RENDERERS.get(fmt) + + +def is_format_supported(fmt: str) -> bool: + """Check if a format can be rendered. + + Args: + fmt: Format name + + Returns: + True if renderer is available or format needs no rendering + """ + info = RENDERERS.get(fmt) + if not info: + return False + return info.available + + +def render_to_png(source_path: str, output_path: Optional[str] = None) -> Tuple[bool, str]: + """Render a diagram file to PNG. + + Args: + source_path: Path to source diagram file + output_path: Optional output PNG path (auto-generated if None) + + Returns: + Tuple of (success, output_path_or_error_message) + """ + fmt = detect_format(source_path) + if not fmt: + fmt = detect_format_by_content(source_path) + if not fmt: + return False, f"Unknown format for: {source_path}" + + info = RENDERERS.get(fmt) + if not info: + return False, f"No renderer configured for: {fmt}" + + if not info.command: + if fmt == 'svg': + # SVG can be converted to PNG using ImageMagick or rendered directly + return render_svg_to_png(source_path, output_path) + elif fmt == 'ascii': + return False, "ASCII diagrams cannot be rendered to PNG" + return False, f"No rendering needed for: {fmt}" + + if not info.available: + return False, f"Renderer '{info.command}' not installed. Install with: {info.install_hint}" + + # Generate output path if not provided + if not output_path: + output_path = str(Path(source_path).with_suffix('.png')) + + # Use absolute paths for consistent behavior across tools + source = Path(source_path).absolute() + output = Path(output_path).absolute() + + try: + if fmt == 'plantuml': + # PlantUML creates output with same basename as input in -o directory + result = subprocess.run( + ['plantuml', '-tpng', '-o', str(output.parent), str(source)], + capture_output=True, text=True, timeout=30 + ) + # Rename to expected output path if needed + actual_output = output.parent / (source.stem + '.png') + if actual_output.exists() and actual_output != output: + actual_output.rename(output) + elif fmt == 'mermaid': + result = subprocess.run( + ['mmdc', '-i', str(source), '-o', str(output)], + capture_output=True, text=True, timeout=30 + ) + elif fmt == 'openscad': + result = subprocess.run( + ['openscad', '--render', '-o', str(output), str(source)], + capture_output=True, text=True, timeout=60 + ) + elif fmt == 'dot': + result = subprocess.run( + ['dot', '-Tpng', '-o', str(output), str(source)], + capture_output=True, text=True, timeout=30 + ) + else: + return False, f"Rendering not implemented for: {fmt}" + + if result.returncode != 0: + return False, f"Render failed: {result.stderr}" + + # Check output exists + if not output.exists(): + # PlantUML names output based on input + expected = source.with_suffix('.png') + if expected.exists(): + return True, str(expected) + return False, "Output file not created" + + return True, str(output) + + except subprocess.TimeoutExpired: + return False, "Rendering timed out" + except Exception as e: + return False, f"Render error: {e}" + + +def render_svg_to_png(source_path: str, output_path: Optional[str] = None) -> Tuple[bool, str]: + """Render SVG to PNG using available tools. + + Args: + source_path: Path to SVG file + output_path: Optional output PNG path + + Returns: + Tuple of (success, output_path_or_error_message) + """ + if not output_path: + output_path = str(Path(source_path).with_suffix('.png')) + + # Try cairosvg (Python library) + try: + import cairosvg + cairosvg.svg2png(url=source_path, write_to=output_path) + return True, output_path + except ImportError: + pass + except Exception as e: + pass + + # Try rsvg-convert + if shutil.which('rsvg-convert'): + try: + result = subprocess.run( + ['rsvg-convert', '-o', output_path, source_path], + capture_output=True, text=True, timeout=30 + ) + if result.returncode == 0: + return True, output_path + except Exception: + pass + + # Try ImageMagick convert + if shutil.which('convert'): + try: + result = subprocess.run( + ['convert', source_path, output_path], + capture_output=True, text=True, timeout=30 + ) + if result.returncode == 0: + return True, output_path + except Exception: + pass + + return False, "No SVG converter available. Install: pip install cairosvg OR sudo apt install librsvg2-bin" + + +def render_to_ascii(source_path: str) -> Tuple[bool, str]: + """Render a diagram to ASCII art for terminal display. + + Args: + source_path: Path to source diagram file + + Returns: + Tuple of (success, ascii_output_or_error_message) + """ + fmt = detect_format(source_path) + if not fmt: + return False, f"Unknown format for: {source_path}" + + if fmt == 'ascii': + # Already ASCII, just read it + try: + content = Path(source_path).read_text() + return True, content + except Exception as e: + return False, f"Error reading file: {e}" + + if fmt == 'plantuml': + # PlantUML has native ASCII output + try: + result = subprocess.run( + ['plantuml', '-tutxt', '-pipe'], + input=Path(source_path).read_text(), + capture_output=True, text=True, timeout=30 + ) + if result.returncode == 0: + return True, result.stdout + except Exception as e: + return False, f"PlantUML ASCII render failed: {e}" + + # For other formats, render to PNG then convert with chafa + success, png_or_error = render_to_png(source_path) + if not success: + return False, png_or_error + + if shutil.which('chafa'): + try: + result = subprocess.run( + ['chafa', '--size=80x40', png_or_error], + capture_output=True, text=True, timeout=30 + ) + if result.returncode == 0: + return True, result.stdout + except Exception as e: + return False, f"Chafa conversion failed: {e}" + + return False, "Cannot convert to ASCII. Install: sudo apt install chafa" + + +def get_format_info(fmt: str) -> dict: + """Get detailed info about a format. + + Args: + fmt: Format name + + Returns: + Dict with format details + """ + info = RENDERERS.get(fmt) + if not info: + return { + 'name': fmt, + 'supported': False, + 'error': 'Unknown format' + } + + return { + 'name': fmt, + 'extensions': FORMAT_EXTENSIONS.get(fmt, []), + 'renderer': info.command, + 'available': info.available, + 'install_hint': info.install_hint, + 'supported': info.available or info.command is None + } + + +def get_all_formats() -> list: + """Get list of all supported formats with their status. + + Returns: + List of format info dicts + """ + return [get_format_info(fmt) for fmt in FORMAT_EXTENSIONS.keys()] + + +# Check tool availability on module load +check_tool_availability() diff --git a/src/discussions/ui/gui.py b/src/discussions/ui/gui.py new file mode 100644 index 0000000..ff85399 --- /dev/null +++ b/src/discussions/ui/gui.py @@ -0,0 +1,3699 @@ +""" +Dear PyGui-based GUI for Orchestrated Discussions. + +Provides a graphical interface with native image viewing for: +- Browsing and selecting discussions +- Viewing discussion content and comments +- Adding human comments with votes +- Running discussion turns +- Viewing diagrams (PlantUML, Mermaid, OpenSCAD, DOT, SVG) +""" + +import dearpygui.dearpygui as dpg +from pathlib import Path +from typing import Optional, Callable +import subprocess +import threading +import json +import os +import tempfile +import uuid +from datetime import datetime + +# Import project modules +try: + from ..discussion import Discussion, Comment + from ..voting import format_vote_summary + from ..markers import extract_diagrams + from ..participant import ( + get_registry, AVAILABLE_VOICES, DEFAULT_VOICE, DEFAULT_PROVIDER, DEFAULT_COLOR, + get_available_providers, save_participant_settings + ) + from .formats import detect_format, render_to_png, get_renderer, get_format_info +except ImportError: + # Allow running standalone for testing + Discussion = None + format_vote_summary = None + extract_diagrams = None + get_registry = None + detect_format = None + render_to_png = None + get_renderer = None + get_format_info = None + + +class DiscussionGUI: + """ + Main GUI application using Dear PyGui. + """ + + @staticmethod + def _copy_to_clipboard(text: str) -> bool: + """Copy text to clipboard using available methods. Returns True on success.""" + # Try xclip first + try: + subprocess.run(['xclip', '-selection', 'clipboard'], + input=text.encode(), check=True, + capture_output=True, timeout=5) + return True + except (FileNotFoundError, subprocess.CalledProcessError, subprocess.TimeoutExpired): + pass + + # Try xsel + try: + subprocess.run(['xsel', '--clipboard', '--input'], + input=text.encode(), check=True, + capture_output=True, timeout=5) + return True + except (FileNotFoundError, subprocess.CalledProcessError, subprocess.TimeoutExpired): + pass + + # Try pyperclip + try: + import pyperclip + pyperclip.copy(text) + return True + except (ImportError, Exception): + pass + + # Try tkinter as last resort + try: + import tkinter as tk + root = tk.Tk() + root.withdraw() + root.clipboard_clear() + root.clipboard_append(text) + root.update() + root.destroy() + return True + except Exception: + pass + + return False + + def _read_aloud_clicked(self, sender, app_data, user_data): + """Handle read/stop button click.""" + button_tag, text, author = user_data + + # If we're currently reading and this is the active button, stop + if self._reading_session_id and self._reading_button_tag == button_tag: + self._stop_reading() + return + + # If we're reading something else, stop that first + if self._reading_session_id: + self._stop_reading() + + # Look up participant's voice from author name + voice = self._get_voice_for_author(author) + + # Start new reading + self._start_reading(text, button_tag, voice) + + def _get_voice_for_author(self, author: str) -> str: + """Look up the voice for a participant by author name.""" + # Default voice + default = DEFAULT_VOICE if DEFAULT_VOICE else "en-US-Neural2-J" + + # Try to extract alias from author name (e.g., "AI-Architect" -> "architect") + if author.startswith("AI-"): + alias = author[3:].lower() + elif author.startswith("ai-"): + alias = author[3:].lower() + else: + # Human or unknown - use default + return default + + # Look up participant + if get_registry: + participant = get_registry().get(alias) + if participant and participant.voice: + return participant.voice + + return default + + def _get_color_for_author(self, author: str) -> tuple: + """Look up the display color for a participant by author name.""" + # Default color (light blue for backwards compatibility) + default = (100, 200, 255) + + # Try to extract alias from author name (e.g., "AI-Architect" -> "architect") + if author.startswith("AI-"): + alias = author[3:].lower() + elif author.startswith("ai-"): + alias = author[3:].lower() + else: + # Human - use a neutral gray + return (180, 180, 180) + + # Look up participant + if get_registry: + participant = get_registry().get(alias) + if participant and participant.color: + return participant.color + + return default + + def _start_reading(self, text: str, button_tag: str, voice: str = None): + """Start reading text aloud.""" + if voice is None: + voice = DEFAULT_VOICE if DEFAULT_VOICE else "en-US-Neural2-J" + session_id = str(uuid.uuid4())[:8] + self._reading_session_id = session_id + self._reading_button_tag = button_tag + + # Update button to show "Stop" + if dpg.does_item_exist(button_tag): + dpg.set_item_label(button_tag, "Stop") + + def run_tts(): + try: + # Use full path since ~/.local/bin may not be in PATH + read_aloud_cmd = os.path.expanduser("~/.local/bin/read-aloud") + # Extract language from voice name (e.g., "en-GB-Neural2-A" -> "en-GB") + lang = voice[:5] if len(voice) >= 5 else "en-US" + result = subprocess.run( + [read_aloud_cmd, "--strip-md", "true", "--voice", voice, "--lang", lang, "--session-id", session_id], + input=text, + capture_output=True, + text=True, + timeout=300 + ) + except Exception: + pass + finally: + # Reset state and button when done (in main thread) + self._on_reading_complete(button_tag) + + thread = threading.Thread(target=run_tts, daemon=True) + thread.start() + + def _stop_reading(self): + """Stop current reading by creating stop signal file.""" + if self._reading_session_id: + stop_file = f"/tmp/read-aloud-{self._reading_session_id}.stop" + try: + with open(stop_file, 'w') as f: + f.write("stop") + except Exception: + pass + # Reset button immediately + if self._reading_button_tag and dpg.does_item_exist(self._reading_button_tag): + dpg.set_item_label(self._reading_button_tag, "Read") + self._reading_session_id = None + self._reading_button_tag = None + + def _on_reading_complete(self, button_tag: str): + """Called when reading completes (from background thread).""" + # Only reset if this is still the active session + if self._reading_button_tag == button_tag: + self._reading_session_id = None + self._reading_button_tag = None + if dpg.does_item_exist(button_tag): + dpg.set_item_label(button_tag, "Read") + + def _dictate_clicked(self): + """Handle dictate button click - toggle recording.""" + if self._dictation_process is not None: + # Currently recording - stop and transcribe + self._stop_dictation() + else: + # Start recording + self._start_dictation() + + def _start_dictation(self): + """Start recording audio from microphone.""" + import tempfile + import signal + + # Create temp file for audio + self._dictation_audio_file = tempfile.mktemp(suffix=".wav") + + # Start arecord without duration limit (records until interrupted) + try: + self._dictation_process = subprocess.Popen( + [ + "arecord", + "-D", "default", + "-f", "S16_LE", + "-r", "16000", + "-c", "1", + "-t", "wav", + self._dictation_audio_file + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + + # Update button to show recording state + if dpg.does_item_exist("dictate_btn"): + dpg.set_item_label("dictate_btn", "Stop Recording") + + except FileNotFoundError: + self._show_error("arecord not found. Install alsa-utils: sudo apt install alsa-utils") + self._dictation_process = None + self._dictation_audio_file = None + except Exception as e: + self._show_error(f"Failed to start recording: {e}") + self._dictation_process = None + self._dictation_audio_file = None + + def _stop_dictation(self): + """Stop recording and transcribe the audio.""" + import signal + + if self._dictation_process is None: + return + + # Send SIGINT to stop arecord gracefully + try: + self._dictation_process.send_signal(signal.SIGINT) + self._dictation_process.wait(timeout=2) + except Exception: + # Force kill if needed + try: + self._dictation_process.kill() + self._dictation_process.wait(timeout=1) + except Exception: + pass + + self._dictation_process = None + + # Update button back to Dictate + if dpg.does_item_exist("dictate_btn"): + dpg.set_item_label("dictate_btn", "Transcribing...") + + # Transcribe in background thread + audio_file = self._dictation_audio_file + self._dictation_audio_file = None + + def transcribe(): + transcript = "" + try: + if audio_file and os.path.exists(audio_file): + transcribe_cmd = os.path.expanduser("~/.local/bin/transcribe") + result = subprocess.run( + [transcribe_cmd], + input=audio_file, + capture_output=True, + text=True, + timeout=60 + ) + transcript = result.stdout.strip() + + # Clean up audio file + os.remove(audio_file) + except Exception as e: + transcript = f"[Transcription error: {e}]" + + # Update UI in main thread + self._on_transcription_complete(transcript) + + thread = threading.Thread(target=transcribe, daemon=True) + thread.start() + + def _on_transcription_complete(self, transcript: str): + """Called when transcription completes - append to comment text.""" + # Reset button + if dpg.does_item_exist("dictate_btn"): + dpg.set_item_label("dictate_btn", "Dictate") + + if transcript and not transcript.startswith("["): + # Append to existing comment text + if dpg.does_item_exist("comment_text"): + current = dpg.get_value("comment_text") or "" + separator = " " if current.strip() else "" + dpg.set_value("comment_text", current + separator + transcript) + + def __init__(self, discussions_dir: Path = None): + if discussions_dir is None: + discussions_dir = Path.cwd() + + self.discussions_dir = Path(discussions_dir) + self.current_discussion: Optional[Discussion] = None + self.discussions_list: list[tuple[Path, Discussion]] = [] + + # UI state + self._turn_running = False + self._output_lines = [] + self._diagram_textures = {} # Cache for loaded textures + + # Read-aloud state + self._reading_session_id: Optional[str] = None + self._reading_button_tag: Optional[str] = None + self._comment_counter = 0 # For generating unique button tags + + # Dictation state + self._dictation_process: Optional[subprocess.Popen] = None + self._dictation_audio_file: Optional[str] = None + + # Initialize Dear PyGui + dpg.create_context() + dpg.create_viewport(title="Orchestrated Discussions", width=1400, height=900) + + # Set up theme + self._setup_theme() + + # Build the UI + self._build_ui() + + def _setup_theme(self): + """Set up the application theme.""" + with dpg.theme() as self.global_theme: + with dpg.theme_component(dpg.mvAll): + dpg.add_theme_style(dpg.mvStyleVar_FrameRounding, 4) + dpg.add_theme_style(dpg.mvStyleVar_WindowRounding, 4) + dpg.add_theme_style(dpg.mvStyleVar_FramePadding, 8, 4) + dpg.add_theme_style(dpg.mvStyleVar_ItemSpacing, 8, 4) + + dpg.bind_theme(self.global_theme) + + # Vote color themes + with dpg.theme() as self.ready_theme: + with dpg.theme_component(dpg.mvText): + dpg.add_theme_color(dpg.mvThemeCol_Text, (100, 255, 100)) + + with dpg.theme() as self.changes_theme: + with dpg.theme_component(dpg.mvText): + dpg.add_theme_color(dpg.mvThemeCol_Text, (255, 200, 100)) + + with dpg.theme() as self.reject_theme: + with dpg.theme_component(dpg.mvText): + dpg.add_theme_color(dpg.mvThemeCol_Text, (255, 100, 100)) + + # Selection theme for buttons + with dpg.theme() as self.selected_theme: + with dpg.theme_component(dpg.mvButton): + dpg.add_theme_color(dpg.mvThemeCol_Button, (60, 100, 160)) + dpg.add_theme_color(dpg.mvThemeCol_ButtonHovered, (80, 120, 180)) + dpg.add_theme_color(dpg.mvThemeCol_ButtonActive, (100, 140, 200)) + + def _build_ui(self): + """Build the main UI layout.""" + # File dialogs (must be created before main window) + with dpg.file_dialog(directory_selector=False, show=False, + callback=self._on_file_selected, tag="file_dialog", + width=700, height=400): + dpg.add_file_extension(".discussion.md", color=(150, 255, 150)) + dpg.add_file_extension(".md", color=(150, 150, 255)) + + with dpg.file_dialog(directory_selector=True, show=False, + callback=self._on_folder_selected, tag="folder_dialog", + width=700, height=400): + pass + + # Main window + with dpg.window(label="Main", tag="main_window", no_title_bar=True, + no_move=True, no_resize=True, no_collapse=True): + + # Menu bar + with dpg.menu_bar(): + with dpg.menu(label="File"): + dpg.add_menu_item(label="Open File...", callback=self._show_open_file_dialog) + dpg.add_menu_item(label="Open Folder...", callback=self._show_open_folder_dialog) + dpg.add_separator() + dpg.add_menu_item(label="New Discussion...", callback=self._show_new_discussion_dialog) + dpg.add_menu_item(label="Manage Templates...", callback=self._show_manage_templates_dialog) + dpg.add_menu_item(label="Manage Participants...", callback=self._show_manage_participants_dialog) + dpg.add_separator() + dpg.add_menu_item(label="Refresh", callback=self._refresh_discussions) + dpg.add_separator() + dpg.add_menu_item(label="Quit", callback=lambda: dpg.stop_dearpygui()) + + with dpg.menu(label="Discussion"): + dpg.add_menu_item(label="Run Turn", callback=self._run_turn, tag="menu_run_turn") + dpg.add_menu_item(label="Add Comment", callback=self._show_comment_dialog, tag="menu_comment") + dpg.add_separator() + dpg.add_menu_item(label="View Diagrams", callback=self._show_diagram_dialog, tag="menu_diagrams") + + with dpg.menu(label="Help"): + dpg.add_menu_item(label="Keyboard Shortcuts", callback=self._show_shortcuts) + + # Main content area with splitter + with dpg.group(horizontal=True): + # Left panel: Discussion browser + with dpg.child_window(width=350, tag="browser_panel"): + with dpg.group(horizontal=True): + dpg.add_text("Discussions", color=(150, 200, 255)) + dpg.add_spacer(width=80) + dpg.add_button(label="+ New", callback=self._show_new_discussion_dialog, + width=80) + dpg.add_separator() + + # Discussion list + with dpg.child_window(tag="discussion_list", height=-1): + dpg.add_text("Loading...", tag="loading_text") + + # Right panel: Discussion view + with dpg.child_window(tag="content_panel"): + # Header + with dpg.group(horizontal=True): + dpg.add_text("Select a discussion", tag="discussion_title", + color=(200, 200, 255)) + dpg.add_spacer(width=20) + dpg.add_text("", tag="discussion_status") + dpg.add_text("", tag="discussion_phase") + + dpg.add_separator() + + # Content tabs + with dpg.tab_bar(tag="content_tabs"): + with dpg.tab(label="Discussion", tag="tab_discussion"): + with dpg.child_window(tag="discussion_content", height=-80): + dpg.add_text("Select a discussion from the left panel.", + tag="content_placeholder") + + with dpg.tab(label="Diagrams", tag="tab_diagrams"): + with dpg.child_window(tag="diagram_panel", height=-80): + dpg.add_text("No diagrams in this discussion.", + tag="diagram_placeholder") + + with dpg.tab(label="Output", tag="tab_output"): + with dpg.child_window(tag="output_panel", height=-80): + dpg.add_text("Turn output will appear here.", + tag="output_placeholder") + + # Action buttons + dpg.add_separator() + with dpg.group(horizontal=True): + dpg.add_button(label="Run Turn (T)", callback=self._run_turn, + tag="btn_run_turn", enabled=False) + dpg.add_button(label="Add Comment (C)", callback=self._show_comment_dialog, + tag="btn_comment", enabled=False) + dpg.add_button(label="View Diagrams (D)", callback=self._show_diagram_dialog, + tag="btn_diagrams", enabled=False) + dpg.add_button(label="Refresh (R)", callback=self._refresh_current) + dpg.add_spacer(width=20) + dpg.add_button(label="Edit", callback=self._show_edit_discussion_dialog, + tag="btn_edit", enabled=False) + dpg.add_button(label="Delete", callback=self._confirm_delete_discussion, + tag="btn_delete", enabled=False) + + # Vote summary + with dpg.group(horizontal=True, tag="vote_summary_group"): + dpg.add_text("Votes: ", tag="votes_label") + dpg.add_text("", tag="votes_ready") + dpg.add_text("", tag="votes_changes") + dpg.add_text("", tag="votes_reject") + + # Keyboard handler + with dpg.handler_registry(): + dpg.add_key_press_handler(dpg.mvKey_Q, callback=self._on_quit) + dpg.add_key_press_handler(dpg.mvKey_R, callback=self._on_refresh) + dpg.add_key_press_handler(dpg.mvKey_T, callback=self._on_turn) + dpg.add_key_press_handler(dpg.mvKey_C, callback=self._on_comment) + dpg.add_key_press_handler(dpg.mvKey_D, callback=self._on_diagrams) + dpg.add_key_press_handler(dpg.mvKey_N, callback=self._on_new_discussion) + dpg.add_key_press_handler(dpg.mvKey_Escape, callback=self._on_escape) + + # Set primary window + dpg.set_primary_window("main_window", True) + + def _show_open_file_dialog(self): + """Show the open file dialog.""" + dpg.set_value("file_dialog", str(self.discussions_dir)) + dpg.show_item("file_dialog") + + def _show_open_folder_dialog(self): + """Show the open folder dialog.""" + dpg.set_value("folder_dialog", str(self.discussions_dir)) + dpg.show_item("folder_dialog") + + def _on_file_selected(self, sender, app_data): + """Handle file selection from file dialog.""" + selections = app_data.get("selections", {}) + if selections: + # Get the first selected file + file_path = list(selections.values())[0] + self._open_discussion(Path(file_path)) + + def _on_folder_selected(self, sender, app_data): + """Handle folder selection from folder dialog.""" + folder_path = app_data.get("file_path_name", "") + if folder_path: + self.discussions_dir = Path(folder_path) + self._refresh_discussions() + + def _refresh_discussions(self): + """Refresh the list of discussions.""" + self.discussions_list = [] + + # Clear existing items + dpg.delete_item("discussion_list", children_only=True) + + if not self.discussions_dir.exists(): + dpg.add_text(f"Directory not found: {self.discussions_dir}", parent="discussion_list") + return + + # Only scan for .discussion.md files + md_files = sorted( + self.discussions_dir.glob("**/*.discussion.md"), + key=lambda p: p.stat().st_mtime, + reverse=True + ) + + for path in md_files: + try: + d = Discussion.load(path) + self.discussions_list.append((path, d)) + except Exception: + pass + + # Build list UI + if not self.discussions_list: + dpg.add_text("No discussions found.", parent="discussion_list") + return + + for path, d in self.discussions_list: + votes = d.get_votes() + vote_str = format_vote_summary(votes) if votes else "No votes" + + with dpg.group(parent="discussion_list"): + # Clickable title + dpg.add_button( + label=d.title or path.stem, + callback=lambda s, a, u: self._open_discussion(u), + user_data=path, + width=-1 + ) + # Status line + with dpg.group(horizontal=True): + dpg.add_text(f" [{d.status}]", color=(150, 150, 150)) + dpg.add_text(f" {d.phase}", color=(100, 150, 200)) + dpg.add_text(f" {vote_str}", color=(120, 120, 120)) + dpg.add_spacer(height=5) + + def _open_discussion(self, path: Path): + """Open a discussion for viewing.""" + try: + self.current_discussion = Discussion.load(path) + self._show_discussion() + except Exception as e: + self._show_error(f"Error loading discussion: {e}") + + def _show_discussion(self): + """Display the current discussion.""" + if not self.current_discussion: + return + + d = self.current_discussion + + # Update header + dpg.set_value("discussion_title", d.title or "Untitled") + dpg.set_value("discussion_status", f"[{d.status}]") + dpg.set_value("discussion_phase", f"Phase: {d.phase}") + + # Update vote summary + votes = d.get_votes() + if votes: + ready = sum(1 for v in votes.values() if v == "READY") + changes = sum(1 for v in votes.values() if v == "CHANGES") + reject = sum(1 for v in votes.values() if v == "REJECT") + + dpg.set_value("votes_ready", f"READY: {ready} ") + dpg.set_value("votes_changes", f"CHANGES: {changes} ") + dpg.set_value("votes_reject", f"REJECT: {reject}") + + dpg.bind_item_theme("votes_ready", self.ready_theme) + dpg.bind_item_theme("votes_changes", self.changes_theme) + dpg.bind_item_theme("votes_reject", self.reject_theme) + else: + dpg.set_value("votes_ready", "No votes yet") + dpg.set_value("votes_changes", "") + dpg.set_value("votes_reject", "") + + # Enable action buttons + dpg.configure_item("btn_run_turn", enabled=True) + dpg.configure_item("btn_comment", enabled=True) + dpg.configure_item("btn_diagrams", enabled=True) + dpg.configure_item("btn_edit", enabled=True) + dpg.configure_item("btn_delete", enabled=True) + + # Clear and rebuild content + dpg.delete_item("discussion_content", children_only=True) + + # Add context/preamble (selectable) + context = d.get_context() + if context: + with dpg.group(parent="discussion_content"): + dpg.add_text("Context", color=(150, 200, 255)) + dpg.add_separator() + # Selectable text area + num_lines = min(context.count('\n') + 1, 15) + dpg.add_input_text( + default_value=context, + multiline=True, + readonly=True, + width=-1, + height=num_lines * 18 + 10, + tab_input=False + ) + dpg.add_spacer(height=10) + + # Add comments + for comment in d.comments: + self._add_comment_widget(comment) + + # Update diagrams tab + self._update_diagrams_panel() + + # Switch to discussion tab + dpg.set_value("content_tabs", "tab_discussion") + + def _add_comment_widget(self, comment: Comment): + """Add a comment widget to the discussion view.""" + parent = "discussion_content" + + # Generate unique tag for this comment's read button + self._comment_counter += 1 + button_tag = f"read_btn_{self._comment_counter}" + + with dpg.group(parent=parent): + dpg.add_separator() + + # Author and vote header with read-aloud button + with dpg.group(horizontal=True): + author_color = self._get_color_for_author(comment.author) + dpg.add_text(comment.author, color=author_color) + if comment.vote: + vote_color = { + "READY": (100, 255, 100), + "CHANGES": (255, 200, 100), + "REJECT": (255, 100, 100), + }.get(comment.vote, (200, 200, 200)) + dpg.add_text(f" [{comment.vote}]", color=vote_color) + dpg.add_spacer(width=10) + # Read aloud button with unique tag for state tracking + dpg.add_button( + label="Read", + tag=button_tag, + callback=self._read_aloud_clicked, + user_data=(button_tag, comment.body, comment.author), + width=50, + height=20 + ) + + # Comment body (selectable) + dpg.add_spacer(height=5) + body = comment.body + num_lines = min(body.count('\n') + 1, 20) # Max 20 lines visible + dpg.add_input_text( + default_value=body, + multiline=True, + readonly=True, + width=-1, + height=max(num_lines * 18 + 10, 60), # Min height 60 + tab_input=False + ) + + dpg.add_spacer(height=10) + + def _update_diagrams_panel(self): + """Update the diagrams panel with available diagrams.""" + dpg.delete_item("diagram_panel", children_only=True) + + # Add "New Artifact" button at top + with dpg.group(parent="diagram_panel", horizontal=True): + dpg.add_button( + label="New Artifact", + callback=lambda: self._create_new_artifact(), + width=100 + ) + dpg.add_text("(opens editor, adds to discussion)", color=(150, 150, 150)) + dpg.add_separator(parent="diagram_panel") + + if not self.current_discussion: + dpg.add_text("No discussion loaded.", parent="diagram_panel") + return + + # Find diagrams using the marker system + diagrams = self._find_diagrams() + + if not diagrams: + dpg.add_text("No diagrams found in this discussion.", parent="diagram_panel") + dpg.add_text("Diagrams are referenced with DIAGRAM: markers.", parent="diagram_panel", + color=(150, 150, 150)) + return + + dpg.add_text(f"Found {len(diagrams)} diagram(s):", parent="diagram_panel", + color=(150, 200, 255)) + dpg.add_separator(parent="diagram_panel") + + for diagram_path in diagrams: + resolved = self._resolve_diagram_path(diagram_path) + name = Path(diagram_path).name + + with dpg.group(parent="diagram_panel"): + with dpg.group(horizontal=True): + dpg.add_button( + label=f"View: {name}", + callback=lambda s, a, u: self._view_diagram(u), + user_data=resolved + ) + dpg.add_button( + label="Edit", + callback=lambda s, a, u: self._launch_artifact_editor(u), + user_data=resolved, + width=50 + ) + if Path(resolved).exists(): + dpg.add_text("(exists)", color=(100, 255, 100)) + else: + dpg.add_text("(not found)", color=(255, 100, 100)) + dpg.add_spacer(height=5) + + def _find_diagrams(self) -> list[str]: + """Find diagram references in current discussion.""" + diagrams = [] + if not self.current_discussion: + return diagrams + + # Use the markers module + content = self.current_discussion._raw_content + + # Look for DIAGRAM: markers + import re + for match in re.finditer(r'^DIAGRAM:\s*(.+)$', content, re.MULTILINE | re.IGNORECASE): + path = match.group(1).strip() + if path not in diagrams: + diagrams.append(path) + + # Also check for **Diagram:** `path` format (legacy) + for match in re.finditer(r'\*\*Diagram:\*\*\s*`([^`]+)`', content): + path = match.group(1) + if path not in diagrams: + diagrams.append(path) + + return diagrams + + def _resolve_diagram_path(self, diagram_path: str) -> str: + """Resolve a diagram path to an absolute path.""" + if diagram_path.startswith('/'): + return diagram_path + + candidates = [ + Path.cwd() / diagram_path, + self.current_discussion.path.parent / diagram_path, + self.discussions_dir / diagram_path, + ] + + for candidate in candidates: + if candidate.exists(): + return str(candidate) + + return str(Path.cwd() / diagram_path) + + def _get_artifact_editor_cmd(self) -> tuple[list[str], dict | None]: + """Get the command and environment for launching artifact editor. + + Returns: + Tuple of (command_list, env_dict or None) + """ + import shutil + + artifact_editor = shutil.which('artifact-editor') + if artifact_editor: + return [artifact_editor], None + + # Try direct path to project + project_path = Path.home() / "PycharmProjects" / "artifact-editor" + if (project_path / "src" / "artifact_editor" / "cli.py").exists(): + env = os.environ.copy() + env["PYTHONPATH"] = str(project_path / "src") + return ["python3", "-m", "artifact_editor.cli"], env + + return None, None + + def _launch_artifact_editor(self, file_path: str = None): + """Launch the stand-alone artifact editor (non-blocking). + + Args: + file_path: Optional path to file to edit. If None, opens empty editor. + """ + cmd, env = self._get_artifact_editor_cmd() + if not cmd: + self._show_error( + "Artifact Editor not installed.\n\n" + "Install from ~/PycharmProjects/artifact-editor:\n" + " cd ~/PycharmProjects/artifact-editor\n" + " ./install.sh\n\n" + "Or run directly with:\n" + " PYTHONPATH=src python3 -m artifact_editor.cli" + ) + return + + if file_path: + cmd.extend(["--output", str(file_path)]) + + # Launch in background (non-blocking) + try: + subprocess.Popen( + cmd, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + start_new_session=True, + env=env, + ) + self._add_output(f"Launched artifact editor" + (f" for {Path(file_path).name}" if file_path else "")) + except Exception as e: + self._show_error(f"Failed to launch artifact editor: {e}") + + def _launch_artifact_editor_blocking(self, file_path: str) -> str | None: + """Launch artifact editor and wait for it to complete. + + Args: + file_path: Suggested output path for the artifact. + + Returns: + The actual saved file path from ARTIFACT_SAVED output, or None if cancelled/failed. + """ + cmd, env = self._get_artifact_editor_cmd() + if not cmd: + return None + + cmd.extend(["--output", str(file_path)]) + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + env=env, + ) + + # Parse ARTIFACT_SAVED:path from stdout + for line in result.stdout.splitlines(): + if line.startswith("ARTIFACT_SAVED:"): + return line.split(":", 1)[1].strip() + + return None + except Exception as e: + return None + + def _create_new_artifact(self): + """Create a new artifact via the artifact editor and add it to the discussion. + + Opens the artifact editor, waits for save, then adds a comment to the + discussion with the DIAGRAM reference. + """ + if not self.current_discussion: + self._show_error("No discussion loaded") + return + + # Create diagrams directory if needed + diagrams_dir = self.current_discussion.path.parent / "diagrams" + diagrams_dir.mkdir(exist_ok=True) + + # Generate suggested filename + import re + title = getattr(self.current_discussion, 'title', 'artifact') or 'artifact' + safe_title = re.sub(r'[^a-z0-9]+', '-', title.lower())[:50] + user = os.environ.get("USER", "human") + + # Find next number - check all formats + existing = list(diagrams_dir.glob(f"{safe_title}_{user}_*.*")) + next_num = len(existing) + 1 + suggested_file = diagrams_dir / f"{safe_title}_{user}_{next_num}.puml" + + self._add_output("Opening artifact editor...") + import threading + + def run_editor(): + saved_path = self._launch_artifact_editor_blocking(str(suggested_file)) + if saved_path: + # Make path relative for the DIAGRAM marker + try: + rel_path = Path(saved_path).relative_to(self.current_discussion.path.parent) + except ValueError: + rel_path = Path(saved_path).name + + # Add a comment with the diagram reference + author = os.environ.get("USER", "Human") + comment_text = f"Added diagram.\n\nDIAGRAM: {rel_path}" + self.current_discussion.add_comment(author, comment_text) + self.current_discussion.save() + + # Refresh the view + self.current_discussion = Discussion.load(self.current_discussion.path) + self._show_discussion() + self._add_output(f"Added artifact: {rel_path}") + else: + self._add_output("Artifact editor closed without saving") + + thread = threading.Thread(target=run_editor, daemon=True) + thread.start() + + def _view_diagram(self, diagram_path: str): + """View a diagram rendered as PNG. Supports multiple formats. + + Supported formats: + - PlantUML (.puml, .plantuml) + - Mermaid (.mmd, .mermaid) + - Graphviz/DOT (.dot, .gv) + - OpenSCAD (.scad) + - SVG (.svg) - displayed directly + """ + if not Path(diagram_path).exists(): + self._show_error(f"Diagram file not found: {diagram_path}") + return + + # Detect format + fmt = detect_format(diagram_path) if detect_format else None + if not fmt: + # Default to plantuml for .puml files (legacy) + if diagram_path.endswith('.puml'): + fmt = 'plantuml' + else: + self._show_error(f"Unknown diagram format: {diagram_path}") + return + + # Check renderer availability + renderer_info = get_renderer(fmt) if get_renderer else None + if renderer_info and renderer_info.command and not renderer_info.available: + self._show_error( + f"Renderer for {fmt} not available.\n\n" + f"Install with: {renderer_info.install_hint}" + ) + return + + # Handle SVG directly (no rendering needed) + if fmt == 'svg': + # Convert SVG to PNG for display + success, result = render_to_png(diagram_path) if render_to_png else (False, "render_to_png not available") + if success: + self._show_image_window(result, Path(diagram_path).name, diagram_path, diagram_path) + else: + # Try displaying SVG directly by reading and showing info + self._show_error(f"SVG display: {result}") + return + + # Render to PNG using format-specific renderer + success, result = render_to_png(diagram_path) if render_to_png else (False, "render_to_png not available") + + if not success: + self._show_error(f"Render failed: {result}") + return + + actual_png = result + + # For PlantUML, also try to generate SVG for element detection + actual_svg = None + if fmt == 'plantuml': + try: + subprocess.run( + ['plantuml', '-tsvg', '-o', str(Path(actual_png).parent), diagram_path], + capture_output=True, + text=True, + timeout=30 + ) + expected_svg = Path(diagram_path).with_suffix('.svg') + if expected_svg.exists(): + actual_svg = str(expected_svg) + except Exception: + pass # SVG is optional for click detection + + self._show_image_window(actual_png, Path(diagram_path).name, diagram_path, actual_svg) + + def _show_image_window(self, png_path: str, title: str, puml_path: str = None, svg_path: str = None): + """Show diagram viewer/editor window with View and Edit modes.""" + import time + unique_id = int(time.time() * 1000) % 1000000 + + window_tag = f"diagram_window_{unique_id}" + texture_tag = f"texture_{unique_id}" + + # Ensure we have a texture registry + if not dpg.does_item_exist("__texture_registry"): + dpg.add_texture_registry(tag="__texture_registry") + + # Track textures for cleanup + if not hasattr(self, '_active_textures'): + self._active_textures = [] + if not hasattr(self, '_textures_to_delete'): + self._textures_to_delete = [] + # Track active diagram windows for keyboard handling + if not hasattr(self, '_active_diagram_windows'): + self._active_diagram_windows = set() + self._active_diagram_windows.add(window_tag) + + # State for this editor instance + editor_state = { + 'puml_path': puml_path, + 'svg_path': svg_path, + 'original_source': Path(puml_path).read_text() if puml_path and Path(puml_path).exists() else '', + 'current_source': '', + 'texture_tag': texture_tag, + 'edit_mode': False, + 'has_changes': False, + 'ai_running': False, + 'image_size': (0, 0), + } + editor_state['current_source'] = editor_state['original_source'] + + def load_and_display_image(png_file): + """Load image and update texture.""" + try: + width, height, channels, data = dpg.load_image(png_file) + + # Store image dimensions for click detection + editor_state['image_size'] = (width, height) + + # Delete old texture if exists + if dpg.does_item_exist(editor_state['texture_tag']): + self._textures_to_delete.append(editor_state['texture_tag']) + + # Create new texture with new unique id + new_texture_tag = f"texture_{int(time.time() * 1000) % 1000000}" + dpg.add_static_texture(width, height, data, tag=new_texture_tag, parent="__texture_registry") + self._active_textures.append(new_texture_tag) + editor_state['texture_tag'] = new_texture_tag + + # Update image widget + if dpg.does_item_exist(f"preview_image_{unique_id}"): + dpg.configure_item(f"preview_image_{unique_id}", texture_tag=new_texture_tag) + + return True + except Exception as e: + return False + + def render_preview(): + """Re-render the PlantUML and update preview.""" + if not editor_state['puml_path']: + return + + # Get current source from editor + if dpg.does_item_exist(f"source_editor_{unique_id}"): + editor_state['current_source'] = dpg.get_value(f"source_editor_{unique_id}") + + # Write to temp file and render both PNG and SVG + try: + with tempfile.NamedTemporaryFile(mode='w', suffix='.puml', delete=False) as tmp: + tmp.write(editor_state['current_source']) + tmp_puml = tmp.name + + # Generate PNG + subprocess.run( + ['plantuml', '-tpng', tmp_puml], + capture_output=True, text=True, timeout=30 + ) + + # Generate SVG for element detection + subprocess.run( + ['plantuml', '-tsvg', tmp_puml], + capture_output=True, text=True, timeout=30 + ) + + tmp_png = Path(tmp_puml).with_suffix('.png') + tmp_svg = Path(tmp_puml).with_suffix('.svg') + + if tmp_png.exists(): + load_and_display_image(str(tmp_png)) + tmp_png.unlink() # Clean up + + if tmp_svg.exists(): + tmp_svg.unlink() # Clean up + + Path(tmp_puml).unlink() # Clean up + + dpg.configure_item(f"status_{unique_id}", default_value="Preview updated", color=(100, 255, 100)) + except Exception as e: + dpg.configure_item(f"status_{unique_id}", default_value=f"Render error: {e}", color=(255, 100, 100)) + + def toggle_edit_mode(): + """Switch between View and Edit modes.""" + editor_state['edit_mode'] = not editor_state['edit_mode'] + + if editor_state['edit_mode']: + # Show edit panels + dpg.configure_item(f"edit_panel_{unique_id}", show=True) + dpg.configure_item(f"prompt_panel_{unique_id}", show=True) + dpg.configure_item(f"btn_edit_{unique_id}", label="View") + dpg.configure_item(f"btn_save_{unique_id}", show=True) + dpg.configure_item(f"btn_discard_{unique_id}", show=True) + # Show edit mode hint + dpg.configure_item(f"status_{unique_id}", + default_value="Edit mode - modify source code on the left", + color=(150, 200, 255)) + else: + # Hide edit panels + dpg.configure_item(f"edit_panel_{unique_id}", show=False) + dpg.configure_item(f"prompt_panel_{unique_id}", show=False) + dpg.configure_item(f"btn_edit_{unique_id}", label="Edit") + dpg.configure_item(f"btn_save_{unique_id}", show=False) + dpg.configure_item(f"btn_discard_{unique_id}", show=False) + dpg.configure_item(f"status_{unique_id}", default_value="", color=(150, 150, 150)) + + def on_source_changed(): + """Called when source text changes.""" + if dpg.does_item_exist(f"source_editor_{unique_id}"): + new_source = dpg.get_value(f"source_editor_{unique_id}") + editor_state['has_changes'] = (new_source != editor_state['original_source']) + editor_state['current_source'] = new_source + + def apply_ai_edit(): + """Apply AI-assisted edit based on prompt.""" + if editor_state['ai_running']: + return + + prompt_text = dpg.get_value(f"prompt_input_{unique_id}") + if not prompt_text.strip(): + dpg.configure_item(f"status_{unique_id}", default_value="Enter an instruction first", color=(255, 200, 100)) + return + + editor_state['ai_running'] = True + dpg.configure_item(f"status_{unique_id}", default_value="⏳ AI is processing your request...", color=(150, 150, 255)) + dpg.configure_item(f"btn_apply_{unique_id}", enabled=False, label="Processing...") + + # Run in background thread + def run_ai_edit(): + try: + current_source = editor_state['current_source'] + result = subprocess.run( + ['discussion-diagram-editor', '--instruction', prompt_text], + input=current_source, + capture_output=True, + text=True, + timeout=60 + ) + + if result.returncode == 0 and result.stdout.strip(): + # Queue the update for main thread + editor_state['pending_source'] = result.stdout.strip() + editor_state['ai_success'] = True + else: + editor_state['ai_error'] = result.stderr or "AI edit failed" + editor_state['ai_success'] = False + except Exception as e: + editor_state['ai_error'] = str(e) + editor_state['ai_success'] = False + finally: + editor_state['ai_running'] = False + editor_state['ai_complete'] = True + + thread = threading.Thread(target=run_ai_edit, daemon=True) + thread.start() + + def save_diagram(): + """Save the modified diagram to file and optionally to discussion.""" + if not editor_state['puml_path']: + return + + try: + # Save to file + Path(editor_state['puml_path']).write_text(editor_state['current_source']) + editor_state['original_source'] = editor_state['current_source'] + editor_state['has_changes'] = False + + # Re-render to update the actual file's PNG + subprocess.run( + ['plantuml', '-tpng', editor_state['puml_path']], + capture_output=True, timeout=30 + ) + + dpg.configure_item(f"status_{unique_id}", default_value="Saved!", color=(100, 255, 100)) + + # Clear prompt + dpg.set_value(f"prompt_input_{unique_id}", "") + + except Exception as e: + dpg.configure_item(f"status_{unique_id}", default_value=f"Save error: {e}", color=(255, 100, 100)) + + def discard_changes(): + """Discard changes and revert to original.""" + editor_state['current_source'] = editor_state['original_source'] + editor_state['has_changes'] = False + + if dpg.does_item_exist(f"source_editor_{unique_id}"): + dpg.set_value(f"source_editor_{unique_id}", editor_state['original_source']) + + dpg.set_value(f"prompt_input_{unique_id}", "") + render_preview() + dpg.configure_item(f"status_{unique_id}", default_value="Changes discarded", color=(200, 200, 100)) + + def close_window(): + """Clean up and close.""" + try: + if dpg.does_item_exist(window_tag): + dpg.delete_item(window_tag) + if editor_state['texture_tag'] in self._active_textures: + self._active_textures.remove(editor_state['texture_tag']) + self._textures_to_delete.append(editor_state['texture_tag']) + # Remove from active diagram windows + if hasattr(self, '_active_diagram_windows'): + self._active_diagram_windows.discard(window_tag) + except Exception: + pass + + def poll_ai_completion(): + """Check if AI edit completed (called from main poll loop).""" + if editor_state.get('ai_complete'): + editor_state['ai_complete'] = False + dpg.configure_item(f"btn_apply_{unique_id}", enabled=True, label="Apply AI Edit") + + if editor_state.get('ai_success') and 'pending_source' in editor_state: + # Update source editor + dpg.set_value(f"source_editor_{unique_id}", editor_state['pending_source']) + editor_state['current_source'] = editor_state['pending_source'] + editor_state['has_changes'] = True + del editor_state['pending_source'] + + # Re-render preview + render_preview() + dpg.configure_item(f"status_{unique_id}", default_value="AI edit applied!", color=(100, 255, 100)) + else: + error = editor_state.get('ai_error', 'Unknown error') + dpg.configure_item(f"status_{unique_id}", default_value=f"AI error: {error}", color=(255, 100, 100)) + + # Store poll function for main loop + if not hasattr(self, '_diagram_editors'): + self._diagram_editors = {} + self._diagram_editors[unique_id] = poll_ai_completion + + # Load initial image + try: + width, height, channels, data = dpg.load_image(png_path) + editor_state['image_size'] = (width, height) + dpg.add_static_texture(width, height, data, tag=texture_tag, parent="__texture_registry") + self._active_textures.append(texture_tag) + + except Exception as e: + self._show_error(f"Failed to load image: {e}") + return + + # Window state + editor_state['maximized'] = False + editor_state['normal_size'] = (1000, 700) + editor_state['normal_pos'] = (100, 100) + + def toggle_maximize(): + """Toggle between normal and maximized window.""" + if editor_state['maximized']: + # Restore to normal size + dpg.configure_item(window_tag, + width=editor_state['normal_size'][0], + height=editor_state['normal_size'][1], + pos=editor_state['normal_pos']) + dpg.configure_item(f"btn_maximize_{unique_id}", label="Maximize Window") + editor_state['maximized'] = False + else: + # Save current size and position + editor_state['normal_size'] = (dpg.get_item_width(window_tag), + dpg.get_item_height(window_tag)) + editor_state['normal_pos'] = dpg.get_item_pos(window_tag) + # Maximize to viewport + vp_width = dpg.get_viewport_width() + vp_height = dpg.get_viewport_height() + dpg.configure_item(window_tag, width=vp_width - 20, height=vp_height - 40, pos=(10, 30)) + dpg.configure_item(f"btn_maximize_{unique_id}", label="Restore Window") + editor_state['maximized'] = True + + def add_line_numbers(source): + """Add line numbers to source code for display.""" + lines = source.split('\n') + max_digits = len(str(len(lines))) + numbered = [] + for i, line in enumerate(lines, 1): + numbered.append(f"{i:>{max_digits}}| {line}") + return '\n'.join(numbered) + + def remove_line_numbers(numbered_source): + """Remove line numbers from source code for saving.""" + import re + lines = numbered_source.split('\n') + cleaned = [] + for line in lines: + # Remove the " N| " prefix + match = re.match(r'^\s*\d+\|\s?(.*)$', line) + if match: + cleaned.append(match.group(1)) + else: + cleaned.append(line) + return '\n'.join(cleaned) + + # Build the window + with dpg.window(label=f"Diagram: {title}", tag=window_tag, + width=1000, height=700, pos=[100, 100], + on_close=lambda: (self._diagram_editors.pop(unique_id, None), close_window())): + + # Top toolbar with window controls on the right + with dpg.group(horizontal=True): + dpg.add_button(label="Edit", tag=f"btn_edit_{unique_id}", callback=toggle_edit_mode) + dpg.add_button(label="Refresh Preview", callback=render_preview) + dpg.add_button(label="Save", tag=f"btn_save_{unique_id}", callback=save_diagram, show=False) + dpg.add_button(label="Discard", tag=f"btn_discard_{unique_id}", callback=discard_changes, show=False) + dpg.add_spacer(width=10) + dpg.add_text("", tag=f"status_{unique_id}", color=(150, 150, 150)) + dpg.add_spacer(width=20) + dpg.add_button(label="Maximize Window", tag=f"btn_maximize_{unique_id}", callback=toggle_maximize) + + dpg.add_separator() + + # Main content area + with dpg.group(horizontal=True, tag=f"main_content_{unique_id}"): + # Left panel: Source editor (hidden by default) + with dpg.child_window(width=400, tag=f"edit_panel_{unique_id}", show=False): + dpg.add_text("PlantUML Source", color=(150, 200, 255)) + dpg.add_input_text( + tag=f"source_editor_{unique_id}", + default_value=editor_state['current_source'], + multiline=True, + width=-1, + height=-1, + callback=on_source_changed, + on_enter=False, + tab_input=True + ) + + # Right panel: Preview (always visible) + with dpg.child_window(tag=f"preview_panel_{unique_id}"): + dpg.add_text("Preview", color=(150, 200, 255)) + with dpg.child_window(horizontal_scrollbar=True, tag=f"preview_scroll_{unique_id}"): + dpg.add_image(texture_tag, tag=f"preview_image_{unique_id}") + + # Bottom panel: Prompt input (hidden by default) + with dpg.child_window(height=80, tag=f"prompt_panel_{unique_id}", show=False): + dpg.add_text("Describe changes:", color=(150, 200, 255)) + with dpg.group(horizontal=True): + dpg.add_input_text( + tag=f"prompt_input_{unique_id}", + hint="e.g., 'Add a cache component between API and Database'", + width=-150 + ) + dpg.add_button(label="Apply AI Edit", tag=f"btn_apply_{unique_id}", callback=apply_ai_edit) + + dpg.add_separator() + dpg.add_button(label="Close", callback=close_window) + + def _run_turn(self): + """Run a discussion turn with all participants.""" + if not self.current_discussion or self._turn_running: + return + + self._turn_running = True + + # Switch to output tab + dpg.set_value("content_tabs", "tab_output") + + # Clear output and create selectable output area + dpg.delete_item("output_panel", children_only=True) + self._output_lines = [] + self._last_output_index = 0 + + # Create a single large selectable text area for output + dpg.add_input_text( + tag="output_text", + default_value="Starting turn...\n", + multiline=True, + readonly=True, + width=-1, + height=-40, # Leave room for copy button + parent="output_panel", + tab_input=False + ) + with dpg.group(horizontal=True, parent="output_panel"): + dpg.add_button(label="Copy Output", callback=self._copy_output) + dpg.add_button(label="Clear", callback=lambda: dpg.set_value("output_text", "")) + + # Run in background thread + thread = threading.Thread(target=self._run_turn_thread, daemon=True) + thread.start() + + def _copy_output(self): + """Copy output to clipboard.""" + output = dpg.get_value("output_text") + if self._copy_to_clipboard(output): + # Brief visual feedback would be nice but we can't easily do that here + pass + + def _add_output(self, text: str): + """Add output line (thread-safe).""" + self._output_lines.append(text) + # Note: UI update happens via polling in main thread, not here + # dpg.split_frame() is NOT thread-safe and causes segfaults + + def _run_turn_thread(self): + """Background thread for running turn.""" + d = self.current_discussion + if not d: + return + + participants = d.participant_aliases or ["architect", "security", "pragmatist"] + + # Create log file + log_dir = Path.home() / ".local" / "share" / "orchestrated-discussions" / "logs" + log_dir.mkdir(parents=True, exist_ok=True) + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + log_file = log_dir / f"turn_{timestamp}.log" + + self._add_output("=== Starting Turn ===") + self._add_output(f"Discussion: {d.title}") + self._add_output(f"Participants: {', '.join(participants)}") + self._add_output(f"Log: {log_file}") + self._add_output("") + + # Start all participants in parallel + processes = {} + discussion_content = d.get_content() + + for alias in participants: + tool_name = f"discussion-{alias}" + self._add_output(f">>> Starting {alias}...") + + try: + process = subprocess.Popen( + [tool_name, "--log-file", str(log_file)], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + process.stdin.write(discussion_content) + process.stdin.close() + processes[alias] = process + except FileNotFoundError: + self._add_output(f" Tool {tool_name} not found") + except Exception as e: + self._add_output(f" ERROR: {e}") + + if not processes: + self._add_output("No participants started!") + self._turn_running = False + return + + self._add_output("") + self._add_output("Waiting for responses...") + self._add_output("") + + # Tail log file while waiting + last_pos = 0 + import time + while any(p.poll() is None for p in processes.values()): + try: + if log_file.exists(): + with open(log_file, 'r') as f: + f.seek(last_pos) + for line in f.readlines(): + self._add_output(line.rstrip()) + last_pos = f.tell() + except Exception: + pass + time.sleep(0.2) + + # Read remaining log entries + try: + if log_file.exists(): + with open(log_file, 'r') as f: + f.seek(last_pos) + for line in f.readlines(): + self._add_output(line.rstrip()) + except Exception: + pass + + # Collect responses + self._add_output("") + self._add_output("Collecting responses...") + responses = [] + + for alias, process in processes.items(): + try: + stdout = process.stdout.read() + + if process.returncode != 0: + self._add_output(f" {alias}: ERROR (exit {process.returncode})") + continue + + json_start = stdout.find("{") + json_end = stdout.rfind("}") + 1 + if json_start >= 0 and json_end > json_start: + response = json.loads(stdout[json_start:json_end]) + + if response.get("sentinel") == "NO_RESPONSE": + self._add_output(f" {alias}: nothing to add") + elif "comment" in response: + vote = response.get("vote") + diagram_file = response.get("diagram_file") + vote_str = f" [{vote}]" if vote else "" + diagram_str = " +diagram" if diagram_file else "" + self._add_output(f" {alias}: responded{vote_str}{diagram_str}") + + comment_text = response["comment"] + if diagram_file: + comment_text += f"\n\nDIAGRAM: {diagram_file}" + + responses.append({ + "author": f"AI-{alias.capitalize()}", + "comment": comment_text, + "vote": vote, + }) + else: + self._add_output(f" {alias}: no JSON in response") + + except json.JSONDecodeError as e: + self._add_output(f" {alias}: JSON parse error") + except Exception as e: + self._add_output(f" {alias}: ERROR: {e}") + + # Append responses + if responses: + self._add_output("") + self._add_output("Appending responses...") + d = Discussion.load(d.path) + for resp in responses: + d.add_comment( + author=resp["author"], + text=resp["comment"], + vote=resp.get("vote"), + ) + d.save() + self._add_output(f" Added {len(responses)} comments") + + # Count votes + self._add_output("") + self._add_output("Counting votes...") + try: + result = subprocess.run( + ["discussion-parser"], + input=Path(d.path).read_text(), + capture_output=True, + text=True, + ) + if result.returncode == 0: + vote_result = subprocess.run( + ["discussion-vote-counter"], + input=result.stdout, + capture_output=True, + text=True, + ) + if vote_result.returncode == 0: + votes = json.loads(vote_result.stdout) + vs = votes.get("vote_summary", {}) + self._add_output(f" READY={vs.get('READY', 0)} " + f"CHANGES={vs.get('CHANGES', 0)} " + f"REJECT={vs.get('REJECT', 0)}") + consensus = votes.get("consensus", {}) + if consensus.get("reached"): + self._add_output(" Consensus: REACHED") + # Auto-advance phase if in a voting phase + self._try_advance_phase(d) + elif consensus.get("reason"): + self._add_output(f" Consensus: {consensus['reason']}") + except Exception as e: + self._add_output(f" Vote counting error: {e}") + + # Refresh view + self._add_output("") + self._add_output("=== Turn Complete ===") + + self._turn_running = False + self._turn_complete = True # Signal main thread to refresh + + def _try_advance_phase(self, discussion: 'Discussion'): + """Auto-advance phase if consensus reached in a voting phase.""" + import yaml + + # Load template to check phase config + template_name = discussion.template or "feature" + template_dirs = [ + Path.cwd() / "templates", + Path(__file__).parent.parent.parent.parent / "templates", + ] + + template = {} + for template_dir in template_dirs: + template_path = template_dir / f"{template_name}.yaml" + if template_path.exists(): + with open(template_path) as f: + template = yaml.safe_load(f) or {} + break + + phases = template.get("phases", {}) + current_phase = discussion.phase + phase_config = phases.get(current_phase, {}) + + # Only advance if this is a voting phase with a next_phase + if not phase_config.get("voting", False): + return + + next_phase = phase_config.get("next_phase") + if not next_phase: + return + + # Advance the phase + d = Discussion.load(discussion.path) + old_phase = d.phase + d.update_phase(next_phase) + d.save() + self._add_output(f" Phase advanced: {old_phase} → {next_phase}") + + def _show_comment_dialog(self): + """Show dialog for adding a human comment.""" + if not self.current_discussion: + return + + window_tag = "comment_dialog" + if dpg.does_item_exist(window_tag): + dpg.delete_item(window_tag) + + with dpg.window(label="Add Comment", tag=window_tag, modal=True, + width=600, height=400, pos=[400, 200]): + + with dpg.group(horizontal=True): + dpg.add_text("Your comment:") + dpg.add_spacer(width=10) + dpg.add_button( + label="Dictate", + tag="dictate_btn", + callback=lambda: self._dictate_clicked(), + width=100 + ) + dpg.add_button( + label="Add Artifact", + callback=lambda: self._launch_artifact_editor_for_comment(), + width=100 + ) + + dpg.add_input_text(tag="comment_text", multiline=True, width=-1, height=200) + + dpg.add_spacer(height=10) + dpg.add_text("Vote (optional):") + dpg.add_radio_button( + items=["None", "READY", "CHANGES", "REJECT"], + tag="comment_vote", + default_value="None", + horizontal=True + ) + + dpg.add_spacer(height=10) + with dpg.group(horizontal=True): + dpg.add_button(label="Submit", callback=self._submit_comment) + dpg.add_button(label="Cancel", callback=lambda: dpg.delete_item(window_tag)) + + def _submit_comment(self): + """Submit the comment from the dialog.""" + text = dpg.get_value("comment_text") + vote_str = dpg.get_value("comment_vote") + vote = vote_str if vote_str != "None" else None + + if text.strip(): + author = os.environ.get("USER", "Human") + self.current_discussion.add_comment(author, text, vote) + self.current_discussion.save() + + # Refresh view + self.current_discussion = Discussion.load(self.current_discussion.path) + self._show_discussion() + + dpg.delete_item("comment_dialog") + + def _launch_artifact_editor_for_comment(self): + """Launch artifact editor to create a new artifact for the current comment. + + Launches editor and waits for it to save. The DIAGRAM: marker is added + to the comment only after we know the actual saved path (user may change format). + """ + if not self.current_discussion: + return + + # Create diagrams directory if needed + diagrams_dir = self.current_discussion.path.parent / "diagrams" + diagrams_dir.mkdir(exist_ok=True) + + # Generate suggested filename (artifact editor may change extension) + import re + title = getattr(self.current_discussion, 'title', 'artifact') or 'artifact' + safe_title = re.sub(r'[^a-z0-9]+', '-', title.lower())[:50] + user = os.environ.get("USER", "human") + + # Find next number - check all formats + existing = list(diagrams_dir.glob(f"{safe_title}_{user}_*.*")) + next_num = len(existing) + 1 + suggested_file = diagrams_dir / f"{safe_title}_{user}_{next_num}.puml" + + # Launch editor and wait for result (in background thread) + self._add_output("Opening artifact editor...") + import threading + + def run_editor(): + saved_path = self._launch_artifact_editor_blocking(str(suggested_file)) + # Schedule UI update on main thread + if saved_path: + # Make path relative to diagrams dir for the DIAGRAM marker + try: + rel_path = Path(saved_path).relative_to(self.current_discussion.path.parent) + except ValueError: + rel_path = Path(saved_path).name + # Update comment text with actual saved path + if dpg.does_item_exist("comment_text"): + current_text = dpg.get_value("comment_text") + diagram_ref = f"\n\nDIAGRAM: {rel_path}" + dpg.set_value("comment_text", current_text + diagram_ref) + self._add_output(f"Added artifact: {rel_path}") + else: + self._add_output("Artifact editor closed without saving") + + thread = threading.Thread(target=run_editor, daemon=True) + thread.start() + + def _show_diagram_dialog(self): + """Show dialog for selecting a diagram to view.""" + diagrams = self._find_diagrams() + + if not diagrams: + self._show_error("No diagrams found in this discussion") + return + + if len(diagrams) == 1: + resolved = self._resolve_diagram_path(diagrams[0]) + self._view_diagram(resolved) + return + + window_tag = "diagram_select_dialog" + if dpg.does_item_exist(window_tag): + dpg.delete_item(window_tag) + + # Track if selection was already made (double-click guard) + self._diagram_selection_made = False + + def on_diagram_selected(sender, app_data, diagram_path): + """Handle diagram selection with double-click guard.""" + if self._diagram_selection_made: + return # Ignore double-clicks + self._diagram_selection_made = True + + # Close popup first + if dpg.does_item_exist(window_tag): + dpg.delete_item(window_tag) + + # Then view diagram + self._view_diagram(diagram_path) + + def on_cancel(): + if dpg.does_item_exist(window_tag): + dpg.delete_item(window_tag) + + with dpg.window(label="Select Diagram", tag=window_tag, modal=True, + width=400, height=300, pos=[500, 300]): + + dpg.add_text("Select a diagram to view:") + dpg.add_separator() + + for diagram_path in diagrams: + resolved = self._resolve_diagram_path(diagram_path) + name = Path(diagram_path).name + + dpg.add_button( + label=name, + callback=on_diagram_selected, + user_data=resolved, + width=-1 + ) + + dpg.add_separator() + dpg.add_button(label="Cancel", callback=on_cancel) + + def _refresh_current(self): + """Refresh the current discussion.""" + if self.current_discussion: + self._open_discussion(self.current_discussion.path) + else: + self._refresh_discussions() + + def _confirm_delete_discussion(self): + """Show confirmation dialog before deleting a discussion.""" + if not self.current_discussion: + return + + window_tag = "delete_confirm_dialog" + if dpg.does_item_exist(window_tag): + dpg.delete_item(window_tag) + + filename = self.current_discussion.path.name + title = self.current_discussion.title or filename + + def do_delete(): + try: + path = self.current_discussion.path + path.unlink() # Delete the file + self.current_discussion = None + + # Clear the display + dpg.set_value("discussion_title", "Select a discussion") + dpg.set_value("discussion_status", "") + dpg.set_value("discussion_phase", "") + dpg.delete_item("discussion_content", children_only=True) + dpg.add_text("Discussion deleted.", parent="discussion_content", + color=(150, 150, 150)) + + # Disable buttons + dpg.configure_item("btn_run_turn", enabled=False) + dpg.configure_item("btn_comment", enabled=False) + dpg.configure_item("btn_diagrams", enabled=False) + dpg.configure_item("btn_edit", enabled=False) + dpg.configure_item("btn_delete", enabled=False) + + # Refresh the list + self._refresh_discussions() + + dpg.delete_item(window_tag) + except Exception as e: + dpg.delete_item(window_tag) + self._show_error(f"Failed to delete: {e}") + + def cancel(): + dpg.delete_item(window_tag) + + with dpg.window(label="Confirm Delete", tag=window_tag, modal=True, + width=400, height=150, pos=[500, 350]): + dpg.add_text("Are you sure you want to delete this discussion?", + color=(255, 200, 100)) + dpg.add_spacer(height=5) + dpg.add_text(f" {title}", color=(200, 200, 255)) + dpg.add_text(f" ({filename})", color=(150, 150, 150)) + dpg.add_spacer(height=15) + with dpg.group(horizontal=True): + dpg.add_button(label="Delete", callback=do_delete, width=100) + dpg.add_spacer(width=20) + dpg.add_button(label="Cancel", callback=cancel, width=100) + + def _show_edit_discussion_dialog(self): + """Show dialog for editing the discussion content.""" + if not self.current_discussion: + return + + window_tag = "edit_discussion_dialog" + if dpg.does_item_exist(window_tag): + dpg.delete_item(window_tag) + + # Get current content + content = self.current_discussion.get_content() + + def save_changes(): + try: + new_content = dpg.get_value("edit_content") + # Update the raw content and save + self.current_discussion._raw_content = new_content + self.current_discussion._parse_content(new_content) + self.current_discussion.save() + + # Refresh display + self._show_discussion() + dpg.delete_item(window_tag) + except Exception as e: + self._show_error(f"Failed to save: {e}") + + def cancel(): + dpg.delete_item(window_tag) + + with dpg.window(label="Edit Discussion", tag=window_tag, + width=800, height=600, pos=[300, 100], no_collapse=True): + dpg.add_text("Edit the discussion markdown below:", color=(150, 200, 255)) + dpg.add_text("(Metadata in <!-- --> comments, context before first ---, comments after)", + color=(120, 120, 120)) + dpg.add_spacer(height=5) + + dpg.add_input_text( + tag="edit_content", + default_value=content, + multiline=True, + width=-1, + height=-60, + tab_input=True + ) + + dpg.add_spacer(height=10) + with dpg.group(horizontal=True): + dpg.add_button(label="Save", callback=save_changes, width=100) + dpg.add_spacer(width=20) + dpg.add_button(label="Cancel", callback=cancel, width=100) + + def _show_info(self, message: str, title: str = "Info"): + """Show an info popup.""" + window_tag = "info_popup" + if dpg.does_item_exist(window_tag): + dpg.delete_item(window_tag) + + with dpg.window(label=title, tag=window_tag, modal=True, + width=400, height=120, pos=[500, 300], no_collapse=True): + dpg.add_text(message, wrap=380) + dpg.add_spacer(height=10) + dpg.add_button(label="OK", callback=lambda: dpg.delete_item(window_tag), width=80) + + def _show_error(self, message: str): + """Show an error popup with copyable text.""" + window_tag = "error_popup" + if dpg.does_item_exist(window_tag): + dpg.delete_item(window_tag) + + def copy_error(): + """Copy error message to clipboard.""" + if self._copy_to_clipboard(message): + dpg.configure_item("copy_status", default_value="Copied!", color=(100, 255, 100)) + else: + dpg.configure_item("copy_status", + default_value="Install xclip: sudo apt install xclip", + color=(255, 200, 100)) + + with dpg.window(label="Error", tag=window_tag, modal=True, + width=500, height=220, pos=[450, 350]): + dpg.add_text("Error:", color=(255, 100, 100)) + # Selectable text field + dpg.add_input_text( + default_value=message, + multiline=True, + readonly=True, + width=-1, + height=100 + ) + dpg.add_spacer(height=10) + with dpg.group(horizontal=True): + dpg.add_button(label="Copy to Clipboard", callback=copy_error) + dpg.add_text("", tag="copy_status", color=(150, 150, 150)) + dpg.add_spacer(width=20) + dpg.add_button(label="OK", callback=lambda: dpg.delete_item(window_tag)) + + def _get_templates(self) -> list[str]: + """Get list of available template names.""" + templates = [] + # Check multiple locations for templates + template_dirs = [ + Path.cwd() / "templates", + Path(__file__).parent.parent.parent.parent / "templates", + ] + for template_dir in template_dirs: + if template_dir.exists(): + for f in template_dir.glob("*.yaml"): + name = f.stem + if name not in templates: + templates.append(name) + return sorted(templates) if templates else ["feature", "brainstorm"] + + def _get_participants(self) -> list[tuple[str, str]]: + """Get list of available participants as (alias, display_name) tuples.""" + if get_registry is None: + return [("architect", "AI-Architect"), ("security", "AI-Security"), + ("pragmatist", "AI-Pragmatist")] + registry = get_registry() + participants = [] + for p in registry.get_all(): + participants.append((p.alias, p.name)) + return sorted(participants) if participants else [ + ("architect", "AI-Architect"), ("security", "AI-Security"), + ("pragmatist", "AI-Pragmatist") + ] + + def _show_new_discussion_dialog(self): + """Show dialog for creating a new discussion.""" + window_tag = "new_discussion_dialog" + if dpg.does_item_exist(window_tag): + dpg.delete_item(window_tag) + + templates = self._get_templates() + participants = self._get_participants() + + # State for the dialog + dialog_state = { + 'selected_participants': ["architect", "security", "pragmatist"], + } + + def toggle_participant(sender, app_data, user_data): + alias = user_data + if alias in dialog_state['selected_participants']: + dialog_state['selected_participants'].remove(alias) + else: + dialog_state['selected_participants'].append(alias) + + def on_template_change(sender, app_data): + if app_data == "+ Create New Template...": + # Reset combo to previous value, then open template dialog on top + dpg.set_value("new_disc_template", templates[0] if templates else "feature") + self._show_new_template_dialog(reopen_new_discussion=False) + + def create_discussion(): + title = dpg.get_value("new_disc_title").strip() + if not title: + self._show_error("Please enter a title") + return + + template = dpg.get_value("new_disc_template") + context = dpg.get_value("new_disc_context").strip() + selected = dialog_state['selected_participants'] + + if not selected: + self._show_error("Please select at least one participant") + return + + # Generate filename with .discussion.md extension + slug = title.lower().replace(" ", "-") + slug = "".join(c for c in slug if c.isalnum() or c == "-") + + # Determine output directory + output_dir = self.discussions_dir if self.discussions_dir else Path.cwd() + output_path = output_dir / f"{slug}.discussion.md" + + # Check if file exists + if output_path.exists(): + self._show_error(f"File already exists: {output_path}") + return + + try: + discussion = Discussion.create( + path=output_path, + title=title, + context=context, + template=template, + participants=selected, + ) + + # Refresh and select the new discussion + self._refresh_discussions() + + # Load and display the new discussion + self.current_discussion = discussion + self._show_discussion() + + dpg.delete_item(window_tag) + + except Exception as e: + self._show_error(f"Failed to create discussion: {e}") + + with dpg.window(label="New Discussion", tag=window_tag, + width=550, height=520, pos=[400, 150], no_collapse=True): + + dpg.add_text("Title:", color=(150, 200, 255)) + dpg.add_input_text(tag="new_disc_title", width=-1, hint="Enter discussion title") + + dpg.add_spacer(height=10) + dpg.add_text("Template:", color=(150, 200, 255)) + template_items = templates + ["+ Create New Template..."] + dpg.add_combo( + items=template_items, + tag="new_disc_template", + default_value=templates[0] if templates else "feature", + width=-1, + callback=on_template_change + ) + + dpg.add_spacer(height=10) + with dpg.group(horizontal=True): + dpg.add_text("Participants:", color=(150, 200, 255)) + dpg.add_spacer(width=20) + + def open_new_participant(): + dpg.delete_item(window_tag) # Close New Discussion first + self._show_new_participant_dialog(reopen_new_discussion=True) + + dpg.add_button(label="+ New Participant", callback=open_new_participant) + + with dpg.child_window(height=150, border=True): + for alias, display_name in participants: + is_default = alias in ["architect", "security", "pragmatist"] + dpg.add_checkbox( + label=f"@{alias} ({display_name})", + default_value=is_default, + callback=toggle_participant, + user_data=alias + ) + + dpg.add_spacer(height=10) + dpg.add_text("Context/Description:", color=(150, 200, 255)) + dpg.add_input_text( + tag="new_disc_context", + multiline=True, + width=-1, + height=100, + hint="Describe what this discussion is about..." + ) + + dpg.add_spacer(height=15) + with dpg.group(horizontal=True): + dpg.add_button(label="Create Discussion", callback=create_discussion, width=150) + dpg.add_spacer(width=20) + dpg.add_button(label="Cancel", callback=lambda: dpg.delete_item(window_tag), width=100) + + dpg.focus_item(window_tag) + + def _show_new_template_dialog(self, reopen_new_discussion=False): + """Show dialog for creating a new template.""" + window_tag = "new_template_dialog" + if dpg.does_item_exist(window_tag): + dpg.delete_item(window_tag) + + # State for phases + template_state = { + 'phases': [ + {'name': 'initial_feedback', 'goal': 'Gather initial perspectives', 'voting': False}, + {'name': 'detailed_review', 'goal': 'Deep dive into details', 'voting': False}, + {'name': 'consensus_vote', 'goal': 'Reach agreement', 'voting': True}, + ] + } + + def add_phase(): + idx = len(template_state['phases']) + template_state['phases'].append({ + 'name': f'phase_{idx + 1}', + 'goal': '', + 'voting': False + }) + refresh_phases() + + def remove_phase(idx): + if len(template_state['phases']) > 1: + template_state['phases'].pop(idx) + refresh_phases() + + def refresh_phases(): + if dpg.does_item_exist("phases_container"): + dpg.delete_item("phases_container", children_only=True) + for i, phase in enumerate(template_state['phases']): + with dpg.group(horizontal=True, parent="phases_container"): + dpg.add_input_text( + default_value=phase['name'], + width=120, + callback=lambda s, a, u=i: template_state['phases'].__setitem__(u, {**template_state['phases'][u], 'name': a}) + ) + dpg.add_input_text( + default_value=phase['goal'], + width=200, + hint="Phase goal", + callback=lambda s, a, u=i: template_state['phases'].__setitem__(u, {**template_state['phases'][u], 'goal': a}) + ) + dpg.add_checkbox( + label="Vote", + default_value=phase['voting'], + callback=lambda s, a, u=i: template_state['phases'].__setitem__(u, {**template_state['phases'][u], 'voting': a}) + ) + dpg.add_button(label="X", callback=lambda s, a, u=i: remove_phase(u), width=25) + + def save_template(): + name = dpg.get_value("template_name").strip() + if not name: + self._show_error("Please enter a template name") + return + + # Build template YAML + import yaml + template_data = { + 'name': name, + 'description': dpg.get_value("template_desc").strip(), + 'phases': {} + } + + prev_phase = None + for i, phase in enumerate(template_state['phases']): + phase_id = phase['name'].strip().replace(' ', '_').lower() + next_phase = template_state['phases'][i + 1]['name'].strip().replace(' ', '_').lower() if i + 1 < len(template_state['phases']) else None + + template_data['phases'][phase_id] = { + 'goal': phase['goal'] or f"Phase {i + 1}", + 'instructions': f"Instructions for {phase_id}", + 'voting': phase['voting'], + 'next_phase': next_phase + } + if phase['voting']: + template_data['phases'][phase_id]['threshold_ready'] = 0.67 + template_data['phases'][phase_id]['human_required'] = True + + # Save to templates directory + template_dir = Path.cwd() / "templates" + template_dir.mkdir(exist_ok=True) + template_path = template_dir / f"{name}.yaml" + + if template_path.exists(): + self._show_error(f"Template already exists: {name}") + return + + try: + with open(template_path, 'w') as f: + yaml.dump(template_data, f, default_flow_style=False, sort_keys=False) + dpg.delete_item(window_tag) + + # Update the template combo in New Discussion dialog if it's open + if dpg.does_item_exist("new_disc_template"): + new_templates = self._get_templates() + template_items = new_templates + ["+ Create New Template..."] + dpg.configure_item("new_disc_template", items=template_items) + dpg.set_value("new_disc_template", name) # Select the new template + + except Exception as e: + self._show_error(f"Failed to save template: {e}") + + def cancel_template(): + dpg.delete_item(window_tag) + + dpg.split_frame() # Force frame update before creating window + + with dpg.window(label="Create New Template", tag=window_tag, + width=600, height=500, pos=[380, 120], no_collapse=True, show=True): + + dpg.add_text("Template Name:", color=(150, 200, 255)) + dpg.add_input_text(tag="template_name", width=-1, hint="e.g., feature, brainstorm, review") + + dpg.add_spacer(height=5) + dpg.add_text("Description:", color=(150, 200, 255)) + dpg.add_input_text(tag="template_desc", width=-1, hint="Brief description of this workflow") + + dpg.add_spacer(height=10) + with dpg.group(horizontal=True): + dpg.add_text("Phases:", color=(150, 200, 255)) + dpg.add_spacer(width=20) + dpg.add_button(label="+ Add Phase", callback=add_phase) + + dpg.add_text("(Name | Goal | Voting Enabled)", color=(120, 120, 120)) + + with dpg.child_window(height=220, border=True, tag="phases_container"): + pass # Will be populated by refresh_phases + + refresh_phases() + + dpg.add_spacer(height=15) + with dpg.group(horizontal=True): + dpg.add_button(label="Save Template", callback=save_template, width=120) + dpg.add_spacer(width=20) + dpg.add_button(label="Cancel", callback=cancel_template, width=100) + + # Bring window to front and focus it + dpg.focus_item(window_tag) + + def _show_new_participant_dialog(self, reopen_new_discussion=False): + """Show dialog for creating a new participant.""" + window_tag = "new_participant_dialog" + if dpg.does_item_exist(window_tag): + dpg.delete_item(window_tag) + + def save_participant(): + alias = dpg.get_value("part_alias").strip().lower() + if not alias: + self._show_error("Please enter an alias") + return + if not alias.replace('_', '').replace('-', '').isalnum(): + self._show_error("Alias should only contain letters, numbers, underscores, and hyphens") + return + + display_name = dpg.get_value("part_display_name").strip() or f"AI-{alias.capitalize()}" + description = dpg.get_value("part_description").strip() + expertise = [e.strip() for e in dpg.get_value("part_expertise").split(",") if e.strip()] + personality = dpg.get_value("part_personality").strip() + voice = dpg.get_value("part_voice") or (DEFAULT_VOICE if DEFAULT_VOICE else "en-US-Neural2-J") + + # Build SmartTool config + import yaml + config = { + 'name': f'discussion-{alias}', + 'description': description or f'{display_name} participant for discussions', + 'category': 'Discussion', + 'meta': { + 'display_name': display_name, + 'alias': alias, + 'type': 'voting', + 'expertise': expertise or ['General discussion'], + 'voice': voice, + }, + 'arguments': [ + {'flag': '--callout', 'variable': 'callout', 'default': '', 'description': 'Specific question or context'}, + {'flag': '--templates-dir', 'variable': 'templates_dir', 'default': 'templates', 'description': 'Path to templates'}, + ], + 'steps': [ + { + 'type': 'prompt', + 'prompt': f'''You are {display_name}, a discussion participant. + +## Your Personality +{personality or "You provide thoughtful, balanced feedback on proposals."} + +## Your Expertise +{chr(10).join(f"- {e}" for e in expertise) if expertise else "- General discussion and review"} + +## Current Discussion +{{input}} + +## Your Task +{{callout}} + +Respond with valid JSON: +{{"comment": "Your response here", "vote": "READY" or "CHANGES" or "REJECT" or null}} + +If you have nothing to add: {{"sentinel": "NO_RESPONSE"}}''', + 'provider': 'claude-sonnet', + 'output_var': 'response' + }, + { + 'type': 'code', + 'code': '''import json +import re +json_text = response.strip() +code_block = re.search(r'```(?:json)?\\s*(.*?)```', json_text, re.DOTALL) +if code_block: + json_text = code_block.group(1).strip() +try: + parsed = json.loads(json_text) +except: + parsed = {"comment": json_text, "vote": None} +final = json.dumps(parsed)''', + 'output_var': 'final' + } + ], + 'output': '{final}' + } + + # Save to ~/.smarttools/ + smarttools_dir = Path.home() / ".smarttools" / f"discussion-{alias}" + smarttools_dir.mkdir(parents=True, exist_ok=True) + config_path = smarttools_dir / "config.yaml" + + if config_path.exists(): + self._show_error(f"Participant @{alias} already exists") + return + + try: + with open(config_path, 'w') as f: + yaml.dump(config, f, default_flow_style=False, sort_keys=False) + + # Refresh participant registry + if get_registry: + get_registry().refresh() + + dpg.delete_item(window_tag) + + # Reopen the new discussion dialog if requested + if reopen_new_discussion: + self._show_new_discussion_dialog() + + except Exception as e: + self._show_error(f"Failed to create participant: {e}") + + def cancel_participant(): + dpg.delete_item(window_tag) + if reopen_new_discussion: + self._show_new_discussion_dialog() + + dpg.split_frame() # Force frame update before creating window + + with dpg.window(label="Create New Participant", tag=window_tag, + width=550, height=480, pos=[400, 130], no_collapse=True, show=True): + + dpg.add_text("Alias (for @mentions):", color=(150, 200, 255)) + dpg.add_input_text(tag="part_alias", width=-1, hint="e.g., designer, researcher, critic") + + dpg.add_spacer(height=5) + dpg.add_text("Display Name:", color=(150, 200, 255)) + dpg.add_input_text(tag="part_display_name", width=-1, hint="e.g., AI-Designer (optional)") + + dpg.add_spacer(height=5) + dpg.add_text("Description:", color=(150, 200, 255)) + dpg.add_input_text(tag="part_description", width=-1, hint="Brief role description") + + dpg.add_spacer(height=5) + dpg.add_text("Expertise Areas (comma-separated):", color=(150, 200, 255)) + dpg.add_input_text(tag="part_expertise", width=-1, + hint="e.g., UI/UX, User research, Accessibility") + + dpg.add_spacer(height=5) + dpg.add_text("Voice (for read-aloud):", color=(150, 200, 255)) + voice_items = [v[0] for v in AVAILABLE_VOICES] if AVAILABLE_VOICES else ["en-US-Neural2-J"] + dpg.add_combo( + items=voice_items, + tag="part_voice", + default_value=DEFAULT_VOICE if DEFAULT_VOICE else "en-US-Neural2-J", + width=-1 + ) + + dpg.add_spacer(height=5) + dpg.add_text("Personality/Perspective:", color=(150, 200, 255)) + dpg.add_input_text( + tag="part_personality", + multiline=True, + width=-1, + height=100, + hint="Describe how this participant thinks and what they focus on..." + ) + + dpg.add_spacer(height=15) + with dpg.group(horizontal=True): + dpg.add_button(label="Create Participant", callback=save_participant, width=140) + dpg.add_spacer(width=20) + dpg.add_button(label="Cancel", callback=cancel_participant, width=100) + + # Bring window to front and focus it + dpg.focus_item(window_tag) + + def _show_manage_templates_dialog(self): + """Show dialog for managing templates (view, edit, delete).""" + window_tag = "manage_templates_dialog" + if dpg.does_item_exist(window_tag): + dpg.delete_item(window_tag) + + # State for tracking selection + dialog_state = {'selected_template': None} + + def get_template_path(name: str) -> Path: + """Get the path to a template file.""" + template_dirs = [ + Path.cwd() / "templates", + Path(__file__).parent.parent.parent.parent / "templates", + ] + for template_dir in template_dirs: + path = template_dir / f"{name}.yaml" + if path.exists(): + return path + return Path.cwd() / "templates" / f"{name}.yaml" + + def select_template(sender, app_data, user_data): + """Handle template selection.""" + name = user_data + dialog_state['selected_template'] = name + # Update selection highlight + for child in dpg.get_item_children("template_list_container", 1) or []: + dpg.bind_item_theme(child, 0) + dpg.bind_item_theme(sender, self.selected_theme) + load_template_details(name) + + def refresh_list(): + """Refresh the template list.""" + if dpg.does_item_exist("template_list_container"): + dpg.delete_item("template_list_container", children_only=True) + templates = self._get_templates() + for name in templates: + dpg.add_button( + label=name, + callback=select_template, + user_data=name, + width=-1, + parent="template_list_container" + ) + + def load_template_details(name: str): + """Load template details into the edit panel.""" + import yaml + path = get_template_path(name) + if not path.exists(): + return + + try: + with open(path) as f: + data = yaml.safe_load(f) + + dpg.set_value("edit_template_name", name) + dpg.set_value("edit_template_desc", data.get('description', '')) + + # Show phases + phases_text = "" + if 'phases' in data: + for phase_id, phase_data in data['phases'].items(): + goal = phase_data.get('goal', '') + voting = "✓" if phase_data.get('voting') else "✗" + phases_text += f"{phase_id}: {goal} [voting: {voting}]\n" + dpg.set_value("edit_template_phases", phases_text) + + # Show pipeline with input/output flow + pipeline_text = "" + pipeline = data.get('turn_pipeline', {}) + steps = pipeline.get('steps', []) + if steps: + for i, step in enumerate(steps): + tool = step.get('tool', '?').replace('discussion-', '') + input_var = step.get('input', '') + output_var = step.get('output', '') + when = step.get('when', '') + for_each = step.get('for_each', '') + parallel = " ||" if step.get('parallel') else "" + + # Build flow: input -> tool -> output + flow = f"{input_var} -> {tool}" if input_var else tool + if output_var: + flow += f" -> {output_var}" + flow += parallel + + cond_str = f" (when: {when})" if when and when != 'always' else "" + for_str = f" [for_each]" if for_each else "" + pipeline_text += f"{i+1}. {flow}{cond_str}{for_str}\n" + else: + pipeline_text = "(using default pipeline)" + dpg.set_value("edit_template_pipeline", pipeline_text) + + except Exception as e: + self._show_error(f"Error loading template: {e}") + + def save_template(): + """Save changes to the selected template.""" + if not dialog_state['selected_template']: + self._show_error("No template selected") + return + + import yaml + name = dialog_state['selected_template'] + path = get_template_path(name) + + try: + # Load existing data + with open(path) as f: + data = yaml.safe_load(f) + + # Update description + data['description'] = dpg.get_value("edit_template_desc") + + # Save back + with open(path, 'w') as f: + yaml.dump(data, f, default_flow_style=False, sort_keys=False) + + self._show_info(f"Template '{name}' saved!", title="Saved") + + except Exception as e: + self._show_error(f"Error saving template: {e}") + + def delete_template(): + """Delete the selected template.""" + if not dialog_state['selected_template']: + self._show_error("No template selected") + return + + name = dialog_state['selected_template'] + path = get_template_path(name) + + if not path.exists(): + self._show_error(f"Template file not found: {path}") + return + + try: + path.unlink() + dialog_state['selected_template'] = None + dpg.set_value("edit_template_name", "") + dpg.set_value("edit_template_desc", "") + dpg.set_value("edit_template_phases", "") + refresh_list() + except Exception as e: + self._show_error(f"Error deleting template: {e}") + + def edit_phases(): + """Open a dialog to edit template phases.""" + if not dialog_state['selected_template']: + self._show_error("No template selected") + return + + import yaml + name = dialog_state['selected_template'] + path = get_template_path(name) + + try: + with open(path) as f: + template_data = yaml.safe_load(f) + except Exception as e: + self._show_error(f"Error loading template: {e}") + return + + phase_window_tag = "edit_phases_dialog" + if dpg.does_item_exist(phase_window_tag): + dpg.delete_item(phase_window_tag) + + # Convert phases dict to list for editing + phases_list = [] + for phase_id, phase_info in template_data.get('phases', {}).items(): + phases_list.append({ + 'id': phase_id, + 'goal': phase_info.get('goal', ''), + 'instructions': phase_info.get('instructions', ''), + 'voting': phase_info.get('voting', False), + 'next_phase': phase_info.get('next_phase'), + }) + + phase_state = {'phases': phases_list, 'selected_idx': None} + + def select_phase(sender, app_data, user_data): + idx = user_data + phase_state['selected_idx'] = idx + # Highlight selection + for child in dpg.get_item_children("phase_list_container", 1) or []: + dpg.bind_item_theme(child, 0) + dpg.bind_item_theme(sender, self.selected_theme) + # Load phase details + phase = phase_state['phases'][idx] + dpg.set_value("phase_id", phase['id']) + dpg.set_value("phase_goal", phase['goal']) + dpg.set_value("phase_instructions", phase['instructions']) + dpg.set_value("phase_voting", phase['voting']) + + def refresh_phase_list(): + if dpg.does_item_exist("phase_list_container"): + dpg.delete_item("phase_list_container", children_only=True) + for i, phase in enumerate(phase_state['phases']): + vote_marker = " [V]" if phase['voting'] else "" + dpg.add_button( + label=f"{phase['id']}{vote_marker}", + callback=select_phase, + user_data=i, + width=-1, + parent="phase_list_container" + ) + + def update_phase(): + idx = phase_state['selected_idx'] + if idx is None: + return + phase_state['phases'][idx]['id'] = dpg.get_value("phase_id") + phase_state['phases'][idx]['goal'] = dpg.get_value("phase_goal") + phase_state['phases'][idx]['instructions'] = dpg.get_value("phase_instructions") + phase_state['phases'][idx]['voting'] = dpg.get_value("phase_voting") + refresh_phase_list() + + def add_phase(): + new_id = f"phase_{len(phase_state['phases']) + 1}" + phase_state['phases'].append({ + 'id': new_id, + 'goal': 'New phase goal', + 'instructions': 'Phase instructions here', + 'voting': False, + 'next_phase': None, + }) + refresh_phase_list() + + def delete_phase(): + idx = phase_state['selected_idx'] + if idx is None: + self._show_error("No phase selected") + return + if len(phase_state['phases']) <= 1: + self._show_error("Cannot delete the only phase") + return + phase_state['phases'].pop(idx) + phase_state['selected_idx'] = None + dpg.set_value("phase_id", "") + dpg.set_value("phase_goal", "") + dpg.set_value("phase_instructions", "") + dpg.set_value("phase_voting", False) + refresh_phase_list() + + def save_phases(): + # Rebuild phases dict with proper next_phase links + new_phases = {} + for i, phase in enumerate(phase_state['phases']): + next_p = phase_state['phases'][i + 1]['id'] if i + 1 < len(phase_state['phases']) else None + new_phases[phase['id']] = { + 'goal': phase['goal'], + 'instructions': phase['instructions'], + 'voting': phase['voting'], + 'next_phase': next_p, + } + + template_data['phases'] = new_phases + + try: + with open(path, 'w') as f: + yaml.dump(template_data, f, default_flow_style=False, sort_keys=False) + dpg.delete_item(phase_window_tag) + # Refresh the template details in parent dialog + load_template_details(name) + except Exception as e: + self._show_error(f"Error saving phases: {e}") + + with dpg.window(label=f"Edit Phases: {name}", tag=phase_window_tag, + width=700, height=450, pos=[360, 130], no_collapse=True): + + with dpg.group(horizontal=True): + # Left: Phase list + with dpg.child_window(width=180, height=-40): + dpg.add_text("Phases", color=(150, 200, 255)) + dpg.add_separator() + with dpg.group(horizontal=True): + dpg.add_button(label="+", callback=add_phase, width=30) + dpg.add_button(label="-", callback=delete_phase, width=30) + with dpg.child_window(tag="phase_list_container", height=-1): + pass + + # Right: Phase details + with dpg.child_window(width=-1, height=-40): + dpg.add_text("Phase Details", color=(150, 200, 255)) + dpg.add_separator() + + dpg.add_text("Phase ID:") + dpg.add_input_text(tag="phase_id", width=-1, callback=lambda: update_phase()) + + dpg.add_spacer(height=5) + dpg.add_text("Goal:") + dpg.add_input_text(tag="phase_goal", width=-1, callback=lambda: update_phase()) + + dpg.add_spacer(height=5) + dpg.add_text("Instructions:") + dpg.add_input_text(tag="phase_instructions", width=-1, height=100, + multiline=True, callback=lambda: update_phase()) + + dpg.add_spacer(height=5) + dpg.add_checkbox(label="Voting Enabled", tag="phase_voting", + callback=lambda: update_phase()) + + dpg.add_spacer(height=10) + dpg.add_button(label="Apply Changes", callback=update_phase, width=120) + + with dpg.group(horizontal=True): + dpg.add_button(label="Save & Close", callback=save_phases, width=120) + dpg.add_spacer(width=20) + dpg.add_button(label="Cancel", + callback=lambda: dpg.delete_item(phase_window_tag), width=100) + + refresh_phase_list() + dpg.focus_item(phase_window_tag) + + def edit_pipeline(): + """Open a dialog to edit template pipeline.""" + if not dialog_state['selected_template']: + self._show_error("No template selected") + return + + import yaml + name = dialog_state['selected_template'] + path = get_template_path(name) + + try: + with open(path) as f: + template_data = yaml.safe_load(f) + except Exception as e: + self._show_error(f"Error loading template: {e}") + return + + pipeline_window_tag = "edit_pipeline_dialog" + if dpg.does_item_exist(pipeline_window_tag): + dpg.delete_item(pipeline_window_tag) + + # Get existing pipeline or default + pipeline = template_data.get('turn_pipeline', {'steps': []}) + steps_list = list(pipeline.get('steps', [])) + + # Available conditions (new $variable syntax) + conditions = ["always", "not $participants_specified", "$phase_voting"] + + # Available variables (for input/output dropdowns) + # "Custom..." triggers a popup for custom variable name + variables = [ + "$discussion", + "$parsed", + "$routing", + "$responses[]", + "$votes", + "$promotion", + "Custom...", + ] + + # Available tools + tools = [ + "discussion-parser", + "discussion-mention-router", + "discussion-{participant}", + "discussion-turn-appender", + "discussion-vote-counter", + "discussion-status-promoter", + "discussion-moderator", + "discussion-summarizer", + "discussion-validator", + ] + + # For each options + for_each_options = ["", "$participants_to_call", "$routing.participants_to_call", "Custom..."] + + # Common arg names for the args editor + common_args = [ + "--callout", + "--templates-dir", + "--default-participants", + "--responses-json", + "--current-status", + "--current-phase", + "--provider", + ] + + pipeline_state = {'steps': steps_list, 'selected_idx': None, 'args': []} + + def select_step(sender, app_data, user_data): + idx = user_data + pipeline_state['selected_idx'] = idx + for child in dpg.get_item_children("pipeline_list_container", 1) or []: + dpg.bind_item_theme(child, 0) + dpg.bind_item_theme(sender, self.selected_theme) + load_step_details(idx) + + def load_step_details(idx): + if idx is None or idx >= len(pipeline_state['steps']): + return + step = pipeline_state['steps'][idx] + dpg.set_value("step_tool", step.get('tool', '')) + + # Handle input - might be custom value not in dropdown + input_val = step.get('input', '$discussion') + if input_val and input_val not in variables[:-1]: # Exclude "Custom..." + dpg.set_value("step_input", "Custom...") + dpg.set_value("step_input_custom", input_val) + dpg.configure_item("step_input_custom", show=True) + else: + dpg.set_value("step_input", input_val or '$discussion') + dpg.configure_item("step_input_custom", show=False) + + # Handle output - might be custom value + output_val = step.get('output', '') + if output_val and output_val not in variables[:-1]: + dpg.set_value("step_output", "Custom...") + dpg.set_value("step_output_custom", output_val) + dpg.configure_item("step_output_custom", show=True) + else: + dpg.set_value("step_output", output_val or '') + dpg.configure_item("step_output_custom", show=False) + + dpg.set_value("step_when", step.get('when', 'always')) + + # Handle for_each - might be custom + for_each_val = step.get('for_each', '') + if for_each_val and for_each_val not in for_each_options[:-1]: + dpg.set_value("step_for_each", "Custom...") + dpg.set_value("step_for_each_custom", for_each_val) + dpg.configure_item("step_for_each_custom", show=True) + else: + dpg.set_value("step_for_each", for_each_val or '') + dpg.configure_item("step_for_each_custom", show=False) + + dpg.set_value("step_parallel", step.get('parallel', False)) + + # Load args into state and refresh args list + args = step.get('args', {}) + pipeline_state['args'] = [[k, v] for k, v in args.items()] + refresh_args_list() + + def refresh_step_list(): + if dpg.does_item_exist("pipeline_list_container"): + dpg.delete_item("pipeline_list_container", children_only=True) + for i, step in enumerate(pipeline_state['steps']): + tool = step.get('tool', '?') + # Shorten display name but keep it readable + short_name = tool.replace('discussion-', '') + if '{participant}' in short_name: + short_name = 'participants' # More descriptive than * + dpg.add_button( + label=f"{i+1}. {short_name}", + callback=select_step, + user_data=i, + width=-1, + parent="pipeline_list_container" + ) + + def refresh_args_list(): + """Refresh the args editor list.""" + if dpg.does_item_exist("args_list_container"): + dpg.delete_item("args_list_container", children_only=True) + for i, (arg_name, arg_val) in enumerate(pipeline_state['args']): + with dpg.group(horizontal=True, parent="args_list_container"): + dpg.add_input_text( + default_value=arg_name, + width=120, + tag=f"arg_name_{i}", + hint="--arg-name" + ) + dpg.add_input_text( + default_value=arg_val, + width=120, + tag=f"arg_val_{i}", + hint="$variable" + ) + dpg.add_button( + label="X", + callback=lambda s, a, u: remove_arg(u), + user_data=i, + width=20 + ) + + def add_arg(): + """Add a new argument row.""" + pipeline_state['args'].append(["", ""]) + refresh_args_list() + + def remove_arg(idx): + """Remove an argument row.""" + if 0 <= idx < len(pipeline_state['args']): + pipeline_state['args'].pop(idx) + refresh_args_list() + + def collect_args() -> dict: + """Collect args from the UI into a dict.""" + args = {} + for i in range(len(pipeline_state['args'])): + name_tag = f"arg_name_{i}" + val_tag = f"arg_val_{i}" + if dpg.does_item_exist(name_tag) and dpg.does_item_exist(val_tag): + name = dpg.get_value(name_tag).strip() + val = dpg.get_value(val_tag).strip() + if name: + args[name] = val + return args + + def get_input_value() -> str: + """Get input value, handling custom input.""" + val = dpg.get_value("step_input") + if val == "Custom...": + return dpg.get_value("step_input_custom").strip() + return val + + def get_output_value() -> str: + """Get output value, handling custom input.""" + val = dpg.get_value("step_output") + if val == "Custom...": + return dpg.get_value("step_output_custom").strip() + return val + + def get_for_each_value() -> str: + """Get for_each value, handling custom input.""" + val = dpg.get_value("step_for_each") + if val == "Custom...": + return dpg.get_value("step_for_each_custom").strip() + return val + + def on_input_change(sender, app_data): + """Show/hide custom input field.""" + dpg.configure_item("step_input_custom", show=(app_data == "Custom...")) + + def on_output_change(sender, app_data): + """Show/hide custom output field.""" + dpg.configure_item("step_output_custom", show=(app_data == "Custom...")) + + def on_for_each_change(sender, app_data): + """Show/hide custom for_each field.""" + dpg.configure_item("step_for_each_custom", show=(app_data == "Custom...")) + + def update_step(): + idx = pipeline_state['selected_idx'] + if idx is None: + return + + # Collect args from UI + args = collect_args() + + pipeline_state['steps'][idx] = { + 'tool': dpg.get_value("step_tool"), + 'input': get_input_value() or None, + 'output': get_output_value() or None, + 'when': dpg.get_value("step_when") if dpg.get_value("step_when") != 'always' else None, + 'for_each': get_for_each_value() or None, + 'parallel': dpg.get_value("step_parallel") if dpg.get_value("step_parallel") else None, + 'args': args if args else None, + } + # Remove None values + pipeline_state['steps'][idx] = {k: v for k, v in pipeline_state['steps'][idx].items() if v is not None} + refresh_step_list() + + def add_step(): + pipeline_state['steps'].append({'tool': 'discussion-parser', 'input': '$discussion', 'output': '$parsed'}) + refresh_step_list() + + def delete_step(): + idx = pipeline_state['selected_idx'] + if idx is None: + self._show_error("No step selected") + return + pipeline_state['steps'].pop(idx) + pipeline_state['selected_idx'] = None + pipeline_state['args'] = [] + dpg.set_value("step_tool", "") + dpg.set_value("step_input", "$discussion") + dpg.set_value("step_output", "") + dpg.configure_item("step_input_custom", show=False) + dpg.configure_item("step_output_custom", show=False) + dpg.configure_item("step_for_each_custom", show=False) + dpg.set_value("step_when", "always") + dpg.set_value("step_for_each", "") + dpg.set_value("step_parallel", False) + refresh_args_list() + refresh_step_list() + + def move_step_up(): + idx = pipeline_state['selected_idx'] + if idx is None or idx == 0: + return + pipeline_state['steps'][idx], pipeline_state['steps'][idx-1] = \ + pipeline_state['steps'][idx-1], pipeline_state['steps'][idx] + pipeline_state['selected_idx'] = idx - 1 + refresh_step_list() + + def move_step_down(): + idx = pipeline_state['selected_idx'] + if idx is None or idx >= len(pipeline_state['steps']) - 1: + return + pipeline_state['steps'][idx], pipeline_state['steps'][idx+1] = \ + pipeline_state['steps'][idx+1], pipeline_state['steps'][idx] + pipeline_state['selected_idx'] = idx + 1 + refresh_step_list() + + def save_pipeline(): + template_data['turn_pipeline'] = {'steps': pipeline_state['steps']} + try: + with open(path, 'w') as f: + yaml.dump(template_data, f, default_flow_style=False, sort_keys=False) + dpg.delete_item(pipeline_window_tag) + load_template_details(name) + except Exception as e: + self._show_error(f"Error saving pipeline: {e}") + + with dpg.window(label=f"Edit Pipeline: {name}", tag=pipeline_window_tag, + width=650, height=400, pos=[380, 150], no_collapse=True): + + with dpg.group(horizontal=True): + # Left: Step list + with dpg.child_window(width=180, height=-40): + dpg.add_text("Pipeline Steps", color=(150, 200, 255)) + dpg.add_separator() + with dpg.group(horizontal=True): + dpg.add_button(label="+", callback=add_step, width=25) + dpg.add_button(label="-", callback=delete_step, width=25) + dpg.add_button(label="Up", callback=move_step_up, width=30) + dpg.add_button(label="Dn", callback=move_step_down, width=30) + with dpg.child_window(tag="pipeline_list_container", height=-1): + pass + + # Right: Step details + with dpg.child_window(width=-1, height=-40): + dpg.add_text("Step Details", color=(150, 200, 255)) + dpg.add_separator() + + dpg.add_text("Tool:") + dpg.add_combo(items=tools, tag="step_tool", width=-1) + + with dpg.group(horizontal=True): + with dpg.group(): + dpg.add_text("Input:") + dpg.add_combo(items=variables, tag="step_input", default_value="$discussion", + width=150, callback=on_input_change) + dpg.add_input_text(tag="step_input_custom", width=150, show=False, + hint="$custom_var") + dpg.add_spacer(width=10) + with dpg.group(): + dpg.add_text("Output:") + dpg.add_combo(items=variables, tag="step_output", width=150, + callback=on_output_change) + dpg.add_input_text(tag="step_output_custom", width=150, show=False, + hint="$custom_var") + + dpg.add_spacer(height=3) + dpg.add_text("Condition (when):") + dpg.add_combo(items=conditions, tag="step_when", default_value="always", width=-1) + + dpg.add_spacer(height=3) + with dpg.group(horizontal=True): + dpg.add_text("For Each:") + dpg.add_checkbox(label="Parallel", tag="step_parallel") + dpg.add_combo(items=for_each_options, tag="step_for_each", width=-1, + callback=on_for_each_change) + dpg.add_input_text(tag="step_for_each_custom", width=-1, show=False, + hint="$custom_array") + + dpg.add_spacer(height=3) + with dpg.group(horizontal=True): + dpg.add_text("Arguments:") + dpg.add_button(label="+", callback=add_arg, width=20) + with dpg.child_window(tag="args_list_container", height=60, border=True): + pass # Populated by refresh_args_list + + dpg.add_spacer(height=5) + dpg.add_button(label="Apply Changes", callback=update_step, width=120) + + with dpg.group(horizontal=True): + dpg.add_button(label="Save & Close", callback=save_pipeline, width=120) + dpg.add_spacer(width=20) + dpg.add_button(label="Cancel", + callback=lambda: dpg.delete_item(pipeline_window_tag), width=100) + + refresh_step_list() + dpg.focus_item(pipeline_window_tag) + + with dpg.window(label="Manage Templates", tag=window_tag, + width=700, height=500, pos=[350, 100], no_collapse=True): + + with dpg.group(horizontal=True): + # Left panel: Template list + with dpg.child_window(width=200, height=-40): + dpg.add_text("Templates", color=(150, 200, 255)) + dpg.add_separator() + with dpg.child_window(tag="template_list_container", height=-1): + pass # Populated by refresh_list + + # Right panel: Edit details + with dpg.child_window(width=-1, height=-40): + dpg.add_text("Template Details", color=(150, 200, 255)) + dpg.add_separator() + + dpg.add_text("Name:") + dpg.add_input_text(tag="edit_template_name", width=-1, readonly=True) + + dpg.add_spacer(height=5) + dpg.add_text("Description:") + dpg.add_input_text(tag="edit_template_desc", width=-1) + + dpg.add_spacer(height=5) + with dpg.group(horizontal=True): + dpg.add_text("Phases:") + dpg.add_button(label="Edit Phases...", callback=edit_phases, small=True) + dpg.add_input_text(tag="edit_template_phases", width=-1, height=100, + multiline=True, readonly=True) + + dpg.add_spacer(height=5) + with dpg.group(horizontal=True): + dpg.add_text("Turn Pipeline:") + dpg.add_button(label="Edit Pipeline...", callback=edit_pipeline, small=True) + dpg.add_input_text(tag="edit_template_pipeline", width=-1, height=80, + multiline=True, readonly=True) + + dpg.add_spacer(height=10) + with dpg.group(horizontal=True): + dpg.add_button(label="Save Changes", callback=save_template, width=120) + dpg.add_button(label="Delete Template", callback=delete_template, width=120) + + with dpg.group(horizontal=True): + dpg.add_button(label="New Template...", + callback=lambda: self._show_new_template_dialog(), width=120) + dpg.add_spacer(width=20) + dpg.add_button(label="Close", + callback=lambda: dpg.delete_item(window_tag), width=100) + + refresh_list() + dpg.focus_item(window_tag) + + def _show_manage_participants_dialog(self): + """Show dialog for managing participants (view, edit, delete).""" + window_tag = "manage_participants_dialog" + if dpg.does_item_exist(window_tag): + dpg.delete_item(window_tag) + + # State for tracking selection + dialog_state = {'selected_participant': None} + + def get_participant_path(alias: str) -> Path: + """Get the path to a participant config.""" + return Path.home() / ".smarttools" / f"discussion-{alias}" / "config.yaml" + + def select_participant(sender, app_data, user_data): + """Handle participant selection.""" + alias = user_data + dialog_state['selected_participant'] = alias + # Update selection highlight + for child in dpg.get_item_children("participant_list_container", 1) or []: + dpg.bind_item_theme(child, 0) + dpg.bind_item_theme(sender, self.selected_theme) + load_participant_details(alias) + + def refresh_list(): + """Refresh the participant list.""" + if dpg.does_item_exist("participant_list_container"): + dpg.delete_item("participant_list_container", children_only=True) + participants = self._get_participants() + for alias, display_name in participants: + dpg.add_button( + label=f"@{alias}", + callback=select_participant, + user_data=alias, + width=-1, + parent="participant_list_container" + ) + + def load_participant_details(alias: str): + """Load participant details into the edit panel.""" + import yaml + import re + path = get_participant_path(alias) + if not path.exists(): + return + + try: + with open(path) as f: + data = yaml.safe_load(f) + + dpg.set_value("edit_part_alias", alias) + + meta = data.get('meta', {}) + dpg.set_value("edit_part_display_name", meta.get('display_name', '')) + dpg.set_value("edit_part_description", data.get('description', '')) + dpg.set_value("edit_part_expertise", ', '.join(meta.get('expertise', []))) + + # Set voice dropdown + voice = meta.get('voice', DEFAULT_VOICE if DEFAULT_VOICE else 'en-US-Neural2-J') + dpg.set_value("edit_part_voice", voice) + + # Set provider dropdown - check meta first, then prompt steps + provider = meta.get('provider', None) + if not provider: + for step in data.get('steps', []): + if step.get('type') == 'prompt' and 'provider' in step: + provider = step['provider'] + break + if not provider: + provider = DEFAULT_PROVIDER if DEFAULT_PROVIDER else 'claude-sonnet' + dpg.set_value("edit_part_provider", provider) + + # Extract personality from prompt step - try multiple formats + personality = "" + for step in data.get('steps', []): + if step.get('type') == 'prompt': + prompt = step.get('prompt', '') + + # Try format 1: ## Your Personality (new format) + if '## Your Personality' in prompt: + start = prompt.find('## Your Personality') + end = prompt.find('##', start + 1) + if end > start: + personality = prompt[start:end].replace('## Your Personality', '').strip() + + # Try format 2: ## Your Role + ## Your Perspective (existing format) + elif '## Your Role' in prompt or '## Your Perspective' in prompt: + parts = [] + # Extract Your Role section + if '## Your Role' in prompt: + start = prompt.find('## Your Role') + end = prompt.find('##', start + 1) + if end > start: + role_text = prompt[start:end].replace('## Your Role', '').strip() + parts.append(f"ROLE:\n{role_text}") + + # Extract Your Perspective section + if '## Your Perspective' in prompt: + start = prompt.find('## Your Perspective') + end = prompt.find('##', start + 1) + if end > start: + persp_text = prompt[start:end].replace('## Your Perspective', '').strip() + parts.append(f"PERSPECTIVE:\n{persp_text}") + + personality = "\n\n".join(parts) + break + + dpg.set_value("edit_part_personality", personality) + + except Exception as e: + self._show_error(f"Error loading participant: {e}") + + def save_participant(): + """Save changes to the selected participant.""" + if not dialog_state['selected_participant']: + self._show_error("No participant selected") + return + + import yaml + alias = dialog_state['selected_participant'] + path = get_participant_path(alias) + + try: + # Load existing data + with open(path) as f: + data = yaml.safe_load(f) + + # Get old voice for comparison + old_voice = data.get('meta', {}).get('voice', '') + + # Update fields + data['description'] = dpg.get_value("edit_part_description") + + if 'meta' not in data: + data['meta'] = {} + data['meta']['display_name'] = dpg.get_value("edit_part_display_name") + expertise_str = dpg.get_value("edit_part_expertise") + data['meta']['expertise'] = [e.strip() for e in expertise_str.split(',') if e.strip()] + + # Save voice and check if changed + new_voice = dpg.get_value("edit_part_voice") + voice_changed = old_voice != new_voice + if new_voice: + data['meta']['voice'] = new_voice + + # Save provider - update both meta and prompt steps + new_provider = dpg.get_value("edit_part_provider") + if new_provider: + data['meta']['provider'] = new_provider + # Also update provider in prompt steps + for step in data.get('steps', []): + if step.get('type') == 'prompt': + step['provider'] = new_provider + + # Update personality in prompt - handle multiple formats + new_personality = dpg.get_value("edit_part_personality") + for step in data.get('steps', []): + if step.get('type') == 'prompt': + prompt = step.get('prompt', '') + + # Format 1: ## Your Personality (new format) + if '## Your Personality' in prompt: + start = prompt.find('## Your Personality') + end = prompt.find('##', start + 1) + if end > start: + new_prompt = prompt[:start] + f"## Your Personality\n{new_personality}\n\n" + prompt[end:] + step['prompt'] = new_prompt + + # Format 2: ## Your Role + ## Your Perspective (existing format) + elif '## Your Role' in prompt or '## Your Perspective' in prompt: + # Parse the edited text back into role/perspective + new_role = "" + new_persp = "" + if "ROLE:" in new_personality and "PERSPECTIVE:" in new_personality: + role_start = new_personality.find("ROLE:") + persp_start = new_personality.find("PERSPECTIVE:") + if role_start < persp_start: + new_role = new_personality[role_start + 5:persp_start].strip() + new_persp = new_personality[persp_start + 12:].strip() + else: + new_persp = new_personality[persp_start + 12:role_start].strip() + new_role = new_personality[role_start + 5:].strip() + elif "ROLE:" in new_personality: + new_role = new_personality.replace("ROLE:", "").strip() + elif "PERSPECTIVE:" in new_personality: + new_persp = new_personality.replace("PERSPECTIVE:", "").strip() + else: + # No markers - treat as perspective + new_persp = new_personality + + # Replace sections in prompt + if new_role and '## Your Role' in prompt: + start = prompt.find('## Your Role') + end = prompt.find('##', start + 1) + if end > start: + prompt = prompt[:start] + f"## Your Role\n{new_role}\n\n" + prompt[end:] + + if new_persp and '## Your Perspective' in prompt: + start = prompt.find('## Your Perspective') + end = prompt.find('##', start + 1) + if end > start: + prompt = prompt[:start] + f"## Your Perspective\n{new_persp}\n\n" + prompt[end:] + + step['prompt'] = prompt + break + + # Save back + with open(path, 'w') as f: + yaml.dump(data, f, default_flow_style=False, sort_keys=False) + + # Refresh participant registry so voice changes take effect + if get_registry: + get_registry().refresh() + + # Clear read-aloud cache if voice changed + cache_cleared = False + if voice_changed: + cache_dir = os.path.expanduser("~/.cache/read-aloud") + if os.path.exists(cache_dir): + try: + for f in os.listdir(cache_dir): + fpath = os.path.join(cache_dir, f) + if os.path.isfile(fpath) and f.endswith('.mp3'): + os.remove(fpath) + cache_cleared = True + except Exception: + pass # Ignore cache clearing errors + + if voice_changed and cache_cleared: + self._show_info(f"Participant '@{alias}' saved!\n\nVoice changed - audio cache cleared.", title="Saved") + else: + self._show_info(f"Participant '@{alias}' saved!", title="Saved") + + except Exception as e: + self._show_error(f"Error saving participant: {e}") + + def delete_participant(): + """Delete the selected participant.""" + if not dialog_state['selected_participant']: + self._show_error("No participant selected") + return + + alias = dialog_state['selected_participant'] + path = get_participant_path(alias) + config_dir = path.parent + + if not path.exists(): + self._show_error(f"Participant config not found: {path}") + return + + try: + # Delete the config file and directory + path.unlink() + if config_dir.exists() and not any(config_dir.iterdir()): + config_dir.rmdir() + + dialog_state['selected_participant'] = None + dpg.set_value("edit_part_alias", "") + dpg.set_value("edit_part_display_name", "") + dpg.set_value("edit_part_description", "") + dpg.set_value("edit_part_expertise", "") + dpg.set_value("edit_part_personality", "") + refresh_list() + + # Refresh registry + if get_registry: + get_registry().refresh() + + except Exception as e: + self._show_error(f"Error deleting participant: {e}") + + with dpg.window(label="Manage Participants", tag=window_tag, + width=750, height=550, pos=[320, 80], no_collapse=True): + + with dpg.group(horizontal=True): + # Left panel: Participant list + with dpg.child_window(width=180, height=-40): + dpg.add_text("Participants", color=(150, 200, 255)) + dpg.add_separator() + with dpg.child_window(tag="participant_list_container", height=-1): + pass # Populated by refresh_list + + # Right panel: Edit details + with dpg.child_window(width=-1, height=-40): + dpg.add_text("Participant Details", color=(150, 200, 255)) + dpg.add_separator() + + dpg.add_text("Alias:") + dpg.add_input_text(tag="edit_part_alias", width=-1, readonly=True) + + dpg.add_spacer(height=5) + dpg.add_text("Display Name:") + dpg.add_input_text(tag="edit_part_display_name", width=-1) + + dpg.add_spacer(height=5) + dpg.add_text("Description:") + dpg.add_input_text(tag="edit_part_description", width=-1) + + dpg.add_spacer(height=5) + dpg.add_text("Expertise (comma-separated):") + dpg.add_input_text(tag="edit_part_expertise", width=-1) + + dpg.add_spacer(height=5) + dpg.add_text("Voice (for read-aloud):") + voice_items = [v[0] for v in AVAILABLE_VOICES] if AVAILABLE_VOICES else ["en-US-Neural2-J"] + dpg.add_combo( + items=voice_items, + tag="edit_part_voice", + default_value=DEFAULT_VOICE if DEFAULT_VOICE else "en-US-Neural2-J", + width=-1 + ) + + dpg.add_spacer(height=5) + dpg.add_text("AI Provider:") + provider_list = get_available_providers() if get_available_providers else [] + provider_items = [p[0] for p in provider_list] if provider_list else ["claude-sonnet"] + dpg.add_combo( + items=provider_items, + tag="edit_part_provider", + default_value=DEFAULT_PROVIDER if DEFAULT_PROVIDER else "claude-sonnet", + width=-1 + ) + + dpg.add_spacer(height=5) + dpg.add_text("Personality/Perspective:") + dpg.add_input_text(tag="edit_part_personality", width=-1, height=100, + multiline=True) + + dpg.add_spacer(height=10) + with dpg.group(horizontal=True): + dpg.add_button(label="Save Changes", callback=save_participant, width=120) + dpg.add_button(label="Delete Participant", callback=delete_participant, width=130) + + with dpg.group(horizontal=True): + dpg.add_button(label="New Participant...", + callback=lambda: self._show_new_participant_dialog(), width=130) + dpg.add_spacer(width=20) + dpg.add_button(label="Close", + callback=lambda: dpg.delete_item(window_tag), width=100) + + refresh_list() + dpg.focus_item(window_tag) + + def _show_shortcuts(self): + """Show keyboard shortcuts dialog.""" + window_tag = "shortcuts_dialog" + if dpg.does_item_exist(window_tag): + dpg.delete_item(window_tag) + + with dpg.window(label="Keyboard Shortcuts", tag=window_tag, + width=300, height=250, pos=[550, 300]): + + shortcuts = [ + ("N", "New Discussion"), + ("Q", "Quit"), + ("R", "Refresh"), + ("T", "Run Turn"), + ("C", "Add Comment"), + ("D", "View Diagrams"), + ("Escape", "Close dialogs"), + ] + + for key, action in shortcuts: + with dpg.group(horizontal=True): + dpg.add_text(f"[{key}]", color=(150, 200, 255)) + dpg.add_text(f" {action}") + + dpg.add_separator() + dpg.add_button(label="Close", callback=lambda: dpg.delete_item(window_tag)) + + # Keyboard handlers + def _is_typing(self): + """Check if user is typing in an input field.""" + # Check if focused item is an input type + focused = dpg.get_focused_item() + if focused: + try: + item_type = dpg.get_item_type(focused) + # Input text types that should capture keyboard + if "input" in item_type.lower() or "text" in item_type.lower(): + return True + except Exception: + pass + + # Also check if any dialog windows with inputs are visible + # When these dialogs are open, disable hotkeys to allow typing + dialog_windows = [ + "comment_dialog", "new_discussion_dialog", "new_template_dialog", + "new_participant_dialog", "edit_phases_dialog", "edit_pipeline_dialog", + "delete_confirm_dialog", "edit_discussion_dialog", + ] + for tag in dialog_windows: + if dpg.does_item_exist(tag) and dpg.is_item_shown(tag): + return True + + # Check for active diagram editor windows + if hasattr(self, '_active_diagram_windows'): + for tag in self._active_diagram_windows: + if dpg.does_item_exist(tag) and dpg.is_item_shown(tag): + return True + + return False + + def _has_modifier_key(self): + """Check if Ctrl, Shift, or Alt is held - used to avoid blocking clipboard shortcuts.""" + return (dpg.is_key_down(dpg.mvKey_LControl) or + dpg.is_key_down(dpg.mvKey_RControl) or + dpg.is_key_down(dpg.mvKey_LShift) or + dpg.is_key_down(dpg.mvKey_RShift) or + dpg.is_key_down(dpg.mvKey_LAlt) or + dpg.is_key_down(dpg.mvKey_RAlt)) + + def _on_quit(self): + if not self._is_typing() and not self._has_modifier_key(): + dpg.stop_dearpygui() + + def _on_refresh(self): + if not self._is_typing() and not self._has_modifier_key(): + self._refresh_current() + + def _on_turn(self): + if not self._is_typing() and not self._has_modifier_key() and self.current_discussion: + self._run_turn() + + def _on_comment(self): + if not self._is_typing() and not self._has_modifier_key() and self.current_discussion: + self._show_comment_dialog() + + def _on_diagrams(self): + if not self._is_typing() and not self._has_modifier_key() and self.current_discussion: + self._show_diagram_dialog() + + def _on_escape(self): + # Escape should always work to close dialogs + for tag in ["comment_dialog", "diagram_select_dialog", "error_popup", "shortcuts_dialog", + "new_discussion_dialog", "new_template_dialog", "new_participant_dialog", + "manage_templates_dialog", "manage_participants_dialog", "edit_phases_dialog", + "edit_pipeline_dialog", "delete_confirm_dialog", "edit_discussion_dialog"]: + if dpg.does_item_exist(tag): + dpg.delete_item(tag) + + def _on_new_discussion(self): + if not self._is_typing() and not self._has_modifier_key(): + self._show_new_discussion_dialog() + + def _poll_background_tasks(self): + """Called from main thread to poll background task updates.""" + # Clean up any textures marked for deletion + if hasattr(self, '_textures_to_delete') and self._textures_to_delete: + for tex_tag in self._textures_to_delete[:]: # Copy list to iterate safely + try: + if dpg.does_item_exist(tex_tag): + dpg.delete_item(tex_tag) + self._textures_to_delete.remove(tex_tag) + except Exception: + pass + + # Poll diagram editors for AI completion + if hasattr(self, '_diagram_editors'): + for poll_func in list(self._diagram_editors.values()): + try: + poll_func() + except Exception: + pass + + # Update output display + if not hasattr(self, '_last_output_index'): + self._last_output_index = 0 + + if len(self._output_lines) > self._last_output_index: + if dpg.does_item_exist("output_text"): + new_text = "\n".join(self._output_lines[self._last_output_index:]) + current = dpg.get_value("output_text") + dpg.set_value("output_text", current + new_text + "\n") + self._last_output_index = len(self._output_lines) + + # Check if turn completed and needs refresh + if getattr(self, '_turn_complete', False): + self._turn_complete = False + if self.current_discussion: + self.current_discussion = Discussion.load(self.current_discussion.path) + self._show_discussion() + + def run(self): + """Start the GUI main loop.""" + dpg.setup_dearpygui() + dpg.show_viewport() + + # Load discussions after UI is shown + self._refresh_discussions() + + # Open specific file if requested + if getattr(self, '_pending_open_file', None): + self._open_discussion(self._pending_open_file) + self._pending_open_file = None + + # Manual render loop with background task polling + while dpg.is_dearpygui_running(): + # Poll for updates from background threads (output, turn completion) + self._poll_background_tasks() + + # Render frame + dpg.render_dearpygui_frame() + + dpg.destroy_context() + + +def main(discussions_dir: str = None, open_file: str = None): + """Entry point for GUI. + + Args: + discussions_dir: Directory to browse for discussions + open_file: Specific discussion file to open immediately + """ + dir_path = Path(discussions_dir) if discussions_dir else None + app = DiscussionGUI(dir_path) + + # If a specific file was requested, open it after UI initializes + if open_file: + app._pending_open_file = Path(open_file) + else: + app._pending_open_file = None + + app.run() + + +if __name__ == "__main__": + import sys + discussions_dir = sys.argv[1] if len(sys.argv) > 1 else None + main(discussions_dir) diff --git a/src/discussions/ui/tui.py b/src/discussions/ui/tui.py new file mode 100644 index 0000000..f5679c3 --- /dev/null +++ b/src/discussions/ui/tui.py @@ -0,0 +1,1314 @@ +""" +Main TUI application for Orchestrated Discussions. + +Provides an interactive terminal interface for: +- Browsing and selecting discussions +- Viewing discussion content and comments +- Adding human comments with votes +- Running discussion turns +""" + +import urwid +from pathlib import Path +from typing import Optional, Callable +import subprocess +import threading +import json +import os +import queue +import uuid + +from ..discussion import Discussion, Comment +from ..voting import format_vote_summary +from ..participant import get_registry, DEFAULT_VOICE +from .widgets import ( + PALETTE, + CommentBlock, + VoteSummary, + PhaseIndicator, + DiscussionListItem, + CommentInput, + StatusBar, + HeaderBar, + DiagramViewer, +) + + +class DiscussionBrowser(urwid.WidgetWrap): + """ + Widget for browsing and selecting discussions. + """ + + def __init__(self, discussions_dir: Path, on_select: Callable[[Path], None]): + self.discussions_dir = discussions_dir + self.on_select = on_select + self.items = [] + + self._refresh_list() + + super().__init__(self.list_box) + + def _refresh_list(self): + """Refresh the list of discussions.""" + self.items = [] + + # Find all .discussion.md files in discussions directory + if self.discussions_dir.exists(): + md_files = sorted( + self.discussions_dir.glob("**/*.discussion.md"), + key=lambda p: p.stat().st_mtime, + reverse=True + ) + + for path in md_files: + try: + d = Discussion.load(path) + votes = d.get_votes() + vote_summary = format_vote_summary(votes) if votes else "No votes" + + item = DiscussionListItem( + title=d.title or path.stem, + status=d.status, + phase=d.phase, + vote_summary=vote_summary, + path=str(path), + ) + + self.items.append(urwid.AttrMap(item, None, focus_map="focus")) + except Exception: + # Skip files that fail to parse + pass + + if not self.items: + self.items = [urwid.Text("No discussions found.")] + + self.list_walker = urwid.SimpleFocusListWalker(self.items) + self.list_box = urwid.ListBox(self.list_walker) + + def keypress(self, size, key): + if key == "enter": + focus = self.list_box.focus + if focus and hasattr(focus.base_widget, "path"): + self.on_select(Path(focus.base_widget.path)) + return None + return super().keypress(size, key) + + +class DiscussionView(urwid.WidgetWrap): + """ + Widget for viewing a single discussion. + + Shows: + - Header with title and phase + - Vote summary sidebar + - Scrollable comment list with read-aloud buttons + """ + + def __init__(self, discussion: Discussion, on_read=None): + self.discussion = discussion + self.on_read = on_read + + # Build content widgets starting with context + content_widgets = [] + + # Add initial context/preamble + context = discussion.get_context() + if context: + context_widget = urwid.Text(("context", context)) + content_widgets.append(context_widget) + content_widgets.append(urwid.Divider("─")) + + # Add comments with read-aloud callback + self.comment_blocks = [] + for i, comment in enumerate(discussion.comments): + block = CommentBlock( + author=comment.author, + body=comment.body, + vote=comment.vote, + on_read=on_read, + comment_id=i, + ) + self.comment_blocks.append(block) + content_widgets.append(block) + + if not content_widgets: + content_widgets = [urwid.Text("Empty discussion.")] + + # Content list (context + comments) + self.comment_list = urwid.ListBox( + urwid.SimpleFocusListWalker(content_widgets) + ) + + # Vote summary + votes = discussion.get_votes() + consensus = discussion.check_consensus() + self.vote_summary = VoteSummary(votes, consensus) + + # Phase indicator + self.phase_indicator = PhaseIndicator(discussion.phase) + + # Build sidebar + sidebar = urwid.Pile([ + ("pack", self.phase_indicator), + urwid.Divider(), + ("pack", self.vote_summary), + ]) + sidebar_box = urwid.BoxAdapter( + urwid.Filler(sidebar, valign="top"), + height=20 + ) + + # Main layout: comments | sidebar + columns = urwid.Columns([ + ("weight", 3, self.comment_list), + ("weight", 1, urwid.Filler(sidebar, valign="top")), + ], dividechars=1) + + super().__init__(columns) + + def refresh(self): + """Reload discussion and refresh view.""" + self.discussion = Discussion.load(self.discussion.path) + # Rebuild would require recreating the widget + + +class OpenFilePopup(urwid.WidgetWrap): + """ + Popup for opening a discussion file by path. + """ + + def __init__(self, on_open: Callable[[str], None], on_cancel: Callable[[], None], initial_dir: str = ""): + self.on_open = on_open + self.on_cancel = on_cancel + + # Path input + self.path_edit = urwid.Edit("Path: ", initial_dir) + + # Buttons + open_btn = urwid.AttrMap( + urwid.Button("Open", on_press=lambda b: self._do_open()), + "button", "button_focus" + ) + cancel_btn = urwid.AttrMap( + urwid.Button("Cancel", on_press=lambda b: self._do_cancel()), + "button", "button_focus" + ) + buttons = urwid.Columns([open_btn, cancel_btn]) + + # Help text + help_text = urwid.Text([ + ("dim", "Enter path to .discussion.md file\n"), + ("dim", "Press Enter to open, ESC to cancel"), + ]) + + content = urwid.Pile([ + ("pack", urwid.AttrMap( + urwid.Text(" Open Discussion ", align="center"), + "header" + )), + ("pack", urwid.Divider()), + ("pack", self.path_edit), + ("pack", urwid.Divider()), + ("pack", help_text), + ("pack", urwid.Divider()), + ("pack", buttons), + ]) + + box = urwid.LineBox(content) + overlay = urwid.Filler(box, valign="middle") + super().__init__(overlay) + + def _do_open(self): + path = self.path_edit.get_edit_text().strip() + if path: + self.on_open(path) + + def _do_cancel(self): + self.on_cancel() + + def keypress(self, size, key): + if key == "enter": + self._do_open() + return None + elif key == "esc": + self._do_cancel() + return None + return super().keypress(size, key) + + +class TurnProgressPopup(urwid.WidgetWrap): + """ + Popup showing live output from running a turn. + """ + + def __init__(self, on_close: Callable[[], None]): + self.on_close = on_close + self.output_lines = [] + self.is_running = True + + # Output display + self.output_text = urwid.Text("Starting turn...\n") + self.output_walker = urwid.SimpleFocusListWalker([self.output_text]) + self.output_list = urwid.ListBox(self.output_walker) + + # Status line + self.status_text = urwid.Text("Running... (ESC to close when done)") + + # Close button + self.close_btn = urwid.AttrMap( + urwid.Button("Close", on_press=lambda b: self._close()), + "button", "button_focus" + ) + + content = urwid.Pile([ + ("pack", urwid.AttrMap( + urwid.Text(" Turn Progress ", align="center"), + "header" + )), + ("pack", urwid.Divider("-")), + self.output_list, + ("pack", urwid.Divider("-")), + ("pack", self.status_text), + ("pack", self.close_btn), + ]) + + box = urwid.LineBox(content) + super().__init__(box) + + def add_output(self, text: str): + """Add output text to the display.""" + self.output_lines.append(text) + # Update display with all lines + display_text = "\n".join(self.output_lines[-50:]) # Keep last 50 lines + self.output_text.set_text(display_text) + # Scroll to bottom + if self.output_walker: + self.output_list.set_focus(len(self.output_walker) - 1) + + def set_status(self, text: str): + """Update status line.""" + self.status_text.set_text(text) + + def mark_complete(self): + """Mark the turn as complete.""" + self.is_running = False + self.set_status("Complete! Press ESC or click Close") + + def _close(self): + self.on_close() + + def keypress(self, size, key): + if key == "esc": + self._close() + return None + return super().keypress(size, key) + + +class CommentInputOverlay(urwid.WidgetWrap): + """ + Overlay for comment input that handles escape key. + """ + + def __init__(self, comment_input: CommentInput, background: urwid.Widget, on_cancel: Callable[[], None]): + self.on_cancel = on_cancel + + overlay = urwid.Overlay( + urwid.LineBox(comment_input), + background, + align="center", + width=("relative", 80), + valign="middle", + height=("relative", 50), + ) + + super().__init__(overlay) + + def keypress(self, size, key): + if key == "esc": + self.on_cancel() + return None + return super().keypress(size, key) + + +class DiscussionTUI: + """ + Main TUI application controller. + """ + + def __init__(self, discussions_dir: Path = None): + if discussions_dir is None: + # Default to cwd (scans recursively for .md files) + discussions_dir = Path.cwd() + + self.discussions_dir = Path(discussions_dir) + self.current_discussion: Optional[Discussion] = None + self.loop: Optional[urwid.MainLoop] = None + self._saved_body: Optional[urwid.Widget] = None + self._turn_popup: Optional[TurnProgressPopup] = None + self._pipe_fd: Optional[int] = None # For waking event loop from threads + self._update_queue: queue.Queue = queue.Queue() # Thread-safe update queue + + # Read-aloud state + self._reading_session_id: Optional[str] = None + self._reading_comment_id: Optional[int] = None + self._reading_block: Optional['CommentBlock'] = None + self._current_view: Optional['DiscussionView'] = None + + # Dictation state + self._dictation_process: Optional[subprocess.Popen] = None + self._dictation_audio_file: Optional[str] = None + self._dictation_widget: Optional['CommentInput'] = None + + # Create UI components + self.header = HeaderBar() + self.status = StatusBar("Welcome to Orchestrated Discussions") + self.body = urwid.SolidFill(" ") + + # Main frame + self.frame = urwid.Frame( + header=self.header, + body=self.body, + footer=self.status, + ) + + # Show browser initially + self._show_browser() + + def _show_browser(self): + """Show the discussion browser.""" + self.header.set_title("Orchestrated Discussions - Browser") + self.status.set_status("Select a discussion with Enter | o:open file | q:quit | r:refresh") + + browser = DiscussionBrowser( + self.discussions_dir, + on_select=self._open_discussion + ) + + self.frame.body = browser + + def _show_open_file_popup(self): + """Show popup to open a discussion file by path.""" + def on_open(path): + # Close popup and open the file + self._close_open_file_popup() + path_obj = Path(path).expanduser().resolve() + if path_obj.exists(): + self._open_discussion(path_obj) + else: + self.status.set_status(f"File not found: {path}") + + def on_cancel(): + self._close_open_file_popup() + + # Save current body + self._saved_body = self.frame.body + + # Show popup + popup = OpenFilePopup(on_open, on_cancel, str(self.discussions_dir) + "/") + self.frame.body = urwid.Overlay( + popup, + self._saved_body, + align="center", width=("relative", 80), + valign="middle", height=("relative", 40), + ) + + def _close_open_file_popup(self): + """Close the open file popup.""" + if self._saved_body: + self.frame.body = self._saved_body + self._saved_body = None + + def _open_discussion(self, path: Path): + """Open a discussion for viewing.""" + try: + self.current_discussion = Discussion.load(path) + self._show_discussion_view() + except Exception as e: + self.status.set_status(f"Error: {e}") + + def _get_voice_for_author(self, author: str) -> str: + """Look up the voice for a participant by author name.""" + # Default voice + default = DEFAULT_VOICE if DEFAULT_VOICE else "en-US-Neural2-J" + + # Try to extract alias from author name (e.g., "AI-Architect" -> "architect") + if author.startswith("AI-"): + alias = author[3:].lower() + elif author.startswith("ai-"): + alias = author[3:].lower() + else: + # Human or unknown - use default + return default + + # Look up participant + registry = get_registry() + if registry: + participant = registry.get(alias) + if participant and participant.voice: + return participant.voice + + return default + + def _read_aloud(self, text: str, comment_id: int, block: 'CommentBlock', author: str = ""): + """Handle read/stop button click for a comment.""" + # If we're currently reading this comment, stop + if self._reading_session_id and self._reading_comment_id == comment_id: + self._stop_reading() + return + + # If we're reading something else, stop that first + if self._reading_session_id: + self._stop_reading() + + # Look up participant's voice + voice = self._get_voice_for_author(author) + + # Start new reading + self._start_reading(text, comment_id, block, voice) + + def _start_reading(self, text: str, comment_id: int, block: 'CommentBlock', voice: str = None): + """Start reading text aloud.""" + if voice is None: + voice = DEFAULT_VOICE if DEFAULT_VOICE else "en-US-Neural2-J" + session_id = str(uuid.uuid4())[:8] + self._reading_session_id = session_id + self._reading_comment_id = comment_id + self._reading_block = block + + # Update button to show "Stop" + block.set_button_label("Stop") + self.status.set_status("Reading comment aloud... (click Stop to cancel)") + + def run_tts(): + try: + # Use full path since ~/.local/bin may not be in PATH + read_aloud_cmd = os.path.expanduser("~/.local/bin/read-aloud") + # Extract language from voice name (e.g., "en-GB-Neural2-A" -> "en-GB") + lang = voice[:5] if len(voice) >= 5 else "en-US" + subprocess.run( + [read_aloud_cmd, "--strip-md", "true", "--voice", voice, "--lang", lang, "--session-id", session_id], + input=text, + capture_output=True, + text=True, + timeout=300 + ) + except Exception: + pass + finally: + # Reset state when done + self._on_reading_complete(comment_id, block) + + thread = threading.Thread(target=run_tts, daemon=True) + thread.start() + + def _stop_reading(self): + """Stop current reading by creating stop signal file.""" + if self._reading_session_id: + stop_file = f"/tmp/read-aloud-{self._reading_session_id}.stop" + try: + with open(stop_file, 'w') as f: + f.write("stop") + except Exception: + pass + # Reset button immediately + if self._reading_block: + self._reading_block.set_button_label("Read") + self.status.set_status("Reading stopped") + self._reading_session_id = None + self._reading_comment_id = None + self._reading_block = None + + def _on_reading_complete(self, comment_id: int, block: 'CommentBlock'): + """Called when reading completes (from background thread).""" + # Only reset if this is still the active session + if self._reading_comment_id == comment_id: + self._reading_session_id = None + self._reading_comment_id = None + self._reading_block = None + block.set_button_label("Read") + self.status.set_status("b:back | c:comment | t:turn | d:diagrams | r:refresh | q:quit") + + def _dictate_clicked(self, widget: 'CommentInput'): + """Handle dictate button click - toggle recording.""" + if self._dictation_process is not None: + # Currently recording - stop and transcribe + self._stop_dictation() + else: + # Start recording + self._start_dictation(widget) + + def _start_dictation(self, widget: 'CommentInput'): + """Start recording audio from microphone.""" + import tempfile + import signal + + # Store widget reference for callback + self._dictation_widget = widget + + # Create temp file for audio + self._dictation_audio_file = tempfile.mktemp(suffix=".wav") + + # Start arecord without duration limit (records until interrupted) + try: + self._dictation_process = subprocess.Popen( + [ + "arecord", + "-D", "default", + "-f", "S16_LE", + "-r", "16000", + "-c", "1", + "-t", "wav", + self._dictation_audio_file + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + + # Update UI + widget.set_dictate_label("Stop Recording") + self.status.set_status("Recording... Click 'Stop Recording' when done") + + except FileNotFoundError: + self.status.set_status("Error: arecord not found. Install alsa-utils") + self._dictation_process = None + self._dictation_audio_file = None + self._dictation_widget = None + except Exception as e: + self.status.set_status(f"Recording error: {e}") + self._dictation_process = None + self._dictation_audio_file = None + self._dictation_widget = None + + def _stop_dictation(self): + """Stop recording and transcribe the audio.""" + import signal + + if self._dictation_process is None: + return + + # Send SIGINT to stop arecord gracefully + try: + self._dictation_process.send_signal(signal.SIGINT) + self._dictation_process.wait(timeout=2) + except Exception: + # Force kill if needed + try: + self._dictation_process.kill() + self._dictation_process.wait(timeout=1) + except Exception: + pass + + self._dictation_process = None + + # Update UI + widget = self._dictation_widget + if widget: + widget.set_dictate_label("Transcribing...") + self.status.set_status("Transcribing audio...") + + # Transcribe in background thread + audio_file = self._dictation_audio_file + self._dictation_audio_file = None + + def transcribe(): + transcript = "" + try: + if audio_file and os.path.exists(audio_file): + transcribe_cmd = os.path.expanduser("~/.local/bin/transcribe") + result = subprocess.run( + [transcribe_cmd], + input=audio_file, + capture_output=True, + text=True, + timeout=60 + ) + transcript = result.stdout.strip() + + # Clean up audio file + os.remove(audio_file) + except Exception as e: + transcript = f"[Transcription error: {e}]" + + # Queue UI update + self._update_queue.put(('transcription', transcript)) + # Wake up main loop + if self._pipe_fd: + os.write(self._pipe_fd, b'x') + + thread = threading.Thread(target=transcribe, daemon=True) + thread.start() + + def _on_transcription_complete(self, transcript: str): + """Called when transcription completes - append to comment text.""" + widget = self._dictation_widget + self._dictation_widget = None + + # Reset button + if widget: + widget.set_dictate_label("Dictate") + + if transcript and not transcript.startswith("["): + widget.append_text(transcript) + self.status.set_status("Transcription added. ESC to cancel | Tab to navigate") + else: + self.status.set_status(f"Transcription failed: {transcript}") + + def _artifact_clicked(self, widget: 'CommentInput'): + """Handle artifact button click - launch editor or fall back to $EDITOR.""" + if not self.current_discussion: + return + + import shutil + import re + + # Create diagrams directory if needed + diagrams_dir = self.current_discussion.path.parent / "diagrams" + diagrams_dir.mkdir(exist_ok=True) + + # Generate suggested filename + title = getattr(self.current_discussion, 'title', 'artifact') or 'artifact' + safe_title = re.sub(r'[^a-z0-9]+', '-', title.lower())[:50] + user = os.environ.get("USER", "human") + + # Find next number + existing = list(diagrams_dir.glob(f"{safe_title}_{user}_*.*")) + next_num = len(existing) + 1 + suggested_file = diagrams_dir / f"{safe_title}_{user}_{next_num}.puml" + + self.status.set_status("Opening artifact editor...") + + # Check if we have a graphical display + has_display = os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY") + + # Try artifact-editor first (if display available) + artifact_editor = shutil.which('artifact-editor') + if has_display and artifact_editor: + self._launch_artifact_editor_tui(widget, str(suggested_file)) + else: + # Fall back to $EDITOR + self._launch_text_editor_tui(widget, str(suggested_file)) + + def _launch_artifact_editor_tui(self, widget: 'CommentInput', file_path: str): + """Launch artifact-editor GUI and capture result.""" + import shutil + + # Check for artifact-editor + artifact_editor = shutil.which('artifact-editor') + if not artifact_editor: + # Try project path + project_path = Path.home() / "PycharmProjects" / "artifact-editor" + if (project_path / "src" / "artifact_editor" / "cli.py").exists(): + cmd = ["python3", "-m", "artifact_editor.cli", "--output", file_path] + env = os.environ.copy() + env["PYTHONPATH"] = str(project_path / "src") + else: + self.status.set_status("Artifact editor not found, falling back to $EDITOR") + self._launch_text_editor_tui(widget, file_path) + return + else: + cmd = [artifact_editor, "--output", file_path] + env = None + + def run_editor(): + try: + result = subprocess.run(cmd, capture_output=True, text=True, env=env) + + # Parse ARTIFACT_SAVED:path from stdout + saved_path = None + for line in result.stdout.splitlines(): + if line.startswith("ARTIFACT_SAVED:"): + saved_path = line.split(":", 1)[1].strip() + break + + if saved_path: + # Make path relative + try: + rel_path = Path(saved_path).relative_to(self.current_discussion.path.parent) + except ValueError: + rel_path = Path(saved_path).name + + # Queue UI update + self._update_queue.put(('artifact', (widget, str(rel_path)))) + else: + self._update_queue.put(('artifact', (widget, None))) + + # Wake up main loop + if self._pipe_fd: + os.write(self._pipe_fd, b'x') + + except Exception as e: + self._update_queue.put(('artifact', (widget, None))) + if self._pipe_fd: + os.write(self._pipe_fd, b'x') + + thread = threading.Thread(target=run_editor, daemon=True) + thread.start() + + def _launch_text_editor_tui(self, widget: 'CommentInput', file_path: str): + """Launch $EDITOR for text-based artifact creation.""" + editor = os.environ.get("EDITOR", "nano") + + # Create file with template if it doesn't exist + path = Path(file_path) + if not path.exists(): + template = """@startuml +title New Diagram + +' Edit this PlantUML diagram +' Save and exit when done + +class Example { + +attribute: type + -- + +method() +} + +@enduml +""" + path.write_text(template) + + # Stop the urwid main loop temporarily + self.loop.stop() + + try: + # Run editor (blocks) + subprocess.run([editor, file_path]) + + # Check if file was modified + if path.exists() and path.stat().st_size > 0: + try: + rel_path = path.relative_to(self.current_discussion.path.parent) + except ValueError: + rel_path = path.name + + # Add diagram reference to comment + widget.append_text(f"\n\nDIAGRAM: {rel_path}") + self.status.set_status(f"Added artifact: {rel_path}") + else: + self.status.set_status("Artifact creation cancelled") + + finally: + # Restart urwid loop + self.loop.start() + + def _on_artifact_complete(self, widget: 'CommentInput', rel_path: str | None): + """Called when artifact editor completes.""" + if rel_path: + widget.append_text(f"\n\nDIAGRAM: {rel_path}") + self.status.set_status(f"Added artifact: {rel_path}") + else: + self.status.set_status("Artifact editor closed without saving") + + def _show_discussion_view(self): + """Show the discussion view.""" + if not self.current_discussion: + return + + d = self.current_discussion + self.header.set_title(f"{d.title} [{d.status}]") + self.status.set_status( + "b:back | c:comment | t:turn | d:diagrams | r:refresh | q:quit" + ) + + self._current_view = DiscussionView(d, on_read=self._read_aloud) + self.frame.body = self._current_view + + def _show_comment_input(self): + """Show the comment input overlay.""" + if not self.current_discussion: + return + + # Save current body for cancel + self._saved_body = self.frame.body + + def on_submit(text, vote): + if text.strip(): + # Get author name from env or use default + author = os.environ.get("USER", "Human") + self.current_discussion.add_comment(author, text, vote) + self.current_discussion.save() + self.status.set_status("Comment added!") + self._show_discussion_view() + + def on_cancel(): + self.status.set_status("Comment cancelled") + self._show_discussion_view() + + comment_input = CommentInput( + on_submit=on_submit, + on_dictate=self._dictate_clicked, + on_artifact=self._artifact_clicked + ) + + # Create overlay with escape handling + overlay = CommentInputOverlay(comment_input, self._saved_body, on_cancel) + self.frame.body = overlay + self.status.set_status("Enter comment (ESC to cancel) | Tab to navigate") + + def _show_turn_progress(self): + """Show turn progress popup.""" + if not self.current_discussion: + return + + # Save current body + self._saved_body = self.frame.body + + def on_close(): + # Reload and show discussion + if self.current_discussion: + self.current_discussion = Discussion.load(self.current_discussion.path) + self._show_discussion_view() + + self._turn_popup = TurnProgressPopup(on_close) + + overlay = urwid.Overlay( + self._turn_popup, + self._saved_body, + align="center", + width=("relative", 90), + valign="middle", + height=("relative", 80), + ) + + self.frame.body = overlay + + def _run_turn(self): + """Run a discussion turn (invoke participants) with progress display.""" + if not self.current_discussion: + return + + # Show progress popup + self._show_turn_progress() + + # Run turn in background thread + thread = threading.Thread(target=self._run_turn_thread, daemon=True) + thread.start() + + def _run_turn_thread(self): + """Background thread for running turn with parallel participants.""" + d = self.current_discussion + + if not d: + return + + participants = d.participant_aliases or ["architect", "security", "pragmatist"] + + # Create log directory and file + log_dir = Path.home() / ".local" / "share" / "orchestrated-discussions" / "logs" + log_dir.mkdir(parents=True, exist_ok=True) + from datetime import datetime + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + log_file = log_dir / f"turn_{timestamp}.log" + + self._add_output("Starting turn (parallel execution)...") + self._add_output(f"Discussion: {d.title}") + self._add_output(f"Participants: {', '.join(participants)}") + self._add_output(f"Log file: {log_file}") + self._add_output("") + + # Start all participants in parallel + processes = {} + discussion_content = d.get_content() + + for alias in participants: + tool_name = f"discussion-{alias}" + self._add_output(f">>> Starting {alias}...") + + try: + process = subprocess.Popen( + [tool_name, "--log-file", str(log_file)], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + process.stdin.write(discussion_content) + process.stdin.close() + processes[alias] = process + except FileNotFoundError: + self._add_output(f" Tool {tool_name} not found") + except Exception as e: + self._add_output(f" ERROR starting {alias}: {e}") + + if not processes: + self._add_output("No participants started!") + self._mark_complete() + return + + self._add_output("") + self._add_output("Waiting for responses (tailing log)...") + self._add_output("") + + # Tail the log file while waiting for processes + last_pos = 0 + while any(p.poll() is None for p in processes.values()): + # Read new log entries + try: + if log_file.exists(): + with open(log_file, 'r') as f: + f.seek(last_pos) + new_lines = f.readlines() + last_pos = f.tell() + for line in new_lines: + self._add_output(line.rstrip()) + except Exception: + pass + + import time + time.sleep(0.2) + + # Read any remaining log entries + try: + if log_file.exists(): + with open(log_file, 'r') as f: + f.seek(last_pos) + for line in f.readlines(): + self._add_output(line.rstrip()) + except Exception: + pass + + # Collect results from all processes + self._add_output("") + self._add_output("Collecting responses...") + responses = [] + + for alias, process in processes.items(): + try: + stdout, stderr = process.stdout.read(), process.stderr.read() + + if process.returncode != 0: + self._add_output(f" {alias}: ERROR (exit {process.returncode})") + continue + + # Parse JSON response + json_start = stdout.find("{") + json_end = stdout.rfind("}") + 1 + if json_start >= 0 and json_end > json_start: + response = json.loads(stdout[json_start:json_end]) + + if response.get("sentinel") == "NO_RESPONSE": + self._add_output(f" {alias}: nothing to add") + elif "comment" in response: + vote = response.get("vote") + diagram_file = response.get("diagram_file") + vote_str = f" [{vote}]" if vote else "" + diagram_str = " +diagram" if diagram_file else "" + self._add_output(f" {alias}: responded{vote_str}{diagram_str}") + + # Include diagram reference in comment if present + comment_text = response["comment"] + if diagram_file: + comment_text += f"\n\n**Diagram:** `{diagram_file}`" + + responses.append({ + "author": f"AI-{alias.capitalize()}", + "comment": comment_text, + "vote": vote, + }) + else: + self._add_output(f" {alias}: no JSON in response") + + except json.JSONDecodeError as e: + self._add_output(f" {alias}: JSON parse error: {e}") + except Exception as e: + self._add_output(f" {alias}: ERROR: {e}") + + # Append responses to discussion + if responses: + self._add_output("") + self._add_output("Appending responses to discussion...") + from ..discussion import Discussion + d = Discussion.load(d.path) + for resp in responses: + d.add_comment( + author=resp["author"], + text=resp["comment"], + vote=resp.get("vote"), + ) + d.save() + self._add_output(f" Added {len(responses)} comments") + + # Count votes + self._add_output("") + self._add_output("Counting votes...") + try: + result = subprocess.run( + ["discussion-parser"], + input=Path(d.path).read_text(), + capture_output=True, + text=True, + ) + if result.returncode == 0: + vote_result = subprocess.run( + ["discussion-vote-counter"], + input=result.stdout, + capture_output=True, + text=True, + ) + if vote_result.returncode == 0: + votes = json.loads(vote_result.stdout) + vs = votes.get("vote_summary", {}) + self._add_output(f" READY={vs.get('READY', 0)} " + f"CHANGES={vs.get('CHANGES', 0)} " + f"REJECT={vs.get('REJECT', 0)}") + consensus = votes.get("consensus", {}) + if consensus.get("reached"): + self._add_output(" Consensus: REACHED") + # Auto-advance phase if in a voting phase + self._try_advance_phase(d) + elif consensus.get("reason"): + self._add_output(f" Consensus: {consensus['reason']}") + except Exception as e: + self._add_output(f" Vote counting error: {e}") + + self._add_output("") + self._add_output("=== Turn complete ===") + self._mark_complete() + + def _try_advance_phase(self, discussion: 'Discussion'): + """Auto-advance phase if consensus reached in a voting phase.""" + import yaml + + # Load template to check phase config + template_name = discussion.template or "feature" + template_dirs = [ + Path.cwd() / "templates", + Path(__file__).parent.parent.parent.parent / "templates", + ] + + template = {} + for template_dir in template_dirs: + template_path = template_dir / f"{template_name}.yaml" + if template_path.exists(): + with open(template_path) as f: + template = yaml.safe_load(f) or {} + break + + phases = template.get("phases", {}) + current_phase = discussion.phase + phase_config = phases.get(current_phase, {}) + + # Only advance if this is a voting phase with a next_phase + if not phase_config.get("voting", False): + return + + next_phase = phase_config.get("next_phase") + if not next_phase: + return + + # Advance the phase + from ..discussion import Discussion + d = Discussion.load(discussion.path) + old_phase = d.phase + d.update_phase(next_phase) + d.save() + self._add_output(f" Phase advanced: {old_phase} → {next_phase}") + + def _add_output(self, text: str): + """Thread-safe add output to popup.""" + if self._pipe_fd is not None: + # Queue the update and wake the event loop via pipe + self._update_queue.put(("output", text)) + os.write(self._pipe_fd, b"x") + + def _mark_complete(self): + """Thread-safe mark turn complete.""" + if self._pipe_fd is not None: + self._update_queue.put(("complete", None)) + os.write(self._pipe_fd, b"x") + + def _process_updates(self, data): + """Process queued updates from background thread (called by watch_pipe).""" + # Process all pending updates + while True: + try: + update_type, value = self._update_queue.get_nowait() + if update_type == "output" and self._turn_popup: + self._turn_popup.add_output(value) + elif update_type == "complete" and self._turn_popup: + self._turn_popup.mark_complete() + elif update_type == "transcription": + self._on_transcription_complete(value) + elif update_type == "artifact": + widget, rel_path = value + self._on_artifact_complete(widget, rel_path) + except queue.Empty: + break + # Return True to keep watching the pipe + return True + + def _handle_input(self, key): + """Handle global key input.""" + if key in ("q", "Q"): + raise urwid.ExitMainLoop() + + elif key == "b": + # Back to browser + self._show_browser() + + elif key == "r": + # Refresh + if self.current_discussion: + self._open_discussion(self.current_discussion.path) + else: + self._show_browser() + + elif key == "c": + # Comment + if self.current_discussion: + self._show_comment_input() + + elif key == "t": + # Run turn + if self.current_discussion: + self._run_turn() + + elif key == "d": + # View diagrams + if self.current_discussion: + self._show_diagram_viewer() + + elif key == "o": + # Open file + self._show_open_file_popup() + + elif key == "h": + # Help + self.status.set_status( + "q:quit | b:back | r:refresh | c:comment | t:turn | d:diagrams" + ) + + def _find_diagrams(self) -> list[str]: + """Find diagram references in current discussion.""" + import re + diagrams = [] + if not self.current_discussion: + return diagrams + + # Search for **Diagram:** `path` pattern in raw content + content = self.current_discussion._raw_content + pattern = r'\*\*Diagram:\*\*\s*`([^`]+)`' + for match in re.finditer(pattern, content): + path = match.group(1) + if path not in diagrams: + diagrams.append(path) + + return diagrams + + def _resolve_diagram_path(self, diagram_path: str) -> str: + """Resolve a diagram path to an absolute path.""" + if diagram_path.startswith('/'): + return diagram_path + + candidates = [ + Path.cwd() / diagram_path, # Relative to CWD (project root) + self.current_discussion.path.parent / diagram_path, # Relative to discussion file + self.discussions_dir / diagram_path, # Relative to discussions dir + ] + for candidate in candidates: + if candidate.exists(): + return str(candidate) + + # Default to CWD-relative if none found + return str(Path.cwd() / diagram_path) + + def _show_diagram_viewer(self): + """Show diagram viewer overlay.""" + diagrams = self._find_diagrams() + + if not diagrams: + self.status.set_status("No diagrams found in this discussion") + return + + if len(diagrams) == 1: + # Single diagram - show directly + self._view_diagram(diagrams[0]) + else: + # Multiple diagrams - show selector + self._show_diagram_selector(diagrams) + + def _show_diagram_selector(self, diagrams: list[str]): + """Show a popup to select which diagram to view.""" + self._saved_body = self.frame.body + + # Create list of diagram buttons + body_items = [urwid.Text("Select a diagram:\n")] + for i, path in enumerate(diagrams): + name = Path(path).name + btn = urwid.Button(f"{i+1}. {name}") + urwid.connect_signal(btn, 'click', lambda b, p=path: self._on_diagram_selected(p)) + body_items.append(urwid.AttrMap(btn, None, focus_map='focus')) + + body_items.append(urwid.Divider()) + cancel_btn = urwid.Button("Cancel (ESC)") + urwid.connect_signal(cancel_btn, 'click', lambda b: self._close_diagram_selector()) + body_items.append(urwid.AttrMap(cancel_btn, None, focus_map='focus')) + + list_box = urwid.ListBox(urwid.SimpleFocusListWalker(body_items)) + box = urwid.LineBox(list_box, title="Diagrams") + + # Create overlay + overlay = urwid.Overlay( + box, + self._saved_body, + align='center', + width=('relative', 60), + valign='middle', + height=('relative', 50), + ) + + self.frame.body = overlay + self.status.set_status("Select diagram | ESC:cancel") + + def _on_diagram_selected(self, diagram_path: str): + """Handle diagram selection.""" + self.frame.body = self._saved_body + self._view_diagram(diagram_path) + + def _close_diagram_selector(self): + """Close the diagram selector.""" + self.frame.body = self._saved_body + self._show_discussion_view() + + def _view_diagram(self, diagram_path: str): + """View a specific diagram.""" + resolved_path = self._resolve_diagram_path(diagram_path) + + # Save current body + self._saved_body = self.frame.body + + def on_close(): + self.frame.body = self._saved_body + self._show_discussion_view() + + viewer = DiagramViewer(resolved_path, on_close) + self.frame.body = viewer + self.status.set_status(f"Viewing: {Path(resolved_path).name} | e:edit | r:refresh | ESC:close") + + def run(self): + """Start the TUI main loop.""" + self.loop = urwid.MainLoop( + self.frame, + palette=PALETTE, + unhandled_input=self._handle_input, + ) + # Set up pipe for thread-safe updates (wakes event loop when written to) + self._pipe_fd = self.loop.watch_pipe(self._process_updates) + self.loop.run() + + +def main(discussions_dir: str = None, open_file: str = None): + """Entry point for TUI. + + Args: + discussions_dir: Directory to browse for discussions + open_file: Specific discussion file to open immediately + """ + path = Path(discussions_dir) if discussions_dir else None + app = DiscussionTUI(path) + + # If a specific file was requested, open it + if open_file: + app._open_discussion(Path(open_file)) + + app.run() diff --git a/src/discussions/ui/widgets.py b/src/discussions/ui/widgets.py new file mode 100644 index 0000000..323bc01 --- /dev/null +++ b/src/discussions/ui/widgets.py @@ -0,0 +1,724 @@ +""" +Custom urwid widgets for Orchestrated Discussions TUI. + +Provides reusable UI components for displaying discussion state: +- CommentBlock: Renders a single participant comment +- VoteSummary: Shows current vote counts and status +- PhaseIndicator: Displays current discussion phase +- DiscussionListItem: List item for discussion browser +- DiagramViewer: Multi-format diagram viewer +""" + +import urwid +from typing import Optional + +from ..voting import ConsensusResult + +# Try to import formats module +try: + from .formats import detect_format, render_to_png, render_to_ascii, get_renderer +except ImportError: + detect_format = None + render_to_png = None + render_to_ascii = None + get_renderer = None + + +# Color palette definitions +PALETTE = [ + # Basic colors + ("title", "white,bold", "dark blue"), + ("header", "white", "dark blue"), + ("footer", "black", "light gray"), + ("body", "white", "black"), + ("divider", "dark gray", "black"), + + # Vote colors + ("vote_ready", "light green,bold", "black"), + ("vote_changes", "yellow,bold", "black"), + ("vote_reject", "light red,bold", "black"), + ("vote_none", "dark gray", "black"), + + # Status colors + ("status_open", "light cyan", "black"), + ("status_ready", "light green", "black"), + ("status_closed", "dark gray", "black"), + + # Phase colors + ("phase_current", "yellow,bold", "black"), + ("phase_completed", "dark green", "black"), + ("phase_pending", "dark gray", "black"), + + # Participant colors + ("author_human", "light cyan,bold", "black"), + ("author_ai", "light magenta", "black"), + ("comment_body", "white", "black"), + ("context", "light gray", "black"), + + # UI elements + ("focus", "white,bold", "dark cyan"), + ("selected", "white", "dark blue"), + ("button", "white", "dark gray"), + ("button_focus", "white,bold", "dark cyan"), + + # Markers + ("question", "light cyan", "black"), + ("concern", "light red", "black"), + ("decision", "light green", "black"), + ("mention", "yellow", "black"), +] + + +def vote_to_style(vote: Optional[str]) -> str: + """Map vote to style name.""" + if not vote: + return "vote_none" + v = vote.upper() + if v == "READY": + return "vote_ready" + elif v == "CHANGES": + return "vote_changes" + elif v == "REJECT": + return "vote_reject" + return "vote_none" + + +class CommentBlock(urwid.WidgetWrap): + """ + Widget for displaying a single comment in a discussion. + + Shows author name, vote badge, read button, and formatted comment body. + """ + + def __init__(self, author: str, body: str, vote: Optional[str] = None, on_read=None, comment_id: int = 0): + self.author = author + self.body = body + self.vote = vote + self.on_read = on_read + self.comment_id = comment_id + self.read_button = None + + # Determine author style (human vs AI) + is_ai = author.lower().startswith(("ai-", "ai_", "bot-", "bot_")) + author_style = "author_ai" if is_ai else "author_human" + + # Build author line with optional vote badge and read button + author_widgets = [ + (len(author) + 2, urwid.AttrMap( + urwid.Text(f" {author} "), + author_style + )) + ] + + if vote: + vote_style = vote_to_style(vote) + vote_text = f" {vote.upper()} " + author_widgets.append( + (len(vote_text), urwid.AttrMap( + urwid.Text(vote_text), + vote_style + )) + ) + + # Add read-aloud button if callback provided + if on_read: + self.read_button = urwid.Button("Read", on_press=lambda b: on_read(body, comment_id, self, author)) + read_btn = urwid.AttrMap(self.read_button, "button", "button_focus") + author_widgets.append((8, read_btn)) + + author_line = urwid.Columns(author_widgets) + + # Format body with marker highlighting + body_text = self._format_body(body) + + # Combine into pile + content = urwid.Pile([ + author_line, + urwid.Divider("-"), + body_text, + urwid.Divider(), + ]) + + # Wrap in a box + box = urwid.LineBox(content) + + super().__init__(box) + + def _format_body(self, body: str) -> urwid.Widget: + """Format comment body with marker highlighting.""" + lines = body.strip().split("\n") + widgets = [] + + for line in lines: + stripped = line.strip() + + # Detect markers and apply styles + if stripped.startswith("Q:"): + widgets.append(urwid.AttrMap(urwid.Text(line), "question")) + elif stripped.startswith("CONCERN:"): + widgets.append(urwid.AttrMap(urwid.Text(line), "concern")) + elif stripped.startswith("DECISION:"): + widgets.append(urwid.AttrMap(urwid.Text(line), "decision")) + elif "@" in line: + # Simple mention highlighting + widgets.append(urwid.AttrMap(urwid.Text(line), "mention")) + else: + widgets.append(urwid.Text(line)) + + return urwid.Pile(widgets) if widgets else urwid.Text("") + + def set_button_label(self, label: str): + """Update the read button label (for Read/Stop toggle).""" + if self.read_button: + self.read_button.set_label(label) + + +class VoteSummary(urwid.WidgetWrap): + """ + Widget showing vote counts and consensus status. + + Displays: + - Vote counts (READY/CHANGES/REJECT) + - Consensus status (reached/not reached) + - Blocking voters if any + """ + + def __init__( + self, + votes: dict[str, str], + consensus: Optional[ConsensusResult] = None + ): + self.votes = votes + self.consensus = consensus + + # Count votes + ready = sum(1 for v in votes.values() if v and v.upper() == "READY") + changes = sum(1 for v in votes.values() if v and v.upper() == "CHANGES") + reject = sum(1 for v in votes.values() if v and v.upper() == "REJECT") + total = ready + changes + reject + + # Build vote count line + vote_widgets = [ + urwid.AttrMap(urwid.Text(f" READY: {ready} "), "vote_ready"), + urwid.Text(" | "), + urwid.AttrMap(urwid.Text(f" CHANGES: {changes} "), "vote_changes"), + urwid.Text(" | "), + urwid.AttrMap(urwid.Text(f" REJECT: {reject} "), "vote_reject"), + ] + + vote_line = urwid.Columns([ + ("pack", w) for w in vote_widgets + ]) + + # Build status line + if consensus: + if consensus.reached: + status_text = f"Consensus: {consensus.outcome}" + status_style = "status_ready" + elif consensus.blocked_by: + status_text = f"Blocked by: {', '.join(consensus.blocked_by)}" + status_style = "vote_reject" + elif consensus.reason: + status_text = consensus.reason + status_style = "status_open" + else: + status_text = "Awaiting votes..." + status_style = "status_open" + else: + status_text = f"Total votes: {total}" + status_style = "status_open" + + status_line = urwid.AttrMap(urwid.Text(status_text), status_style) + + # Per-voter details + voter_lines = [] + for name, vote in sorted(votes.items()): + if vote: + style = vote_to_style(vote) + voter_lines.append( + urwid.AttrMap( + urwid.Text(f" {name}: {vote.upper()}"), + style + ) + ) + + # Combine + content = urwid.Pile([ + urwid.Text("Votes", align="center"), + urwid.Divider("-"), + vote_line, + urwid.Divider(), + status_line, + urwid.Divider(), + ] + voter_lines) + + box = urwid.LineBox(content) + super().__init__(box) + + +class PhaseIndicator(urwid.WidgetWrap): + """ + Widget showing current discussion phase. + + Displays phase name and optionally the phase list with current highlighted. + """ + + def __init__( + self, + current_phase: str, + all_phases: Optional[list[str]] = None, + compact: bool = True + ): + self.current_phase = current_phase + self.all_phases = all_phases or [] + + if compact or not all_phases: + # Just show current phase + content = urwid.AttrMap( + urwid.Text(f" Phase: {current_phase} ", align="center"), + "phase_current" + ) + else: + # Show phase progression + phase_widgets = [] + for i, phase in enumerate(all_phases): + current_idx = ( + all_phases.index(current_phase) + if current_phase in all_phases + else -1 + ) + + if phase == current_phase: + style = "phase_current" + marker = ">" + elif i < current_idx: + style = "phase_completed" + marker = "✓" + else: + style = "phase_pending" + marker = " " + + phase_widgets.append( + urwid.AttrMap( + urwid.Text(f" {marker} {phase}"), + style + ) + ) + + content = urwid.Pile([ + urwid.Text("Phases", align="center"), + urwid.Divider("-"), + ] + phase_widgets) + + super().__init__(content) + + +class DiscussionListItem(urwid.WidgetWrap): + """ + Widget for a single discussion in the browser list. + + Shows title, status badge, and vote summary. + """ + + def __init__( + self, + title: str, + status: str, + phase: str, + vote_summary: str, + path: str + ): + self.title = title + self.status = status + self.path = path + + # Status style + status_lower = status.lower() + if "ready" in status_lower or "approved" in status_lower: + status_style = "status_ready" + elif "closed" in status_lower: + status_style = "status_closed" + else: + status_style = "status_open" + + # Build row + title_w = urwid.Text(title) + status_w = urwid.AttrMap(urwid.Text(f"[{status}]"), status_style) + phase_w = urwid.Text(f"({phase})") + votes_w = urwid.Text(vote_summary, align="right") + + columns = urwid.Columns([ + ("weight", 3, title_w), + ("pack", status_w), + ("weight", 1, phase_w), + ("weight", 1, votes_w), + ], dividechars=1) + + self._selectable_columns = urwid.AttrMap( + columns, + None, + focus_map="focus" + ) + + super().__init__(self._selectable_columns) + + def selectable(self): + return True + + def keypress(self, size, key): + return key + + +class CommentInput(urwid.WidgetWrap): + """ + Widget for human comment input with vote selection. + """ + + def __init__(self, on_submit=None, on_dictate=None, on_artifact=None): + self.on_submit = on_submit + self.on_dictate = on_dictate + self.on_artifact = on_artifact + self.current_vote = None + + # Comment text edit + self.comment_edit = urwid.Edit( + caption="Comment: ", + multiline=True + ) + + # Dictate button + self.dictate_btn = urwid.Button("Dictate", on_press=self._dictate) + + # Artifact button + self.artifact_btn = urwid.Button("Artifact", on_press=self._artifact) + + # Vote buttons + self.vote_buttons = urwid.Columns([ + urwid.AttrMap( + urwid.Button("READY", on_press=lambda b: self._set_vote("READY")), + "button", "button_focus" + ), + urwid.AttrMap( + urwid.Button("CHANGES", on_press=lambda b: self._set_vote("CHANGES")), + "button", "button_focus" + ), + urwid.AttrMap( + urwid.Button("REJECT", on_press=lambda b: self._set_vote("REJECT")), + "button", "button_focus" + ), + urwid.AttrMap( + urwid.Button("No Vote", on_press=lambda b: self._set_vote(None)), + "button", "button_focus" + ), + ]) + + # Vote status display + self.vote_display = urwid.Text("Selected vote: None") + + # Submit button + self.submit_btn = urwid.AttrMap( + urwid.Button("Submit Comment", on_press=self._submit), + "button", "button_focus" + ) + + content = urwid.Pile([ + urwid.Text("Add Your Comment"), + urwid.Divider("-"), + urwid.Columns([ + urwid.AttrMap(self.dictate_btn, "button", "button_focus"), + urwid.AttrMap(self.artifact_btn, "button", "button_focus"), + ('weight', 2, urwid.Text("")), # Spacer + ]), + urwid.BoxAdapter(urwid.Filler(self.comment_edit, valign="top"), 5), + urwid.Divider(), + self.vote_display, + self.vote_buttons, + urwid.Divider(), + self.submit_btn, + ]) + + box = urwid.LineBox(content) + super().__init__(box) + + def _dictate(self, button): + """Handle dictate button press.""" + if self.on_dictate: + self.on_dictate(self) + + def _artifact(self, button): + """Handle artifact button press.""" + if self.on_artifact: + self.on_artifact(self) + + def set_dictate_label(self, label: str): + """Update the dictate button label.""" + self.dictate_btn.set_label(label) + + def append_text(self, text: str): + """Append text to the comment edit field.""" + current = self.comment_edit.get_edit_text() + separator = " " if current.strip() else "" + self.comment_edit.set_edit_text(current + separator + text) + + def _set_vote(self, vote: Optional[str]): + self.current_vote = vote + display = vote if vote else "None" + self.vote_display.set_text(f"Selected vote: {display}") + + def _submit(self, button): + if self.on_submit: + text = self.comment_edit.get_edit_text() + self.on_submit(text, self.current_vote) + + def clear(self): + """Clear the input fields.""" + self.comment_edit.set_edit_text("") + self._set_vote(None) + + +class StatusBar(urwid.WidgetWrap): + """ + Bottom status bar showing current state and available commands. + """ + + def __init__(self, text: str = ""): + self.status_text = urwid.Text(text) + self.help_text = urwid.Text( + "q:quit | r:refresh | c:comment | t:turn | h:help", + align="right" + ) + + columns = urwid.Columns([ + self.status_text, + self.help_text, + ]) + + bar = urwid.AttrMap(columns, "footer") + super().__init__(bar) + + def set_status(self, text: str): + self.status_text.set_text(text) + + +class HeaderBar(urwid.WidgetWrap): + """ + Top header bar showing discussion title and info. + """ + + def __init__(self, title: str = "Orchestrated Discussions"): + self.title_text = urwid.Text(f" {title} ", align="center") + bar = urwid.AttrMap(self.title_text, "header") + super().__init__(bar) + + def set_title(self, title: str): + self.title_text.set_text(f" {title} ") + + +class DiagramViewer(urwid.WidgetWrap): + """ + Split-view widget for viewing diagrams in multiple formats. + + Supported formats: + - PlantUML (.puml, .plantuml) - native ASCII or chafa + - Mermaid (.mmd, .mermaid) - via chafa + - Graphviz/DOT (.dot, .gv) - via chafa + - OpenSCAD (.scad) - via chafa + - SVG (.svg) - via chafa + - ASCII (.txt, .ascii) - displayed directly + + Shows: + - Left: Source code + - Right: ASCII art preview + + Keyboard: + - 'e': Open in external editor + - 'r': Re-render preview + - 'ESC': Close viewer + """ + + def __init__(self, diagram_path: str, on_close: callable): + import subprocess + import shutil + from pathlib import Path + + self.diagram_path = Path(diagram_path) + self.on_close = on_close + + # Detect format + self.format = detect_format(str(self.diagram_path)) if detect_format else None + if not self.format and str(self.diagram_path).endswith('.puml'): + self.format = 'plantuml' # Legacy default + + # Load source + if self.diagram_path.exists(): + self.source = self.diagram_path.read_text() + else: + self.source = f"(File not found: {diagram_path})" + + # Source panel (left) + source_text = urwid.Text(self.source) + source_list = urwid.ListBox(urwid.SimpleFocusListWalker([source_text])) + format_label = f"Source ({self.format or 'unknown'})" + source_box = urwid.LineBox(source_list, title=format_label) + + # Render preview (right) + preview_text = self._render_preview() + self.preview_widget = urwid.Text(preview_text) + preview_list = urwid.ListBox(urwid.SimpleFocusListWalker([self.preview_widget])) + preview_box = urwid.LineBox(preview_list, title="Preview") + + # Split view + columns = urwid.Columns([ + ("weight", 1, source_box), + ("weight", 1, preview_box), + ]) + + # Header and footer + header = urwid.AttrMap( + urwid.Text(f" Diagram: {self.diagram_path.name} [{self.format or '?'}] ", align="center"), + "header" + ) + footer = urwid.AttrMap( + urwid.Text(" e:edit | r:refresh | ESC:close "), + "footer" + ) + + frame = urwid.Frame(columns, header=header, footer=footer) + super().__init__(frame) + + # For backward compatibility + self.puml_path = self.diagram_path + + def _render_preview(self) -> str: + """Render diagram to ASCII art. Supports multiple formats.""" + import subprocess + import shutil + import tempfile + from pathlib import Path + + if not self.diagram_path.exists(): + return "(File not found)" + + # ASCII format: display directly + if self.format == 'ascii': + return self.source + + # Try using the formats module first + if render_to_ascii: + success, result = render_to_ascii(str(self.diagram_path)) + if success: + return result + # Fall through to legacy methods + + # Legacy PlantUML handling + chafa_path = shutil.which("chafa") + plantuml_path = shutil.which("plantuml") + + # For PlantUML, try native ASCII first + if self.format == 'plantuml' and plantuml_path: + try: + # Filter out problematic directives + filtered_lines = [ + line for line in self.source.split('\n') + if not line.strip().startswith('!theme') + ] + filtered_source = '\n'.join(filtered_lines) + + result = subprocess.run( + ["plantuml", "-tutxt", "-pipe"], + input=filtered_source, + capture_output=True, + text=True, + ) + if result.returncode == 0: + return result.stdout + except Exception: + pass # Fall through to PNG + chafa + + # For all formats: try PNG render + chafa + if chafa_path: + try: + with tempfile.TemporaryDirectory() as tmpdir: + png_path = Path(tmpdir) / "diagram.png" + + # Use formats module if available + if render_to_png: + success, png_result = render_to_png( + str(self.diagram_path), + str(png_path) + ) + if success and Path(png_result).exists(): + chafa_result = subprocess.run( + ["chafa", "--size=80x40", png_result], + capture_output=True, + text=True, + ) + if chafa_result.returncode == 0: + return chafa_result.stdout + + # Legacy PlantUML pipe method + elif self.format == 'plantuml' and plantuml_path: + filtered_lines = [ + line for line in self.source.split('\n') + if not line.strip().startswith('!theme') + ] + filtered_source = '\n'.join(filtered_lines) + + result = subprocess.run( + ["plantuml", "-tpng", "-pipe"], + input=filtered_source, + capture_output=True, + text=False, + ) + if result.returncode == 0: + png_path.write_bytes(result.stdout) + chafa_result = subprocess.run( + ["chafa", "--size=80x40", str(png_path)], + capture_output=True, + text=True, + ) + if chafa_result.returncode == 0: + return chafa_result.stdout + except Exception as e: + return f"(Render error: {e})" + + # Format-specific error messages + renderer = get_renderer(self.format) if get_renderer and self.format else None + if renderer and not renderer.available: + return f"(Renderer not found for {self.format})\n\nInstall with:\n{renderer.install_hint}" + + if not chafa_path and self.format != 'plantuml': + return "(chafa not found - install with: apt install chafa)" + + if self.format == 'plantuml' and not plantuml_path: + return "(plantuml not found - install with: apt install plantuml)" + + return f"(Cannot render {self.format or 'unknown'} format)" + + def keypress(self, size, key): + if key == "esc": + self.on_close() + return None + elif key == "e": + self._open_editor() + return None + elif key == "r": + self._refresh_preview() + return None + return super().keypress(size, key) + + def _open_editor(self): + """Open diagram in external editor.""" + import subprocess + import os + editor = os.environ.get("EDITOR", "nano") + subprocess.call([editor, str(self.puml_path)]) + # Reload source after editing + if self.puml_path.exists(): + self.source = self.puml_path.read_text() + + def _refresh_preview(self): + """Re-render the preview (call after editing).""" + # This would need to update the widget - for now just reload source + if self.puml_path.exists(): + self.source = self.puml_path.read_text() diff --git a/src/discussions/voting.py b/src/discussions/voting.py index 751c67c..c70357e 100644 --- a/src/discussions/voting.py +++ b/src/discussions/voting.py @@ -1,7 +1,13 @@ """ -Voting and consensus logic for Orchestrated Discussions. +Voting data structures and display helpers for Orchestrated Discussions. + +NOTE: This module provides convenience functions for quick local display +(CLI status, TUI widgets). The authoritative implementation lives in the +`discussion-vote-counter` SmartTool, which is used by the runner pipeline. + +These implementations should stay in sync. If voting logic changes, +update BOTH this file AND smarttools/discussion-vote-counter/config.yaml. -Handles vote counting, threshold checking, and consensus determination. See docs/DESIGN.md for voting rules specification. """ diff --git a/stand-alone-artifact-editor---ai-enhanced-diagram-and-3d-model-creator.discussion.md b/stand-alone-artifact-editor---ai-enhanced-diagram-and-3d-model-creator.discussion.md new file mode 100644 index 0000000..e7bc9f3 --- /dev/null +++ b/stand-alone-artifact-editor---ai-enhanced-diagram-and-3d-model-creator.discussion.md @@ -0,0 +1,872 @@ +<!-- DISCUSSION --> +<!-- Title: Stand Alone Artifact Editor - AI-Enhanced Diagram and 3D Model Creator --> +<!-- Phase: initial_feedback --> +<!-- Status: OPEN --> +<!-- Created: 2025-12-19T12:23:18Z --> +<!-- Template: brainstorm --> +<!-- Participants: architect, pragmatist, designer, moderator, performance, ux --> + +# Stand Alone Artifact Editor - AI-Enhanced Diagram and 3D Model Creator + +## Context + +We want to build a standalone **Artifact Editor** that can be launched from any application to create visual artifacts like diagrams, sketches, and 3D models. + +### Why We're Building This + +I have been designing an **orchestrated-discussions** application that enables users to collaborate with a team of AI developers to design software. I want users to be able to click a button to add an artifact, choose the type of artifact they want, and then create it visually. + +### How It Should Work + +**Multiple Input Methods:** +- **Graphical interface** - Draw directly using standard UI tools +- **Text/code editing** - Write Mermaid, PlantUML, OpenSCAD code directly +- **Voice commands** - Click a dictate button and say things like: + - "Place a circle in the center of the screen with a square on the right" + - "Make the circle bigger" + - "Add an arrow from the circle to the square" + +The AI would interpret these commands and generate or modify the artifact accordingly. This same interaction pattern applies to UML diagrams, 3D models, flowcharts, etc. + +### Architecture Philosophy + +The orchestrated-discussions project uses another project I developed called **SmartTools**. This allows me to create standalone Linux commands that are AI-enabled, following a Unix philosophy approach. This keeps everything modular, composable, and stable. + +Much of the functionality this app needs already exists as SmartTools: +- `tts` - Text-to-speech for reading content aloud +- `dictate` - Speech-to-text for voice input +- `transcribe` - Audio file transcription +- Various discussion participant tools + +The reason I'm making this a separate project is that it may be useful on its own, but I want it to integrate seamlessly with orchestrated-discussions. + +### Code-Based Artifacts + +I want the artifacts produced to be **generated by code** (Mermaid, PlantUML, OpenSCAD, SVG) rather than binary formats. This is intentional because: +1. AI can understand and modify code more easily than binary formats +2. Code-based artifacts are version-controllable +3. They can be regenerated, parameterized, and templated + +### Related Projects + +To understand the ecosystem, explore these projects: +- `~/PycharmProjects/orchestrated-discussions` - The parent discussion application +- `~/PycharmProjects/SmartTools` - The AI-enabled CLI tool framework +- `~/PycharmProjects/artifact-editor` - This project (scaffold exists) + +## Key Requirements + +1. **Standalone executable** - Can be launched via command line with arguments +2. **Multiple artifact types** - Mermaid, PlantUML, OpenSCAD/SolidPython, SVG, ASCII art +3. **AI assistance** - Voice input, conversational editing, auto-generation +4. **Integration contract** - Simple stdin/stdout/exit-code interface for parent apps +5. **Live preview** - See rendered output as you edit code + +## Integration Target + +Primary integration with **orchestrated-discussions** project: +- User clicks 'Add Artifact' in comment dialog +- Launches artifact-editor with output path argument +- User creates/edits artifact +- On save, outputs 'ARTIFACT_SAVED:/path/to/file' +- Parent app embeds artifact reference in comment + +## Questions to Explore + +1. **UI Framework** - The discussion project uses Dear PyGui. Can we use the same framework, or do we need something more capable for graphical editing (drawing shapes, drag-and-drop)? + +2. **AI Assistance** - I want SmartTools integration. Should we create new SmartTools like `artifact-generate`, `artifact-modify`, or integrate AI directly into the editor? + +3. **3D Model Preview** - OpenSCAD is slow to render. How do we provide responsive preview without blocking the UI? + +4. **Drawing vs Code** - For sketches/diagrams, do we need a full drawing canvas, or can we get away with AI-generated code from voice/text commands? + +## Project Scaffold + +Initial scaffold already created at `~/PycharmProjects/artifact-editor` with: +- `pyproject.toml` - Package configuration +- `src/artifact_editor/cli.py` - CLI entry point with `--type`, `--output`, `--headless` flags +- `src/artifact_editor/renderers/__init__.py` - Base `Renderer` class and registry +- `src/artifact_editor/renderers/mermaid.py` - Mermaid diagram renderer (calls `mmdc`) +- `src/artifact_editor/renderers/openscad.py` - OpenSCAD/SolidPython renderer + +## MVP Recommendation + +Based on previous discussion feedback, consider starting with: +1. **Single format first** - Mermaid or PlantUML (most common in discussions) +2. **Text editor + AI generation** - Skip graphical drawing for v1 +3. **Voice input via existing SmartTools** - Use `dictate` tool we already have +4. **Simple preview** - Render on save, not live preview initially + +--- + +*Discussion begins below.* + +--- + +Name: AI-Architect +## Architectural Assessment + +This is a well-conceived project that follows sound architectural principles. Let me highlight key strengths and raise some concerns. + +### Strengths + +**1. Code-First Artifact Philosophy** +The decision to use code-based formats (Mermaid, PlantUML, OpenSCAD) rather than binary is architecturally excellent. This enables: +- AI comprehension and modification +- Version control friendliness +- Deterministic regeneration +- Text-based diffing in discussions + +**2. Unix Philosophy Alignment** +Leveraging SmartTools for composability is the right call. The stdin/stdout/exit-code contract keeps the integration surface minimal and well-defined. + +**3. Clean Separation** +Making this standalone rather than embedding it in orchestrated-discussions is wise. It follows the Single Responsibility Principle and enables reuse. + +### Architectural Concerns + +**CONCERN: UI Framework Decision** + +Dear PyGui is excellent for data visualization dashboards but has limitations for: +- Complex graphical editing (node-based diagram manipulation) +- Custom drawing primitives +- Rich text editing with syntax highlighting + +For v1 (text + AI generation), Dear PyGui is adequate. For v2 (graphical editing), consider: +- PyQt/PySide6 (mature, extensive widget library) +- Custom web view with Electron (diagram editors like draw.io exist) + +**CONCERN: AI Integration Architecture** + +Q: Should AI be SmartTools or embedded? + +Recommendation: **SmartTools exclusively**. Create: +- `artifact-generate` - Natural language → code +- `artifact-modify` - Existing code + instruction → modified code +- `artifact-describe` - Code → natural language description + +This maintains composability and allows using these tools outside the editor. + +**CONCERN: Render Process Isolation** + +OpenSCAD and even Mermaid CLI can hang, crash, or consume excessive memory. The renderer architecture should: +1. Run renderers in subprocess (not just function calls) +2. Implement timeouts (5-10s default, configurable) +3. Cache rendered output with content hash +4. Support cancellation for long renders + +**CONCERN: State Management** + +The editor will need to track: +- Current code state +- Render state (pending/success/error) +- Undo/redo history +- Dirty flag for unsave detection + +Recommend a clean state machine rather than scattered flags. + +### Questions + +Q: What's the expected artifact size? PlantUML diagrams can grow large—will we need pagination or zoom? + +Q: Will artifacts ever reference external files (imports in OpenSCAD, includes in PlantUML)? This complicates the "single file" model. + +### Recommended Architecture + +ARTIFACT: diagrams/artifact-editor-architecture.puml + +VOTE: CHANGES + +I'm voting CHANGES because we need clarity on: +1. UI framework decision for v2 roadmap +2. Confirmation that AI will be SmartTools-only +3. Renderer isolation strategy +VOTE: CHANGES + +--- + +Name: AI-Pragmatist +CONCERN: COMPLEXITY - This is the same over-engineering pattern from the previous discussion. The new proposal adds even more features: graphical interface, voice commands, conversational editing, live preview, 3D models. This is 9+ months of work. + +We already solved this in the previous discussion: MVP = text editor + save contract for ONE format. The scaffold exists, we just need to ship it. + +Key insight: The orchestrated-discussions project already has a diagram-editor SmartTool. Why not extend that instead of building a standalone editor? Use existing `discussion-diagram-editor` as the AI component, add simple TUI for editing. + +Simplest path: 1) Extend discussion-diagram-editor to accept `--edit` flag with file path, 2) Add urwid TUI wrapper that calls it, 3) Output ARTIFACT_SAVED path. Done in days, not months. + +DIAGRAM: diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_1.puml + +DIAGRAM: diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_1.puml +VOTE: CHANGES + +--- + +Name: AI-Moderator +## Discussion Status: Awaiting Initial Feedback + +This discussion has just opened for brainstorming a **standalone Artifact Editor** - an AI-enhanced tool for creating diagrams and 3D models with voice/text/graphical input. + +### Pending Responses + +Waiting for initial feedback from: +- @architect - Please analyze the proposed architecture and integration with SmartTools +- @security - Please review security implications of AI-assisted code generation and external tool integrations +- @pragmatist - Please assess the MVP scope and implementation priorities + +### Key Questions to Address + +1. **UI Framework** - Dear PyGui (used by parent project) vs. more capable graphical editing framework +2. **AI Integration** - New SmartTools vs. direct integration in editor +3. **3D Preview** - How to handle slow OpenSCAD rendering without blocking UI +4. **Drawing vs Code** - Need for full drawing canvas vs. AI-generated code approach + +### Context Highlights + +- **Code-first philosophy**: Artifacts should be generated as code (Mermaid, PlantUML, OpenSCAD) not binaries +- **SmartTools ecosystem**: Leverages existing tools (`dictate`, `tts`, `transcribe`) +- **Integration target**: Primary use in orchestrated-discussions via stdin/stdout contract +- **Scaffold exists**: Basic project structure already in place at `~/PycharmProjects/artifact-editor` + +### Next Steps + +Once all participants provide initial feedback, we can identify consensus areas and concerns to guide the brainstorming forward. + +*Phase Goal: Facilitate the discussion by gathering diverse perspectives* + +--- + +Name: AI-Architect +## Architectural Follow-up + +I've already provided my initial architectural assessment above. Let me add some additional thoughts based on AI-Pragmatist's feedback. + +### Response to Pragmatist's Concerns + +The pragmatist raises a valid point about leveraging existing `discussion-diagram-editor`. However, I want to clarify the architectural distinction: + +**Extending existing SmartTool vs. Standalone Editor** + +These serve different purposes: +- `discussion-diagram-editor` is a **SmartTool** (stateless, stdin/stdout, AI-driven transformation) +- Artifact Editor is an **application** (stateful, interactive, user-facing) + +The Unix philosophy says tools should be composable - the editor should *use* the SmartTool, not *become* the SmartTool. + +### Refined Architecture Recommendation + +Based on the discussion so far: + +``` +artifact-editor (TUI/GUI application) + │ uses subprocess + ├── discussion-diagram-editor (AI generation/modification) + ├── dictate (voice input) + └── renderer subprocess (mermaid, plantuml, openscad) +``` + +### Agreement with Pragmatist + +I do agree with limiting MVP scope: +1. **Single format** - PlantUML (more expressive than Mermaid for architecture) +2. **Text editor** - urwid TUI, no graphical canvas +3. **AI via SmartTools** - Call existing tools, don't embed +4. **Render on save** - Not live preview + +### Outstanding Architectural Questions + +Q: @pragmatist - The `discussion-diagram-editor` SmartTool - does it currently support editing existing files, or only generation? If generation-only, we need `artifact-modify` SmartTool for iterative editing. + +Q: What's the file ownership model? Does the artifact-editor create files, or does the parent app create them and pass the path? + +### State Machine Proposal + +For the editor application, I recommend this state model: + +ARTIFACT: diagrams/artifact-editor-state-machine.puml + +This keeps the application behavior predictable and testable. + +VOTE: CHANGES +VOTE: CHANGES + +--- + +Name: AI-Pragmatist +CONCERN: COMPLEXITY - We're repeating the same mistake. The scaffold already exists at ~/PycharmProjects/artifact-editor with CLI, renderers, and pyproject.toml. We don't need a new standalone project. + +Key insight: The orchestrated-discussions TUI already has a diagram viewer (widgets.py:497) and dictation support (tui.py:531). We can extend the existing TUI with an 'Add Artifact' button that launches a simple text editor. + +Simplest path to shipping: +1. Add 'Add Artifact' button to orchestrated-discussions TUI comment dialog +2. Launch `nano` or `vim` with the output file path +3. On save, output ARTIFACT_SAVED path +4. Embed reference in comment + +No new UI framework, no renderers, no AI integration needed for v1. Use existing discussion-diagram-editor SmartTool for AI modifications later. + +DIAGRAM: diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_2.puml + +DIAGRAM: diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_2.puml +VOTE: CHANGES + +--- + +Name: AI-Designer +## Design Analysis: Artifact Editor UI/UX\n\nI'm reviewing this from a **visual design and user experience perspective**, considering the integration context with orchestrated-discussions.\n\n### Key Design Observations\n\n**Context Understanding:**\nThe orchestrated-discussions project uses two parallel UI paradigms:\n- **GUI (Dear PyGui)**: For visual users who want native rendering\n- **TUI (urwid)**: For terminal-native users with keyboard-driven workflows\n\nThis dual-interface approach reveals the user base: developers who work across both graphical and terminal environments. The artifact editor should honor this pattern.\n\n### Design Concerns\n\n**1. CONSISTENCY: Modal vs. Integrated**\nLaunching artifact-editor as a separate application creates a visual/interaction discontinuity:\n- User is in orchestrated-discussions GUI → clicks \"Add Artifact\" → launches completely different window/interface\n- Lost visual context and workflow continuity\n- Different styling, keyboard shortcuts, interaction patterns\n\n**Design principle violated:** Consistency builds trust and reduces cognitive load.\n\nBetter approach: Embed as a modal/dialog within existing UI rather than separate process (for v1 at least).\n\n**2. VISUAL HIERARCHY: Too Many Input Methods**\nThe proposal lists three input methods (graphical, text/code, voice) but doesn't establish priority:\n- What's the default path for users unfamiliar with code?\n- What's the power-user path?\n- Do all three methods surface equally in the UI, creating choice paralysis?\n\nRecommendation: Visual hierarchy should guide users to the optimal path:\n1. **Primary (largest, centered)**: Text editor for code-based editing\n2. **Secondary (side panel)**: Voice input button with clear affordance\n3. **Tertiary**: AI generation helpers if drawing is really needed\n\n**3. PREVIEW PLACEMENT: Real Estate Trade-off**\nThe proposal mentions \"live preview\" but doesn't address layout. Critical design question:\n- **Split view** (code left, preview right)? → Less screen space for code editing\n- **Tabs** (Code/Preview tabs)? → Context switching on every change\n- **Overlay** (preview on hover/click)? → Hidden content, discovery problem\n\nSuggestion: Start with **split view 50/50** for v1. Most code editors establish this pattern, reducing cognitive load.\n\n**4. ERROR PRESENTATION: Rendering Failures**\nWhen OpenSCAD hangs or Mermaid syntax is invalid:\n- How are errors presented? Red box? Inline? Modal?\n- Can users see what's broken while editing?\n- Is error messaging clear for non-technical users?\n\nDesign principle: Show errors **inline** where they occur (syntax highlighting, red underlines) not modal pop-ups.\n\n**5. VOICE INPUT UX: Discoverability**\nA voice button alone isn't enough visual guidance:\n- Icon + label (e.g., \"🎤 Say It\" vs just a microphone)\n- Visual feedback while listening (animated waveform?)\n- Transcript preview so user sees what was heard\n- Clear cancel/redo affordance\n\nDesign principle: Affordance + feedback prevents user confusion.\n\n### Positive Design Decisions\n\n✓ **Code-first approach** aligns with the project's philosophy and improves debuggability\n✓ **Integration via simple contract** (stdin/stdout) keeps concerns separated\n✓ **Text editor as primary** is familiar to the target audience\n\n### Diagram Reference\nI've created a wireframe showing recommended v1 layout:\n\nDIAGRAM: diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_designer_2.puml\n\n### Recommendation\n\nFor v1, focus on **consistency and clarity** over feature breadth:\n1. Match orchestrated-discussions visual language (colors, spacing, typography)\n2. Establish clear visual hierarchy: Code editor dominant, preview secondary, tools tertiary\n3. Implement inline error feedback, not modals\n4. Make voice input discoverable with icon + label + feedback affordances\n5. Use split-view layout to avoid tab switching\n\nThis creates a coherent experience that feels like part of the orchestrated-discussions ecosystem rather than a foreign tool. + +DIAGRAM: diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_designer_2.puml +VOTE: CHANGES + +--- + +Name: AI-Moderator +## Discussion Summary + +We have received strong initial feedback from two participants with contrasting perspectives: + +**AI-Architect** provided thorough architectural analysis: +- ✅ Endorsed code-first approach and Unix philosophy alignment +- ❓ Raised concerns about UI framework choice (Dear PyGui limitations for graphical editing) +- ❓ Questioned AI integration strategy (SmartTools vs embedded) +- ❓ Highlighted need for render process isolation and state management +- 📊 Created architecture diagram: `diagrams/artifact-editor-architecture.puml` +- **Vote: CHANGES** - needs clarity on UI framework, AI integration, and renderer isolation + +**AI-Pragmatist** warned against scope creep: +- ⚠️ **CONCERN: COMPLEXITY** - Pattern of over-engineering, 9+ months of work +- 💡 Proposed leveraging existing `discussion-diagram-editor` SmartTool +- 🎯 Suggested minimal path: Extend existing tool + add TUI wrapper +- 📊 Created MVP diagram: `diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_1.puml` +- **Vote: CHANGES** - advocates for dramatically simplified approach + +### Still Pending + +@security - We need your assessment of: +- Security implications of AI-assisted code generation +- External tool integrations (mmdc, plantuml, openscad) +- Subprocess execution risks +- Input validation for voice/text commands + +@designer - Your input needed on: +- UI/UX approach for artifact creation +- Balance between code editing and graphical manipulation +- User workflows for different artifact types + +@performance - Please evaluate: +- OpenSCAD rendering performance concerns +- Preview responsiveness strategies +- Caching and optimization approaches + +@ux - Your perspective on: +- Multi-modal input (voice, text, graphical) +- User workflows and interaction patterns +- Integration with parent application + +### Key Tension + +There's a fundamental disagreement between **ambitious vision** (graphical editor, voice commands, 3D models) and **pragmatic MVP** (extend existing SmartTool). This needs resolution before detailed planning. + +### Open Questions + +1. Should AI be SmartTools or embedded? (Architect recommends SmartTools) +2. What's the expected artifact size and complexity? +3. Will artifacts reference external files? +4. Should we build new standalone editor or extend existing `discussion-diagram-editor`? + +### Recommendation + +**Do not advance phase yet.** We need: +1. Remaining participant feedback (4 of 6 participants pending) +2. Resolution of the scope disagreement +3. Decision on whether to build standalone or extend existing tool + +This is healthy early-stage debate that will prevent costly mistakes later. + +--- + +Name: AI-Ux +The proposal for the Stand Alone Artifact Editor presents a compelling vision for AI-enhanced artifact creation. From a user experience perspective, the core idea of code-based artifacts, AI assistance, and seamless integration with `orchestrated-discussions` is strong. However, several key areas need careful consideration to ensure an intuitive, accessible, and delightful user experience.\n\n**UX Checklist Assessment:**\n\n* **User Flow Clarity:** The high-level flow (Add Artifact -> Editor -> Save) is clear. However, the internal flow within the editor, especially switching between input methods (text, voice, and eventually graphical), needs to be exceptionally intuitive. Onboarding for voice commands and AI interaction will be critical. Users need clear visual cues for when the AI is listening, processing, and when it has made changes.\n\n* **Error Handling and Recovery:** This is a major concern. What happens when AI generates invalid code? How are rendering errors (e.g., OpenSCAD timeouts, syntax errors) presented to the user? The system must provide actionable feedback, not just cryptic error messages. Undo/redo functionality, especially after AI modifications, is paramount for user confidence.\n\n* **Loading States and Feedback:** For AI generation and particularly for slow renderers like OpenSCAD, clear and continuous feedback is essential. A simple 'Rendering...' message is a minimum; progress bars or animated indicators would be better. Lack of feedback leads to user frustration and perceived slowness.\n\n* **Keyboard Navigation & Screen Reader Compatibility:** If a GUI is used, standard accessibility features like keyboard navigation, focus management, and screen reader support must be built in from the start. This includes the text editor, preview panel, and all interactive elements.\n\n* **Onboarding and Discoverability:** How will users discover the capabilities of the AI assistant and the specific syntax for different artifact types? In-app help, tooltips, and clear examples will be necessary. The voice command interface needs to be discoverable and its capabilities clearly communicated.\n\n* **Cognitive Load Assessment:** The combination of multiple input methods, AI interaction, and code-based editing can be cognitively demanding. The UI should strive for simplicity, consistency, and clear separation of concerns to minimize this load.\n\n**Specific UX Feedback:**\n\n1. **UI Framework (Dear PyGui vs. others):** The AI-Architect's concern is valid. While Dear PyGui might suffice for a text-only MVP, if the long-term vision includes graphical editing, investing in a more capable framework (e.g., PyQt/PySide6, or even a web-based solution) early on would prevent a costly and disruptive UI overhaul later. Users expect modern graphical interaction for "drawing directly."\n\n2. **AI Integration (SmartTools vs. embedded):** While using SmartTools for modularity is architecturally sound, from a UX perspective, the AI assistance must feel seamlessly integrated into the editor. The user should not perceive they are interacting with separate command-line tools; it should feel like a natural extension of the editor's capabilities.\n\n3. **Render Process Isolation & 3D Model Preview:** This is a critical UX point. Slow or crashing renderers will severely degrade the user experience. The AI-Architect's recommendations (subprocesses, timeouts, caching, cancellation) are essential for a responsive and stable preview. Users need to see their changes reflected quickly, or at least understand why there's a delay.\n\n4. **Drawing vs Code (MVP):** The MVP's decision to skip graphical drawing initially is pragmatic. However, it means the "Graphical interface - Draw directly" feature is deferred. The AI-generated code from voice/text commands must be highly accurate and easily verifiable by the user, as this will be the primary means of visual creation in v1.\n\n**Recommendation:** Prioritize robust error handling, clear loading states, and intuitive feedback mechanisms for AI and rendering processes in the MVP. Plan for a UI framework that can scale to the full vision, even if only a subset is used initially.\n\nDIAGRAM: diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_ux_2.puml + +DIAGRAM: diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_ux_2.puml +VOTE: CHANGES + +--- + +Name: Human (Rob) +## Time for Decisions + +We've heard from everyone and the feedback is useful, but we're circling without converging. Everyone voted CHANGES but for different reasons. Let's make some concrete decisions now. + +**Decision 1: Standalone vs Extend Existing** + +I'm deciding: **Standalone project**. The scaffold already exists at `~/PycharmProjects/artifact-editor`. The orchestrated-discussions project should remain focused on discussions, not artifact editing. The Unix philosophy is correct here - separate tools that compose. + +**Decision 2: MVP Scope** + +The MVP is: +- **One format**: PlantUML (more expressive than Mermaid, already have renderer) +- **Text editor and graphical canvas** This feature development is the point of this colaboration. +- **AI via SmartTools**: Use existing `discussion-diagram-editor` or create `artifact-generate` +- Live preview shouldn't be any harder then a render on save approach, we could have a submit button or a timer interval for updates +- **Simple integration**: `artifact-editor --output path.puml` → outputs `ARTIFACT_SAVED:path.puml` + +**Decision 3: UI Framework** + +For GUI frameworks graphical editing is necessary if we can use the same gui as the orchestrated-discussions project that would be great if we need more let me know. + +**Questions for Next Turn** + +Now I need focused answers, not more analysis: + +1. @architect - Given these decisions, what's the minimal state machine for the editor? (Not a full architecture - just: states and transitions) + +2. @pragmatist - The existing `discussion-diagram-editor` SmartTool - can it accept an existing file and modify it, or only generate new? One sentence answer. + +3. @performance - For PlantUML specifically, what's a reasonable render timeout? Just give me a number. + +4. @designer @ux - You both covered similar ground. Pick ONE most important UX requirement for the MVP text editor. One sentence each. + +Let's ship something. + +VOTE: READY + +--- + +Name: AI-Architect +## Minimal State Machine for MVP Editor + +Per Rob's request - here's the minimal state machine, no over-engineering: + +``` +STATES: + EDITING - User is modifying code in text area + RENDERING - PlantUML subprocess running (show spinner) + PREVIEW - Render complete, showing result + ERROR - Render failed, showing error inline + SAVED - File written, ready to output contract + +TRANSITIONS: + EDITING → RENDERING (timer tick or manual trigger) + RENDERING → PREVIEW (subprocess success) + RENDERING → ERROR (subprocess fail/timeout) + ERROR → EDITING (user edits code) + PREVIEW → EDITING (user edits code) + EDITING → SAVED (user clicks Save) + SAVED → [exit] (output ARTIFACT_SAVED:path) +``` + +That's 5 states, 7 transitions. The dirty flag is implicit in EDITING state. + +### On UI Framework + +Dear PyGui can handle this MVP. For the graphical canvas requirement (drag shapes, connect nodes), Dear PyGui has `draw_*` primitives (draw_circle, draw_rectangle, draw_line) on a drawlist. It's not as rich as Qt's QGraphicsScene but it's workable for basic shape placement. + +If we hit walls with Dear PyGui's drawing capabilities, the fallback is PyQt6 - but let's try Dear PyGui first since orchestrated-discussions already uses it. + +### Integration Contract + +```bash +# Launch +artifact-editor --type plantuml --output diagrams/feature.puml + +# On save, outputs to stdout: +ARTIFACT_SAVED:diagrams/feature.puml + +# Exit codes: +# 0 = saved successfully +# 1 = user cancelled +# 2 = error +``` + +This is the full contract. Parent app reads stdout, checks exit code, done. + +VOTE: READY +VOTE: READY + +--- + +Name: AI-Pragmatist +CONCERN: COMPLEXITY - The decision to include 'graphical canvas' in MVP contradicts the entire pragmatic approach. This is scope creep disguised as a decision. The existing scaffold has renderers, not drawing tools. Adding graphical canvas means: 1) New UI framework decision (Dear PyGui can't do drawing), 2) Canvas widget implementation, 3) Shape drawing logic, 4) Export to PlantUML conversion. That's 2-3 months of work. + +Simplest path: Use existing discussion-diagram-editor SmartTool (it accepts stdin, modifies existing diagrams). Launch it with current file content, get modified PlantUML back. No canvas needed. + +Answer to your question: discussion-diagram-editor accepts stdin (existing diagram) and modifies it via --instruction. + +DIAGRAM: diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_5.puml + +DIAGRAM: diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_5.puml +VOTE: CHANGES + +--- + +Name: AI-Designer +Rob has made decisive calls that resolve the major scope creep issues. From a design perspective, the MVP decisions are sound. + +**Design Assessment of Decisions:** + +✓ **Standalone project**: Correct. Keeps concerns separated and allows orchestrated-discussions to remain focused. The integration contract (stdin/stdout) is clean. + +✓ **PlantUML + text editor**: Good choice. PlantUML is expressive enough for architectural diagrams and code-based editing aligns with the project philosophy. + +✓ **Graphical canvas in MVP**: This is the right call for visual artifact creation. Combined with text editing, it gives users choice—code-first developers can edit text, visual-first users can draw. Critical for accessibility. + +✓ **SmartTools for AI**: Maintains composability and keeps the editor from becoming a monolithic tool. + +✓ **Dear PyGui consistency**: Reusing the same framework as orchestrated-discussions is a strong design decision. Users see visual/interaction consistency across your tools. + +**Remaining Design Concern:** + +You asked about graphical canvas viability in Dear PyGui. This is the one area I need to flag: Dear PyGui's drawing primitives are limited compared to PyQt or web-based solutions. For an MVP canvas that supports: +- Drawing shapes (boxes, circles, diamonds for UML) +- Connector lines with arrows +- Text labels +- Basic selection/drag operations + +Dear PyGui *can* do this, but the implementation will be more manual than with a dedicated graphics framework. The trade-off is clear though: visual consistency with orchestrated-discussions outweighs drawing ease. + +**Design Requirements for MVP Editor:** + +To make the text + canvas approach work well: + +1. **Code and canvas must stay in sync**: When user edits text, canvas updates. When user draws, text updates. This bidirectional binding is critical for user confidence. + +2. **Split view layout** (50/50 code left, canvas right) is optimal. Establishes clear visual hierarchy and prevents tab-switching fatigue. + +3. **Error feedback inline**: Syntax errors in PlantUML should highlight the offending line in the code editor, not show a modal popup. + +4. **Clear affordances for mode switching**: Make it visually obvious whether user is in "text edit mode" or "canvas draw mode" (e.g., highlighted borders, status bar indicator). + +**Answer to Your Direct Questions:** + +Since you asked designers for ONE most important UX requirement: + +**From design perspective, the single most critical UX requirement is: bidirectional sync between text editor and visual canvas.** Users must be able to seamlessly switch between editing code and drawing shapes without losing changes or breaking consistency. This is what separates a professional tool from a clunky prototype. + +DIAGRAM: diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_designer_5.puml + +DIAGRAM: diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_designer_5.puml +VOTE: READY + +--- + +Name: AI-Moderator +## Decision Point Reached + +Rob has made three critical decisions that resolve the architectural debate: + +### Decisions Made + +**1. Standalone Project** ✓ +- Settled the Architect vs. Pragmatist debate +- Maintains separation of concerns (discussions vs. artifact editing) +- Leverages existing scaffold at `~/PycharmProjects/artifact-editor` + +**2. MVP Scope Defined** ✓ +- Single format: PlantUML +- Text editor AND graphical canvas (not deferred) +- AI via SmartTools (existing or new) +- Live preview with submit button or timer +- Simple integration contract: `ARTIFACT_SAVED:path` + +**3. UI Framework Direction** ✓ +- Preference: Dear PyGui (consistency with orchestrated-discussions) +- Open to alternatives if graphical editing requires more capability + +### Outstanding Responses Needed + +Rob has asked for **focused, concise answers** (not more analysis): + +1. @architect - Minimal state machine: states and transitions only +2. @pragmatist - One sentence: Can `discussion-diagram-editor` modify existing files? +3. @performance - One number: PlantUML render timeout in seconds +4. @designer - One sentence: Most important UX requirement for MVP +5. @ux - One sentence: Most important UX requirement for MVP + +### Current Status + +- **Phase**: initial_feedback +- **Votes**: 5 CHANGES, 1 READY (Rob) +- **Consensus**: Not yet reached (need targeted responses) +- **Tone**: Rob is signaling urgency to ship + +### Recommendation + +**Do not advance phase yet.** We need the five specific answers requested above. These are quick, targeted responses that will unblock implementation planning. + +Once we have these answers, we should have sufficient clarity to advance to detailed planning or implementation. + +--- + +*Waiting for concise responses from @architect, @pragmatist, @performance, @designer, and @ux.* + +--- + +Name: AI-Ux +The most important UX requirement for the MVP text editor is to provide clear, inline feedback for both syntax errors and AI-generated modifications, ensuring users can easily understand and verify changes. +VOTE: CHANGES + +--- + +Name: rob +I hear what pragmatist is saying about a feature creep. I don't mind, let's do this completely and all the way, I don't mind how long it takes. let's use a different GUI. Give me some suggestions of something that'll work for what we need. And let's make some decisions about the layout that would be best for the user experience. + +--- + +Name: AI-Architect +## GUI Framework Recommendations for Full-Featured Artifact Editor + +Rob, I appreciate the clarity. If we're building this properly without time constraints, let's choose the right tool for the job. + +### Framework Options Ranked + +**1. PyQt6/PySide6 (Recommended)** +- **QGraphicsScene/QGraphicsView**: Purpose-built for interactive 2D graphics with shapes, connectors, drag-drop, zoom, pan +- **QScintilla**: Professional code editor widget with syntax highlighting, line numbers, folding +- **Mature ecosystem**: 25+ years of development, extensive documentation +- **Cross-platform**: Linux, Windows, macOS native look +- **Trade-off**: Steeper learning curve than Dear PyGui, different visual style than orchestrated-discussions + +**2. Tkinter + CustomTkinter + TkinterDnD** +- Built into Python, no external dependencies +- Canvas widget handles drawing primitives +- Less polished than Qt but simpler API +- Trade-off: Looks dated without heavy customization + +**3. wxPython** +- Native widgets on each platform +- Good graphics capabilities +- Trade-off: Smaller community than Qt + +**4. Kivy** +- Modern, touch-friendly +- Good for custom UIs +- Trade-off: Non-native look, overkill for desktop-only app + +### My Recommendation: PyQt6 + +For a full-featured artifact editor with graphical canvas, bidirectional code sync, and professional polish, **PyQt6 is the clear choice**. Here's why: + +1. **QGraphicsScene** handles exactly what we need: shapes as objects, connectors, selection, z-ordering, grouping +2. **QSplitter** for resizable split views +3. **QPlainTextEdit** or QScintilla for syntax-highlighted code editing +4. **QProcess** for subprocess management (renderers) +5. **Signals/Slots** pattern aligns with state machine architecture + +### Proposed Layout Architecture + +ARTIFACT: diagrams/artifact-editor-layout.puml + +### Layout Decisions + +**Main Window Split (Horizontal)** +- **Left Panel (40%)**: Code Editor + - Syntax-highlighted PlantUML/Mermaid/OpenSCAD + - Line numbers, error highlighting + - Minimap for large files + +- **Right Panel (60%)**: Visual Canvas + - Interactive shape editing + - Rendered preview toggle + - Zoom/pan controls + +**Top Toolbar** +- Format selector (PlantUML, Mermaid, OpenSCAD, SVG) +- Voice input button with waveform feedback +- AI assist button (calls SmartTools) +- Render/Refresh button +- Save button + +**Bottom Status Bar** +- Current mode (Text Edit / Canvas Draw) +- Render status (Idle / Rendering / Error) +- Cursor position / selection info + +**Floating Panels (Dockable)** +- Shape palette for canvas mode +- Properties panel for selected shapes +- AI chat/command history + +### Bidirectional Sync Architecture + +This is the critical design challenge. My proposed approach: + +``` +Code Editor ←→ AST Representation ←→ Canvas Objects + (Intermediate Model) +``` + +1. Parse PlantUML code into intermediate model (shapes, connections, labels) +2. Canvas renders from intermediate model +3. Canvas edits update intermediate model +4. Intermediate model serializes back to code +5. Debounced sync (300ms) prevents thrashing + +This decouples the code format from the visual representation, making it easier to support multiple formats. + +### State Machine (Expanded for Full Editor) + +``` +STATES: + IDLE - Ready for input + TEXT_EDITING - User typing in code editor + CANVAS_EDITING - User manipulating shapes + AI_PROCESSING - Waiting for SmartTool response + RENDERING - PlantUML subprocess running + PREVIEW - Showing rendered output + ERROR - Displaying error state + SAVING - Writing file + +TRANSITIONS: + IDLE → TEXT_EDITING (focus code editor) + IDLE → CANVAS_EDITING (click canvas) + TEXT_EDITING → RENDERING (debounce timer / manual) + CANVAS_EDITING → TEXT_EDITING (sync to code) + * → AI_PROCESSING (voice/AI button) + AI_PROCESSING → TEXT_EDITING (SmartTool response) + RENDERING → PREVIEW (success) + RENDERING → ERROR (failure) + * → SAVING (Ctrl+S / Save button) + SAVING → IDLE (success, output contract) +``` + +### Questions for Other Participants + +Q: @designer - For the shape palette in canvas mode, should shapes be PlantUML-specific (actor, component, database) or generic (rectangle, circle, diamond) that map to PlantUML? + +Q: @ux - For bidirectional sync, should code changes immediately update canvas (potentially jarring) or require explicit sync (button/hotkey)? + +Q: @performance - For complex diagrams, should we render the full PlantUML output or use a lightweight "draft" mode showing just shapes without PlantUML styling? +VOTE: READY + +--- + +Name: AI-Pragmatist +CONCERN: COMPLEXITY - You're asking for GUI framework suggestions, but that's putting the cart before the horse. The real question is: what's the simplest thing that ships value? + +GUI framework options: +1. PyQt6/PySide6 - Mature, rich drawing capabilities, but heavy dependencies and learning curve +2. Dear PyGui - Already used by orchestrated-discussions, limited drawing but consistent +3. Web-based (Electron) - Most flexible, heaviest dependencies +4. TUI (urwid) - Simplest, no graphical canvas + +But here's the pragmatic truth: You don't need a graphical canvas for MVP. The existing discussion-diagram-editor SmartTool accepts stdin and modifies PlantUML. Launch it with current file, get modified code back. No canvas needed. + +Simplest layout: Split view (50/50) - text editor left, rendered preview right. Preview updates on save or timer. That's it. + +DIAGRAM: diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_7.puml + +DIAGRAM: diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_mvp_7.puml +VOTE: CHANGES + +--- + +Name: AI-Designer +## GUI Framework & Layout Analysis for Complete Vision\n\nRob's decision to build the full artifact editor with a proper GUI framework is the right call. A text editor + graphical canvas requires professional-grade graphics capabilities that Dear PyGui simply wasn't designed for.\n\n### GUI Framework Recommendations\n\nI'm evaluating frameworks for:\n- **Text editor with syntax highlighting** (PlantUML)\n- **Graphical canvas** (draw shapes, connect nodes, drag operations)\n- **Live preview rendering**\n- **Voice/text input integration**\n- **Cross-platform** (Linux, macOS, Windows)\n\n#### Option 1: PyQt6 (RECOMMENDED)\n**Pros:**\n- Industry standard for desktop apps (Qt Creator, many professional tools use Qt)\n- QGraphicsScene/QGraphicsView: Purpose-built for graphical editing\n- QPlainTextEdit with syntax highlighting support (QSyntaxHighlighter)\n- Native widgets feel professional\n- Excellent performance for canvas rendering\n- Rich ecosystem of extensions\n- Mature documentation and community\n\n**Cons:**\n- Larger binary footprint than alternatives\n- Slightly steeper learning curve\n- GPL licensing (but PyQt6 is commercial-friendly)\n\n**Canvas capability:** Excellent. QGraphicsScene handles complex drawing, selection, drag-drop natively.\n\n#### Option 2: PySide6 (ALTERNATIVE)\n**Pros:**\n- Same capabilities as PyQt6 (actually the official Qt bindings)\n- Better licensing story (LGPL)\n- Slightly better documentation\n\n**Cons:**\n- Newer than PyQt (less mature in practice)\n- Same binary size\n\n**Canvas capability:** Excellent (identical to PyQt6).\n\n#### Option 3: Tkinter + Pillow (NOT RECOMMENDED)\n**Pros:**\n- Lightweight\n- Built into Python\n\n**Cons:**\n- Canvas is limited for modern graphical editing\n- No native syntax highlighting\n- Looks dated\n- Poor for drag-and-drop operations\n\n**Canvas capability:** Poor. Text rendering on canvas is cumbersome.\n\n#### Option 4: Web-based (Electron + React) (POSSIBLE BUT OVERKILL)\n**Pros:**\n- Beautiful rendering, modern UI patterns\n- Excellent drawing libraries (Konva.js, Fabric.js)\n- Easy to build sophisticated interactions\n\n**Cons:**\n- Heavy runtime (Electron bundle ~150MB)\n- Overkill for a focused editing tool\n- Diverges from orchestrated-discussions tech stack\n\n**Canvas capability:** Excellent but over-engineered for this scope.\n\n**RECOMMENDATION: PyQt6** - It's the Goldilocks choice. Professional capability without unnecessary complexity.\n\n### Optimal Layout for Text + Canvas Editing\n\nI've analyzed the user workflows and here's the layout that best serves both code-first and visual-first users:\n\nDIAGRAM: diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_designer_layout.puml\n\n**Layout Structure:**\n\n```\n┌─────────────────────────────────────────────────────┐\n│ Menu: File | Edit | Tools | Help │\n├──────────────────────────┬──────────────────────────┤\n│ │ │\n│ TEXT EDITOR │ GRAPHICAL CANVAS │\n│ (50% width) │ (50% width) │\n│ │ │\n│ - PlantUML syntax │ - Rendered preview │\n│ highlighting │ - Interactive shapes │\n│ - Line numbers │ - Drag to move │\n│ - Error markers │ - Click to select │\n│ │ - Connector arrows │\n│ │ │\n├──────────────────────────┴──────────────────────────┤\n│ Status Bar: [Mode: Edit] [Render: ✓] [Ln 12, Col 5]│\n└─────────────────────────────────────────────────────┘\n```\n\n**Design Rationale:**\n\n1. **50/50 Split View**: Equal visual weight prevents one panel from dominating. Users can resize if they prefer more canvas or more code space. This balances the code-first and visual-first workflows.\n\n2. **Left = Text (Primary for Experts)**: Developers comfortable with PlantUML syntax get direct access to the code they're creating. Line numbers aid reference.\n\n3. **Right = Canvas (Primary for Visual Designers)**: Users who prefer pointing and clicking see immediate graphical feedback without needing to understand syntax.\n\n4. **Bidirectional Sync**: This is CRITICAL (as @ux noted). Changes in code must update canvas instantly. Changes via canvas must update code. This requires a robust parser/generator.\n\n5. **Inline Error Display**: PlantUML syntax errors show:\n - Red squiggly underline on offending line in editor\n - Error message in status bar\n - Canvas shows \"last valid render\" while error exists\n - User never loses their work to a syntax error\n\n### Design Requirements for Bidirectional Sync\n\n**The Core Challenge**: Parsing PlantUML diagram code, building an AST, allowing canvas edits, and regenerating valid PlantUML is complex.\n\n**Solution Layers:**\n\n1. **Text → Canvas**: Parse PlantUML into diagram model, render as QGraphicsItems\n2. **Canvas → Text**: When user draws/moves shapes, serialize back to PlantUML\n\nThis requires a PlantUML AST library or building one. Consider:\n- `plantuml-parser` (Python) - might not be mature enough\n- Manual parser for a PlantUML subset (only diagram types needed for MVP)\n- Fallback: Limited graphical editing (only AI-generated → view/edit → save)\n\n### UI/UX Components\n\n**Text Editor Panel:**\n- QPlainTextEdit with custom QSyntaxHighlighter for PlantUML\n- Gutter with line numbers and error indicators\n- Keyboard shortcuts for common operations (Ctrl+/ for comment)\n- Status shows: cursor position, selection size\n\n**Canvas Panel:**\n- QGraphicsView + QGraphicsScene for rendering\n- Interactive selection (click to select, Shift-click to multi-select)\n- Drag to move shapes\n- Context menu for delete/duplicate\n- Zoom controls (Ctrl+Mouse wheel)\n- Pan (Space+drag or middle-mouse)\n\n**Integration Points:**\n- Save button (Ctrl+S) → writes PlantUML to file, outputs ARTIFACT_SAVED:path\n- Voice input button → calls `dictate` SmartTool → updates code\n- AI generation button → calls `artifact-generate` → inserts/replaces code\n- Live preview toggle (optional) → auto-render on interval vs. on-demand\n\n### Design Consistency with Orchestrated-Discussions\n\nWhile we're moving to PyQt6 (not Dear PyGui), we can maintain visual harmony:\n- **Color scheme**: Match orchestrated-discussions theme (dark mode friendly)\n- **Typography**: Use system fonts consistently\n- **Spacing**: Follow 8px grid for padding/margins\n- **Icons**: Use a consistent icon library (Material Icons or simple SVGs)\n\n### Most Important UX Requirement for Complete Vision\n\n**Bidirectional sync with visual feedback must be bulletproof.** Users must never feel confused about whether they're editing code or canvas—changes should be instantly visible in both. The tool lives or dies on this.\n\nSecondary: **Error handling must be graceful.** Invalid syntax shouldn't crash the editor or lose user work. + +DIAGRAM: diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_designer_7.puml +VOTE: READY + +--- + +Name: AI-Moderator +## Major Direction Change: Full-Featured GUI + +Rob has made a decisive pivot that changes the entire MVP scope. + +### What Changed + +**Previous Decision (5 minutes ago):** +- MVP with PlantUML text editor + basic canvas +- Try Dear PyGui first +- Keep it simple to ship quickly + +**New Decision (just now):** +- "Let's do this completely and all the way, I don't mind how long it takes" +- Switch to a different GUI framework +- Full graphical editing capabilities +- Comprehensive layout design + +### What This Means + +This resolves AI-Pragmatist's concern about scope creep **by embracing it intentionally**. Rob is explicitly choosing the full-featured path over the minimal MVP. + +### GUI Framework Recommendations Needed + +@architect @designer - Please provide specific GUI framework recommendations that support: +- Rich graphical canvas (drag-drop, shape drawing, connectors) +- Text editing with syntax highlighting +- Split-view or tabbed layouts +- PlantUML rendering integration + +Consider these options: +1. **PyQt6/PySide6** - Most mature, extensive widget library, excellent graphics framework +2. **Electron + React** - Web-based, access to existing diagram libraries (mxGraph, joint.js) +3. **GTK4** - Native Linux feel, good graphics support +4. **wxPython** - Cross-platform, good native controls + +### Layout Design Needed + +@designer @ux - Given full creative freedom now, please propose: +- Optimal layout for text + canvas + preview + AI controls +- Multi-panel arrangement (horizontal/vertical splits?) +- Mode switching UX (text edit vs. canvas draw vs. AI assist) +- Toolbar/menubar organization + +### Status Update + +**Votes:** +- READY: AI-Architect, AI-Designer, Human (Rob) +- CHANGES: AI-Pragmatist, AI-Moderator, AI-Ux + +**Pending Response:** +- @performance - Still need PlantUML render timeout recommendation + +**Open Questions:** +1. Which GUI framework should we use for full-featured graphical editing? +2. What's the optimal layout for text + canvas + preview? +3. PlantUML render timeout? (still waiting) + +### Recommendation + +**Do not advance phase yet.** We need: +1. GUI framework recommendations from technical participants +2. Layout design proposals from design participants +3. Performance timeout answer + +Once we have these, we'll have a complete technical foundation to move forward with full implementation planning. + +--- + +*This is a significant scope expansion. The pragmatist's warnings are noted but overruled by the project owner. Let's design the full solution.* + +--- + +Name: AI-Ux +Rob, thank you for the clear decisions. This provides a solid foundation for moving forward with a user-centric design. The commitment to a graphical canvas, even if it means moving beyond Dear PyGui, is excellent for the user experience, as it directly addresses the 'drawing directly' requirement. + +## GUI Framework Recommendation + +Given the need for a robust graphical canvas with drawing capabilities, and the potential limitations of Dear PyGui for this specific task, I recommend **PyQt6 (or PySide6)**. + +**Why PyQt6/PySide6?** +* **Powerful Graphics Scene**: Its `QGraphicsView` and `QGraphicsScene` architecture is specifically designed for managing and rendering complex 2D graphics, making it ideal for a diagramming canvas with interactive shapes, lines, and text. +* **Mature & Stable**: A well-established framework with extensive documentation and a large community. +* **Rich Widget Set**: Provides all necessary standard UI elements (text editors, buttons, panels) that can be styled to maintain consistency with `orchestrated-discussions`' aesthetic, even if the underlying framework differs. +* **Cross-Platform**: Ensures a consistent experience across different operating systems. + +## Layout Decisions for Optimal UX + +Building on previous feedback, the optimal layout for the MVP should prioritize the core interaction of code-to-canvas and canvas-to-code synchronization. + +1. **Split-View (Code Left, Canvas Right)**: This remains the strongest choice. It provides immediate visual feedback for code changes and allows direct manipulation on the canvas. A resizable divider between the two panels would allow users to adjust focus. +2. **Bidirectional Synchronization**: As highlighted, this is paramount. Any change in the text editor must instantly reflect on the canvas, and any manipulation on the canvas (e.g., dragging a shape, adding a connection) must update the PlantUML code in the editor. This creates a powerful, intuitive feedback loop. +3. **Toolbar for Canvas Tools**: A dedicated toolbar above or alongside the canvas for drawing tools (e.g., add rectangle, add circle, add arrow, select tool) will make graphical editing discoverable and efficient. +4. **Status Bar**: A persistent status bar at the bottom should provide real-time feedback on rendering status (e.g., 'Rendering...', 'Render Complete', 'Syntax Error: Line X'), AI processing, and current mode (e.g., 'Text Edit Mode', 'Draw Mode'). +5. **Inline Error Feedback**: Syntax errors in the PlantUML code should be highlighted directly in the text editor (e.g., red squiggly underline), and corresponding visual errors (e.g., missing elements) should be clearly indicated on the canvas. +6. **Voice Input Affordance**: A prominent, clearly labeled 'Dictate' button (e.g., '🎤 Dictate') should be easily accessible, perhaps in a bottom control bar, with visual feedback (e.g., animated waveform) when active. + +DIAGRAM: diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_ux_7.puml + +DIAGRAM: diagrams/stand-alone-artifact-editor-ai-enhanced-diagram-and-3d-model-creator_ux_7.puml +VOTE: READY diff --git a/templates/brainstorm.yaml b/templates/brainstorm.yaml new file mode 100644 index 0000000..ee53c30 --- /dev/null +++ b/templates/brainstorm.yaml @@ -0,0 +1,131 @@ +# Brainstorm Discussion Template +# Referenced by: <!-- Template: brainstorm --> in discussion files + +name: brainstorm +description: Structured ideation with diverge/converge phases + +# Turn pipeline - optimized for fast ideation in early phases +# Uses variable-based schema: $input -> tool -> $output +turn_pipeline: + steps: + # Step 1: Parse discussion + - tool: discussion-parser + input: $discussion + output: $parsed + + # Step 2: Route mentions (if no participants specified) + - tool: discussion-mention-router + input: $parsed + output: $routing + when: not $participants_specified + args: + --default-participants: $participants_csv + + # Step 3: Call each participant (parallel for fast ideation) + - tool: discussion-{participant} + for_each: $participants_to_call + parallel: true + input: $discussion + output: $responses[] + args: + --callout: $callout + --templates-dir: $templates_dir + + # Step 4: Append responses + - tool: discussion-turn-appender + input: $discussion + output: $discussion + args: + --responses-json: $responses_json + + # Step 5: Count votes (voting phases only) + - tool: discussion-vote-counter + input: $parsed + output: $votes + when: $phase_voting + + # Step 6: Check status promotion + - tool: discussion-status-promoter + input: $votes + output: $promotion + when: $phase_voting + args: + --current-status: $status + --current-phase: $phase + +phases: + seed: + goal: Frame the problem clearly + instructions: | + - State the problem or opportunity in one sentence + - List known constraints (time, tech, resources) + - Define what success looks like + - Ask clarifying questions only, no solutions yet + voting: false + next_phase: diverge + + diverge: + goal: Generate ideas without judgment + instructions: | + - Quantity over quality + - Build on others' ideas with "Yes, and..." + - No criticism, no "but", no "that won't work" + - Wilder ideas are welcome + - Short bullet points, not essays + voting: false + next_phase: cluster + + cluster: + goal: Group related ideas into themes + instructions: | + - Moderator synthesizes ideas into 3-5 themes + - Name each theme clearly + - Vote on which theme(s) to explore further + - READY = approve these themes + - CHANGES = suggest different grouping + voting: true + threshold_ready: 0.5 + next_phase: sketch + + sketch: + goal: Rough out the selected idea with diagrams + artifacts: + formats: [plantuml, mermaid, svg] + output_dir: diagrams/ + file_pattern: "{title}_{participant}_{n}" + instructions: | + - Create simple diagrams (sequence, component, flowchart) + - Identify 3-5 key pieces + - Mark unknowns with "?" + - Review existing artifacts in diagrams/ folder + - Build on or modify others' diagrams as needed + - Reference each artifact with "ARTIFACT: path/to/file" + - Keep it rough, not polished + voting: false + next_phase: reality_check + + reality_check: + goal: Ground the idea in reality + instructions: | + - What's the MVP version? (1 week of work max) + - What's the biggest risk? + - What would we prototype first? + - What can we cut? + - Flag over-engineering with CONCERN: COMPLEXITY + voting: false + next_phase: decide + + decide: + goal: Commit to an approach + instructions: | + - READY = proceed with this approach + - CHANGES = need to iterate (say what) + - REJECT = start over with different theme + voting: true + threshold_ready: 0.67 + human_required: true + next_phase: null + +status_transitions: + OPEN: + on_consensus: READY_FOR_DESIGN diff --git a/templates/feature.yaml b/templates/feature.yaml new file mode 100644 index 0000000..8bdc77d --- /dev/null +++ b/templates/feature.yaml @@ -0,0 +1,97 @@ +# Feature Discussion Template +# Referenced by: <!-- Template: feature --> in discussion files + +name: feature +description: Feature proposal and review workflow + +# Turn pipeline - defines what SmartTools run during each turn +# Uses variable-based schema: $input -> tool -> $output +turn_pipeline: + steps: + # Step 1: Parse discussion to get metadata + - tool: discussion-parser + input: $discussion + output: $parsed + + # Step 2: Route mentions (only if participants not specified) + - tool: discussion-mention-router + input: $parsed + output: $routing + when: not $participants_specified + args: + --default-participants: $participants_csv + + # Step 3: Call each participant + - tool: discussion-{participant} + for_each: $participants_to_call + parallel: true + input: $discussion + output: $responses[] + args: + --callout: $callout + --templates-dir: $templates_dir + + # Step 4: Append responses to discussion + - tool: discussion-turn-appender + input: $discussion + output: $discussion + args: + --responses-json: $responses_json + + # Step 5: Count votes (only in voting phases) + - tool: discussion-vote-counter + input: $parsed + output: $votes + when: $phase_voting + + # Step 6: Check status promotion + - tool: discussion-status-promoter + input: $votes + output: $promotion + when: $phase_voting + args: + --current-status: $status + --current-phase: $phase + +phases: + initial_feedback: + goal: Gather diverse perspectives on the proposal + instructions: | + - Focus on feasibility, risks, and high-level concerns + - Raise blocking issues early + - Ask clarifying questions about requirements + - Suggest alternatives if you see better approaches + - Don't dive deep into implementation details yet + voting: false + next_phase: detailed_review + + detailed_review: + goal: Deep dive into implementation approach + instructions: | + - Address concerns raised in initial feedback + - Discuss specific implementation trade-offs + - Review proposed architecture and design + - Identify edge cases and error handling needs + - Ensure security and performance are considered + voting: false + next_phase: consensus_vote + + consensus_vote: + goal: Reach agreement on the approach + instructions: | + - All major concerns must be addressed before voting READY + - Vote CHANGES if you have unresolved concerns + - Vote REJECT only for fundamental issues + - Summarize your position clearly with your vote + - If voting CHANGES, specify what needs to change + voting: true + threshold_ready: 0.67 + human_required: true + next_phase: null + +# Status transitions triggered by consensus +status_transitions: + OPEN: + on_consensus: READY_FOR_DESIGN + READY_FOR_DESIGN: + on_consensus: READY_FOR_IMPLEMENTATION